diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..cc93cff0 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,15 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +polar: # Replace with a single Polar username +buy_me_a_coffee: # Replace with a single Buy Me a Coffee username +thanks_dev: # Replace with a single thanks.dev username +custom: https://firebirdsql.org/en/donate/ diff --git a/.gitignore b/.gitignore index 9f146d8d..db5f2b74 100644 --- a/.gitignore +++ b/.gitignore @@ -134,4 +134,10 @@ dmypy.json *.wpu # Sphinx build -docs/_build \ No newline at end of file +docs/_build + +# Other local files +/dbcache + +# Personal local files +/personal diff --git a/CHANGELOG.md b/CHANGELOG.md index 8af7ed68..367c3221 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.20.2] - 2025-06-08 + +### Fixed + +- Fixed problem with utf8 db filenames in `Database`. + +## [0.20.1] - 2025-04-29 + +### Changed + +- Dependency on `firebird-base` changed to "~=1.8" +- Updated `hatch` configuration + ## [0.20.0] - 2024-05-09 ### Added diff --git a/PLUGIN-README.md b/PLUGIN-README.md index 96a87759..19ac7453 100644 --- a/PLUGIN-README.md +++ b/PLUGIN-README.md @@ -6,8 +6,7 @@ If you plan to use this plugin for personal purposes (not related to Firebird pr we recommend to use `pipx` to install `pytest` together with `firebird-qa` plugin: ``` -pipx install pytest -pipx inject pytest firebird-qa +pipx --include-deps firebird-qa ``` ## Configuration diff --git a/README.substitutions.md b/README.substitutions.md new file mode 100644 index 00000000..465e3dfc --- /dev/null +++ b/README.substitutions.md @@ -0,0 +1,43 @@ +Since 6.0.0.834 SQL schemas were intruduced in Firebird DBMS (commit: b8be591c0f9e1811f47fac792c8dd2f10c4cea28). +Following changes did appear since that snapshot: + * every DB object name that must be shown by test output now has prefix if its schema, with default name `"PUBLIC"`; + * the displayed object names become enclosed in double quotes, even if they are ascii-only and without inner spaces, e.g. `"ID"` etc; + * there is no ability to remove PUBLIC schema (to suppress its output); +Because of that, many tests needed to be re-implemented in order to make them pass on all checked FB (3.x ... 6.x and future versions). + +In order to reduce volume of this job and to avoid separation of expected values (depending on whether major version is 6.x or prior), +every such test has to be changed as follows: + * initiate 'substitutions' variable with list of tuples that were needed before (or make it empty, but anyway it must be created), + e.g.: + ``` + substitutions = [('[ \t]+', ' '), ('(-)?At trigger\\s+\\S+', 'At trigger')] + ``` + or + ``` + substitutions = [] + ``` + * append to this list pre-defined values from EXTERNAL file `act.files_dir/test_config.ini` as show below: + ``` + addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] + addi_subst_tokens = addi_subst_settings['addi_subst'] + for p in addi_subst_tokens.split(): + substitutions.append( (p, '') ) + ``` + * NOTE. + File `act.files_dir/test_config.ini` must exist in the `$QA_HOME/files/` directory. + Content of this file is used in `$QA_HOME/firebird-qa/src/firebird/qa/plugin.py`, see QA_GLOBALS dictionary in its source. + Among other sections, following must present in this file: + ``` + [schema_n_quotes_suppress] + addi_subst="PUBLIC". " ' + ``` + If another schema will appear, e.g. "SYSTEM", and we need to suppress its output then `addi_subst` must be changed like this: + ``` + addi_subst="PUBLIC". "SYSTEM". " ' + ``` + + Applying of tokens from 'addi_subst' parameter to the 'substitutions' will chage it to follwing: + ``` + substitutions = [ ( , ('"PUBLIC".', ''), ('"', ''), ("'", '') ] + ``` +After this, every single/double quotes along with schema prefix(es) will be removed from DB object names in the test output. diff --git a/configs/fb40_all.conf b/configs/fb40_all.conf index a6acff9f..80ea8205 100644 --- a/configs/fb40_all.conf +++ b/configs/fb40_all.conf @@ -68,7 +68,7 @@ MaxIdentifierByteLength = 252 MaxIdentifierCharLength = 63 WireCryptPlugin = ChaCha, Arc4 -StatementTimeout = 7200 +StatementTimeout = 600 ConnectionIdleTimeout = 0 ClearGTTAtRetaining = 0 diff --git a/configs/fb50_all.conf b/configs/fb50_all.conf index f9cf2095..fa264e6b 100644 --- a/configs/fb50_all.conf +++ b/configs/fb50_all.conf @@ -70,7 +70,7 @@ MaxIdentifierByteLength = 252 MaxIdentifierCharLength = 63 WireCryptPlugin = ChaCha, Arc4 -StatementTimeout = 7200 +StatementTimeout = 600 ConnectionIdleTimeout = 0 ClearGTTAtRetaining = 0 @@ -78,6 +78,9 @@ ClientBatchBuffer = 131072 SnapshotsMemSize = 64K TipCacheBlockSize = 4M -# Added 03-apr-2023: -ParallelWorkers = 1 +# ParallelWorkers must be GREATER than 1 since 28.09.2024, see bugs/gh_8263_test.py +ParallelWorkers = 2 MaxParallelWorkers = 8 + +# SubQueryConversion = false +# OptimizeForFirstRows = false diff --git a/configs/fb60_all.conf b/configs/fb60_all.conf index 21796ec5..4519f757 100644 --- a/configs/fb60_all.conf +++ b/configs/fb60_all.conf @@ -70,7 +70,7 @@ MaxIdentifierByteLength = 252 MaxIdentifierCharLength = 63 WireCryptPlugin = ChaCha, Arc4 -StatementTimeout = 7200 +StatementTimeout = 600 ConnectionIdleTimeout = 0 ClearGTTAtRetaining = 0 @@ -78,6 +78,8 @@ ClientBatchBuffer = 131072 SnapshotsMemSize = 64K TipCacheBlockSize = 4M -# Added 03-apr-2023: -ParallelWorkers = 1 +# ParallelWorkers must be GREATER than 1 since 28.09.2024, see bugs/gh_8263_test.py +ParallelWorkers = 2 MaxParallelWorkers = 8 + +#OptimizeForFirstRows = false diff --git a/core_5201_test.py b/core_5201_test.py deleted file mode 100644 index 8136705e..00000000 --- a/core_5201_test.py +++ /dev/null @@ -1,80 +0,0 @@ -#coding:utf-8 - -""" -ID: issue-5482 -ISSUE: 5482 -TITLE: Return nonzero result code when restore fails on activating and creating deferred user index -DESCRIPTION: - According to Alex response on letter 25-apr-2016 19:15, zero retcode returned ONLY when restore - was done WITH '-verbose' switch, and this was fixed. When restore performed without additional - switches, retcode was 1. - - We create table with UNIQUE computed-by index which expression refers to other table (Firebird allows this!). - Because other table (test_2) initially is empty, index _can_ be created. But after this we insert record into - this table and do commit. Since that moment backup of this database will have table test_1 but its index will - NOT be able to restore (unless '-i' switch specified). - We will use this inability of restore index by checking 'gbak -rep -v ...' return code: it should be NON zero. - If code will skip exception then this will mean FAIL of test. -JIRA: CORE-5201 -FBTEST: bugs.core_5201 - -NOTES: - [07.02.2023] pzotov - Adjusted tail of restore log: added messages: - gbak: ERROR:Database is not online due to failure to activate one or more indices. - gbak: ERROR: Run gfix -online to bring database online without active indices. - (actual since 5.0.0.932; will be soon also for FB 3.x and 4.x - see letter from Alex, 07.02.2023 11:53). -""" - -import pytest -from pathlib import Path -from firebird.qa import * - -init_script = """ - create table test_1(x int); - create table test_2(x int); - insert into test_1 values(1); - insert into test_1 values(2); - insert into test_1 values(3); - commit; - create unique index test_1_unq on test_1 computed by( iif( exists(select * from test_2), 1, x ) ); - commit; - insert into test_2 values(1000); - commit; -""" - -db = db_factory(init=init_script) - -act = python_act('db') - -expected_stdout = """ - gbak: ERROR:attempt to store duplicate value (visible to active transactions) in unique index "TEST_1_UNQ" - gbak: ERROR: Problematic key value is ( = 1) -""" -# gbak: ERROR:Database is not online due to failure to activate one or more indices. -# gbak: ERROR: Run gfix -online to bring database online without active indices. - -fbk_file = temp_file('core_5201.fbk') -tmp_db_file = temp_file('tmp_core_5201.fdb') - -@pytest.mark.version('>=3.0') -def test_1(act: Action, fbk_file: Path, tmp_db_file: Path): - with act.connect_server() as srv: - srv.database.backup(database=act.db.db_path, backup=fbk_file) - assert srv.readlines() == [] - # - #act.expected_stderr = 'We expect error' - act.expected_stdout = expected_stdout - - act.gbak(switches=['-rep', '-v', str(fbk_file), str(tmp_db_file)], combine_output = True) - - #with act.connect_server() as srv: - # srv.database.restore(database=act.db.db_path, backup=fbk_file, flags=SrvRestoreFlag.REPLACE) - - # filter stdout - #act.stdout = '\n'.join([line for line in act.stdout.splitlines() if ' ERROR:' in line]) - #act.stdout = '\n'.join([line for line in act.stdout.splitlines() if ' ERROR:' in line]) - assert act.return_code == 2 - assert act.stdout == '' - #assert act.clean_stdout == act.clean_expected_stdout - diff --git a/docker/run.sh b/docker/run.sh index 10c687fd..ce0d7970 100755 --- a/docker/run.sh +++ b/docker/run.sh @@ -21,5 +21,5 @@ pytest \ --md-report-output /qa-out/md_report.md \ --ignore=tests/functional/replication \ --ignore=tests/functional/basic/isql/test_08.py \ - -m "not replication" \ + -m "not replication and not encryption" \ "$@" diff --git a/docs/changelog.txt b/docs/changelog.txt index afcb196f..4e2763bd 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -4,6 +4,17 @@ Changelog .. currentmodule:: firebird.qa.plugin +Version 0.20.2 +============== + +* Fixed problem with utf8 db filenames in `Database`. + +Version 0.20.1 +============== + +* Dependency on `firebird-base` changed to "~=1.8" +* Updated `hatch` configuration + Version 0.20.0 ============== diff --git a/files/gh_6915_cs_cz.zip b/files/gh_6915_cs_cz.zip new file mode 100644 index 00000000..2c4eb30c Binary files /dev/null and b/files/gh_6915_cs_cz.zip differ diff --git a/files/gh_6915_hu_hu.zip b/files/gh_6915_hu_hu.zip new file mode 100644 index 00000000..974b1716 Binary files /dev/null and b/files/gh_6915_hu_hu.zip differ diff --git a/files/gh_7269.zip b/files/gh_7269.zip new file mode 100644 index 00000000..956db23a Binary files /dev/null and b/files/gh_7269.zip differ diff --git a/files/gh_7398.zip b/files/gh_7398.zip new file mode 100644 index 00000000..f367ca15 Binary files /dev/null and b/files/gh_7398.zip differ diff --git a/files/gh_8115.zip b/files/gh_8115.zip new file mode 100644 index 00000000..9a549d65 Binary files /dev/null and b/files/gh_8115.zip differ diff --git a/files/gh_8161.zip b/files/gh_8161.zip new file mode 100644 index 00000000..5d95df07 Binary files /dev/null and b/files/gh_8161.zip differ diff --git a/files/gh_8444.zip b/files/gh_8444.zip new file mode 100644 index 00000000..564ceedc Binary files /dev/null and b/files/gh_8444.zip differ diff --git a/files/gh_8595-ods13_1.zip b/files/gh_8595-ods13_1.zip new file mode 100644 index 00000000..1ae41e86 Binary files /dev/null and b/files/gh_8595-ods13_1.zip differ diff --git a/files/gh_8597-ods13_1.zip b/files/gh_8597-ods13_1.zip new file mode 100644 index 00000000..ea3e32b9 Binary files /dev/null and b/files/gh_8597-ods13_1.zip differ diff --git a/files/gtcs-cast-gen-ddl.sql b/files/gtcs-cast-gen-ddl.sql index c2dc4069..ba47928a 100644 --- a/files/gtcs-cast-gen-ddl.sql +++ b/files/gtcs-cast-gen-ddl.sql @@ -391,6 +391,7 @@ commit; -- set heading off; set bail off; -set list on; -select txt as " " from sp_gen_code; +set list off; +set heading off; +select txt from sp_gen_code; commit; diff --git a/files/qa-databases.conf b/files/qa-databases.conf index 0bd50daa..23d9c767 100644 --- a/files/qa-databases.conf +++ b/files/qa-databases.conf @@ -14,13 +14,24 @@ # Test issue: https://github.com/FirebirdSQL/firebird/issues/5160 # https://github.com/FirebirdSQL/firebird/issues/5255 # ex. core-4964 -tmp_core_4964_alias = $(dir_sampleDb)/qa/tmp_core_4964.fdb +tmp_core_4964_alias_5x = $(dir_sampleDb)/qa/tmp_core_4964_5x.fdb { # dir_msg - directory where messages file (firebird.msg) is located. # We put here path+name of file that has for sure not .fdb format: SecurityDatabase = $(dir_msg)/firebird.msg } +tmp_core_4964_alias_win = $(dir_sampleDb)/qa/tmp_core_4964_win.fdb +{ + SecurityDatabase = $(dir_sample)/prebuilt/plugins/fbSampleDbCrypt.dll + # SecurityDatabase = $(dir_sample)/prebuilt/bin/fbSampleDbCryptApp.exe +} + +tmp_core_4964_alias_nix = $(dir_sampleDb)/qa/tmp_core_4964_nix.fdb +{ + SecurityDatabase = $(dir_sample)/prebuilt/plugins/libfbSampleDbCrypt.so + # SecurityDatabase = $(dir_sample)/prebuilt/bin/fbSampleDbCryptApp +} # Test issue: https://github.com/FirebirdSQL/firebird/issues/5160 # ex. core-4864 @@ -256,6 +267,51 @@ tmp_gh_8062_alias = $(dir_sampleDb)/qa/tmp_gh_8062.fdb SecurityDatabase = tmp_gh_8062_alias } +# Test issue: https://github.com/FirebirdSQL/firebird/issues/8194 +tmp_gh_8194_alias = $(dir_sampleDb)/qa/tmp_gh_8194.fdb +{ + DefaultDbCachePages = 128 +} + +# https://github.com/FirebirdSQL/firebird/commit/fd0fa8a3a58fbfe7fdc0641b4e48258643d72127 +# Let include file name into error message when creation of temp file failed +tmp_fd0fa8a3_alias = $(dir_sampleDb)/qa/tmp_fd0fa8a3.fdb +{ + TempTableDirectory = <> +} + +tmp_gh_6416_alias = $(dir_sampleDb)/qa/tmp_gh_6416.fdb +{ + DataTypeCompatibility = 3.0 +} + +# https://github.com/FirebirdSQL/firebird/issues/8253 +tmp_gh_8253_alias = $(dir_sampleDb)/qa/tmp_gh_8253.fdb +{ + SecurityDatabase = tmp_gh_8253_alias +} + +# Test issue: https://github.com/FirebirdSQL/firebird/issues/8391 +tmp_gh_8391_alias = $(dir_sampleDb)/qa/tmp_gh_8391.fdb +{ + DefaultDbCachePages = 128 +} + + +gh_8429_alias_a = $(dir_sampleDb)/qa/tmp_gh_8429_a.fdb +{ + KeyHolderPlugin = KH2 +} + +gh_8429_alias_b = $(dir_sampleDb)/qa/tmp_gh_8429_b.fdb +{ + KeyHolderPlugin = KH2 +} + +gh_8429_alias_c = $(dir_sampleDb)/qa/tmp_gh_8429_c.fdb +{ + KeyHolderPlugin = KH2 +} # Databases for replication tests: # diff --git a/files/standard_sample_databases.zip b/files/standard_sample_databases.zip index 017216ac..21a8e44f 100644 Binary files a/files/standard_sample_databases.zip and b/files/standard_sample_databases.zip differ diff --git a/files/test_config.ini b/files/test_config.ini index 11d44ebf..40ef118c 100644 --- a/files/test_config.ini +++ b/files/test_config.ini @@ -86,3 +86,6 @@ ARCHIVE_SUB_DIR = repl_archive # related to record format) set RUN_SWEEP_AT_END to 1. # RUN_SWEEP_AT_END = 0 + +[schema_n_quotes_suppress] +addi_subst="PUBLIC". "SYSTEM". PUBLIC. SYSTEM. " ' diff --git a/files/unlist-unexpected.zip b/files/unlist-unexpected.zip new file mode 100644 index 00000000..fe26552a Binary files /dev/null and b/files/unlist-unexpected.zip differ diff --git a/git-commit-adjust-output.bat b/git-commit-adjust-output.bat new file mode 100644 index 00000000..1c468f18 --- /dev/null +++ b/git-commit-adjust-output.bat @@ -0,0 +1,162 @@ +@echo off +setlocal enabledelayedexpansion enableextensions + +@rem git config --global user.email "you@example.com" +@rem git config --global user.name "Your Name" +set GITCMD=C:\mix\Git\bin\git.exe + +set qa_root=%~dp0 +set file_ext=^ +set git_text=Updated "!file_ext!": adjust expected stdout/stderr to current FB version. +if .%1.==.. goto syntax + +set pytest_file=%1 + +if NOT .%2.==.. ( + set /a i=0 + @rem echo all inp arguments: ^|"%*"^| + for /f "tokens=1* delims= " %%a in ("%*") do ( + set customer_comment=%%b + ) + @rem echo customer comment: ^|!customer_comment!^| + + set git_text=Added/Updated "!file_ext!": !customer_comment! +) + +set joblog=%~dpn0.log +set tmplog=%~dpn0.tmp + +del !joblog! 2>nul +del !tmplog! 2>nul + +for /f %%a in ("!pytest_file!") do ( + @rem set file_ext=%%~nxa + set file_ext=%%a + set text_chk=!file_ext:%qa_root%=! + if .!text_chk!.==.!file_ext!. ( + @rem .fbt was specified WITHOUT path + set file_ext=%cd%\!file_ext! + ) + set file_ext=!file_ext:%qa_root%=! + +) +set git_text=!git_text:^=%file_ext%! + +( + echo Log for: %~f0 !pytest_file! + echo Created !date! !time! on host '%COMPUTERNAME%' + echo. + echo Comment is: git_text=!git_text! +) >>!joblog! + +set msg=!date! !time! Processing command: !GITCMD! add !pytest_file! +echo !msg! +echo !msg!>>!joblog! + +@rem ############################ +@rem ### g i t a d d ### +@rem ############################ +!GITCMD! add !pytest_file! 1>!tmplog! 2>&1 +set /a elevel=!errorlevel! +echo elevel=!elevel! +echo elevel=!elevel!>>!joblog! + +type !tmplog! +type !tmplog! >>!joblog! + +if !elevel! GTR 0 ( + echo ERROR OCCURED. Check log: + echo ------------------------- + type !tmplog! + echo ------------------------- + del !tmplog! + goto :final +) + +!GITCMD! status !pytest_file! 1>>!joblog! 2>&1 + +set msg=!date! !time! Processing command: !GITCMD! commit -m "!git_text!" -- !pytest_file! +echo !msg! +echo !msg!>>!joblog! + +@rem ################################## +@rem ### g i t c o m m i t ### +@rem ################################## +!GITCMD! commit -m "!git_text!" -- !pytest_file! 1>!tmplog! 2>&1 +set elevel=!errorlevel! +echo elevel=!elevel! +echo elevel=!elevel!>>!joblog! +type !tmplog! >>!joblog! +if !elevel! GTR 0 ( + echo ERROR OCCURED. Check log: + echo ------------------------- + type !tmplog! + echo ------------------------- + del !tmplog! + goto :final +) + + +set msg=!date! !time! Processing command: !GITCMD! push +echo !msg! +echo !msg!>>!joblog! + +@rem ############################## +@rem ### g i t p u s h ### +@rem ############################## +!GITCMD! push 1>!tmplog! 2>&1 +set elevel=!errorlevel! +echo elevel=!elevel! +echo elevel=!elevel!>>!joblog! +type !tmplog! >>!joblog! +if !elevel! GTR 0 ( + echo ERROR OCCURED. Check log: + echo ------------------------- + type !tmplog! + echo ------------------------- + del !tmplog! + goto :final +) + +del !tmplog! 2>nul + +echo ------ OVERALL LOG: -------- +type !joblog! +echo ---------------------------- + +set msg=!date! !time! Check result of commits here: +echo !msg! +!GITCMD! config --get remote.origin.url +( + echo !msg! + !GITCMD! config --get remote.origin.url +) >> !joblog! + +@rem ####################################### +@rem ### s e n d i n g m a i l ### +@rem ####################################### +call %~dp0qa-sendmail.bat "!git_text!" !joblog! + +@rem https://github.com/FirebirdSQL/fbt-repository.git + +goto final + +:syntax + echo. + echo Syntax: + echo 1. %~f0 ^ + echo. + echo. Commit will be done with comment: + echo. !git_text! + echo. + echo. + echo 2. %~f0 ^ some very clever comment here + echo. + echo. Commit will be done with comment: + echo. some very clever comment here + echo. + pause + goto final +:final + echo Bye-bye from %~f0 + diff --git a/pyproject.toml b/pyproject.toml index 0288846a..5a568146 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ classifiers = [ "Framework :: Pytest", ] dependencies = [ - "firebird-base>=1.8.0", + "firebird-base~=1.8", "firebird-driver~=1.10", "pytest>=7.4", "psutil~=5.9", @@ -64,6 +64,11 @@ packages = ["src/firebird"] dependencies = [ ] +[tool.hatch.envs.hatch-test] +extra-args = ["-vv", "--server", "local"] +dependencies = [ +] + [tool.hatch.envs.test] dependencies = [ "coverage[toml]>=6.5", @@ -101,63 +106,13 @@ docset = [ "cd docs; VERSION=`hatch version` ; tar --exclude='.DS_Store' -cvzf ../dist/firebird-qa-$VERSION-docset.tgz firebird-qa.docset", ] -[tool.hatch.envs.lint] -detached = true -dependencies = [ - "black>=23.1.0", - "mypy>=1.0.0", - "ruff>=0.0.243", -] -[tool.hatch.envs.lint.scripts] -typing = "mypy --install-types --non-interactive {args:src/firebird/qa tests}" -style = [ - "ruff {args:.}", - "black --check --diff {args:.}", -] -fmt = [ - "black {args:.}", - "ruff --fix {args:.}", - "style", -] -all = [ - "style", - "typing", -] - -[tool.black] -target-version = ["py38"] -line-length = 120 -skip-string-normalization = true - [tool.ruff] -target-version = "py38" +target-version = "py311" line-length = 120 -select = [ - "A", - "ARG", - "B", - "C", - "DTZ", - "E", - "EM", - "F", - "FBT", - "I", - "ICN", - "ISC", - "N", - "PLC", - "PLE", - "PLR", - "PLW", - "Q", - "RUF", - "S", - "T", - "TID", - "UP", - "W", - "YTT", + +[tool.ruff.lint] +select = ["A", "ARG", "B", "C", "DTZ", "E", "EM", "F", "FBT", "I", "ICN", "ISC", "N", + "PLC", "PLE", "PLR", "PLW", "Q", "RUF", "S", "T", "TID", "UP", "W", "YTT", ] ignore = [ # Allow non-abstract empty methods in abstract base classes @@ -168,24 +123,35 @@ ignore = [ "S105", "S106", "S107", # Ignore complexity "C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915", + # + "E741", + # Allow relative imports + "TID252", + # Allow literals in exceptions + "EM101", "EM102", + # Single quotes instead double + "Q000" ] unfixable = [ # Don't touch unused imports "F401", + # Don't change single quotes to double + "Q000" ] +exclude = ["*_pb2.py", "*.pyi", "tests/*", "docs/*", "personal/*"] -[tool.ruff.isort] -known-first-party = ["firebird.qa"] +[tool.ruff.lint.isort] +known-first-party = ["firebird.base"] -[tool.ruff.flake8-tidy-imports] +[tool.ruff.lint.flake8-tidy-imports] ban-relative-imports = "all" -[tool.ruff.per-file-ignores] +[tool.ruff.lint.extend-per-file-ignores] # Tests can use magic values, assertions, and relative imports "tests/**/*" = ["PLR2004", "S101", "TID252"] [tool.coverage.run] -source_pkgs = ["firebird.qa", "tests"] +source_pkgs = ["firebird.qa"] branch = true parallel = true omit = [ @@ -193,8 +159,8 @@ omit = [ ] [tool.coverage.paths] -firebird_qa = ["src/python", "*/python/src/firebird/qa"] -tests = ["tests", "*/python/tests"] +firebird_qa = ["*/src/firebird/qa"] +tests = ["tests"] [tool.coverage.report] exclude_lines = [ diff --git a/pytest.ini b/pytest.ini index 3c98979a..9da79511 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,6 +3,10 @@ console_output_style = count # testpaths = tests # addopts = --server local --install-terminal markers = + intl: mark a test as dealing with non-ascii characters + scroll_cur: mark a test as dealing with scrollable cursor + es_eds: mark a test as dealing with ES/EDS mechanism + trace: mark a test as dealing with trace encryption: mark a test as dealing with encryption plugin replication: mark a test as dealing with database participating in replication disabled_in_forks: mark a test which must be SKIPPED when running QA against any FB forks (because of missed fix/feature etc). \ No newline at end of file diff --git a/src/firebird/qa/__about__.py b/src/firebird/qa/__about__.py index bf7b2388..2ec32406 100644 --- a/src/firebird/qa/__about__.py +++ b/src/firebird/qa/__about__.py @@ -1,4 +1,4 @@ # SPDX-FileCopyrightText: 2021-present The Firebird Projects # # SPDX-License-Identifier: MIT -__version__ = "0.20.0" +__version__ = "0.20.2" diff --git a/src/firebird/qa/__init__.py b/src/firebird/qa/__init__.py index 6d2af991..4eec60b6 100644 --- a/src/firebird/qa/__init__.py +++ b/src/firebird/qa/__init__.py @@ -40,4 +40,4 @@ from .plugin import db_factory, Database, user_factory, User, isql_act, python_act, Action, \ temp_file, temp_files, role_factory, Role, envar_factory, Envar, Mapping, mapping_factory, \ - ServerKeeper, ExecutionError, QA_GLOBALS, existing_db_factory + ServerKeeper, ExecutionError, QA_GLOBALS, existing_db_factory, ConfigManager diff --git a/src/firebird/qa/plugin.py b/src/firebird/qa/plugin.py index 9b0d0f54..cea31bf8 100644 --- a/src/firebird/qa/plugin.py +++ b/src/firebird/qa/plugin.py @@ -61,6 +61,7 @@ from packaging.version import parse import time from datetime import datetime +from dataclasses import dataclass from threading import Thread, Barrier, Event from firebird.driver import connect, connect_server, create_database, driver_config, \ NetProtocol, Server, CHARSET_MAP, Connection, Cursor, \ @@ -827,7 +828,7 @@ def drop(self) -> None: """ __tracebackhide__ = True - with connect_server(_vars_['server']) as srv: + with connect_server(_vars_['server'], encoding='utf8' if self.utf8filename else None) as srv: srv.database.no_linger(database=self.db_path) self._make_config() with connect(self.config_name) as db: @@ -880,12 +881,12 @@ def connect(self, *, user: Optional[str]=None, password: Optional[str]=None, def set_async_write(self) -> None: "Set the database to `async write` mode." __tracebackhide__ = True - with connect_server(_vars_['server']) as srv: + with connect_server(_vars_['server'], encoding='utf8' if self.utf8filename else None) as srv: srv.database.set_write_mode(database=self.db_path, mode=DbWriteMode.ASYNC) def set_sync_write(self) -> None: "Set the database to `sync write` mode." __tracebackhide__ = True - with connect_server(_vars_['server']) as srv: + with connect_server(_vars_['server'], encoding='utf8' if self.utf8filename else None) as srv: srv.database.set_write_mode(database=self.db_path, mode=DbWriteMode.SYNC) def existing_db_factory(*, filename: str='test.fdb', charset: Optional[str]=None, @@ -1265,17 +1266,24 @@ def trace_thread(act: Action, b: Barrier, cfg: List[str], output: List[str], kee role: User role stop: Event used to stop the trace thread """ - with act.connect_server(encoding=encoding, encoding_errors=encoding_errors, - user=user, password=password) as srv: - output.append(srv.trace.start(config='\n'.join(cfg))) - b.wait() - while not stop.is_set(): - line = srv.readline_timed(1) - if line is not TIMEOUT: - if not line: - stop.set() - elif keep_log: - output.append(line) + connected = False + try: + with act.connect_server(encoding=encoding, encoding_errors=encoding_errors, + user=user, password=password) as srv: + output.append(srv.trace.start(config='\n'.join(cfg))) + b.wait() + connected = True + while not stop.is_set(): + line = srv.readline_timed(1) + if line is not TIMEOUT: + if not line: + stop.set() + elif keep_log: + output.append(line) + except: + if not connected: + b.wait() + raise class TraceSession: """Object to manage Firebird trace session. @@ -1340,12 +1348,10 @@ def __enter__(self) -> TraceSession: def __exit__(self, exc_type, exc_value, traceback) -> None: time.sleep(2) session = self.output.pop(0) - with self.act.connect_server() as srv: - srv.trace.stop(session_id=session) - self.stop_event.set() - self.trace_thread.join(5.0) - if self.trace_thread.is_alive(): - pytest.fail('Trace thread still alive') + self.stop_event.set() + self.trace_thread.join(5.0) + if self.trace_thread.is_alive(): + pytest.fail('Trace thread still alive') self.act.trace_log = self.output class ServerKeeper: @@ -2746,3 +2752,76 @@ def temp_files_fixture(tmp_path) -> List[Path]: return temp_files_fixture +@dataclass +class ConfigManagerBackup: + action: str + config_file: Path + backup_file: Path = None + +class ConfigManager: + """Object to replace specified server configuration file. + + Arguments: + tmp_path: Path to directory where backup will be stored. + old_config: Old config file which will be keeped in backup (e.g. databases.conf) + + .. important:: + + Do not create instances of this class directly! Use **only** fixtures created by `store_config`. + """ + + def __init__(self, tmp_path: Path): + self.__tmp_path = tmp_path + self.__bak_configs: Dict[str, ConfigManagerBackup] = {} + + def _backup(self, config_name: str): + old_config = _vars_['home-dir'] / config_name + if config_name in self.__bak_configs: + return old_config + if old_config.exists(): + backup = self.__tmp_path / (config_name.replace('/', '_') + '.bak') + if backup.exists(): + backup.unlink() + shutil.copy(str(old_config), str(backup)) + self.__bak_configs[config_name] = ConfigManagerBackup('replace', old_config, backup) + else: + self.__bak_configs[config_name] = ConfigManagerBackup('delete', old_config) + return old_config + + def replace(self, config_name: str, new_config: Union[Path, str]): + """ + config_name: Relative path to config in server + new_config: Path to new config or content of config + """ + old_config = self._backup(config_name) + if isinstance(new_config, Path): + shutil.copy(str(new_config), str(old_config)) + else: + with open(old_config, 'w') as old: + old.write(new_config) + + def add(self, config_name: str, new_config: Union[Path, str]): + """ + config_name: Relative path to config in server + new_config: Path to new config or content of config + """ + old_config = self._backup(config_name) + new_content = new_config.read_text() if isinstance(new_config, Path) else new_config + with open(old_config, 'a') as old: + old.write(new_content) + + def restore(self, final=False): + for backup in self.__bak_configs.values(): + if backup.action == 'replace': + shutil.copy(str(backup.backup_file), str(backup.config_file)) + if final: + backup.backup_file.unlink() + elif backup.action == 'delete': + backup.config_file.unlink() + self.__bak_configs = {} + +@pytest.fixture +def store_config(db_path) -> ConfigManager: + manager = ConfigManager(db_path) + yield manager + manager.restore(final=True) diff --git a/tests/bugs/core_0053_test.py b/tests/bugs/core_0053_test.py index 2c981626..3a843dc2 100644 --- a/tests/bugs/core_0053_test.py +++ b/tests/bugs/core_0053_test.py @@ -25,6 +25,13 @@ reset like this: c = gen_id(g_gather_stat, -gen_id(g_gather_stat, 0)); JIRA: CORE-53 FBTEST: bugs.core_0053 +NOTES: + [22.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 3.0.13.33813. """ import pytest @@ -32,6 +39,16 @@ db = db_factory(from_backup='mon-stat-gathering-2_5.fbk') +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [ ('line: \\d+, col: \\d++', '') ] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + test_script = """ set list on; @@ -118,7 +135,7 @@ -- on 2.5 = {5, 5}, on 3.0 = {5, 3} ==> ratio 3.00 should be always enough. """ -act = isql_act('db', test_script) +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ PLAN (T ORDER TEST_F1_F2) diff --git a/tests/bugs/core_0058_test.py b/tests/bugs/core_0058_test.py index 69ddc6d2..878353e9 100644 --- a/tests/bugs/core_0058_test.py +++ b/tests/bugs/core_0058_test.py @@ -5,15 +5,27 @@ ISSUE: 383 TITLE: WHERE CURRENT OF doesn't work DESCRIPTION: -JIRA: CORE-58 -FBTEST: bugs.core_0058 +NOTES: + [13.06.2025] pzotov + 1. Increased the 'subsitutions' list to suppress "PUBLIC" schema prefix and remove single/double quotes from object names. Need since 6.0.0.834. + ::: NB ::: + File act.files_dir/'test_config.ini' must contain section: + [schema_n_quotes_suppress] + addi_subst="PUBLIC". " ' + (thi file is used in qa/plugin.py, see QA_GLOBALS dictionary). + + Value of parameter 'addi_subst' is splitted on tokens using space character and we add every token to 'substitutions' list which + eventually will be like this: + substitutions = [ ( , ('"PUBLIC".', ''), ('"', ''), ("'", '') ] + + 2. Adjusted expected output: removed single quotes from DB object name(s). + + Checked on 6.0.0.835; 5.0.3.1661; 4.0.6.3207; 3.0.13.33807. """ import pytest from firebird.qa import * -substitutions = [('line: [0-9]+, col: [0-9]+', '')] - db = db_factory() test_script = """ @@ -49,12 +61,22 @@ execute procedure test_upd (2); """ +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [ ('line: \\d+, col: \\d+', '') ] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + act = isql_act('db', test_script, substitutions=substitutions) expected_stderr = """ Statement failed, SQLSTATE = 22000 no current record for fetch operation - -At procedure 'TEST_UPD' + -At procedure TEST_UPD """ @pytest.mark.version('>=3') diff --git a/tests/bugs/core_0059_test.py b/tests/bugs/core_0059_test.py index ba86d9b6..c90bb922 100644 --- a/tests/bugs/core_0059_test.py +++ b/tests/bugs/core_0059_test.py @@ -7,6 +7,13 @@ DESCRIPTION: JIRA: CORE-59 FBTEST: bugs.core_0059 +NOTES: + [22.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -23,7 +30,17 @@ insert into test default values; """ -act = isql_act('db', test_script) +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [ ('line: \\d+, col: \\d+', '') ] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + +act = isql_act('db', test_script, substitutions = substitutions) expected_stderr = """ Statement failed, SQLSTATE = 23000 diff --git a/tests/bugs/core_0070_test.py b/tests/bugs/core_0070_test.py index ede9ff92..0ddd9651 100644 --- a/tests/bugs/core_0070_test.py +++ b/tests/bugs/core_0070_test.py @@ -7,6 +7,13 @@ DESCRIPTION: JIRA: CORE-70 FBTEST: bugs.core_0070 +NOTES: + [22.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -26,7 +33,17 @@ select * from t1 where upper(col1) = '1'; """ -act = isql_act('db', test_script) +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [ ('line: \\d+, col: \\d+', '') ] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ PLAN (T1 INDEX (IDX1)) diff --git a/tests/bugs/core_0088_test.py b/tests/bugs/core_0088_test.py index 7489becf..f698a43f 100644 --- a/tests/bugs/core_0088_test.py +++ b/tests/bugs/core_0088_test.py @@ -2,79 +2,73 @@ """ ID: issue-413 -ISSUE: 413 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/413 TITLE: Join on diffrent datatypes DESCRIPTION: JIRA: CORE-88 FBTEST: bugs.core_0088 +NOTES: + [23.03.2025] pzotov + Removed PLAN output because it differs on 6.x vs previous versions since commit fc12c0ef39 + ("Unnest IN/ANY/EXISTS subqueries and optimize them using semi-join algorithm (#8061)"). + This test must check only returned data. """ import pytest from firebird.qa import * -init_script = """CREATE TABLE TEST_A ( - ID INTEGER NOT NULL PRIMARY KEY, - SNUM CHAR(10) UNIQUE -); - -CREATE TABLE TEST_B ( - ID INTEGER NOT NULL PRIMARY KEY, - NUM NUMERIC(15,2) UNIQUE -); - -commit; - -INSERT INTO TEST_A (ID, SNUM) VALUES (1, '01'); -INSERT INTO TEST_A (ID, SNUM) VALUES (2, '02'); -INSERT INTO TEST_A (ID, SNUM) VALUES (3, '03'); -INSERT INTO TEST_A (ID, SNUM) VALUES (5, '05'); - -commit; - -INSERT INTO TEST_B (ID, NUM) VALUES (1, 1); -INSERT INTO TEST_B (ID, NUM) VALUES (2, 2); -INSERT INTO TEST_B (ID, NUM) VALUES (3, 3); -INSERT INTO TEST_B (ID, NUM) VALUES (4, 4); - -commit; +init_script = """ + recreate table test1 ( + id integer not null primary key, + snum char(10) unique using index test1_snum_unq + ); + + recreate table test2 ( + id integer not null primary key, + inum numeric(15,2) unique using index test2_inum_unq + ); + commit; + + insert into test1 (id, snum) values (1, '01'); + insert into test1 (id, snum) values (2, '02'); + insert into test1 (id, snum) values (3, '03'); + insert into test1 (id, snum) values (5, '05'); + commit; + + insert into test2 (id, inum) values (1, 1); + insert into test2 (id, inum) values (2, 2); + insert into test2 (id, inum) values (3, 3); + insert into test2 (id, inum) values (4, 4); + commit; """ db = db_factory(init=init_script) -test_script = """SET PLAN ON; - -SELECT * FROM test_b WHERE num NOT IN (SELECT snum FROM test_a) ; - -SELECT * FROM test_b WHERE num IN (SELECT snum FROM test_a) ; - +test_script = """ + set list on; + set count on; + select * from test2 where inum not in (select snum from test1) order by id; + select * from test2 where inum in (select snum from test1) order by id; """ act = isql_act('db', test_script) -expected_stdout = """ -PLAN (TEST_A NATURAL) -PLAN (TEST_A NATURAL) -PLAN (TEST_B NATURAL) - - ID NUM -============ ===================== - 4 4.00 - - -PLAN (TEST_A NATURAL) -PLAN (TEST_B NATURAL) - - ID NUM -============ ===================== - 1 1.00 - 2 2.00 - 3 3.00 - -""" - @pytest.mark.version('>=3') def test_1(act: Action): + expected_stdout = """ + ID 4 + INUM 4.00 + Records affected: 1 + + ID 1 + INUM 1.00 + ID 2 + INUM 2.00 + ID 3 + INUM 3.00 + Records affected: 3 + """ + act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_0104_test.py b/tests/bugs/core_0104_test.py index b5f17378..7c54a729 100644 --- a/tests/bugs/core_0104_test.py +++ b/tests/bugs/core_0104_test.py @@ -7,6 +7,13 @@ DESCRIPTION: JIRA: CORE-104 FBTEST: bugs.core_0104 +NOTES: + [22.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -31,7 +38,17 @@ commit; """ -act = isql_act('db', test_script) +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [ ('line: \\d+, col: \\d+', ''), ('INTEG_\\d+', 'INTEG') ] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + +act = isql_act('db', test_script, substitutions = substitutions) expected_stderr = """Statement failed, SQLSTATE = 23000 violation of PRIMARY or UNIQUE KEY constraint "INTEG_4" on table "TEST" diff --git a/tests/bugs/core_0115_test.py b/tests/bugs/core_0115_test.py index ca49dfd4..a7b55895 100644 --- a/tests/bugs/core_0115_test.py +++ b/tests/bugs/core_0115_test.py @@ -7,6 +7,13 @@ DESCRIPTION: JIRA: CORE-115 FBTEST: bugs.core_0115 +NOTES: + [22.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -40,7 +47,17 @@ set count off; """ -act = isql_act('db', test_script) +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [ ('line: \\d+, col: \\d+', '') ] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ PLAN (TEST NATURAL) diff --git a/tests/bugs/core_0116_test.py b/tests/bugs/core_0116_test.py index c004e40c..9232b4e1 100644 --- a/tests/bugs/core_0116_test.py +++ b/tests/bugs/core_0116_test.py @@ -7,6 +7,13 @@ DESCRIPTION: JIRA: CORE-116 FBTEST: bugs.core_0116 +NOTES: + [22.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -22,7 +29,17 @@ create table ext_log external file '$(DATABASE_LOCATION)z.dat' (F1 INT, F2 BLOB SUB_TYPE TEXT); """ -act = isql_act('db', test_script) +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [ ('line: \\d+, col: \\d+', '') ] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + +act = isql_act('db', test_script, substitutions = substitutions) expected_stderr = """ Statement failed, SQLSTATE = HY004 diff --git a/tests/bugs/core_0166_test.py b/tests/bugs/core_0166_test.py index fa3c5eab..76263f5c 100644 --- a/tests/bugs/core_0166_test.py +++ b/tests/bugs/core_0166_test.py @@ -7,6 +7,13 @@ DESCRIPTION: JIRA: CORE-166 FBTEST: bugs.core_0166 +NOTES: + [22.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -58,7 +65,17 @@ from company c order by c.id; """ -act = isql_act('db', test_script) +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [ ('line: \\d+, col: \\d+', '') ] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ ID 100 diff --git a/tests/bugs/core_0196_test.py b/tests/bugs/core_0196_test.py index 6f0b9e8f..84cb60f0 100644 --- a/tests/bugs/core_0196_test.py +++ b/tests/bugs/core_0196_test.py @@ -7,6 +7,13 @@ DESCRIPTION: JIRA: CORE-196 FBTEST: bugs.core_0190 +NOTES: + [22.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -25,7 +32,17 @@ substitutions = [('Statement failed, SQLSTATE = HY000', ''), ('record not found for user:.*', '')] -act = isql_act('db', test_script, substitutions=substitutions) +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [ ('line: \\d+, col: \\d+', '') ] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ Records affected: 0 diff --git a/tests/bugs/core_0203_test.py b/tests/bugs/core_0203_test.py index 343454aa..fcc9b6e0 100644 --- a/tests/bugs/core_0203_test.py +++ b/tests/bugs/core_0203_test.py @@ -5,33 +5,32 @@ ISSUE: 530 TITLE: CREATE VIEW ignores PLAN DESCRIPTION: -Test verifies that: -1. "PLAN <...>" clause inside view DLL is always ignored and actual plan will be one of following: - 1.1. That is specified explicitly by client who runs a query to this view; - 1.2. If no explicitly specified plan that optimizer will be allowed to choose that. -2. One may to specify PLAN on client side and it *WILL* be taken in account. -NOTES: -Suppose that some view contains explicitly specified PLAN NATURAL it its DDL. -If underlying query became suitable to be run with PLAN INDEX (e.g. such index was added to the table) -then this 'PLAN NATURAL' will be IGNORED until it is explicitly specified in the client query. -See below example #4 for view v_test1 defined as "select * from ... plan (t natural)". + Test verifies that: + 1. "PLAN <...>" clause inside view DLL is always ignored and actual plan will be one of following: + 1.1. That is specified explicitly by client who runs a query to this view; + 1.2. If no explicitly specified plan that optimizer will be allowed to choose that. + 2. One may to specify PLAN on client side and it *WILL* be taken in account. + + It is supposed that some view contains explicitly specified PLAN NATURAL it its DDL. + If underlying query became suitable to be run with PLAN INDEX (e.g. such index was added to the table) + then this 'PLAN NATURAL' will be IGNORED until it is explicitly specified in the client query. + See below example #4 for view v_test1 defined as "select * from ... plan (t natural)". JIRA: CORE-203 FBTEST: bugs.core_0203 +NOTES: + [03.07.2025] pzotov + Re-implemented: use explained form of plans for check. + Output is organized to be more suitable for reading and search for mismatches (see 'qry_map' dict). + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.892; 5.0.3.1668; 4.0.6.3214. """ import pytest from firebird.qa import * -db = db_factory() - -test_script = """ +init_script = """ set bail on; - - recreate view v_test4 as select 1 x from rdb$database; - recreate view v_test3 as select 1 x from rdb$database; - recreate view v_test2 as select 1 x from rdb$database; - recreate view v_test1 as select 1 x from rdb$database; - recreate table test(x int, y int); commit; @@ -54,92 +53,424 @@ recreate view v_test3 as select * from test t where x = 0 plan (t index(test_x_desc)); recreate view v_test4 as select * from v_test3; commit; - - - set planonly; - --set echo on; - - select * from test t where x = 0 plan (t natural); -- 1 - - select * from v_test1 v1; -- 2 - - select * from v_test1 v2; -- 3 - - select * from v_test1 v1 where v1.x = 0 plan (v1 natural); -- 4 - - select * from v_test2 v2 where v2.x = 0 plan (v2 natural); -- 5 - - select * from v_test1 v1 where v1.x = 0 PLAN (V1 INDEX (TEST_X_DESC)) ; -- 6 - - select * from v_test2 v2 where v2.x = 0 PLAN (V2 INDEX (TEST_X_DESC)) ; -- 7 - - select * from v_test1 v1 where v1.x = 50 and v1.y = 5000 PLAN (V1 INDEX (test_x_y)) ; -- 8 - - select * from v_test1 v2 where v2.x = 50 and v2.y = 5000 PLAN (V2 INDEX (test_y_x)) ; -- 9 - - select * from v_test1 v1 where v1.x + v1.y = 1000 PLAN (V1 INDEX (test_x_y)); -- 10 - - select * from v_test2 v2 where v2.x - v2.y = 1000 PLAN (V2 INDEX (test_x_y)); -- 11 - - select * from v_test1 v1 where v1.x + v1.y = 1000 PLAN (V1 INDEX (test_sum_x_y)); -- 12 - - select * from v_test2 v2 where v2.x - v2.y = 1000 PLAN (V2 INDEX (test_sub_x_y)); -- 13 - - -- NB: here optimizer will use index __NOT__ from view V3 DDL: - -- PLAN (V3 T INDEX (TEST_X_ASC)) - select * from v_test3 v3; -- 14 - - select * from v_test3 v3 plan ( v3 index(test_x_y) ); - - -- NB: here optimizer will use index __NOT__ from view V3 DDL: - -- PLAN (V4 V_TEST3 T INDEX (TEST_X_ASC)) - select * from v_test4 v4; -- 15 - - select * from v_test4 v4 PLAN (V4 V_TEST3 T INDEX (TEST_X_Y)); -- 16 """ -act = isql_act('db', test_script, substitutions=[('[ ]+', ' ')]) - -expected_stdout = """ - PLAN (T NATURAL) - - PLAN (V1 T INDEX (TEST_X_ASC)) - - PLAN (V2 T INDEX (TEST_X_ASC)) - - PLAN (V1 T NATURAL) - - PLAN (V2 T NATURAL) - PLAN (V1 T INDEX (TEST_X_DESC)) +db = db_factory(init = init_script) - PLAN (V2 T INDEX (TEST_X_DESC)) +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', substitutions = substitutions) - PLAN (V1 T INDEX (TEST_X_Y)) +#----------------------------------------------------------- - PLAN (V2 T INDEX (TEST_Y_X)) +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped - PLAN (V1 T INDEX (TEST_X_Y)) - - PLAN (V2 T INDEX (TEST_X_Y)) - - PLAN (V1 T INDEX (TEST_SUM_X_Y)) - - PLAN (V2 T INDEX (TEST_SUB_X_Y)) - - PLAN (V3 T INDEX (TEST_X_ASC)) - - PLAN (V3 T INDEX (TEST_X_Y)) - - PLAN (V4 V_TEST3 T INDEX (TEST_X_ASC)) - - PLAN (V4 V_TEST3 T INDEX (TEST_X_Y)) - -""" +#----------------------------------------------------------- @pytest.mark.version('>=3') -def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() +def test_1(act: Action, capsys): + + qry_map = { + 1 : ( 'select * from test t where x = 0 plan (t natural)', '' ) + , 2 : ( 'select * from v_test1 v1', '' ) + , 3 : ( 'select * from v_test1 v2', '' ) + , 4 : ( 'select * from v_test1 v1 where v1.x = 0 plan (v1 natural)', '' ) + , 5 : ( 'select * from v_test2 v2 where v2.x = 0 plan (v2 natural)', '' ) + , 6 : ( 'select * from v_test1 v1 where v1.x = 0 PLAN (V1 INDEX (TEST_X_DESC))', '' ) + , 7 : ( 'select * from v_test2 v2 where v2.x = 0 PLAN (V2 INDEX (TEST_X_DESC))', '' ) + , 8 : ( 'select * from v_test1 v1 where v1.x = 50 and v1.y = 5000 PLAN (V1 INDEX (test_x_y))', '' ) + , 9 : ( 'select * from v_test1 v2 where v2.x = 50 and v2.y = 5000 PLAN (V2 INDEX (test_y_x))', '' ) + ,10 : ( 'select * from v_test1 v1 where v1.x + v1.y = 1000 PLAN (V1 INDEX (test_x_y))', '' ) + ,11 : ( 'select * from v_test2 v2 where v2.x - v2.y = 1000 PLAN (V2 INDEX (test_x_y))', '' ) + ,12 : ( 'select * from v_test1 v1 where v1.x + v1.y = 1000 PLAN (V1 INDEX (test_sum_x_y))', '' ) + ,13 : ( 'select * from v_test2 v2 where v2.x - v2.y = 1000 PLAN (V2 INDEX (test_sub_x_y))', '' ) + ,14 : ( 'select * from v_test3 v3', 'must use index TEST_X_ASC' ) + ,15 : ( 'select * from v_test3 v3 plan ( v3 index(test_x_y) )', '' ) + ,16 : ( 'select * from v_test4 v4', 'must use index TEST_X_ASC' ) + ,17 : ( 'select * from v_test4 v4 PLAN (V4 V_TEST3 T INDEX (TEST_X_Y))', '' ) + } + for qry_idx,v in qry_map.items(): + qry_comment = f'{qry_idx=} ' + v[1] + qry_map[qry_idx] = (v[0], qry_comment) + + + with act.db.connect() as con: + cur = con.cursor() + for qry_idx, qry_data in qry_map.items(): + test_sql, qry_comment = qry_data[:2] + ps, rs = None, None + try: + cur = con.cursor() + ps = cur.prepare(test_sql) + + print(qry_comment) + # Print explained plan with padding eash line by dots in order to see indentations: + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + except DatabaseError as e: + print(e.__str__()) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() + + + expected_stdout_3x = f""" + {qry_map[ 1][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "T" Full Scan + + {qry_map[ 2][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V1 T" Access By ID + ............-> Bitmap + ................-> Index "TEST_X_ASC" Range Scan (full match) + + {qry_map[ 3][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V2 T" Access By ID + ............-> Bitmap + ................-> Index "TEST_X_ASC" Range Scan (full match) + + {qry_map[ 4][1]} + Select Expression + ....-> Filter + ........-> Filter + ............-> Table "TEST" as "V1 T" Full Scan + + {qry_map[ 5][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V2 T" Full Scan + + {qry_map[ 6][1]} + Select Expression + ....-> Filter + ........-> Filter + ............-> Table "TEST" as "V1 T" Access By ID + ................-> Bitmap + ....................-> Index "TEST_X_DESC" Range Scan (full match) + + {qry_map[ 7][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V2 T" Access By ID + ............-> Bitmap + ................-> Index "TEST_X_DESC" Range Scan (full match) + + {qry_map[ 8][1]} + Select Expression + ....-> Filter + ........-> Filter + ............-> Table "TEST" as "V1 T" Access By ID + ................-> Bitmap + ....................-> Index "TEST_X_Y" Range Scan (full match) + + {qry_map[ 9][1]} + Select Expression + ....-> Filter + ........-> Filter + ............-> Table "TEST" as "V2 T" Access By ID + ................-> Bitmap + ....................-> Index "TEST_Y_X" Range Scan (full match) + + {qry_map[10][1]} + Select Expression + ....-> Filter + ........-> Filter + ............-> Table "TEST" as "V1 T" Access By ID + ................-> Bitmap + ....................-> Index "TEST_X_Y" Range Scan (partial match: 1/2) + + {qry_map[11][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V2 T" Access By ID + ............-> Bitmap + ................-> Index "TEST_X_Y" Range Scan (partial match: 1/2) + + {qry_map[12][1]} + Select Expression + ....-> Filter + ........-> Filter + ............-> Table "TEST" as "V1 T" Access By ID + ................-> Bitmap + ....................-> Index "TEST_SUM_X_Y" Range Scan (full match) + + {qry_map[13][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V2 T" Access By ID + ............-> Bitmap + ................-> Index "TEST_SUB_X_Y" Range Scan (full match) + + {qry_map[14][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V3 T" Access By ID + ............-> Bitmap + ................-> Index "TEST_X_ASC" Range Scan (full match) + + {qry_map[15][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V3 T" Access By ID + ............-> Bitmap + ................-> Index "TEST_X_Y" Range Scan (partial match: 1/2) + + {qry_map[16][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V4 V_TEST3 T" Access By ID + ............-> Bitmap + ................-> Index "TEST_X_ASC" Range Scan (full match) + + {qry_map[17][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V4 V_TEST3 T" Access By ID + ............-> Bitmap + ................-> Index "TEST_X_Y" Range Scan (partial match: 1/2) + """ + + expected_stdout_5x = f""" + {qry_map[ 1][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "T" Full Scan + + {qry_map[ 2][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V1 T" Access By ID + ............-> Bitmap + ................-> Index "TEST_X_ASC" Range Scan (full match) + + {qry_map[ 3][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V2 T" Access By ID + ............-> Bitmap + ................-> Index "TEST_X_ASC" Range Scan (full match) + + {qry_map[ 4][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V1 T" Full Scan + + {qry_map[ 5][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V2 T" Full Scan + + {qry_map[ 6][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V1 T" Access By ID + ............-> Bitmap + ................-> Index "TEST_X_DESC" Range Scan (full match) + + {qry_map[ 7][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V2 T" Access By ID + ............-> Bitmap + ................-> Index "TEST_X_DESC" Range Scan (full match) + + {qry_map[ 8][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V1 T" Access By ID + ............-> Bitmap + ................-> Index "TEST_X_Y" Range Scan (full match) + + {qry_map[ 9][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V2 T" Access By ID + ............-> Bitmap + ................-> Index "TEST_Y_X" Range Scan (full match) + + {qry_map[10][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V1 T" Access By ID + ............-> Bitmap + ................-> Index "TEST_X_Y" Range Scan (partial match: 1/2) + + {qry_map[11][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V2 T" Access By ID + ............-> Bitmap + ................-> Index "TEST_X_Y" Range Scan (partial match: 1/2) + + {qry_map[12][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V1 T" Access By ID + ............-> Bitmap + ................-> Index "TEST_SUM_X_Y" Range Scan (full match) + + {qry_map[13][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V2 T" Access By ID + ............-> Bitmap + ................-> Index "TEST_SUB_X_Y" Range Scan (full match) + + {qry_map[14][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V3 T" Access By ID + ............-> Bitmap + ................-> Index "TEST_X_ASC" Range Scan (full match) + + {qry_map[15][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V3 T" Access By ID + ............-> Bitmap + ................-> Index "TEST_X_Y" Range Scan (partial match: 1/2) + + {qry_map[16][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V4 V_TEST3 T" Access By ID + ............-> Bitmap + ................-> Index "TEST_X_ASC" Range Scan (full match) + + {qry_map[17][1]} + Select Expression + ....-> Filter + ........-> Table "TEST" as "V4 V_TEST3 T" Access By ID + ............-> Bitmap + ................-> Index "TEST_X_Y" Range Scan (partial match: 1/2) + + """ + + expected_stdout_6x = f""" + {qry_map[ 1][1]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST" as "T" Full Scan + + {qry_map[ 2][1]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST" as "V1" "T" Access By ID + ............-> Bitmap + ................-> Index "PUBLIC"."TEST_X_ASC" Range Scan (full match) + + {qry_map[ 3][1]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST" as "V2" "T" Access By ID + ............-> Bitmap + ................-> Index "PUBLIC"."TEST_X_ASC" Range Scan (full match) + + {qry_map[ 4][1]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST" as "V1" "T" Full Scan + + {qry_map[ 5][1]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST" as "V2" "T" Full Scan + + {qry_map[ 6][1]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST" as "V1" "T" Access By ID + ............-> Bitmap + ................-> Index "PUBLIC"."TEST_X_DESC" Range Scan (full match) + + {qry_map[ 7][1]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST" as "V2" "T" Access By ID + ............-> Bitmap + ................-> Index "PUBLIC"."TEST_X_DESC" Range Scan (full match) + + {qry_map[ 8][1]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST" as "V1" "T" Access By ID + ............-> Bitmap + ................-> Index "PUBLIC"."TEST_X_Y" Range Scan (full match) + + {qry_map[ 9][1]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST" as "V2" "T" Access By ID + ............-> Bitmap + ................-> Index "PUBLIC"."TEST_Y_X" Range Scan (full match) + + {qry_map[10][1]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST" as "V1" "T" Access By ID + ............-> Bitmap + ................-> Index "PUBLIC"."TEST_X_Y" Range Scan (partial match: 1/2) + + {qry_map[11][1]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST" as "V2" "T" Access By ID + ............-> Bitmap + ................-> Index "PUBLIC"."TEST_X_Y" Range Scan (partial match: 1/2) + + {qry_map[12][1]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST" as "V1" "T" Access By ID + ............-> Bitmap + ................-> Index "PUBLIC"."TEST_SUM_X_Y" Range Scan (full match) + + {qry_map[13][1]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST" as "V2" "T" Access By ID + ............-> Bitmap + ................-> Index "PUBLIC"."TEST_SUB_X_Y" Range Scan (full match) + + {qry_map[14][1]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST" as "V3" "T" Access By ID + ............-> Bitmap + ................-> Index "PUBLIC"."TEST_X_ASC" Range Scan (full match) + + {qry_map[15][1]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST" as "V3" "T" Access By ID + ............-> Bitmap + ................-> Index "PUBLIC"."TEST_X_Y" Range Scan (partial match: 1/2) + + {qry_map[16][1]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST" as "V4" "PUBLIC"."V_TEST3" "T" Access By ID + ............-> Bitmap + ................-> Index "PUBLIC"."TEST_X_ASC" Range Scan (full match) + + {qry_map[17][1]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST" as "V4" "PUBLIC"."V_TEST3" "T" Access By ID + ............-> Bitmap + ................-> Index "PUBLIC"."TEST_X_Y" Range Scan (partial match: 1/2) + """ + + act.expected_stdout = expected_stdout_3x if act.is_version('<5') else expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_0209_test.py b/tests/bugs/core_0209_test.py index 8626eb61..622b848e 100644 --- a/tests/bugs/core_0209_test.py +++ b/tests/bugs/core_0209_test.py @@ -7,6 +7,13 @@ DESCRIPTION: JIRA: CORE-209 FBTEST: bugs.core_0209 +NOTES: + [22.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -66,8 +73,20 @@ select * from test; """ -act = isql_act('db', test_script, - substitutions=[("-At trigger 'V_TEST_BIU' line.*", "-At trigger 'V_TEST_BIU' line")]) +substitutions=[("-At trigger 'V_TEST_BIU' line.*", "-At trigger 'V_TEST_BIU' line")] + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [ ('line: \\d+, col: \\d+', '') ] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + +act = isql_act('db', test_script, substitutions = substitutions) + expected_stdout = """ insert into v_test values (11, 'a'); diff --git a/tests/bugs/core_0214_test.py b/tests/bugs/core_0214_test.py index 0f7ddf5c..ef90073d 100644 --- a/tests/bugs/core_0214_test.py +++ b/tests/bugs/core_0214_test.py @@ -2,195 +2,294 @@ """ ID: issue-542 -ISSUE: 542 -TITLE: Count ( DISTINCT ... ) is too slow +ISSUE: https://github.com/FirebirdSQL/firebird/issues/542 +TITLE: Count (DISTINCT ...) is too slow DESCRIPTION: - This test does following: - 1. Creates several tables with different number of unique values in field ID. - 2. Measures for each table time for two statements: - 2.1. select count(*) from ( select distinct id from ... ) - vs - 2.2. select count(distinct id) from ... - 3. If time for 2.1 exceeds time for 2.2 more than times - output message - about possible regression. After multiple runs it was found that ratio for - 2.1 vs 2.2 is about 1.05 ... 1.10. Constant (threshold) was selected - to be enough for not to be "violated". JIRA: CORE-214 FBTEST: bugs.core_0214 +NOTES: + [29.11.2024] pzotov + 1. Fix was 31-may-2015, https://sourceforge.net/p/firebird/code/61671/ (3.0.0.31846) + Query 'select count(distinct id) from ' (hereafter "q1") elapsed time: + * before fix: ~840 ms + * after fix: ~248 ms + (and this value is close to the time for query 'select count(*) from (select distinct id from ...)', hereafter "q2") + 2. Test was fully re-implemented: we have to measure difference between cpu.user_time values instead of datediff(). + Each query for each datatype is done times, with storing cpu.user_time difference as array element. + Then we evaluate median value for this array and save it for further comparison with . + 3. It was encountered that for several datatypes ratio between CPU time for q1 and q2 remains unusually high: + * for DECFLOAT it can be about 3...4; + * for VARCHAR it depends on the column length and number of bytes in the charset: + ** for single-byte charset (e.g. win1251 etc) ratio is ~2 for field size ~100 and more than 10 for size 1k ... 4k; + ** for mylti-byte (utf8) ratio is 5...7 for field size ~100 and 35...60 for size 1k ...4k (depends on FB version) + For all other datatypes ratio is about 0.9 ... 1.2. + Time ratio for LIST(distinct ...) was also measured. Results are the same as for COUNT. + Test is considered as passed if ratios for all datatypes are less than . + 4. Because of necessity to measure ratio for datatypes that absent in FB 3.x, it was decided to increase min_version to 4.0 + (plus, there won't be any niticeable changes in FB 3.x code). + 5. A new ticket has been created to describe problem with DECFLOAT and VARCHAR datatypes: + https://github.com/FirebirdSQL/firebird/issues/8330 + (it contains two excel files with comparison for misc datatypes and different declared length of varchar column). + Test for these datatypes will be added after fix of this ticked. + + Checked on Windiws (SS/CS): 6.0.0.535; 5.0.2.1569; 4.0.6.3169. + + [03.12.2024] pzotov + Made MAX_RATIO different for Windows vs Linux. Increased its value on Linux: in some cases it can be more than 2.33 + + [18.01.2025] pzotov + Resultset of cursor that executes using instance of selectable PreparedStatement must be stored + in some variable in order to have ability close it EXPLICITLY (before PS will be freed). + Otherwise access violation raises during Python GC and pytest hangs at final point (does not return control to OS). + This occurs at least for: Python 3.11.2 / pytest: 7.4.4 / firebird.driver: 1.10.6 / Firebird.Qa: 0.19.3 + The reason of that was explained by Vlad, 26.10.24 17:42 ("oddities when use instances of selective statements"). """ - +import os +import psutil +import time import pytest from firebird.qa import * -db = db_factory() - -MAX_DIFF = 3.0 - -test_script = f""" - recreate table test1e1(id int); -- 10^1 distinct values - recreate table test1e2(id int); -- 10^2 distinct values - recreate table test1e3(id int); -- 10^3 distinct values - recreate table test1e4(id int); -- 10^4 distinct values - recreate table test1e5(id int); -- 10^5 distinct values - commit; - - create or alter view v_fill as - with recursive - r as(select 0 i from rdb$database union all select r.i+1 from r where r.i<9) - select r4.i * 10000 + r3.i * 1000 + r2.i * 100 + r1.i * 10 + r0.i as id - from r r4, r r3, r r2, r r1, r r0; - commit; - - insert into test1e1 select mod(id, 10) from v_fill; - insert into test1e2 select mod(id, 100) from v_fill; - insert into test1e3 select mod(id, 1000) from v_fill; - insert into test1e4 select mod(id, 10000) from v_fill; - insert into test1e5 select mod(id, 100000) from v_fill; +#-------------------------------------------------------------------- +def median(lst): + n = len(lst) + s = sorted(lst) + return (sum(s[n//2-1:n//2+1])/2.0, s[n//2])[n % 2] if n else None +#-------------------------------------------------------------------- + +########################### +### S E T T I N G S ### +########################### +N_MEASURES = 11 +PAGE_SIZE = 8192 +N_ROWS_CNT= 100000 +MAX_RATIO = 2.0 if os.name == 'nt' else 3.0 + +#---------------------------------------------------- +# NOT yet used. See #8330: +TXT_WIDTH_SINGLE_BYTE_ENCODING = int(PAGE_SIZE/2 + 1) +TXT_WIDTH_MULTI_BYTE_ENCODING = int(PAGE_SIZE/2 + 1) +SINGLE_BYTE_TEXT = 'u' * 4097 +MULTI_BYTE_TEXT = 'λ' * 4097 +#---------------------------------------------------- + +init_sql = f""" + set bail on; + recreate table test_one_unique_value( + f_sml smallint -- 0 + ,f_int int -- 1 + ,f_big bigint -- 2 + ,f_i128 int128 -- 3 + ,f_bool boolean -- 4 + ,f_dt date -- 5 + ,f_tm time -- 6 + ,f_ts timestamp -- 7 + ,f_tmtz time with time zone -- 8 + ,f_tstz timestamp with time zone -- 9 + ,f_num numeric(2,2) -- 10 + ,f_dec decimal(2,2) -- 11 + ,f_dbl double precision -- 12 + + -- commented out until #8330 remains unfixed: + --------------------------------------------- + --,f_decf decfloat -- 13 + --,f_txt_1251 varchar({TXT_WIDTH_SINGLE_BYTE_ENCODING}) character set win1251 -- 14 + --,f_txt_utf8 varchar({TXT_WIDTH_MULTI_BYTE_ENCODING}) character set utf8 -- 15 + --------------------------------------------- + ); + + recreate table test_null_in_all_rows( + nul_sml smallint + ,nul_int int + ,nul_big bigint + ,nul_i128 int128 + ,nul_bool boolean + ,nul_dt date + ,nul_tm time + ,nul_ts timestamp + ,nul_tmtz time with time zone + ,nul_tstz timestamp with time zone + ,nul_num numeric(2,2) + ,nul_dec decimal(2,2) + ,nul_dbl double precision + --,nul_decf decfloat + --,nul_txt_1251 varchar({TXT_WIDTH_SINGLE_BYTE_ENCODING}) character set win1251 + --,nul_txt_utf8 varchar({TXT_WIDTH_MULTI_BYTE_ENCODING}) character set utf8 + ); commit; - set list on; - set term ^; - - execute block returns ( - ratio_for_1e1 varchar(150) - ,ratio_for_1e2 varchar(150) - ,ratio_for_1e3 varchar(150) - ,ratio_for_1e4 varchar(150) - ,ratio_for_1e5 varchar(150) - ) - as - -- ############################################ - -- ############ T H R E S H O L D ######## - - -- Before 28.10.2015: 1.85 (changed after letter by dimitr). - -- Probably random disturbance was caused by other (concurrent) processes on test host. - -- Check with new threshold was done on: WI-V2.5.5.26942 (SC) and WI-V3.0.0.32134 (CS/SC/SS). - - declare max_diff_threshold numeric(10,4) = {MAX_DIFF}; - - -- ############################################ - - declare ratio_select_vs_count_1e1 numeric(10,4); - declare ratio_select_vs_count_1e2 numeric(10,4); - declare ratio_select_vs_count_1e3 numeric(10,4); - declare ratio_select_vs_count_1e4 numeric(10,4); - declare ratio_select_vs_count_1e5 numeric(10,4); - declare sel_distinct_1e1_ms int; - declare cnt_distinct_1e1_ms int; - declare sel_distinct_1e2_ms int; - declare cnt_distinct_1e2_ms int; - declare sel_distinct_1e3_ms int; - declare cnt_distinct_1e3_ms int; - declare sel_distinct_1e4_ms int; - declare cnt_distinct_1e4_ms int; - declare sel_distinct_1e5_ms int; - declare cnt_distinct_1e5_ms int; - declare n int; - declare t0 timestamp; + create or alter procedure sp_fill(a_cnt int) returns(id int) as begin - t0='now'; - select count(*) from ( select distinct id from test1e1 ) into n; - sel_distinct_1e1_ms = datediff(millisecond from t0 to cast('now' as timestamp)); - - t0='now'; - select count(distinct id) from test1e1 into n; - cnt_distinct_1e1_ms = datediff(millisecond from t0 to cast('now' as timestamp)); - - ratio_select_vs_count_1e1 = 1.0000 * sel_distinct_1e1_ms / cnt_distinct_1e1_ms; - - ------------ - - t0='now'; - select count(*) from ( select distinct id from test1e2 ) into n; - sel_distinct_1e2_ms = datediff(millisecond from t0 to cast('now' as timestamp)); - - t0='now'; - select count(distinct id) from test1e2 into n; - cnt_distinct_1e2_ms = datediff(millisecond from t0 to cast('now' as timestamp)); - - ratio_select_vs_count_1e2 = 1.0000 * sel_distinct_1e2_ms / cnt_distinct_1e2_ms; - - ------------ - - t0='now'; - select count(*) from ( select distinct id from test1e3 ) into n; - sel_distinct_1e3_ms = datediff(millisecond from t0 to cast('now' as timestamp)); - - t0='now'; - select count(distinct id) from test1e3 into n; - cnt_distinct_1e3_ms = datediff(millisecond from t0 to cast('now' as timestamp)); - - ratio_select_vs_count_1e3 = 1.0000 * sel_distinct_1e3_ms / cnt_distinct_1e3_ms; - - ------------ - - t0='now'; - select count(*) from ( select distinct id from test1e4 ) into n; - sel_distinct_1e4_ms = datediff(millisecond from t0 to cast('now' as timestamp)); - - t0='now'; - select count(distinct id) from test1e4 into n; - cnt_distinct_1e4_ms = datediff(millisecond from t0 to cast('now' as timestamp)); - - ratio_select_vs_count_1e4 = 1.0000 * sel_distinct_1e4_ms / cnt_distinct_1e4_ms; - - ------------ - - t0='now'; - select count(*) from ( select distinct id from test1e5 ) into n; - sel_distinct_1e5_ms = datediff(millisecond from t0 to cast('now' as timestamp)); - - t0='now'; - select count(distinct id) from test1e5 into n; - cnt_distinct_1e5_ms = datediff(millisecond from t0 to cast('now' as timestamp)); - - ratio_select_vs_count_1e5 = 1.0000 * sel_distinct_1e5_ms / cnt_distinct_1e5_ms; - - ------------ - - ratio_for_1e1 = 'Acceptable'; - ratio_for_1e2 = 'Acceptable'; - ratio_for_1e3 = 'Acceptable'; - ratio_for_1e4 = 'Acceptable'; - ratio_for_1e5 = 'Acceptable'; - - if (1=0 or ratio_select_vs_count_1e1 > max_diff_threshold) then - -- Example: RATIO_FOR_1E1 Regression /* perf_issue_tag */: ratio = 3.3695 > 3.0000 - ratio_for_1e1 = 'Regression /* perf_issue_tag */: ratio = '||ratio_select_vs_count_1e1||' > '||max_diff_threshold; - - if (1=0 or ratio_select_vs_count_1e2 > max_diff_threshold) then - ratio_for_1e2 = 'Regression /* perf_issue_tag */: ratio = '||ratio_select_vs_count_1e2||' > '||max_diff_threshold; - - if (1=0 or ratio_select_vs_count_1e3 > max_diff_threshold) then - ratio_for_1e3 = 'Regression /* perf_issue_tag */: ratio = '||ratio_select_vs_count_1e3||' > '||max_diff_threshold; - - if (1=0 or ratio_select_vs_count_1e4 > max_diff_threshold) then - ratio_for_1e4 = 'Regression /* perf_issue_tag */: ratio = '||ratio_select_vs_count_1e4||' > '||max_diff_threshold; - - if (1=0 or ratio_select_vs_count_1e5 > max_diff_threshold) then - ratio_for_1e5 = 'Regression /* perf_issue_tag */: ratio = '||ratio_select_vs_count_1e5||' > '||max_diff_threshold; - - - suspend; - - end - ^ set term ;^ -""" - -act = isql_act('db', test_script,substitutions = [('[ \t]+', ' ')]) + id = 0; + while (id < a_cnt) do + begin + suspend; + id = id + 1; + end + end ^ + set term ;^ + commit; -expected_stdout = f""" - RATIO_FOR_1E1 Acceptable - RATIO_FOR_1E2 Acceptable - RATIO_FOR_1E3 Acceptable - RATIO_FOR_1E4 Acceptable - RATIO_FOR_1E5 Acceptable + insert into test_one_unique_value ( + f_sml -- 0 + ,f_int -- 1 + ,f_big -- 2 + ,f_i128 -- 3 + ,f_bool -- 4 + ,f_dt -- 5 + ,f_tm -- 6 + ,f_ts -- 7 + ,f_tmtz -- 8 + ,f_tstz -- 9 + ,f_num -- 10 + ,f_dec -- 11 + ,f_dbl -- 12 + --,f_decf -- 13 + --,f_txt_1251 -- 14 + --,f_txt_utf8 -- 15 + ) + select + -32768 -- 0 + ,-2147483648 -- 1 + ,-9223372036854775808 -- 2 + ,-170141183460469231731687303715884105728 -- 3 + ,true -- 4 + ,date '19.12.2023' -- 5 + ,time '23:59:59' -- 6 + ,timestamp '19.12.2023 23:59:59' -- 7 + ,time '11:11:11.111 Indian/Cocos' -- 8 + ,timestamp '2018-12-31 12:31:42.543 Pacific/Fiji' -- 9 + ,-327.68 -- 10 + ,-327.68 -- 11 + ,pi() -- 12 + --,exp(1) -- cast(-9.999999999999999999999999999999999E6144 as decfloat(34)) -- 13 + --,lpad('', {TXT_WIDTH_SINGLE_BYTE_ENCODING}, '{SINGLE_BYTE_TEXT}') -- 14 + --,lpad('', {TXT_WIDTH_MULTI_BYTE_ENCODING}, '{MULTI_BYTE_TEXT}') -- 15 + from sp_fill({N_ROWS_CNT}) as p; + + insert into test_null_in_all_rows + select + null -- 0 + ,null -- 1 + ,null -- 2 + ,null -- 3 + ,null -- 4 + ,null -- 5 + ,null -- 6 + ,null -- 7 + ,null -- 8 + ,null -- 9 + ,null -- 10 + ,null -- 11 + ,null -- 12 + --,null -- 13 + --,null -- 14 + --,null -- 15 + from test_one_unique_value; + commit; """ - -@pytest.mark.version('>=3') -def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute(combine_output = True) +db = db_factory(init = init_sql, page_size = PAGE_SIZE) + +act = python_act('db', substitutions = [('[ \t]+', ' ')]) + +@pytest.mark.version('>=4') +def test_1(act: Action, capsys): + + with act.db.connect() as con: + cur1=con.cursor() + cur2=con.cursor() + + cur1.execute('select mon$server_pid as p from mon$attachments where mon$attachment_id = current_connection') + fb_pid = int(cur1.fetchone()[0]) + + run_cpu_map = {} + for t_name in ('test_one_unique_value', 'test_null_in_all_rows'): + + fields_qry = f""" + select rf.rdb$field_name + from rdb$relation_fields rf + where rf.rdb$relation_name = upper('{t_name}') + order by rf.rdb$field_position + """ + cur1.execute(fields_qry) + fields_lst = [x[0].strip() for x in cur1.fetchall()] + + for f_name in fields_lst: + query1 = f'select count(*) from (select distinct {f_name} from {t_name})' + query2 = f'select count(distinct {f_name}) from {t_name}' + ps1 = cur1.prepare(query1) + ps2 = cur2.prepare(query2) + for c in (cur1, cur2): + cpu_usage_values = [] + psc = ps1 if c == cur1 else ps2 + for i in range(0, N_MEASURES): + fb_info_init = psutil.Process(fb_pid).cpu_times() + + # ::: NB ::: 'psc' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + rs = c.execute(psc) + c.fetchall() + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + + fb_info_curr = psutil.Process(fb_pid).cpu_times() + cpu_usage_values.append( max(fb_info_curr.user - fb_info_init.user, 0.000001) ) + + v = run_cpu_map.get( (t_name,f_name), [0,0,0, '','']) + if psc == ps1: + v[0] = median(cpu_usage_values) # 'select count(*) from (select distinct ...)' + else: + v[1] = median(cpu_usage_values) # 'select count(distinct ...) from ...' + v[2] = v[0] / v[1] + v[3] = query1 + v[4] = query2 + run_cpu_map[ (t_name,f_name) ] = v + + ps1.free() + ps2.free() + + poor_ratios_lst = [] + + #for k,v in run_cpu_map.items(): + # print(':::',k,':::') + # cpu_median_1, cpu_median_2, cpu_medians_ratio, query_1, query_2 = v + # # f'{ra=:12.4f}' + # msg = '\n'.join( + # ( f'{query_1=}' + # ,f'{query_2=}' + # ,f'{cpu_median_1=:12.4f} {cpu_median_2=:12.4f} {cpu_medians_ratio=:12.6f}' + # ) + # ) + # print(msg) + # print('-------------------------------------------------------------------------------------') + + + msg_prefix = 'CPU time medians ratio: ' + msg_expected = msg_prefix + 'EXPECTED.' + for k,v in run_cpu_map.items(): + if v[2] > MAX_RATIO: + poor_ratios_lst.append( '\n'.join( + ( 'query_1: ' + v[3] + ,'query_2: ' + v[4] + ,f'cpu_median_1: {v[0]:12.6f}' + ,f'cpu_median_2: {v[1]:12.6f}' + ,f'cpu_median_1 / cpu_median_2: {v[0]/v[1]:12.6f}' + ) + ) + ) + + if poor_ratios_lst: + print(f'{msg_prefix} /* perf_issue_tag */ UNEXPECTED. Following ratio(s) exceeds MAX_RATIO={MAX_RATIO}:') + for x in poor_ratios_lst: + print(x) + else: + print(msg_expected) + + + act.expected_stdout = msg_expected + act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_0223_test.py b/tests/bugs/core_0223_test.py index 703830cd..f136a45b 100644 --- a/tests/bugs/core_0223_test.py +++ b/tests/bugs/core_0223_test.py @@ -7,6 +7,13 @@ DESCRIPTION: JIRA: CORE-223 FBTEST: bugs.core_0223 +NOTES: + [22.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -37,7 +44,19 @@ commit; """ -act = isql_act('db', test_script) +substitutions = [] +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [ ('line: \\d+, col: \\d+', '') ] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + +act = isql_act('db', test_script, substitutions = substitutions) + expected_stdout = """ X 50000000 diff --git a/tests/bugs/core_0282_test.py b/tests/bugs/core_0282_test.py index 57ad3ca4..218207e0 100644 --- a/tests/bugs/core_0282_test.py +++ b/tests/bugs/core_0282_test.py @@ -7,41 +7,68 @@ DESCRIPTION: JIRA: CORE-282 FBTEST: bugs.core_0282 +NOTES: + [22.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """create table t(a int); -create domain d int check(value > (select max(a) from t)); -commit;""" -db = db_factory(init=init_script) +db = db_factory() -test_script = """drop table t; -commit; -create table u(a d); -commit; -show table u; +test_script = """ + set list on; + create table test(f01 int); + create domain dm_int int check(value > (select max(f01) from test)); + commit; + drop table test; + commit; + create table test2(f01 dm_int); + commit; + show table test2; """ -act = isql_act('db', test_script) +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' ')] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """A (D) INTEGER Nullable - check(value > (select max(a) from t)) +expected_stdout_5x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -cannot delete + -COLUMN TEST.F01 + -there are 1 dependencies + F01 (DM_INT) INTEGER Nullable + check(value > (select max(f01) from test)) """ -expected_stderr = """Statement failed, SQLSTATE = 42000 -unsuccessful metadata update --cannot delete --COLUMN T.A --there are 1 dependencies + +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -cannot delete + -COLUMN TEST.F01 + -there are 1 dependencies + Table: TEST2 + F01 (DM_INT) INTEGER Nullable + check(value > (select max(f01) from test)) """ @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_0304_test.py b/tests/bugs/core_0304_test.py index 719fa9ad..bcdd47a1 100644 --- a/tests/bugs/core_0304_test.py +++ b/tests/bugs/core_0304_test.py @@ -5,392 +5,310 @@ ISSUE: 637 TITLE: Any user can drop procedures, generators, exceptions DESCRIPTION: +JIRA: CORE-304 +FBTEST: bugs.core_0304 NOTES: -[24.01.2019] Added separate code for running on FB 4.0+. - UDF usage is deprecated in FB 4+, see: ".../doc/README.incompatibilities.3to4.txt". - Functions div, frac, dow, sdow, getExactTimestampUTC and isLeapYear got safe replacement - in UDR library "udf_compat", see it in folder: ../plugins/udr/ -[01.06.2021] Adjusted STDERR caused by fixes + [24.01.2019] Added separate code for running on FB 4.0+. + UDF usage is deprecated in FB 4+, see: ".../doc/README.incompatibilities.3to4.txt". + Functions div, frac, dow, sdow, getExactTimestampUTC and isLeapYear got safe replacement + in UDR library "udf_compat", see it in folder: ../plugins/udr/ + + [01.06.2021] Adjusted STDERR caused by fixes https://github.com/FirebirdSQL/firebird/pull/6833 https://github.com/FirebirdSQL/firebird/pull/6825 ("Correct error message for DROP VIEW") -JIRA: CORE-304 -FBTEST: bugs.core_0304 + + [24.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Adjusted explained plan in 6.x to actual. + + Checked on 6.0.0.858; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -# version: 3.0 - db = db_factory() tmp_user = user_factory('db', name='tmp$c0304', password='123') -test_script_1 = """ - - DECLARE EXTERNAL FUNCTION strlen CSTRING(32767) RETURNS INTEGER BY VALUE ENTRY_POINT 'IB_UDF_strlen' MODULE_NAME 'ib_udf'; - - create domain dm_test int; - create collation name_coll for utf8 from unicode case insensitive; - create sequence g_test; - create exception e_test 'foo'; - create or alter procedure sp_test as begin end; - create table test(id int not null, x int); - alter table test add constraint test_pk primary key(id) using index test_pk; - create index test_x on test(x); - create view v_test as select * from test; - create role manager; - commit; - - set term ^; - create or alter trigger test_bi for test active - before insert position 0 - as - begin - new.id = coalesce(new.id, gen_id(g_test, 1) ); - end - ^ - set term ;^ - commit; - - connect '$(DSN)' user 'tmp$c0304' password '123'; - - -- All following statements should FAIL if current user is not SYSDBA: - - execute procedure sp_test; - - show sequence g_test; - - alter domain dm_test set default 123; - - alter domain dm_test set not null; - - alter domain dm_test drop not null; - - alter trigger test_bi inactive; - - - alter table test add z int; - - alter table test drop constraint test_pk; - - drop index test_x; - - drop view v_test; - - drop trigger test_bi; - - drop table test; - - drop role manager; - - drop procedure sp_test; - - drop sequence g_test; - - drop exception e_test; - - drop function strlen; - - - drop collation name_coll; - - rollback; -""" - -act_1 = isql_act('db', test_script_1) - - -expected_stderr_1 = """ - Statement failed, SQLSTATE = 28000 - no permission for EXECUTE access to PROCEDURE SP_TEST - - Statement failed, SQLSTATE = 28000 - no permission for USAGE access to GENERATOR G_TEST - There is no generator G_TEST in this database - - Statement failed, SQLSTATE = 28000 - unsuccessful metadata update - -ALTER DOMAIN DM_TEST failed - -no permission for ALTER access to DOMAIN DM_TEST - - Statement failed, SQLSTATE = 28000 - unsuccessful metadata update - -ALTER DOMAIN DM_TEST failed - -no permission for ALTER access to DOMAIN DM_TEST - - Statement failed, SQLSTATE = 28000 - unsuccessful metadata update - -ALTER DOMAIN DM_TEST failed - -no permission for ALTER access to DOMAIN DM_TEST - - Statement failed, SQLSTATE = 28000 - unsuccessful metadata update - -ALTER TRIGGER TEST_BI failed - -no permission for ALTER access to TABLE TEST - - Statement failed, SQLSTATE = 28000 - unsuccessful metadata update - -ALTER TABLE TEST failed - -no permission for ALTER access to TABLE TEST - - Statement failed, SQLSTATE = 28000 - unsuccessful metadata update - -ALTER TABLE TEST failed - -no permission for ALTER access to TABLE TEST - - Statement failed, SQLSTATE = 28000 - unsuccessful metadata update - -DROP INDEX TEST_X failed - -no permission for ALTER access to TABLE TEST - - Statement failed, SQLSTATE = 28000 - unsuccessful metadata update - -DROP VIEW V_TEST failed - -no permission for DROP access to VIEW V_TEST - - Statement failed, SQLSTATE = 28000 - unsuccessful metadata update - -DROP TRIGGER TEST_BI failed - -no permission for ALTER access to TABLE TEST - - Statement failed, SQLSTATE = 28000 - unsuccessful metadata update - -DROP TABLE TEST failed - -no permission for DROP access to TABLE TEST - - Statement failed, SQLSTATE = 28000 - unsuccessful metadata update - -DROP ROLE MANAGER failed - -no permission for DROP access to ROLE MANAGER - - Statement failed, SQLSTATE = 28000 - unsuccessful metadata update - -DROP PROCEDURE SP_TEST failed - -no permission for DROP access to PROCEDURE SP_TEST - - - Statement failed, SQLSTATE = 28000 - unsuccessful metadata update - -DROP SEQUENCE G_TEST failed - -no permission for DROP access to GENERATOR G_TEST - - - Statement failed, SQLSTATE = 28000 - unsuccessful metadata update - -DROP EXCEPTION E_TEST failed - -no permission for DROP access to EXCEPTION E_TEST - - Statement failed, SQLSTATE = 28000 - unsuccessful metadata update - -DROP FUNCTION STRLEN failed - -no permission for DROP access to FUNCTION STRLEN - - Statement failed, SQLSTATE = 28000 - unsuccessful metadata update - -DROP COLLATION NAME_COLL failed - -no permission for DROP access to COLLATION NAME_COLL - -""" - -@pytest.mark.version('>=3.0.8,<4.0') -def test_1(act_1: Action, tmp_user: User): - act_1.expected_stderr = expected_stderr_1 - act_1.execute() - assert act_1.clean_stderr == act_1.clean_expected_stderr - -# version: 4.0 - -test_script_2 = """ - -- See declaration sample in plugins\\udr\\UdfBackwardCompatibility.sql: - - create function UDR40_frac ( - val double precision - ) returns double precision - external name 'udf_compat!UC_frac' - engine udr; - - - create domain dm_test int; - create collation name_coll for utf8 from unicode case insensitive; - create sequence g_test; - create exception e_test 'foo'; - create or alter procedure sp_test as begin end; - create table test(id int not null, x int); - alter table test add constraint test_pk primary key(id) using index test_pk; - create index test_x on test(x); - create view v_test as select * from test; - create role manager; - commit; - - set term ^; - create or alter trigger test_bi for test active - before insert position 0 - as - begin - new.id = coalesce(new.id, gen_id(g_test, 1) ); - end - ^ - set term ;^ - commit; - - connect '$(DSN)' user 'tmp$c0304' password '123'; - - -- All following statements should FAIL if current user is not SYSDBA: - - execute procedure sp_test; - - show sequence g_test; - - alter domain dm_test set default 123; - - alter domain dm_test set not null; - - alter domain dm_test drop not null; - - alter trigger test_bi inactive; - - - alter table test add z int; - - alter table test drop constraint test_pk; - - drop index test_x; - - drop view v_test; - - drop trigger test_bi; - - drop table test; - - drop role manager; - - drop procedure sp_test; - - drop sequence g_test; - - drop exception e_test; - - drop function UDR40_frac; - - drop collation name_coll; - - rollback; -""" - -act_2 = isql_act('db', test_script_2) - -expected_stderr_2 = """ -Statement failed, SQLSTATE = 28000 -no permission for EXECUTE access to PROCEDURE SP_TEST --Effective user is TMP$C0304 - -Statement failed, SQLSTATE = 28000 -no permission for USAGE access to GENERATOR G_TEST --Effective user is TMP$C0304 - -There is no generator G_TEST in this database -Statement failed, SQLSTATE = 28000 -unsuccessful metadata update --ALTER DOMAIN DM_TEST failed --no permission for ALTER access to DOMAIN DM_TEST --Effective user is TMP$C0304 - -Statement failed, SQLSTATE = 28000 -unsuccessful metadata update --ALTER DOMAIN DM_TEST failed --no permission for ALTER access to DOMAIN DM_TEST --Effective user is TMP$C0304 - -Statement failed, SQLSTATE = 28000 -unsuccessful metadata update --ALTER DOMAIN DM_TEST failed --no permission for ALTER access to DOMAIN DM_TEST --Effective user is TMP$C0304 - -Statement failed, SQLSTATE = 28000 -unsuccessful metadata update --ALTER TRIGGER TEST_BI failed --no permission for ALTER access to TABLE TEST --Effective user is TMP$C0304 - -Statement failed, SQLSTATE = 28000 -unsuccessful metadata update --ALTER TABLE TEST failed --no permission for ALTER access to TABLE TEST --Effective user is TMP$C0304 - -Statement failed, SQLSTATE = 28000 -unsuccessful metadata update --ALTER TABLE TEST failed --no permission for ALTER access to TABLE TEST --Effective user is TMP$C0304 - -Statement failed, SQLSTATE = 28000 -unsuccessful metadata update --DROP INDEX TEST_X failed --no permission for ALTER access to TABLE TEST --Effective user is TMP$C0304 - -Statement failed, SQLSTATE = 28000 -unsuccessful metadata update --DROP VIEW V_TEST failed --no permission for DROP access to VIEW V_TEST --Effective user is TMP$C0304 - -Statement failed, SQLSTATE = 28000 -unsuccessful metadata update --DROP TRIGGER TEST_BI failed --no permission for ALTER access to TABLE TEST --Effective user is TMP$C0304 - -Statement failed, SQLSTATE = 28000 -unsuccessful metadata update --DROP TABLE TEST failed --no permission for DROP access to TABLE TEST --Effective user is TMP$C0304 - -Statement failed, SQLSTATE = 28000 -unsuccessful metadata update --DROP ROLE MANAGER failed --no permission for DROP access to ROLE MANAGER --Effective user is TMP$C0304 - -Statement failed, SQLSTATE = 28000 -unsuccessful metadata update --DROP PROCEDURE SP_TEST failed --no permission for DROP access to PROCEDURE SP_TEST --Effective user is TMP$C0304 - -Statement failed, SQLSTATE = 28000 -unsuccessful metadata update --DROP SEQUENCE G_TEST failed --no permission for DROP access to GENERATOR G_TEST --Effective user is TMP$C0304 - -Statement failed, SQLSTATE = 28000 -unsuccessful metadata update --DROP EXCEPTION E_TEST failed --no permission for DROP access to EXCEPTION E_TEST --Effective user is TMP$C0304 - -Statement failed, SQLSTATE = 28000 -unsuccessful metadata update --DROP FUNCTION UDR40_FRAC failed --no permission for DROP access to FUNCTION UDR40_FRAC --Effective user is TMP$C0304 - -Statement failed, SQLSTATE = 28000 -unsuccessful metadata update --DROP COLLATION NAME_COLL failed --no permission for DROP access to COLLATION NAME_COLL --Effective user is TMP$C0304 - -""" - -@pytest.mark.version('>=4.0') -def test_2(act_2: Action, tmp_user: User): - act_2.expected_stderr = expected_stderr_2 - act_2.execute() - assert act_2.clean_stderr == act_2.clean_expected_stderr +substitutions = [] +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + +act = isql_act('db', substitutions = substitutions) + +@pytest.mark.version('>=3.0.8') +def test(act: Action, tmp_user: User): + if act.is_version('>=3.0.8,<4.0'): + special_ddl = """ + DECLARE EXTERNAL FUNCTION strlen CSTRING(32767) RETURNS INTEGER BY VALUE ENTRY_POINT 'IB_UDF_strlen' MODULE_NAME 'ib_udf'; + """ + else: + special_ddl = """ + create function strlen ( + val double precision + ) returns double precision + external name 'udf_compat!UC_frac' + engine udr; + """ + + test_sql = f""" + + {special_ddl} + + create domain dm_test int; + create collation name_coll for utf8 from unicode case insensitive; + create sequence g_test; + create exception e_test 'foo'; + create or alter procedure sp_test as begin end; + create table test(id int not null, x int); + alter table test add constraint test_pk primary key(id) using index test_pk; + create index test_x on test(x); + create view v_test as select * from test; + create role manager; + commit; + + set term ^; + create or alter trigger test_bi for test active + before insert position 0 + as + begin + new.id = coalesce(new.id, gen_id(g_test, 1) ); + end + ^ + set term ;^ + commit; + + connect '{act.db.dsn}' user '{tmp_user.name}' password '{tmp_user.password}'; + + -- All following statements should FAIL if current user is not SYSDBA: + + execute procedure sp_test; + show sequence g_test; + alter domain dm_test set default 123; + alter domain dm_test set not null; + alter domain dm_test drop not null; + alter trigger test_bi inactive; + alter table test add z int; + alter table test drop constraint test_pk; + drop index test_x; + drop view v_test; + drop trigger test_bi; + drop table test; + drop role manager; + drop procedure sp_test; + drop sequence g_test; + drop exception e_test; + drop function strlen; + drop collation name_coll; + rollback; + """ + + expected_stdout_3x = """ + Statement failed, SQLSTATE = 28000 + no permission for EXECUTE access to PROCEDURE SP_TEST + Statement failed, SQLSTATE = 28000 + no permission for USAGE access to GENERATOR G_TEST + There is no generator G_TEST in this database + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER DOMAIN DM_TEST failed + -no permission for ALTER access to DOMAIN DM_TEST + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER DOMAIN DM_TEST failed + -no permission for ALTER access to DOMAIN DM_TEST + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER DOMAIN DM_TEST failed + -no permission for ALTER access to DOMAIN DM_TEST + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER TRIGGER TEST_BI failed + -no permission for ALTER access to TABLE TEST + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER TABLE TEST failed + -no permission for ALTER access to TABLE TEST + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER TABLE TEST failed + -no permission for ALTER access to TABLE TEST + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -DROP INDEX TEST_X failed + -no permission for ALTER access to TABLE TEST + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -DROP VIEW V_TEST failed + -no permission for DROP access to VIEW V_TEST + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -DROP TRIGGER TEST_BI failed + -no permission for ALTER access to TABLE TEST + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -DROP TABLE TEST failed + -no permission for DROP access to TABLE TEST + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -DROP ROLE MANAGER failed + -no permission for DROP access to ROLE MANAGER + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -DROP PROCEDURE SP_TEST failed + -no permission for DROP access to PROCEDURE SP_TEST + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -DROP SEQUENCE G_TEST failed + -no permission for DROP access to GENERATOR G_TEST + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -DROP EXCEPTION E_TEST failed + -no permission for DROP access to EXCEPTION E_TEST + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -DROP FUNCTION STRLEN failed + -no permission for DROP access to FUNCTION STRLEN + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -DROP COLLATION NAME_COLL failed + -no permission for DROP access to COLLATION NAME_COLL + """ + + expected_stdout_4x = f""" + Statement failed, SQLSTATE = 28000 + no permission for EXECUTE access to PROCEDURE SP_TEST + -Effective user is {tmp_user.name.upper()} + + Statement failed, SQLSTATE = 28000 + no permission for USAGE access to GENERATOR G_TEST + -Effective user is {tmp_user.name.upper()} + There is no generator G_TEST in this database + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER DOMAIN DM_TEST failed + -no permission for ALTER access to DOMAIN DM_TEST + -Effective user is {tmp_user.name.upper()} + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER DOMAIN DM_TEST failed + -no permission for ALTER access to DOMAIN DM_TEST + -Effective user is {tmp_user.name.upper()} + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER DOMAIN DM_TEST failed + -no permission for ALTER access to DOMAIN DM_TEST + -Effective user is {tmp_user.name.upper()} + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER TRIGGER TEST_BI failed + -no permission for ALTER access to TABLE TEST + -Effective user is {tmp_user.name.upper()} + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER TABLE TEST failed + -no permission for ALTER access to TABLE TEST + -Effective user is {tmp_user.name.upper()} + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER TABLE TEST failed + -no permission for ALTER access to TABLE TEST + -Effective user is {tmp_user.name.upper()} + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -DROP INDEX TEST_X failed + -no permission for ALTER access to TABLE TEST + -Effective user is {tmp_user.name.upper()} + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -DROP VIEW V_TEST failed + -no permission for DROP access to VIEW V_TEST + -Effective user is {tmp_user.name.upper()} + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -DROP TRIGGER TEST_BI failed + -no permission for ALTER access to TABLE TEST + -Effective user is {tmp_user.name.upper()} + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -DROP TABLE TEST failed + -no permission for DROP access to TABLE TEST + -Effective user is {tmp_user.name.upper()} + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -DROP ROLE MANAGER failed + -no permission for DROP access to ROLE MANAGER + -Effective user is {tmp_user.name.upper()} + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -DROP PROCEDURE SP_TEST failed + -no permission for DROP access to PROCEDURE SP_TEST + -Effective user is {tmp_user.name.upper()} + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -DROP SEQUENCE G_TEST failed + -no permission for DROP access to GENERATOR G_TEST + -Effective user is {tmp_user.name.upper()} + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -DROP EXCEPTION E_TEST failed + -no permission for DROP access to EXCEPTION E_TEST + -Effective user is {tmp_user.name.upper()} + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -DROP FUNCTION STRLEN failed + -no permission for DROP access to FUNCTION STRLEN + -Effective user is {tmp_user.name.upper()} + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -DROP COLLATION NAME_COLL failed + -no permission for DROP access to COLLATION NAME_COLL + -Effective user is {tmp_user.name.upper()} + """ + + act.expected_stdout = expected_stdout_3x if act.is_version('<4') else expected_stdout_4x + act.isql(switches = ['-q'], input = test_sql, combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_0479_test.py b/tests/bugs/core_0479_test.py index 2c2a636c..04b98453 100644 --- a/tests/bugs/core_0479_test.py +++ b/tests/bugs/core_0479_test.py @@ -274,6 +274,7 @@ def test_1(act_1: Action): Records affected: 0 """ +@pytest.mark.intl @pytest.mark.version('>=4.0') def test_2(act_2: Action): act_2.expected_stdout = expected_stdout_2 diff --git a/tests/bugs/core_0480_test.py b/tests/bugs/core_0480_test.py index cf1402f1..2d85f570 100644 --- a/tests/bugs/core_0480_test.py +++ b/tests/bugs/core_0480_test.py @@ -7,32 +7,48 @@ DESCRIPTION: JIRA: CORE-480 FBTEST: bugs.core_0480 +NOTES: + [22.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """create table T1 (PK1 INTEGER, COL VARCHAR(10)); -commit;""" +db = db_factory() -db = db_factory(init=init_script) +test_script = """ + create table t1 (pk1 integer, col varchar(10)); + create table t2 (pk2 integer, fk1 varchar(10), col varchar(10), + foreign key (fk1) references t1 (pk1)); +""" -test_script = """create table T2 (PK2 INTEGER, FK1 VARCHAR(10), COL VARCHAR(10), -foreign key (FK1) references T1 (PK1)); +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] -""" +substitutions = [('[ \t]+', ' ')] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) -act = isql_act('db', test_script) +act = isql_act('db', test_script, substitutions = substitutions) -expected_stderr = """Statement failed, SQLSTATE = 42000 -unsuccessful metadata update --CREATE TABLE T2 failed --could not find UNIQUE or PRIMARY KEY constraint in table T1 with specified columns +expected_stdout = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -CREATE TABLE T2 failed + -could not find UNIQUE or PRIMARY KEY constraint in table T1 with specified columns """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_0501_test.py b/tests/bugs/core_0501_test.py index 9b670a23..fb6e2b47 100644 --- a/tests/bugs/core_0501_test.py +++ b/tests/bugs/core_0501_test.py @@ -5,13 +5,25 @@ ISSUE: 852 TITLE: Optimization problem with COALESCE DESCRIPTION: - It tests many problems Adriano found when fixing CORE-501, CORE-1343 and CORE-2041. + Verify fixes for many issues Adriano found for CORE-501, CORE-1343 and CORE-2041. NOTES: -[25.04.2020] Fixed lot of bugs related to wrong count of updatable columns (they were not specified in DML). - Replaced test_type to 'ISQL' because all can be done wo Python calls. Checked on 3.0.6.33289, 4.0.0.1935. -[18.11.2020] Changed expected_stderr for parametrized statement "select coalesce(1 + cast(? ...), 2 + cast(? ...)) ...": - now it must be "-No SQLDA for input values provided" (was: "-Wrong number of parameters (expected 3, got 0)"). - Output became proper since CORE-6447 was fixed. + [25.04.2020] pzotov + Fixed lot of bugs related to wrong count of updatable columns (they were not specified in DML). + Replaced test_type to 'ISQL' because all can be done w/o Python calls. + Checked on 3.0.6.33289, 4.0.0.1935. + + [18.11.2020] pzotov + Changed expected_stderr for parametrized statement "select coalesce(1 + cast(? ...), 2 + cast(? ...)) ...": + now it must be "-No SQLDA for input values provided" (was: "-Wrong number of parameters (expected 3, got 0)"). + Output became proper since CORE-6447 was fixed. + + [22.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + Also, for this test 'schema:' in SQLDA output is suppressed because as not relevant to check. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. JIRA: CORE-501 FBTEST: bugs.core_0501 """ @@ -19,8 +31,6 @@ import pytest from firebird.qa import * -substitutions = [('[ \t]+', ' ')] - db = db_factory() test_script = """ @@ -352,732 +362,518 @@ """ -act_1 = isql_act('db', test_script, substitutions=substitutions) - -expected_stdout = """ -MSG point-01 -N 1 -X 100 -CN 1 -CX 100 - -MSG point-01 -N 2 -X 20 -CN 2 -CX 20 - -MSG point-01 -N 3 -X 30 -CN 3 -CX 30 - - - -MSG point-02 -N 1 -X 100 -CN 1 -CX 100 - -MSG point-02 -N 2 -X 200 -CN 2 -CX 200 - -MSG point-02 -N 3 -X 30 -CN 3 -CX 30 - - - -MSG point-03 -N 1 -X 100 -CN 1 -CX 100 -VCN 1 - -MSG point-03 -N 2 -X 200 -CN 2 -CX 200 -VCN 2 - -MSG point-03 -N 3 -X 30 -CN 3 -CX 30 -VCN 3 - - - -MSG point-04 -N 1 -X 100 -CN 1 -CX 100 -VCN 1 - -MSG point-04 -N 2 -X 200 -CN 2 -CX 200 -VCN 2 - -MSG point-04 -N 3 -X 30 -CN 3 -CX 30 -VCN 3 - -MSG point-04 -N 4 -X 40 -CN 4 -CX 40 -VCN 4 - - - -MSG point-05 -N 1 -X 100 -CN 1 -CX 100 - -MSG point-05 -N 2 -X 200 -CN 2 -CX 200 - -MSG point-05 -N 3 -X 300 -CN 3 -CX 300 - -MSG point-05 -N 4 -X 40 -CN 4 -CX 40 - - - -MSG point-06 -N 1 -X 100 -CN 1 -CX 100 -VCN 1 - -MSG point-06 -N 2 -X 200 -CN 2 -CX 200 -VCN 2 - -MSG point-06 -N 3 -X 300 -CN 3 -CX 300 -VCN 3 - -MSG point-06 -N 4 -X 40 -CN 4 -CX 40 -VCN 4 - -MSG point-06 -N 1008 -X 88 -CN 1008 -CX 88 -VCN 1008 - - - -MSG point-07 -COAL_01 1 - -MSG point-07 -COAL_01 2 - -MSG point-07 -COAL_01 3 - -MSG point-07 -COAL_01 4 - -MSG point-07 -COAL_01 1008 - - - -MSG point-08 -COAL_02 1 - -MSG point-08 -COAL_02 2 - -MSG point-08 -COAL_02 3 - -MSG point-08 -COAL_02 4 - -MSG point-08 -COAL_02 1008 - - - -MSG point-09 -COAL_03 1 - -MSG point-09 -COAL_03 2 - -MSG point-09 -COAL_03 3 - -MSG point-09 -COAL_03 4 - -MSG point-09 -COAL_03 1008 - - - -MSG point-10 -COAL_04 1 - -MSG point-10 -COAL_04 2 - -MSG point-10 -COAL_04 3 - -MSG point-10 -COAL_04 4 - -MSG point-10 -COAL_04 1008 - - - -MSG point-11 -COAL_05 1 - -MSG point-11 -COAL_05 2 - -MSG point-11 -COAL_05 3 - -MSG point-11 -COAL_05 4 - - - -MSG point-12 -COAL_06 10 - -MSG point-12 -COAL_06 20 - -MSG point-12 -COAL_06 30 - -MSG point-12 -COAL_06 40 - -MSG point-12 -COAL_06 10080 - - - -MSG point-13 -COAL_07A 30 -COAL_07B 3000 - -MSG point-13 -COAL_07A 20 -COAL_07B 2000 - -MSG point-13 -COAL_07A 10 -COAL_07B 1000 - -MSG point-13 -COAL_07A 10080 -COAL_07B 880 - -MSG point-13 -COAL_07A 40 -COAL_07B 400 - +# NB: 'schema:' presents in the SQLDA output for FB 6.x, we can suppress it for *this* test: +substitutions = [('[ \t]+', ' '), ('table: schema: owner:', 'table: owner:')] +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] -MSG point-14 -COAL_08A 10080 -COAL_08B 880 +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) -MSG point-14 -COAL_08A 40 -COAL_08B 400 +act = isql_act('db', test_script, substitutions=substitutions) -MSG point-14 -COAL_08A 30 -COAL_08B 3000 - -MSG point-14 -COAL_08A 20 -COAL_08B 2000 - -MSG point-14 -COAL_08A 10 -COAL_08B 1000 - - - -MSG point-15 -CASE_GROUP_BY_01 1 - -MSG point-15 -CASE_GROUP_BY_01 2 - -MSG point-15 -CASE_GROUP_BY_01 3 - -MSG point-15 -CASE_GROUP_BY_01 4 - -MSG point-15 -CASE_GROUP_BY_01 1008 - - - -MSG point-16 -CASE_GROUP_BY_02 1 - -MSG point-16 -CASE_GROUP_BY_02 2 - -MSG point-16 -CASE_GROUP_BY_02 3 - -MSG point-16 -CASE_GROUP_BY_02 4 - -MSG point-16 -CASE_GROUP_BY_02 1008 - - - -MSG point-17 -CASE_GROUP_BY_03 1 - -MSG point-17 -CASE_GROUP_BY_03 2 - -MSG point-17 -CASE_GROUP_BY_03 3 - -MSG point-17 -CASE_GROUP_BY_03 4 - - - -MSG point-18 -CASE_GROUP_BY_04 1008 - -MSG point-18 -CASE_GROUP_BY_04 4 - -MSG point-18 -CASE_GROUP_BY_04 3 - -MSG point-18 -CASE_GROUP_BY_04 2 - -MSG point-18 -CASE_GROUP_BY_04 1 - - - -MSG point-22 -N 1 - -MSG point-22 -N 2 - -MSG point-22 -N 3 - -MSG point-22 -N 4 - -MSG point-22 -N 1008 - - - -MSG point-23 -V2_N 1 -V2_X1 2 -V2_X2 3 - -MSG point-23 -V2_N 2 -V2_X1 3 -V2_X2 4 - -MSG point-23 -V2_N 3 -V2_X1 4 -V2_X2 5 - -MSG point-23 -V2_N 4 -V2_X1 5 -V2_X2 6 - -MSG point-23 -V2_N 1008 -V2_X1 1009 -V2_X2 1010 - - - -MSG point-24 -V3_N 1 -V3_X1 14 -V3_X2 26 - -MSG point-24 -V3_N 2 -V3_X1 16 -V3_X2 28 - -MSG point-24 -V3_N 3 -V3_X1 18 -V3_X2 30 - -MSG point-24 -V3_N 4 -V3_X1 20 -V3_X2 32 - -MSG point-24 -V3_N 1008 -V3_X1 2028 -V3_X2 2040 - - - -MSG point-25 -V4_N 1 -V4_X1 1 -V4_X2 3 - -MSG point-25 -V4_N 2 -V4_X1 1 -V4_X2 3 - -MSG point-25 -V4_N 3 -V4_X1 1 -V4_X2 3 - -MSG point-25 -V4_N 4 -V4_X1 1 -V4_X2 3 - -MSG point-25 -V4_N 1008 -V4_X1 1 -V4_X2 3 - - - -MSG point-26 -N 1 -X1 14 -X2 26 - -MSG point-26 -N 2 -X1 16 -X2 28 - -MSG point-26 -N 3 -X1 18 -X2 30 - -MSG point-26 -N 4 -X1 20 -X2 32 - -MSG point-26 -N 1008 -X1 2028 -X2 2040 - -MSG point-26 -N 1 -X1 14 -X2 26 - -MSG point-26 -N 2 -X1 16 -X2 28 - -MSG point-26 -N 3 -X1 18 -X2 30 - -MSG point-26 -N 4 -X1 20 -X2 32 - -MSG point-26 -N 1008 -X1 2028 -X2 2040 - - - -MSG point-27 -N 1 -X 100 -CN 1 -CX 100 - -MSG point-27 -N 2 -X 200 -CN 2 -CX 200 - -MSG point-27 -N 3 -X 300 -CN 3 -CX 300 - -MSG point-27 -N 4 -X 40 -CN 4 -CX 40 - -MSG point-27 -N 1008 -X 88 -CN 1008 -CX 88 - -MSG point-27 -N 5 -X 5 -CN 5 -CX 5 - - - -MSG point-28 -N 1 -X 100 -CN 1 -CX 100 -DC1 -DC2 - -MSG point-28 -N 2 -X 200 -CN 2 -CX 200 -DC1 -DC2 - -MSG point-28 -N 3 -X 300 -CN 3 -CX 300 -DC1 -DC2 - -MSG point-28 -N 4 -X 40 -CN 4 -CX 40 -DC1 -DC2 - -MSG point-28 -N 1008 -X 88 -CN 1008 -CX 88 -DC1 -DC2 - -MSG point-28 -N 5 -X 5 -CN 5 -CX 5 -DC1 -DC2 - -MSG point-28 -N 6 -X -CN 6 -CX -DC1 6 -DC2 - -MSG point-28 -N 7 -X -CN 7 -CX -DC1 -DC2 7 - - - -MSG point-29 -BC -1 - - - -INPUT message field count: 3 -01: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 - : name: alias: - : table: owner: -02: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 - : name: alias: - : table: owner: -03: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 - : name: alias: - : table: owner: - -OUTPUT message field count: 2 -01: sqltype: 452 TEXT scale: 0 subtype: 0 len: 8 charset: 0 NONE - : name: CONSTANT alias: MSG - : table: owner: -02: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 - : name: COALESCE alias: COALESCE - : table: owner: -""" - -expected_stderr_1 = """ -Statement failed, SQLSTATE = 42000 -Dynamic SQL Error --SQL error code = -104 --Invalid expression in the HAVING clause (neither an aggregate function nor a part of the GROUP BY clause) -Statement failed, SQLSTATE = 42000 -Dynamic SQL Error --SQL error code = -104 --Invalid expression in the select list (not contained in either an aggregate function or the GROUP BY clause) -Statement failed, SQLSTATE = 42000 -Dynamic SQL Error --SQL error code = -104 --Invalid expression in the HAVING clause (neither an aggregate function nor a part of the GROUP BY clause) -Statement failed, SQLSTATE = 23000 -Operation violates CHECK constraint T1_N on view or table T1 --At trigger 'CHECK_1' -Statement failed, SQLSTATE = 23000 -Operation violates CHECK constraint T1_CX on view or table T1 --At trigger 'CHECK_3' -Statement failed, SQLSTATE = 23000 -validation error for column "T1"."DC1", value "10" -Statement failed, SQLSTATE = 23000 -validation error for column "T1"."DC2", value "10" -Statement failed, SQLSTATE = 22012 -arithmetic exception, numeric overflow, or string truncation --Integer divide by zero. The code attempted to divide an integer value by an integer divisor of zero. -Statement failed, SQLSTATE = 07002 -Dynamic SQL Error --SQLDA error --Wrong number of parameters (expected 3, got 0) -""" - -@pytest.mark.version('>=3.0,<3.0.8') -def test_1(act_1: Action): - act_1.expected_stdout = expected_stdout - act_1.expected_stderr = expected_stderr_1 - act_1.execute() - assert (act_1.clean_stderr == act_1.clean_expected_stderr and - act_1.clean_stdout == act_1.clean_expected_stdout) - -act_2 = isql_act('db', test_script, substitutions=substitutions) - -expected_stderr_2 = """ -Statement failed, SQLSTATE = 42000 -Dynamic SQL Error --SQL error code = -104 --Invalid expression in the HAVING clause (neither an aggregate function nor a part of the GROUP BY clause) -Statement failed, SQLSTATE = 42000 -Dynamic SQL Error --SQL error code = -104 --Invalid expression in the select list (not contained in either an aggregate function or the GROUP BY clause) -Statement failed, SQLSTATE = 42000 -Dynamic SQL Error --SQL error code = -104 --Invalid expression in the HAVING clause (neither an aggregate function nor a part of the GROUP BY clause) -Statement failed, SQLSTATE = 23000 -Operation violates CHECK constraint T1_N on view or table T1 --At trigger 'CHECK_1' -Statement failed, SQLSTATE = 23000 -Operation violates CHECK constraint T1_CX on view or table T1 --At trigger 'CHECK_3' -Statement failed, SQLSTATE = 23000 -validation error for column "T1"."DC1", value "10" -Statement failed, SQLSTATE = 23000 -validation error for column "T1"."DC2", value "10" -Statement failed, SQLSTATE = 22012 -arithmetic exception, numeric overflow, or string truncation --Integer divide by zero. The code attempted to divide an integer value by an integer divisor of zero. -Statement failed, SQLSTATE = 07002 -Dynamic SQL Error --SQLDA error --No SQLDA for input values provided +expected_stdout = """ + MSG point-01 + N 1 + X 100 + CN 1 + CX 100 + MSG point-01 + N 2 + X 20 + CN 2 + CX 20 + MSG point-01 + N 3 + X 30 + CN 3 + CX 30 + MSG point-02 + N 1 + X 100 + CN 1 + CX 100 + MSG point-02 + N 2 + X 200 + CN 2 + CX 200 + MSG point-02 + N 3 + X 30 + CN 3 + CX 30 + MSG point-03 + N 1 + X 100 + CN 1 + CX 100 + VCN 1 + MSG point-03 + N 2 + X 200 + CN 2 + CX 200 + VCN 2 + MSG point-03 + N 3 + X 30 + CN 3 + CX 30 + VCN 3 + MSG point-04 + N 1 + X 100 + CN 1 + CX 100 + VCN 1 + MSG point-04 + N 2 + X 200 + CN 2 + CX 200 + VCN 2 + MSG point-04 + N 3 + X 30 + CN 3 + CX 30 + VCN 3 + MSG point-04 + N 4 + X 40 + CN 4 + CX 40 + VCN 4 + MSG point-05 + N 1 + X 100 + CN 1 + CX 100 + MSG point-05 + N 2 + X 200 + CN 2 + CX 200 + MSG point-05 + N 3 + X 300 + CN 3 + CX 300 + MSG point-05 + N 4 + X 40 + CN 4 + CX 40 + MSG point-06 + N 1 + X 100 + CN 1 + CX 100 + VCN 1 + MSG point-06 + N 2 + X 200 + CN 2 + CX 200 + VCN 2 + MSG point-06 + N 3 + X 300 + CN 3 + CX 300 + VCN 3 + MSG point-06 + N 4 + X 40 + CN 4 + CX 40 + VCN 4 + MSG point-06 + N 1008 + X 88 + CN 1008 + CX 88 + VCN 1008 + MSG point-07 + COAL_01 1 + MSG point-07 + COAL_01 2 + MSG point-07 + COAL_01 3 + MSG point-07 + COAL_01 4 + MSG point-07 + COAL_01 1008 + MSG point-08 + COAL_02 1 + MSG point-08 + COAL_02 2 + MSG point-08 + COAL_02 3 + MSG point-08 + COAL_02 4 + MSG point-08 + COAL_02 1008 + MSG point-09 + COAL_03 1 + MSG point-09 + COAL_03 2 + MSG point-09 + COAL_03 3 + MSG point-09 + COAL_03 4 + MSG point-09 + COAL_03 1008 + MSG point-10 + COAL_04 1 + MSG point-10 + COAL_04 2 + MSG point-10 + COAL_04 3 + MSG point-10 + COAL_04 4 + MSG point-10 + COAL_04 1008 + MSG point-11 + COAL_05 1 + MSG point-11 + COAL_05 2 + MSG point-11 + COAL_05 3 + MSG point-11 + COAL_05 4 + MSG point-12 + COAL_06 10 + MSG point-12 + COAL_06 20 + MSG point-12 + COAL_06 30 + MSG point-12 + COAL_06 40 + MSG point-12 + COAL_06 10080 + MSG point-13 + COAL_07A 30 + COAL_07B 3000 + MSG point-13 + COAL_07A 20 + COAL_07B 2000 + MSG point-13 + COAL_07A 10 + COAL_07B 1000 + MSG point-13 + COAL_07A 10080 + COAL_07B 880 + MSG point-13 + COAL_07A 40 + COAL_07B 400 + MSG point-14 + COAL_08A 10080 + COAL_08B 880 + MSG point-14 + COAL_08A 40 + COAL_08B 400 + MSG point-14 + COAL_08A 30 + COAL_08B 3000 + MSG point-14 + COAL_08A 20 + COAL_08B 2000 + MSG point-14 + COAL_08A 10 + COAL_08B 1000 + MSG point-15 + CASE_GROUP_BY_01 1 + MSG point-15 + CASE_GROUP_BY_01 2 + MSG point-15 + CASE_GROUP_BY_01 3 + MSG point-15 + CASE_GROUP_BY_01 4 + MSG point-15 + CASE_GROUP_BY_01 1008 + MSG point-16 + CASE_GROUP_BY_02 1 + MSG point-16 + CASE_GROUP_BY_02 2 + MSG point-16 + CASE_GROUP_BY_02 3 + MSG point-16 + CASE_GROUP_BY_02 4 + MSG point-16 + CASE_GROUP_BY_02 1008 + MSG point-17 + CASE_GROUP_BY_03 1 + MSG point-17 + CASE_GROUP_BY_03 2 + MSG point-17 + CASE_GROUP_BY_03 3 + MSG point-17 + CASE_GROUP_BY_03 4 + MSG point-18 + CASE_GROUP_BY_04 1008 + MSG point-18 + CASE_GROUP_BY_04 4 + MSG point-18 + CASE_GROUP_BY_04 3 + MSG point-18 + CASE_GROUP_BY_04 2 + MSG point-18 + CASE_GROUP_BY_04 1 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Invalid expression in the HAVING clause (neither an aggregate function nor a part of the GROUP BY clause) + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Invalid expression in the select list (not contained in either an aggregate function or the GROUP BY clause) + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Invalid expression in the HAVING clause (neither an aggregate function nor a part of the GROUP BY clause) + MSG point-22 + N 1 + MSG point-22 + N 2 + MSG point-22 + N 3 + MSG point-22 + N 4 + MSG point-22 + N 1008 + MSG point-23 + V2_N 1 + V2_X1 2 + V2_X2 3 + MSG point-23 + V2_N 2 + V2_X1 3 + V2_X2 4 + MSG point-23 + V2_N 3 + V2_X1 4 + V2_X2 5 + MSG point-23 + V2_N 4 + V2_X1 5 + V2_X2 6 + MSG point-23 + V2_N 1008 + V2_X1 1009 + V2_X2 1010 + MSG point-24 + V3_N 1 + V3_X1 14 + V3_X2 26 + MSG point-24 + V3_N 2 + V3_X1 16 + V3_X2 28 + MSG point-24 + V3_N 3 + V3_X1 18 + V3_X2 30 + MSG point-24 + V3_N 4 + V3_X1 20 + V3_X2 32 + MSG point-24 + V3_N 1008 + V3_X1 2028 + V3_X2 2040 + MSG point-25 + V4_N 1 + V4_X1 1 + V4_X2 3 + MSG point-25 + V4_N 2 + V4_X1 1 + V4_X2 3 + MSG point-25 + V4_N 3 + V4_X1 1 + V4_X2 3 + MSG point-25 + V4_N 4 + V4_X1 1 + V4_X2 3 + MSG point-25 + V4_N 1008 + V4_X1 1 + V4_X2 3 + MSG point-26 + N 1 + X1 14 + X2 26 + MSG point-26 + N 2 + X1 16 + X2 28 + MSG point-26 + N 3 + X1 18 + X2 30 + MSG point-26 + N 4 + X1 20 + X2 32 + MSG point-26 + N 1008 + X1 2028 + X2 2040 + MSG point-26 + N 1 + X1 14 + X2 26 + MSG point-26 + N 2 + X1 16 + X2 28 + MSG point-26 + N 3 + X1 18 + X2 30 + MSG point-26 + N 4 + X1 20 + X2 32 + MSG point-26 + N 1008 + X1 2028 + X2 2040 + Statement failed, SQLSTATE = 23000 + Operation violates CHECK constraint T1_N on view or table T1 + -At trigger CHECK_1 + Statement failed, SQLSTATE = 23000 + Operation violates CHECK constraint T1_CX on view or table T1 + -At trigger CHECK_3 + MSG point-27 + N 1 + X 100 + CN 1 + CX 100 + MSG point-27 + N 2 + X 200 + CN 2 + CX 200 + MSG point-27 + N 3 + X 300 + CN 3 + CX 300 + MSG point-27 + N 4 + X 40 + CN 4 + CX 40 + MSG point-27 + N 1008 + X 88 + CN 1008 + CX 88 + MSG point-27 + N 5 + X 5 + CN 5 + CX 5 + Statement failed, SQLSTATE = 23000 + validation error for column T1.DC1, value 10 + Statement failed, SQLSTATE = 23000 + validation error for column T1.DC2, value 10 + MSG point-28 + N 1 + X 100 + CN 1 + CX 100 + DC1 + DC2 + MSG point-28 + N 2 + X 200 + CN 2 + CX 200 + DC1 + DC2 + MSG point-28 + N 3 + X 300 + CN 3 + CX 300 + DC1 + DC2 + MSG point-28 + N 4 + X 40 + CN 4 + CX 40 + DC1 + DC2 + MSG point-28 + N 1008 + X 88 + CN 1008 + CX 88 + DC1 + DC2 + MSG point-28 + N 5 + X 5 + CN 5 + CX 5 + DC1 + DC2 + MSG point-28 + N 6 + X + CN 6 + CX + DC1 6 + DC2 + MSG point-28 + N 7 + X + CN 7 + CX + DC1 + DC2 7 + MSG point-29 + BC -1 + Statement failed, SQLSTATE = 22012 + arithmetic exception, numeric overflow, or string truncation + -Integer divide by zero. The code attempted to divide an integer value by an integer divisor of zero. + INPUT message field count: 3 + 01: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + : name: alias: + : table: schema: owner: + 02: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + : name: alias: + : table: schema: owner: + 03: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: alias: + : table: schema: owner: + OUTPUT message field count: 2 + 01: sqltype: 452 TEXT scale: 0 subtype: 0 len: 8 charset: 0 NONE + : name: CONSTANT alias: MSG + : table: schema: owner: + 02: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: COALESCE alias: COALESCE + : table: schema: owner: + Statement failed, SQLSTATE = 07002 + Dynamic SQL Error + -SQLDA error + -No SQLDA for input values provided """ -#-Wrong number of parameters (expected 3, got 0) @pytest.mark.version('>=3.0.8') -def test_2(act_2: Action): - act_2.expected_stdout = expected_stdout - act_2.expected_stderr = expected_stderr_2 - act_2.execute() - assert (act_2.clean_stderr == act_2.clean_expected_stderr and - act_2.clean_stdout == act_2.clean_expected_stdout) +def test_2(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_0521_test.py b/tests/bugs/core_0521_test.py index 2ff73dd7..2f3488fa 100644 --- a/tests/bugs/core_0521_test.py +++ b/tests/bugs/core_0521_test.py @@ -7,6 +7,9 @@ DESCRIPTION: JIRA: CORE-521 FBTEST: bugs.core_0521 + [23.06.2025] pzotov + Expected output was separated depending on FB version: we have to show SCHEMA name as prefix for DB object (since 6.0.0.834). + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -51,22 +54,25 @@ commit; """ -act = isql_act('db', test_script, substitutions=[('execute', 'EXECUTE'), ('-Effective user is.*', '')]) +substitutions = [('[ \t]+', ' '), ('execute', 'EXECUTE'), ('-Effective user is.*', '')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ - WHOAMI TMP$C0521 +expected_stdout_5x = """ + WHOAMI TMP$C0521 + Statement failed, SQLSTATE = 28000 + no permission for EXECUTE access to PROCEDURE PeRm """ -expected_stderr = """ +expected_stdout_6x = """ + WHOAMI TMP$C0521 Statement failed, SQLSTATE = 28000 - no permission for EXECUTE access to PROCEDURE PeRm + no permission for EXECUTE access to PROCEDURE "PUBLIC"."PeRm" """ @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout + diff --git a/tests/bugs/core_0583_test.py b/tests/bugs/core_0583_test.py index 46253533..4781665e 100644 --- a/tests/bugs/core_0583_test.py +++ b/tests/bugs/core_0583_test.py @@ -7,6 +7,13 @@ DESCRIPTION: JIRA: CORE-583 FBTEST: bugs.core_0583 +NOTES: + [22.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -40,26 +47,31 @@ select * from test1; """ -act = isql_act('db', test_script, substitutions=[('-At trigger.*', '-At trigger')]) +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' '), ('(-)?At trigger.*', 'At trigger')] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + +act = isql_act('db', test_script, substitutions = substitutions) + expected_stdout = """ + Statement failed, SQLSTATE = 23000 + Operation violates CHECK constraint TEST1_CHK on view or table TEST1 + At trigger Records affected: 0 Records affected: 0 Records affected: 0 Records affected: 0 """ -expected_stderr = """ - Statement failed, SQLSTATE = 23000 - Operation violates CHECK constraint TEST1_CHK on view or table TEST1 - -At trigger 'CHECK_3' -""" - @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_0606_test.py b/tests/bugs/core_0606_test.py index 52bfaa93..d0ebf6bc 100644 --- a/tests/bugs/core_0606_test.py +++ b/tests/bugs/core_0606_test.py @@ -7,105 +7,105 @@ DESCRIPTION: JIRA: CORE-606 FBTEST: bugs.core_0606 +FBTEST: bugs.core_0521 + [23.06.2025] pzotov + Expected output was separated depending on FB version: we have to show SCHEMA name as prefix for DB object (since 6.0.0.834). + Reimplemented: removed usage of hard-coded values for user and role name. Added substitutions to reduce irrelevant lines. + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -substitutions = [('Statement failed, SQLSTATE = HY000', ''), - ('record not found for user:.*', ''), ('read/select', 'SELECT'), - ('Data source : Firebird::.*', 'Data source : Firebird::'), - ('-At block line: [\\d]+, col: [\\d]+', '-At block line'), - ('335545254 : Effective user is.*', '')] +substitutions = [ + ('[ \t]+', ' '), + ('.* Grant permissions .*', ''), + ('Statement failed, SQLSTATE = HY000', ''), + ('record not found for user:.*', ''), ('read/select', 'SELECT'), + ('Data source : Firebird::.*', 'Data source : Firebird::'), + ('(-)?At block line: [\\d]+, col: [\\d]+', ''), + ('335545254 : Effective user is.*', '') +] db = db_factory() for_cvc_role = role_factory('db', name='"FOR CVC"') for_role = role_factory('db', name='"FOR"') cvc_user = user_factory('db', name='cvc', password='pw') -test_script = """ - recreate table "t t"(data int); - commit; - insert into "t t" values(123456); - commit; - - grant "FOR CVC" to user cvc; - grant select on table "t t" to "FOR"; - commit; - - show grants; - commit; - - set list on; - set term ^; - execute block returns(who_am_i varchar(31), i_m_playing_role varchar(31)) as - begin - for - execute statement 'select current_user, current_role from rdb$database' - on external 'localhost:' || rdb$get_context('SYSTEM','DB_NAME') - as user 'cvc' password 'pw' role '"FOR CVC"' - into who_am_i, i_m_playing_role - do - suspend; - end - ^ - - execute block returns(data int) as - begin - for - execute statement 'select data from "t t"' - on external 'localhost:' || rdb$get_context('SYSTEM','DB_NAME') - as user 'cvc' password 'pw' role '"FOR CVC"' - into data - do - suspend; - end - ^ - set term ;^ - commit; - - -- |||||||||||||||||||||||||||| - -- ###################################||| FB 4.0+, SS and SC |||############################## - -- |||||||||||||||||||||||||||| - -- If we check SS or SC and ExtConnPoolLifeTime > 0 (config parameter FB 4.0+) then current - -- DB (bugs.core_NNNN.fdb) will be 'captured' by firebird.exe process and fbt_run utility - -- will not able to drop this database at the final point of test. - -- Moreover, DB file will be hold until all activity in firebird.exe completed and AFTER this - -- we have to wait for seconds after it (discussion and small test see - -- in the letter to hvlad and dimitr 13.10.2019 11:10). - -- This means that one need to kill all connections to prevent from exception on cleanup phase: - -- SQLCODE: -901 / lock time-out on wait transaction / object is in use - -- ############################################################################################# - delete from mon$attachments where mon$attachment_id != current_connection; - commit; -""" - -act = isql_act('db', test_script, substitutions=substitutions) - -expected_stdout = """ -/* Grant permissions for this database */ -GRANT SELECT ON t t TO ROLE FOR -GRANT FOR CVC TO CVC +act = isql_act('db', substitutions = substitutions) -WHO_AM_I CVC -I_M_PLAYING_ROLE FOR CVC -""" - -expected_stderr = """ +expected_stdout_5x = """ + GRANT SELECT ON t t TO ROLE FOR + GRANT FOR CVC TO CVC + WHO_AM_I CVC + I_M_PLAYING_ROLE FOR CVC Statement failed, SQLSTATE = 42000 Execute statement error at isc_dsql_prepare : 335544352 : no permission for SELECT access to TABLE t t Statement : select data from "t t" - Data source : Firebird::localhost:C:\\FBTESTING\\QA\\FBT-REPO\\TMP\\E30.FDB - -At block line: 3, col: 7 + Data source : Firebird:: +""" +expected_stdout_6x = """ + GRANT SELECT ON PUBLIC."t t" TO ROLE "FOR" + GRANT "FOR CVC" TO CVC + GRANT USAGE ON SCHEMA PUBLIC TO PUBLIC + WHO_AM_I CVC + I_M_PLAYING_ROLE FOR CVC + Statement failed, SQLSTATE = 42000 + Execute statement error at isc_dsql_prepare : + 335544352 : no permission for SELECT access to TABLE "PUBLIC"."t t" + Statement : select data from "t t" + Data source : Firebird:: """ - +@pytest.mark.es_eds @pytest.mark.version('>=3') def test_1(act: Action, cvc_user: User, for_role: Role, for_cvc_role: Role): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + + test_sql = f""" + recreate table "t t"(data int); + commit; + insert into "t t" values(123456); + commit; + + grant {for_cvc_role.name} to user {cvc_user.name}; + grant select on table "t t" to {for_role.name}; + commit; + + show grants; + commit; + + set list on; + set term ^; + execute block returns(who_am_i varchar(31), i_m_playing_role varchar(31)) as + begin + for + execute statement 'select current_user, current_role from rdb$database' + on external 'localhost:' || rdb$get_context('SYSTEM','DB_NAME') + as user '{cvc_user.name}' password '{cvc_user.password}' role '{for_cvc_role.name}' + into who_am_i, i_m_playing_role + do + suspend; + end + ^ + + execute block returns(data int) as + begin + for + execute statement 'select data from "t t"' + on external 'localhost:' || rdb$get_context('SYSTEM','DB_NAME') + as user '{cvc_user.name}' password '{cvc_user.password}' role '{for_cvc_role.name}' + into data + do + suspend; + end + ^ + set term ;^ + commit; + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.isql(switches = ['-q'], input = test_sql, combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_0625_test.py b/tests/bugs/core_0625_test.py index 89fef563..47532cfa 100644 --- a/tests/bugs/core_0625_test.py +++ b/tests/bugs/core_0625_test.py @@ -7,6 +7,13 @@ DESCRIPTION: Getting SQL error code = -104, Token unknown count. JIRA: CORE-625 FBTEST: bugs.core_0625 +NOTES: + [23.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -37,7 +44,17 @@ -- ^^ ^^ """ -act = isql_act('db', test_script, substitutions=[('\\(+', ''), ('\\)+', '')]) +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' '), ('\\(+', ''), ('\\)+', '')] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ PLAN SORT CUSTOMERS ORDER CUSTOMERS_COUNTRY diff --git a/tests/bugs/core_0800_test.py b/tests/bugs/core_0800_test.py index ac1319a7..030ba6fb 100644 --- a/tests/bugs/core_0800_test.py +++ b/tests/bugs/core_0800_test.py @@ -7,6 +7,15 @@ DESCRIPTION: Domain DDL: move its CHECK clause from 'create' to 'alter' statement. JIRA: CORE-800 FBTEST: bugs.core_0800 +NOTES: + [24.06.2025] pzotov + FB-6.x snapshot must be 6.0.0.854-10b585b or newer, + see: #8622 (Regression: ISQL crashes on attempt to extract metadata when domain with reference to user-defined collation presents.) + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.858; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -43,7 +52,17 @@ db = db_factory(charset='UTF8', init=init_script) -act = python_act('db') +substitutions = [] +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + +act = python_act('db', substitutions = substitutions) expected_stdout = """ ALTER DOMAIN DM_TEST ADD CONSTRAINT diff --git a/tests/bugs/core_0847_test.py b/tests/bugs/core_0847_test.py index b6aaf3e6..1d9d44e4 100644 --- a/tests/bugs/core_0847_test.py +++ b/tests/bugs/core_0847_test.py @@ -7,76 +7,83 @@ DESCRIPTION: JIRA: CORE-847 FBTEST: bugs.core_0847 +NOTES: + [23.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """ +test_script = """ + set blob off; + set list on; + recreate table t ( - f1 varchar(10), - f2 varchar(10), - cf computed by (f1 || ' - ' || f2) + f1 smallint, + f2 smallint, + sum_f1_f2 computed by (f1+f2) ); - insert into t (f1,f2) values ('0123456789','abcdefghij'); + insert into t (f1,f2) values (1,2); commit; -""" -db = db_factory(init=init_script) - -test_script = """ - set blob off; - set list on; - - select f1,f2,cf as cf_before_altering from t; + select f1,f2,sum_f1_f2 as cf_before_altering from t; select b.rdb$field_name field_name, cast(a.rdb$computed_source as varchar(80)) computed_source_before_altering from rdb$fields a join rdb$relation_fields b on a.rdb$field_name = b.rdb$field_source - where b.rdb$field_name = upper('CF'); + where b.rdb$field_name = upper('SUM_F1_F2'); - alter table t alter cf type varchar(30); + alter table t alter sum_f1_f2 type bigint; commit; - select f1,f2,cf as cf_after_altering from t; + select f1,f2,sum_f1_f2 as cf_after_altering from t; select b.rdb$field_name field_name, cast(a.rdb$computed_source as varchar(80)) computed_source_after_altering from rdb$fields a join rdb$relation_fields b on a.rdb$field_name = b.rdb$field_source - where b.rdb$field_name = upper('CF'); + where b.rdb$field_name = upper('SUM_F1_F2'); """ -act = isql_act('db', test_script) +db = db_factory() -expected_stdout = """ - F1 0123456789 - F2 abcdefghij - CF_BEFORE_ALTERING 0123456789 - abcdefghij +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] - FIELD_NAME CF - COMPUTED_SOURCE_BEFORE_ALTERING (f1 || ' - ' || f2) +substitutions = [('[ \t]+', ' ')] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) - F1 0123456789 - F2 abcdefghij - CF_AFTER_ALTERING 0123456789 - abcdefghij +act = isql_act('db', test_script, substitutions = substitutions) - FIELD_NAME CF - COMPUTED_SOURCE_AFTER_ALTERING (f1 || ' - ' || f2) -""" - -expected_stderr = """ +expected_stdout = """ + F1 1 + F2 2 + CF_BEFORE_ALTERING 3 + FIELD_NAME SUM_F1_F2 + COMPUTED_SOURCE_BEFORE_ALTERING (f1+f2) Statement failed, SQLSTATE = 42000 unsuccessful metadata update -ALTER TABLE T failed - -Cannot add or remove COMPUTED from column CF + -Cannot add or remove COMPUTED from column SUM_F1_F2 + F1 1 + F2 2 + CF_AFTER_ALTERING 3 + FIELD_NAME SUM_F1_F2 + COMPUTED_SOURCE_AFTER_ALTERING (f1+f2) """ @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_0850_test.py b/tests/bugs/core_0850_test.py index 84ebbd62..899b1382 100644 --- a/tests/bugs/core_0850_test.py +++ b/tests/bugs/core_0850_test.py @@ -7,31 +7,46 @@ DESCRIPTION: JIRA: CORE-850 FBTEST: bugs.core_0850 +NOTES: + [23.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """create table t2(a int, b int computed by (00)); -commit; +db = db_factory() + +test_script = """ + create table t2(a int, b int computed by (00)); + alter table t2 alter b set default 5; """ -db = db_factory(init=init_script) +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] -test_script = """alter table t2 alter b set default 5; -""" +substitutions = [('[ \t]+', ' ')] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) -act = isql_act('db', test_script) +act = isql_act('db', test_script, substitutions = substitutions) -expected_stderr = """Statement failed, SQLSTATE = 42000 -unsuccessful metadata update --ALTER TABLE T2 failed --Cannot add or remove COMPUTED from column B +expected_stdout = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -ALTER TABLE T2 failed + -Cannot add or remove COMPUTED from column B """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr - + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_0851_test.py b/tests/bugs/core_0851_test.py index 9c196903..1aea6f16 100644 --- a/tests/bugs/core_0851_test.py +++ b/tests/bugs/core_0851_test.py @@ -7,31 +7,47 @@ DESCRIPTION: JIRA: CORE-851 FBTEST: bugs.core_0851 +NOTES: + [23.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """create table t (i integer); -commit; +db = db_factory() + +test_script = """ + create table t (f01 int); + create index ti on t(f01,f01); """ -db = db_factory(init=init_script) +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] -test_script = """create index ti on t(i,i); -""" +substitutions = [('[ \t]+', ' ')] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) -act = isql_act('db', test_script) +act = isql_act('db', test_script, substitutions = substitutions) -expected_stderr = """Statement failed, SQLSTATE = 42000 -unsuccessful metadata update --CREATE INDEX TI failed --Field I cannot be used twice in index TI +expected_stdout = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -CREATE INDEX TI failed + -Field F01 cannot be used twice in index TI """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_0856_test.py b/tests/bugs/core_0856_test.py index 81fe1e0d..5e134f70 100644 --- a/tests/bugs/core_0856_test.py +++ b/tests/bugs/core_0856_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-856 FBTEST: bugs.core_0856 +NOTES: + [25.06.2025] pzotov + Minimal snapshot number for 6.x: 6.0.0.863, see letter to Adriano, 24.06.2025 16:01. Fixed in commit: + https://github.com/FirebirdSQL/firebird/commit/b3da90583735da2b01c8c8129240cfffced6c1dc + + Checked on 6.0.0.863; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -24,7 +30,7 @@ set list on; select sec$user_name, sec$first_name, sec$middle_name, sec$last_name - from sec$users where upper(sec$user_name)=upper('tmp$c0856'); + from sec$users where upper(sec$user_name) = upper('tmp$c0856'); alter user tmp$c0856 firstname '' @@ -68,6 +74,6 @@ @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_0857_test.py b/tests/bugs/core_0857_test.py index 0b8ae763..47506058 100644 --- a/tests/bugs/core_0857_test.py +++ b/tests/bugs/core_0857_test.py @@ -1,76 +1,78 @@ -#coding:utf-8 - -""" -ID: issue-1247 -ISSUE: 1247 -TITLE: Containing not working correctly -DESCRIPTION: -JIRA: CORE-857 -FBTEST: bugs.core_0857 -NOTES: - [06.10.2022] pzotov - Could not complete adjusting for LINUX in new-qa. - DEFERRED. - -""" - -import platform -import pytest -from firebird.qa import * - -init_script = """ -set echo on; -set bail on; - create collation test_coll_ci_ai for win1252 from WIN_PTBR - case insensitive - accent insensitive - ; - - create table test ( - id int, - f01 varchar(100), - f02 varchar(100) collate WIN_PTBR - ); - - insert into test(id, f01) values(1, 'IHF|groß|850xC|P1'); - update test set f02=f01; - commit; - create view v_test as - select octet_length(t.f01) - octet_length(replace(t.f01, 'ß', '')) as "octet_length diff:" from test t; -""" - -db = db_factory(charset='WIN1252', init=init_script) - -expected_stdout = """ - CONNECTION_CSET WIN1252 - test_1 result: - test_2 result: 1 - ci_ai result: 1 - between result: 1 - octet_length diff: 1 -""" - -test_script = """ - set list on; - select c.rdb$character_set_name as connection_cset - from mon$attachments a - join rdb$character_sets c on a.mon$character_set_id = c.rdb$character_set_id - where a.mon$attachment_id = current_connection; - - select t.id as "test_1 result:" from rdb$database r left join test t on t.f01 not containing 'P1' and t.f01 like 'IHF|gro_|850_C|P1'; - select t.id as "test_2 result:" from rdb$database r left join test t on t.f01 containing 'P1' and t.f01 like 'IHF|gro_|850_C|P1'; - select t.id as "ci_ai result:" from rdb$database r left join test t on lower(t.f02) = upper(t.f02); - select t.id as "between result:" from rdb$database r left join test t on lower(t.f01) between lower(t.f02) and upper(t.f02); - select * from v_test; -""" - -act = isql_act('db', test_script) - -@pytest.mark.skipif(platform.system() != 'Windows', reason='FIXME: see notes') -@pytest.mark.version('>=3') -def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() - assert act.clean_stdout == act.clean_expected_stdout - - +#coding:utf-8 + +""" +ID: issue-1247 +ISSUE: 1247 +TITLE: Containing not working correctly +DESCRIPTION: +JIRA: CORE-857 +FBTEST: bugs.core_0857 +NOTES: + [31.10.2024] pzotov + Bug was fixed for too old FB (2.0 RC4 / 2.1 Alpha 1), firebird-driver and/or QA-plugin + will not able to run on this version in order to reproduce problem. + + Checked on 6.0.0.511 (Windows/Linux); 5.0.2.1550; 4.0.6.3165; 3.0.2.32670, 3,0,1,32609 +""" +from pathlib import Path + +import pytest +from firebird.qa import * + +db = db_factory(charset='WIN1252') +act = isql_act('db', substitutions=[('[ \\t]+', ' ')]) +tmp_sql = temp_file('tmp_core_0857.sql') + +@pytest.mark.intl +@pytest.mark.version('>=3.0.0') +def test_1(act: Action, tmp_sql: Path): + + test_script = """ + set bail on; + create collation test_coll_ci_ai for win1252 from WIN_PTBR + case insensitive + accent insensitive + ; + + create table test ( + id int, + f01 varchar(100), + f02 varchar(100) collate WIN_PTBR + ); + + insert into test(id, f01) values(1, 'IHF|groß|850xC|P1'); + update test set f02=f01; + commit; + create view v_test as + select octet_length(t.f01) - octet_length(replace(t.f01, 'ß', '')) as "octet_length diff:" from test t; + commit; + + set list on; + select c.rdb$character_set_name as connection_cset + from mon$attachments a + join rdb$character_sets c on a.mon$character_set_id = c.rdb$character_set_id + where a.mon$attachment_id = current_connection; + + select t.id as "test_1 result:" from rdb$database r left join test t on t.f01 not containing 'P1' and t.f01 like 'IHF|gro_|850_C|P1'; + select t.id as "test_2 result:" from rdb$database r left join test t on t.f01 containing 'P1' and t.f01 like 'IHF|gro_|850_C|P1'; + select t.id as "ci_ai result:" from rdb$database r left join test t on lower(t.f02) = upper(t.f02); + select t.id as "between result:" from rdb$database r left join test t on lower(t.f01) between lower(t.f02) and upper(t.f02); + select * from v_test; + """ + + # ::: NB ::: + # For proper output of test, input script must be encoded in cp1252 rather than in UTF-8. + # + tmp_sql.write_text(test_script, encoding = 'cp1252') + + act.expected_stdout = """ + CONNECTION_CSET WIN1252 + test_1 result: + test_2 result: 1 + ci_ai result: 1 + between result: 1 + octet_length diff: 1 + """ + + act.isql(switches = ['-q'], input_file = tmp_sql, charset = 'win1252', combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_0866_test.py b/tests/bugs/core_0866_test.py index 03f7037c..3d0189dd 100644 --- a/tests/bugs/core_0866_test.py +++ b/tests/bugs/core_0866_test.py @@ -7,23 +7,26 @@ DESCRIPTION: JIRA: CORE-866 FBTEST: bugs.core_0866 +NOTES: + [23.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """ + +test_script = """ recreate table test ( id integer not null, col varchar(20) not null ); insert into test (id, col) values (1, 'data'); commit; -""" - -db = db_factory(init=init_script) - -test_script = """ update rdb$relation_fields set rdb$null_flag = null where (rdb$field_name = upper('col')) and (rdb$relation_name = upper('test')); @@ -32,9 +35,21 @@ update test set col = null where id = 1; """ -act = isql_act('db', test_script) +db = db_factory() + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' ')] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + +act = isql_act('db', test_script, substitutions = substitutions) -expected_stderr = """ +expected_stdout = """ Statement failed, SQLSTATE = 42000 UPDATE operation is not allowed for system table RDB$RELATION_FIELDS Statement failed, SQLSTATE = 23000 @@ -43,7 +58,7 @@ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_0878_test.py b/tests/bugs/core_0878_test.py index c10ceafe..be02d607 100644 --- a/tests/bugs/core_0878_test.py +++ b/tests/bugs/core_0878_test.py @@ -2,11 +2,18 @@ """ ID: issue-1271 -ISSUE: 1271 -TITLE: Problem when dropping column that is a primary key +ISSUE: https://github.com/FirebirdSQL/firebird/issues/1271 +TITLE: Column involved in the constraint (e.g. PK) could NOT be dropped if constraint has user-defined name DESCRIPTION: JIRA: CORE-878 FBTEST: bugs.core_0878 +NOTES: + [23.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -14,55 +21,74 @@ db = db_factory() -test_script = """create table pk1 (i1 integer not null, i2 integer); -alter table pk1 add primary key (i1); -commit; -show table pk1; -alter table pk1 drop i1; -commit; +test_script = """ + create table pk1 (i1 integer not null, i2 integer); + alter table pk1 add primary key (i1); + commit; + show table pk1; + alter table pk1 drop i1; + commit; -create table pk2 (i1 integer not null, i2 integer); -alter table pk2 add constraint pk2_pk primary key (i1); -commit; -show table pk2; -alter table pk2 drop i1; -commit; + create table pk2 (i1 integer not null, i2 integer); + alter table pk2 add constraint pk2_pk primary key (i1); + commit; + show table pk2; + alter table pk2 drop i1; + commit; -create table pk3 (i1 integer not null primary key, i2 integer); -commit; -show table pk3; -alter table pk3 drop i1; -commit; + create table pk3 (i1 integer not null primary key, i2 integer); + commit; + show table pk3; + alter table pk3 drop i1; + commit; -show table pk1; + show table pk1; -show table pk2; + show table pk2; -show table pk3; + show table pk3; """ -act = isql_act('db', test_script) - -expected_stdout = """I1 INTEGER Not Null -I2 INTEGER Nullable -CONSTRAINT INTEG_2: - Primary key (I1) -I1 INTEGER Not Null -I2 INTEGER Nullable -CONSTRAINT PK2_PK: - Primary key (I1) -I1 INTEGER Not Null -I2 INTEGER Nullable -CONSTRAINT INTEG_5: - Primary key (I1) -I2 INTEGER Nullable -I2 INTEGER Nullable -I2 INTEGER Nullable + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' '), ('Table: .*', '')] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + +act = isql_act('db', test_script, substitutions = substitutions) + + +expected_stdout = """ + I1 INTEGER Not Null + I2 INTEGER Nullable + CONSTRAINT INTEG_2: + Primary key (I1) + + I1 INTEGER Not Null + I2 INTEGER Nullable + CONSTRAINT PK2_PK: + Primary key (I1) + + I1 INTEGER Not Null + I2 INTEGER Nullable + CONSTRAINT INTEG_5: + Primary key (I1) + + I2 INTEGER Nullable + + I2 INTEGER Nullable + + I2 INTEGER Nullable """ @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_0879_test.py b/tests/bugs/core_0879_test.py index 3b460bc8..e31cda21 100644 --- a/tests/bugs/core_0879_test.py +++ b/tests/bugs/core_0879_test.py @@ -7,6 +7,13 @@ DESCRIPTION: JIRA: CORE-879 FBTEST: bugs.core_0879 +NOTES: + [23.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -23,9 +30,19 @@ show table tab; """ -act = isql_act('db', test_script) +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' '), ('Table: .*', '')] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + +act = isql_act('db', test_script, substitutions = substitutions) -expected_stderr = """ +expected_stdout = """ Statement failed, SQLSTATE = 42000 unsuccessful metadata update -key size exceeds implementation restriction for index "IX" @@ -34,7 +51,7 @@ @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_0883_test.py b/tests/bugs/core_0883_test.py index 5fb07caa..bc435bcd 100644 --- a/tests/bugs/core_0883_test.py +++ b/tests/bugs/core_0883_test.py @@ -7,14 +7,52 @@ DESCRIPTION: JIRA: CORE-883 FBTEST: bugs.core_0883 +NOTES: + [25.06.2025] pzotov + Important change has performed vs previous version of this test: we *create* new database here instead of using .fbk + Additional lines present in BLR if we restore DB: + "blr_flags / blr_flags_search_system_cache, 0,0 / blr_end" + Sent letter to Adriano, 23.06.2025 17:07 + (subj: "BLR for stored procedure differs in 6.x depending on whether we create this SP in empty DB or this database was restored ...") + See replies from Adriano: 23.06.2025 17:28, 24.06.2025 14:39 (summary: these addtitional lines are *still* required). + + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.863; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -db = db_factory(from_backup='core0883-ods12.fbk') +db = db_factory() test_script = """ + set term ^; + create or alter procedure sp1 as + declare v_time time; + declare v_timestamp timestamp; + declare v_sp_id int; + declare v_src_text blob; + declare v_blr_text blob; + declare c_sttm cursor for ( + select p.rdb$procedure_id, p.rdb$procedure_source, p.rdb$procedure_blr + from rdb$procedures p + ); + begin + v_time = current_time (3); + v_timestamp = current_timestamp(3); + open c_sttm; + while (1=1) do + begin + fetch c_sttm into v_sp_id, v_src_text, v_blr_text; + if ( row_count = 0 ) then leave; + end + close c_sttm; + end ^ + set term ;^ + commit; + set list on; set blob all; select rdb$procedure_blr @@ -22,114 +60,205 @@ where rdb$procedure_name = upper('sp1'); """ -act = isql_act('db', test_script, substitutions=[('RDB\\$PROCEDURE_BLR.*', '')]) - -expected_stdout = """ -RDB$PROCEDURE_BLR 1a:f1 - blr_version5, - blr_begin, - blr_message, 1, 1,0, - blr_short, 0, - blr_begin, - blr_declare, 0,0, blr_sql_time, - blr_assignment, - blr_null, - blr_variable, 0,0, - blr_declare, 1,0, blr_timestamp, - blr_assignment, - blr_null, - blr_variable, 1,0, - blr_declare, 2,0, blr_long, 0, - blr_assignment, - blr_null, - blr_variable, 2,0, - blr_declare, 3,0, blr_blob2, 0,0, 0,0, - blr_assignment, - blr_null, - blr_variable, 3,0, - blr_declare, 4,0, blr_blob2, 0,0, 0,0, - blr_assignment, - blr_null, - blr_variable, 4,0, - blr_dcl_cursor, 0,0, - blr_rse, 1, - blr_relation2, 14, 'R','D','B','$','P','R','O','C','E','D','U','R','E','S', - 8, 'C','_','S','T','T','M',32,'P', 0, - blr_end, - 3,0, - blr_derived_expr, 1, 0, - blr_field, 0, 16, 'R','D','B','$','P','R','O','C','E','D','U','R','E','_','I','D', - blr_derived_expr, 1, 0, - blr_field, 0, 20, 'R','D','B','$','P','R','O','C','E','D','U','R','E','_','S','O','U','R','C','E', - blr_derived_expr, 1, 0, - blr_field, 0, 17, 'R','D','B','$','P','R','O','C','E','D','U','R','E','_','B','L','R', - blr_stall, - blr_label, 0, - blr_begin, - blr_begin, - blr_assignment, - blr_current_time2, 3, - blr_variable, 0,0, - blr_assignment, - blr_current_timestamp, - blr_variable, 1,0, - blr_cursor_stmt, 0, 0,0, +#act = isql_act('db', test_script, substitutions = [('[ \t]+', ' '), ('RDB\\$PROCEDURE_BLR.*', '')]) +act = isql_act('db', test_script, substitutions = [ ('RDB\\$PROCEDURE_BLR.*', '')]) - blr_begin, - blr_end, - blr_label, 1, - blr_loop, - blr_begin, - blr_if, - blr_eql, - blr_literal, blr_long, 0, 1,0,0,0, - blr_literal, blr_long, 0, 1,0,0,0, - blr_begin, - blr_begin, - blr_cursor_stmt, 2, 0,0, - - blr_begin, - blr_assignment, - blr_field, 0, 16, 'R','D','B','$','P','R','O','C','E','D','U','R','E','_','I','D', - blr_variable, 2,0, - blr_assignment, - blr_field, 0, 20, 'R','D','B','$','P','R','O','C','E','D','U','R','E','_','S','O','U','R','C','E', - blr_variable, 3,0, - blr_assignment, - blr_field, 0, 17, 'R','D','B','$','P','R','O','C','E','D','U','R','E','_','B','L','R', - blr_variable, 4,0, - blr_end, - blr_if, - blr_eql, - blr_internal_info, - blr_literal, blr_long, 0, 5,0,0,0, - blr_literal, blr_long, 0, 0,0,0,0, - blr_leave, 1, - blr_end, - blr_end, - blr_end, - blr_leave, 1, - blr_end, - blr_cursor_stmt, 1, 0,0, - - blr_begin, - blr_end, - blr_end, - blr_end, - blr_end, - blr_send, 1, - blr_begin, - blr_assignment, - blr_literal, blr_short, 0, 0,0, - blr_parameter, 1, 0,0, - blr_end, - blr_end, - blr_eoc +expected_stdout_5x = """ + RDB$PROCEDURE_BLR 1a:1e2 + blr_version5, + blr_begin, + blr_message, 1, 1,0, + blr_short, 0, + blr_begin, + blr_declare, 0,0, blr_sql_time, + blr_assignment, + blr_null, + blr_variable, 0,0, + blr_declare, 1,0, blr_timestamp, + blr_assignment, + blr_null, + blr_variable, 1,0, + blr_declare, 2,0, blr_long, 0, + blr_assignment, + blr_null, + blr_variable, 2,0, + blr_declare, 3,0, blr_blob2, 0,0, 0,0, + blr_assignment, + blr_null, + blr_variable, 3,0, + blr_declare, 4,0, blr_blob2, 0,0, 0,0, + blr_assignment, + blr_null, + blr_variable, 4,0, + blr_dcl_cursor, 0,0, + blr_rse, 1, + blr_relation2, 14, 'R','D','B','$','P','R','O','C','E','D','U','R','E','S', + 8, 'C','_','S','T','T','M',32,'P', 0, + blr_end, + 3,0, + blr_derived_expr, 1, 0, + blr_field, 0, 16, 'R','D','B','$','P','R','O','C','E','D','U','R','E','_','I','D', + blr_derived_expr, 1, 0, + blr_field, 0, 20, 'R','D','B','$','P','R','O','C','E','D','U','R','E','_','S','O','U','R','C','E', + blr_derived_expr, 1, 0, + blr_field, 0, 17, 'R','D','B','$','P','R','O','C','E','D','U','R','E','_','B','L','R', + blr_stall, + blr_label, 0, + blr_begin, + blr_begin, + blr_assignment, + blr_current_time2, 3, + blr_variable, 0,0, + blr_assignment, + blr_current_timestamp, + blr_variable, 1,0, + blr_cursor_stmt, 0, 0,0, + blr_label, 1, + blr_loop, + blr_begin, + blr_if, + blr_eql, + blr_literal, blr_long, 0, 1,0,0,0, + blr_literal, blr_long, 0, 1,0,0,0, + blr_begin, + blr_begin, + blr_cursor_stmt, 2, 0,0, + blr_begin, + blr_assignment, + blr_field, 0, 16, 'R','D','B','$','P','R','O','C','E','D','U','R','E','_','I','D', + blr_variable, 2,0, + blr_assignment, + blr_field, 0, 20, 'R','D','B','$','P','R','O','C','E','D','U','R','E','_','S','O','U','R','C','E', + blr_variable, 3,0, + blr_assignment, + blr_field, 0, 17, 'R','D','B','$','P','R','O','C','E','D','U','R','E','_','B','L','R', + blr_variable, 4,0, + blr_end, + blr_if, + blr_eql, + blr_internal_info, + blr_literal, blr_long, 0, 5,0,0,0, + blr_literal, blr_long, 0, 0,0,0,0, + blr_leave, 1, + blr_end, + blr_end, + blr_end, + blr_leave, 1, + blr_end, + blr_cursor_stmt, 1, 0,0, + blr_end, + blr_end, + blr_end, + blr_send, 1, + blr_begin, + blr_assignment, + blr_literal, blr_short, 0, 0,0, + blr_parameter, 1, 0,0, + blr_end, + blr_end, + blr_eoc """ +expected_stdout_6x = """ + RDB$PROCEDURE_BLR 1a:1e2 + blr_version5, + blr_begin, + blr_message, 1, 1,0, + blr_short, 0, + blr_begin, + blr_declare, 0,0, blr_sql_time, + blr_declare, 1,0, blr_timestamp, + blr_declare, 2,0, blr_long, 0, + blr_declare, 3,0, blr_blob2, 0,0, 0,0, + blr_declare, 4,0, blr_blob2, 0,0, 0,0, + blr_dcl_cursor, 0,0, + blr_rse, 1, + blr_relation3, + 6, 'S','Y','S','T','E','M', + 14, 'R','D','B','$','P','R','O','C','E','D','U','R','E','S', + 12, 34,'C','_','S','T','T','M',34,32,34,'P',34, + 0, + blr_end, + 3,0, + blr_derived_expr, 1, 0, + blr_field, 0, 16, 'R','D','B','$','P','R','O','C','E','D','U','R','E','_','I','D', + blr_derived_expr, 1, 0, + blr_field, 0, 20, 'R','D','B','$','P','R','O','C','E','D','U','R','E','_','S','O','U','R','C','E', + blr_derived_expr, 1, 0, + blr_field, 0, 17, 'R','D','B','$','P','R','O','C','E','D','U','R','E','_','B','L','R', + blr_assignment, + blr_null, + blr_variable, 0,0, + blr_assignment, + blr_null, + blr_variable, 1,0, + blr_assignment, + blr_null, + blr_variable, 2,0, + blr_assignment, + blr_null, + blr_variable, 3,0, + blr_assignment, + blr_null, + blr_variable, 4,0, + blr_stall, + blr_label, 0, + blr_begin, + blr_begin, + blr_assignment, + blr_current_time2, 3, + blr_variable, 0,0, + blr_assignment, + blr_current_timestamp, + blr_variable, 1,0, + blr_cursor_stmt, 0, 0,0, + blr_label, 1, + blr_loop, + blr_begin, + blr_if, + blr_eql, + blr_literal, blr_long, 0, 1,0,0,0, + blr_literal, blr_long, 0, 1,0,0,0, + blr_begin, + blr_begin, + blr_cursor_stmt, 2, 0,0, + blr_begin, + blr_assignment, + blr_field, 0, 16, 'R','D','B','$','P','R','O','C','E','D','U','R','E','_','I','D', + blr_variable, 2,0, + blr_assignment, + blr_field, 0, 20, 'R','D','B','$','P','R','O','C','E','D','U','R','E','_','S','O','U','R','C','E', + blr_variable, 3,0, + blr_assignment, + blr_field, 0, 17, 'R','D','B','$','P','R','O','C','E','D','U','R','E','_','B','L','R', + blr_variable, 4,0, + blr_end, + blr_if, + blr_eql, + blr_internal_info, + blr_literal, blr_long, 0, 5,0,0,0, + blr_literal, blr_long, 0, 0,0,0,0, + blr_leave, 1, + blr_end, + blr_end, + blr_end, + blr_leave, 1, + blr_end, + blr_cursor_stmt, 1, 0,0, + blr_end, + blr_end, + blr_end, + blr_send, 1, + blr_begin, + blr_assignment, + blr_literal, blr_short, 0, 0,0, + blr_parameter, 1, 0,0, + blr_end, + blr_end, + blr_eoc +""" @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_0885_test.py b/tests/bugs/core_0885_test.py index 2e0aaa98..4baf8d23 100644 --- a/tests/bugs/core_0885_test.py +++ b/tests/bugs/core_0885_test.py @@ -7,169 +7,178 @@ DESCRIPTION: JIRA: CORE-885 FBTEST: bugs.core_0885 +NOTES: + [23.06.2025] pzotov + Reimplemented: removed usage of hard-coded values for user and role name. + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ +import locale import pytest from firebird.qa import * db = db_factory() -user_1_senior = user_factory('db', name='john_senior', password='sen') -user_1_junior = user_factory('db', name='mick_junior', password='jun') -role_1 = role_factory('db', name='modifier', do_not_create=True) - -test_script = """ - set wng off; - set list on; - - recreate table test(id int, text varchar(30), changed_by_user varchar(31), changed_by_role varchar(31)); - commit; - - set term ^; - create trigger test_biu for test active before insert or update position 0 as - begin - new.changed_by_user = current_user; - new.changed_by_role = current_role; - end - ^ - set term ;^ - commit; - - insert into test(id, text) values(1, 'Initial data, added by SYSDBA'); - insert into test(id, text) values(2, 'Initial data, added by SYSDBA'); - insert into test(id, text) values(3, 'Initial data, added by SYSDBA'); - select * from test; - commit; - - grant select on test to public; - grant create role to john_senior; - grant update(text) on test to john_senior with grant option; - commit; - - ---------------------------------------- - - --set echo on; - --show grants; - connect '$(DSN)' user 'JOHN_SENIOR' password 'sen'; - create role modifier; - commit; - grant update (text) on test to modifier; -- this CAN be done by john_senior because he was granted to control access on this field - grant modifier to mick_junior; -- this CAN be done by john_senior because he CREATED role 'modifier' and thus he is MEMBER of it. - commit; - --show grants; - - connect '$(DSN)' user 'MICK_JUNIOR' password 'jun' role 'MODIFIER'; - select current_user, current_role from rdb$database; - update test set text = 'Update-1: through the ROLE' where id = 1; - select * from test; - - commit; - - connect '$(DSN)' user 'JOHN_SENIOR' password 'sen'; - select current_user, current_role from rdb$database; - update test set text = 'Update-2: directly by USER' where id = 2; - select * from test; - commit; - - connect '$(DSN)' user 'JOHN_SENIOR' password 'sen'; - - -- ########################################################################################### - -- ### H e r e w e R E V O K E r i g h t t o u p d a t e c o l u m n ### - -- ########################################################################################### - -- ::: NB ::: See CORE-4836: - -- As of WI-T3.0.0.31873, if we want to revoke privilege on certain COLUMN update, we must do - -- it immediatelly after reconnect, NO issuing any DML here (like `select * from test` etc). - - revoke update(text) on test from modifier; ------------- ########### R E V O K E ######## - commit; - - connect '$(DSN)' user 'MICK_JUNIOR' password 'jun' role 'MODIFIER'; - select current_user, current_role from rdb$database; - update test set text = 'Update-3: again using ROLE' where id = 3; - select * from test; - - commit; - - connect '$(DSN)' user 'SYSDBA' password 'masterkey'; - - --drop role modifier; - drop table test; - commit; -""" -act = isql_act('db', test_script, substitutions=[('-Effective user is.*', '')]) +tmp_usr_senior = user_factory('db', name='tmp_0885_john_senior', password='sen') +tmp_usr_junior = user_factory('db', name='tmp_0885_mick_junior', password='jun') +tmp_role = role_factory('db', name='tmp_0885_modifier', do_not_create=True) -expected_stdout = """ - ID 1 - TEXT Initial data, added by SYSDBA - CHANGED_BY_USER SYSDBA - CHANGED_BY_ROLE NONE - ID 2 - TEXT Initial data, added by SYSDBA - CHANGED_BY_USER SYSDBA - CHANGED_BY_ROLE NONE - ID 3 - TEXT Initial data, added by SYSDBA - CHANGED_BY_USER SYSDBA - CHANGED_BY_ROLE NONE - - - USER MICK_JUNIOR - ROLE MODIFIER - ID 1 - TEXT Update-1: through the ROLE - CHANGED_BY_USER MICK_JUNIOR - CHANGED_BY_ROLE MODIFIER - ID 2 - TEXT Initial data, added by SYSDBA - CHANGED_BY_USER SYSDBA - CHANGED_BY_ROLE NONE - ID 3 - TEXT Initial data, added by SYSDBA - CHANGED_BY_USER SYSDBA - CHANGED_BY_ROLE NONE - - USER JOHN_SENIOR - ROLE NONE - ID 1 - TEXT Update-1: through the ROLE - CHANGED_BY_USER MICK_JUNIOR - CHANGED_BY_ROLE MODIFIER - ID 2 - TEXT Update-2: directly by USER - CHANGED_BY_USER JOHN_SENIOR - CHANGED_BY_ROLE NONE - ID 3 - TEXT Initial data, added by SYSDBA - CHANGED_BY_USER SYSDBA - CHANGED_BY_ROLE NONE - - USER MICK_JUNIOR - ROLE MODIFIER - ID 1 - TEXT Update-1: through the ROLE - CHANGED_BY_USER MICK_JUNIOR - CHANGED_BY_ROLE MODIFIER - ID 2 - TEXT Update-2: directly by USER - CHANGED_BY_USER JOHN_SENIOR - CHANGED_BY_ROLE NONE - ID 3 - TEXT Initial data, added by SYSDBA - CHANGED_BY_USER SYSDBA - CHANGED_BY_ROLE NONE -""" +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' '), ('-Effective user is.*', '')] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) -expected_stderr = """ +act = isql_act('db', substitutions = substitutions) + +expected_stdout = """ + ID 1 + TEXT Initial data, added by SYSDBA + CHANGED_BY_USER SYSDBA + CHANGED_BY_ROLE NONE + ID 2 + TEXT Initial data, added by SYSDBA + CHANGED_BY_USER SYSDBA + CHANGED_BY_ROLE NONE + ID 3 + TEXT Initial data, added by SYSDBA + CHANGED_BY_USER SYSDBA + CHANGED_BY_ROLE NONE + USER TMP_0885_MICK_JUNIOR + ROLE TMP_0885_MODIFIER + ID 1 + TEXT Update-1: through the ROLE + CHANGED_BY_USER TMP_0885_MICK_JUNIOR + CHANGED_BY_ROLE TMP_0885_MODIFIER + ID 2 + TEXT Initial data, added by SYSDBA + CHANGED_BY_USER SYSDBA + CHANGED_BY_ROLE NONE + ID 3 + TEXT Initial data, added by SYSDBA + CHANGED_BY_USER SYSDBA + CHANGED_BY_ROLE NONE + USER TMP_0885_JOHN_SENIOR + ROLE NONE + ID 1 + TEXT Update-1: through the ROLE + CHANGED_BY_USER TMP_0885_MICK_JUNIOR + CHANGED_BY_ROLE TMP_0885_MODIFIER + ID 2 + TEXT Update-2: directly by USER + CHANGED_BY_USER TMP_0885_JOHN_SENIOR + CHANGED_BY_ROLE NONE + ID 3 + TEXT Initial data, added by SYSDBA + CHANGED_BY_USER SYSDBA + CHANGED_BY_ROLE NONE + USER TMP_0885_MICK_JUNIOR + ROLE TMP_0885_MODIFIER Statement failed, SQLSTATE = 28000 no permission for UPDATE access to TABLE TEST + ID 1 + TEXT Update-1: through the ROLE + CHANGED_BY_USER TMP_0885_MICK_JUNIOR + CHANGED_BY_ROLE TMP_0885_MODIFIER + ID 2 + TEXT Update-2: directly by USER + CHANGED_BY_USER TMP_0885_JOHN_SENIOR + CHANGED_BY_ROLE NONE + ID 3 + TEXT Initial data, added by SYSDBA + CHANGED_BY_USER SYSDBA + CHANGED_BY_ROLE NONE """ - @pytest.mark.version('>=3.0') -def test_1(act: Action, user_1_senior: User, user_1_junior: User, role_1: Role): +def test_1(act: Action, tmp_usr_senior: User, tmp_usr_junior: User, tmp_role: Role): + + test_sql = f""" + set wng off; + set list on; + + recreate table test(id int, text varchar(30), changed_by_user varchar(31), changed_by_role varchar(31)); + commit; + + set term ^; + create trigger test_biu for test active before insert or update position 0 as + begin + new.changed_by_user = current_user; + new.changed_by_role = current_role; + end + ^ + set term ;^ + commit; + + insert into test(id, text) values(1, 'Initial data, added by SYSDBA'); + insert into test(id, text) values(2, 'Initial data, added by SYSDBA'); + insert into test(id, text) values(3, 'Initial data, added by SYSDBA'); + select * from test; + commit; + + grant select on test to public; + grant create role to {tmp_usr_senior.name}; + grant update(text) on test to {tmp_usr_senior.name} with grant option; + commit; + + ---------------------------------------- + + --set echo on; + connect '{act.db.dsn}' user '{tmp_usr_senior.name}' password '{tmp_usr_senior.password}'; + create role {tmp_role.name}; + commit; + grant update (text) on test to {tmp_role.name}; -- this CAN be done by john_senior because he was granted to control access on this field + grant {tmp_role.name} to {tmp_usr_junior.name}; -- this CAN be done by john_senior because he CREATED role 'modifier' and thus he is MEMBER of it. + commit; + + connect '{act.db.dsn}' user '{tmp_usr_junior.name}' password '{tmp_usr_junior.password}' role '{tmp_role.name}'; + select current_user, current_role from rdb$database; + update test set text = 'Update-1: through the ROLE' where id = 1; + select * from test; + + commit; + + connect '{act.db.dsn}' user '{tmp_usr_senior.name}' password '{tmp_usr_senior.password}'; + select current_user, current_role from rdb$database; + update test set text = 'Update-2: directly by USER' where id = 2; + select * from test; + commit; + + connect '{act.db.dsn}' user '{tmp_usr_senior.name}' password '{tmp_usr_senior.password}'; + + -- ########################################################################################### + -- ### H e r e w e R E V O K E r i g h t t o u p d a t e c o l u m n ### + -- ########################################################################################### + -- ::: NB ::: See CORE-4836: + -- As of WI-T3.0.0.31873, if we want to revoke privilege on certain COLUMN update, we must do + -- it immediatelly after reconnect, NO issuing any DML here (like `select * from test` etc). + + revoke update(text) on test from {tmp_role.name}; ------------- ########### R E V O K E ######## + commit; + + connect '{act.db.dsn}' user '{tmp_usr_junior.name}' password '{tmp_usr_junior.password}' role '{tmp_role.name}'; + select current_user, current_role from rdb$database; + update test set text = 'Update-3: again using ROLE' where id = 3; + select * from test; + + commit; + + connect '{act.db.dsn}' user 'SYSDBA' password 'masterkey'; + + --drop role {tmp_role.name}; + drop table test; + commit; + """ + act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + act.isql(switches = ['-q'], input = test_sql, combine_output = True, io_enc = locale.getpreferredencoding()) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_0886_test.py b/tests/bugs/core_0886_test.py index c1d1db86..f01c21c7 100644 --- a/tests/bugs/core_0886_test.py +++ b/tests/bugs/core_0886_test.py @@ -2,55 +2,66 @@ """ ID: issue-1279 -ISSUE: 1279 -TITLE: SPs in views +ISSUE: https://github.com/FirebirdSQL/firebird/issues/1279 +TITLE: Ability to query a stored procedur from view. DESCRIPTION: JIRA: CORE-886 FBTEST: bugs.core_0886 +NOTES: + [23.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """set term !!; -create procedure MY_PROCEDURE (input1 INTEGER) -returns (output1 INTEGER) -as begin - output1 = input1+1; - suspend; -end !! -set term ;!! -commit; +db = db_factory() -""" +test_script = """ + set list on; + set blob all; + set term ^; + create procedure MY_PROCEDURE (input1 INTEGER) + returns (output1 INTEGER) as + begin + output1 = input1+1; + suspend; + end ^ + set term ;^ + commit; -db = db_factory(init=init_script) + create view a_view as + select * from MY_PROCEDURE(1); + commit; -test_script = """create view a_view as -select * from MY_PROCEDURE(1); -commit; -show view a_view; -select *from a_view; + select rdb$view_source as blob_id from rdb$relations where rdb$relation_name = upper('A_VIEW'); + select * from a_view; """ -act = isql_act('db', test_script) +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] -expected_stdout = """Database: test.fdb, User: SYSDBA -SQL> CON> SQL> SQL> OUTPUT1 INTEGER Nullable -View Source: -==== ====== +substitutions = [('[ \t]+', ' '), ('BLOB_ID.*', '')] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) -select * from MY_PROCEDURE(1) -SQL> - OUTPUT1 -============ - 2 +act = isql_act('db', test_script, substitutions = substitutions) -SQL> SQL>""" +expected_stdout = """ + select * from MY_PROCEDURE(1) + OUTPUT1 2 +""" @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_0899_test.py b/tests/bugs/core_0899_test.py index ac45fa5c..690860ba 100644 --- a/tests/bugs/core_0899_test.py +++ b/tests/bugs/core_0899_test.py @@ -2,139 +2,147 @@ """ ID: issue-1296 -ISSUE: 1296 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/1296 TITLE: Problems with explicit cursors in unwanted states DESCRIPTION: JIRA: CORE-899 FBTEST: bugs.core_0899 +NOTES: + [23.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """create table T (ID integer, TXT varchar(30)); -commit; - -insert into T values (1,'Text description'); -commit; - -set term ^; - -create procedure SP_OK returns (ID integer, TXT varchar(30)) -as - declare C cursor for ( select ID, TXT from T ); -begin - open C; - while (1 = 1) do - begin - fetch C into :ID, :TXT; - if (ROW_COUNT = 0) then - leave; - update T set TXT = 'OK' where current of C; - suspend; - end - close C; -end ^ - -create procedure SP_CLOSED returns (ID integer, TXT varchar(30)) -as - declare C cursor for ( select ID, TXT from T ); -begin - open C; - while (1 = 1) do - begin - fetch C into :ID, :TXT; - if (ROW_COUNT = 0) then - leave; - suspend; - end - close C; - update T set TXT = 'SP_CLOSED' where current of C; -end ^ - -create procedure SP_NOTOPEN returns (ID integer, TXT varchar(30)) -as - declare C cursor for ( select ID, TXT from T ); -begin - update T set TXT = 'SP_NOTOPEN' where current of C; - open C; - while (1 = 1) do - begin - fetch C into :ID, :TXT; - if (ROW_COUNT = 0) then - leave; - suspend; - end - close C; -end ^ - -create procedure SP_FETCHED returns (ID integer, TXT varchar(30)) -as - declare C cursor for ( select ID, TXT from T ); -begin - open C; - while (1 = 1) do - begin - fetch C into :ID, :TXT; - if (ROW_COUNT = 0) then - leave; - suspend; - end - update T set TXT = 'SP_FETCHED' where current of C; - close C; -end ^ - -set term ; ^ - -commit; +db = db_factory() + +test_script = """ + create table T (ID integer, TXT varchar(30)); + commit; + + insert into T values (1,'Text description'); + commit; + + set term ^; + + create procedure SP_OK returns (ID integer, TXT varchar(30)) + as + declare C cursor for ( select ID, TXT from T ); + begin + open C; + while (1 = 1) do + begin + fetch C into :ID, :TXT; + if (ROW_COUNT = 0) then + leave; + update T set TXT = 'OK' where current of C; + suspend; + end + close C; + end ^ + + create procedure SP_CLOSED returns (ID integer, TXT varchar(30)) + as + declare C cursor for ( select ID, TXT from T ); + begin + open C; + while (1 = 1) do + begin + fetch C into :ID, :TXT; + if (ROW_COUNT = 0) then + leave; + suspend; + end + close C; + update T set TXT = 'SP_CLOSED' where current of C; + end ^ + + create procedure SP_NOTOPEN returns (ID integer, TXT varchar(30)) + as + declare C cursor for ( select ID, TXT from T ); + begin + update T set TXT = 'SP_NOTOPEN' where current of C; + open C; + while (1 = 1) do + begin + fetch C into :ID, :TXT; + if (ROW_COUNT = 0) then + leave; + suspend; + end + close C; + end ^ + + create procedure SP_FETCHED returns (ID integer, TXT varchar(30)) + as + declare C cursor for ( select ID, TXT from T ); + begin + open C; + while (1 = 1) do + begin + fetch C into :ID, :TXT; + if (ROW_COUNT = 0) then + leave; + suspend; + end + update T set TXT = 'SP_FETCHED' where current of C; + close C; + end ^ + + set term ; ^ + + commit; + + set list on; + select * from SP_OK; + select * from SP_CLOSED; + select * from SP_NOTOPEN; + select * from SP_FETCHED; """ -db = db_factory(init=init_script) +substitutions=[('[ \t]+', ' '), ('line:\\s+\\d+,', 'line: x'), ('col:\\s+\\d+', 'col: y')] -test_script = """select * from SP_OK; -select * from SP_CLOSED; -select * from SP_NOTOPEN; -select * from SP_FETCHED; +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] -""" +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) -act = isql_act('db', test_script, - substitutions=[('line:\\s[0-9]+,', 'line: x'), ('col:\\s[0-9]+', 'col: y')]) +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ - ID TXT -============ ============================== - 1 Text description + ID 1 + TXT Text description + ID 1 + TXT OK - ID TXT -============ ============================== - 1 OK + Statement failed, SQLSTATE = 22000 + no current record for fetch operation + -At procedure SP_CLOSED line: x col: y - ID TXT -============ ============================== + Statement failed, SQLSTATE = 22000 + no current record for fetch operation + -At procedure SP_NOTOPEN line: x col: y - ID TXT -============ ============================== - 1 OK -""" + ID 1 + TXT OK -expected_stderr = """Statement failed, SQLSTATE = 22000 -no current record for fetch operation --At procedure 'SP_CLOSED' line: 14, col: 3 -Statement failed, SQLSTATE = 22000 -no current record for fetch operation --At procedure 'SP_NOTOPEN' line: 5, col: 3 -Statement failed, SQLSTATE = 22000 -no current record for fetch operation --At procedure 'SP_FETCHED' line: 13, col: 3 + Statement failed, SQLSTATE = 22000 + no current record for fetch operation + -At procedure SP_FETCHED line: x col: y """ @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_0907_test.py b/tests/bugs/core_0907_test.py index 0273390b..fee0de1b 100644 --- a/tests/bugs/core_0907_test.py +++ b/tests/bugs/core_0907_test.py @@ -7,6 +7,13 @@ DESCRIPTION: JIRA: CORE-907 FBTEST: bugs.core_0907 +NOTES: + [23.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -56,29 +63,38 @@ show table crash; """ -act = isql_act('db', test_script) +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] -expected_stdout = """ - A1 INTEGER Not Null - A2 INTEGER Not Null - A3 INTEGER Not Null - A4 INTEGER Not Null - A5 Computed by: (a2*a3*a4) -""" +substitutions = [('[ \t]+', ' '), ('Table: .*', '')] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) -expected_stderr = """ +act = isql_act('db', test_script, substitutions = substitutions) + +expected_stdout = """ Statement failed, SQLSTATE = 22006 unsuccessful metadata update -Cannot make field A5 of table CRASH NOT NULL because there are NULLs present Statement failed, SQLSTATE = 23000 - validation error for column "CRASH"."A1", value "*** null ***" + validation error for column CRASH.A1, value *** null *** + + A1 INTEGER Not Null + A2 INTEGER Not Null + A3 INTEGER Not Null + A4 INTEGER Not Null + A5 Computed by: (a2*a3*a4) +""" + +expected_stderr = """ """ @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_0908_test.py b/tests/bugs/core_0908_test.py index 2afff4a0..7e08a265 100644 --- a/tests/bugs/core_0908_test.py +++ b/tests/bugs/core_0908_test.py @@ -5,149 +5,167 @@ ISSUE: 1307 TITLE: Garbage in plan output of complex statement DESCRIPTION: - This is unfortunate case. The fix for 2.1 went through several "adjustments" and we've - get lost in changes. The result is that this was not properly fixed in 2.1 line (server - doesn't crash, but don't returns the truncated plan as supposed either). Now when 2.1 - line is at 2.1.3 we can hope for proper fix in 2.1.4. It should work as intended in 2.5 line. + This is unfortunate case. The fix for 2.1 went through several "adjustments" and we've + get lost in changes. The result is that this was not properly fixed in 2.1 line (server + doesn't crash, but don't returns the truncated plan as supposed either). Now when 2.1 + line is at 2.1.3 we can hope for proper fix in 2.1.4. It should work as intended in 2.5 line. JIRA: CORE-908 FBTEST: bugs.core_0908 +NOTES: + [23.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """set term ^; - -create procedure big_plan - returns (x integer) -as -begin - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; - select 1 from rdb$database into :x; -/* select 1 from rdb$relations into :x; */ - suspend; -end ^ -set term ;^ +init_script = """ + set term ^; + create procedure big_plan + returns (x integer) + as + begin + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + select 1 from rdb$database into :x; + /* select 1 from rdb$relations into :x; */ + suspend; + end ^ + set term ;^ """ db = db_factory(init=init_script) -test_script = """set plan on; -select * from big_plan ; +test_script = """ + set plan on; + set list on; + select * from big_plan ; """ -act = isql_act('db', test_script) +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' ')] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """PLAN (BIG_PLAN NATURAL) -X -============ -1 +expected_stdout = """ + PLAN (BIG_PLAN NATURAL) + X 1 """ @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_0945_test.py b/tests/bugs/core_0945_test.py index d3c069e5..23c92c0d 100644 --- a/tests/bugs/core_0945_test.py +++ b/tests/bugs/core_0945_test.py @@ -7,6 +7,13 @@ DESCRIPTION: JIRA: CORE-945 FBTEST: bugs.core_0945 +NOTES: + [23.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -14,28 +21,38 @@ db = db_factory() -test_script = """CREATE TABLE TAB_TestA ( - UID INTEGER NOT NULL PRIMARY KEY -); +test_script = """ + create table tmain ( + uid int not null primary key + ); + create table tdetl ( + uid int not null primary key, + pid integer constraint fk_tdetl references non_existing_0945(uid) on update cascade + ); +""" -CREATE TABLE TAB_TestB ( - UID INTEGER NOT NULL PRIMARY KEY, - TestA INTEGER CONSTRAINT FK_TestA REFERENCES TABTestA(UID) ON UPDATE CASCADE -); +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] -""" +substitutions = [('[ \t]+', ' ')] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) -act = isql_act('db', test_script) +act = isql_act('db', test_script, substitutions = substitutions) -expected_stderr = """Statement failed, SQLSTATE = 42000 -unsuccessful metadata update --CREATE TABLE TAB_TESTB failed --Table TABTESTA not found +expected_stdout = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -CREATE TABLE TDETL failed + -Table NON_EXISTING_0945 not found """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_0967_test.py b/tests/bugs/core_0967_test.py index 0cbe2091..bbc48172 100644 --- a/tests/bugs/core_0967_test.py +++ b/tests/bugs/core_0967_test.py @@ -22,6 +22,7 @@ act = python_act('db') +@pytest.mark.intl @pytest.mark.version('>=2.1') def test_1(act: Action): with act.db.connect() as con: diff --git a/tests/bugs/core_0986_test.py b/tests/bugs/core_0986_test.py index 0402b37d..da60b44f 100644 --- a/tests/bugs/core_0986_test.py +++ b/tests/bugs/core_0986_test.py @@ -2,14 +2,14 @@ """ ID: issue-1393 -ISSUE: 1393 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/1393 TITLE: Non-ASCII quoted identifiers are not converted to metadata (UNICODE_FSS) charset DESCRIPTION: JIRA: CORE-986 FBTEST: bugs.core_0986 NOTES: [25.11.2023] pzotov - Writing code requires more care since 6.0.0.150: ISQL does not allow specifying duplicate delimiters without any statements between them (two semicolon, two carets etc). + Writing code requires more care since 6.0.0.150: ISQL does not allow specifying duplicate delimiters without any statements between them (two semicolon, two carets etc) """ import pytest @@ -23,18 +23,18 @@ expected_stdout_a = """create collation "Циферки" for utf8 from unicode case insensitive 'NUMERIC-SORT=1';""" expected_stderr_a_40 = """ -Statement failed, SQLSTATE = 22018 -arithmetic exception, numeric overflow, or string truncation --Cannot transliterate character between character sets -After line 4 in file non_ascii_ddl.sql + Statement failed, SQLSTATE = 22018 + arithmetic exception, numeric overflow, or string truncation + -Cannot transliterate character between character sets + After line 4 in file non_ascii_ddl.sql """ expected_stderr_a_30 = """ -Statement failed, SQLSTATE = 22000 -unsuccessful metadata update --CREATE COLLATION Циферки failed --Malformed string -After line 4 in file non_ascii_ddl.sql + Statement failed, SQLSTATE = 22000 + unsuccessful metadata update + -CREATE COLLATION Циферки failed + -Malformed string + After line 4 in file non_ascii_ddl.sql """ non_ascii_ddl=''' @@ -62,8 +62,7 @@ create role "манагер"; create role "начсклд"; - -- TEMPLY COMMENTED UNTIL CORE-5209 IS OPEN: - -- ISQL -X ignores connection charset for text of EXCEPTION message (restoring it in initial charset when exception was created) + -- enabled since CORE-5209 was fixed: recreate exception "Невзлет" 'Запись обломалась, ваши не пляшут. Но не стесняйтесь и обязательно заходите еще, мы всегда рады видеть вас. До скорой встречи, товарищ!'; commit; @@ -171,8 +170,6 @@ '; -------------------------------------------------- commit; - --/* - --TEMPLY COMMENTED UNTIL CORE-5221 IS OPEN: set echo on; show collation; show domain; @@ -183,27 +180,29 @@ show view; show procedure; show role; - --*/ set list on; set echo off; select 'Metadata created OK.' as msg from rdb$database; ''' -tmp_file = temp_file('non_ascii_ddl.sql') +tmp_file = temp_file('tmp_0986_non_ascii_ddl.sql') +@pytest.mark.intl @pytest.mark.version('>=3.0') def test_1(act: Action, tmp_file: Path): tmp_file.write_bytes(non_ascii_ddl.encode('cp1251')) + # run without specifying charset + ################################ act.expected_stdout = expected_stdout_a act.expected_stderr = expected_stderr_a_40 if act.is_version('>=4.0') else expected_stderr_a_30 act.isql(switches=['-q'], input_file=tmp_file, charset=None, io_enc='cp1251') assert (act.clean_stdout == act.clean_expected_stdout and act.clean_stderr == act.clean_expected_stderr) - # run with charset + + # run _with_ charset + #################### act.reset() act.isql(switches=['-q'], input_file=tmp_file, charset='win1251', io_enc='cp1251') assert act.clean_stdout.endswith('Metadata created OK.') - - diff --git a/tests/bugs/core_0995_test.py b/tests/bugs/core_0995_test.py index 08284fa6..31932517 100644 --- a/tests/bugs/core_0995_test.py +++ b/tests/bugs/core_0995_test.py @@ -7,6 +7,13 @@ DESCRIPTION: JIRA: CORE-995 FBTEST: bugs.core_0995 +NOTES: + [23.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -69,7 +76,18 @@ order by orgaccount.id; """ -act = isql_act('db', test_script) + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' ')] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ PLAN JOIN (ORGACCOUNT ORDER PK_ORGACCOUNT, ORG INDEX (PK_ORG)) @@ -79,6 +97,6 @@ @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1010_test.py b/tests/bugs/core_1010_test.py index 0c7d02cb..ae5b5db1 100644 --- a/tests/bugs/core_1010_test.py +++ b/tests/bugs/core_1010_test.py @@ -5,11 +5,20 @@ ISSUE: 1420 TITLE: Local buffer overrun in DYN_error() that takes down the server DESCRIPTION: - We have a local buffer overrun in DYN_error(), while copying tdbb_status_vector to - local_status. It seems to be the first time (DYN errors + stack trace facility) when 20 - status words are not enough to store the complete error info. + We have a local buffer overrun in DYN_error(), while copying tdbb_status_vector to + local_status. It seems to be the first time (DYN errors + stack trace facility) when 20 + status words are not enough to store the complete error info. JIRA: CORE-1010 FBTEST: bugs.core_1010 +NOTES: + [25.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Minimal snapshot number for 6.x: 6.0.0.863, see letter from Adriano, 24.06.2025 13:12, commit: + https://github.com/FirebirdSQL/firebird/commit/9c6855b516de4e4aea78e7df782e297f4e220287 + + Checked on 6.0.0.863; 3.0.13.33813. """ import pytest @@ -18,14 +27,13 @@ db = db_factory() test_script = """ - -- Removed old code: all attempts to create triggers on SYSTEM tables now are prohibited, even for SYSDBA. - create exception ex_test '!!!'; + + create exception ex_test 'Trigger suddenly was created!'; commit; set term ^ ; create or alter trigger rdb$procedures_biu for rdb$procedures - active after update or delete position 0 - as + active after update or delete position 0 as begin exception ex_test; end @@ -41,18 +49,24 @@ commit; """ -act = isql_act('db', test_script, substitutions=[('line:.*', ''), ('col:.*', '')]) +act = isql_act('db', test_script) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 28000 unsuccessful metadata update -CREATE OR ALTER TRIGGER RDB$PROCEDURES_BIU failed -no permission for ALTER access to TABLE RDB$PROCEDURES """ +expected_stdout_6x = """ + Statement failed, SQLSTATE = 28000 + CREATE OR ALTER TRIGGER "SYSTEM"."RDB$PROCEDURES_BIU" failed + -Cannot CREATE/ALTER/DROP TRIGGER in SYSTEM schema +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1026_utf8_test.py b/tests/bugs/core_1026_utf8_test.py index 3710434b..15e8bb07 100644 --- a/tests/bugs/core_1026_utf8_test.py +++ b/tests/bugs/core_1026_utf8_test.py @@ -261,6 +261,7 @@ Records affected: 30 """ +@pytest.mark.intl @pytest.mark.version('>=4.0') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_1029_test.py b/tests/bugs/core_1029_test.py index 4f7c5b45..16ec6afa 100644 --- a/tests/bugs/core_1029_test.py +++ b/tests/bugs/core_1029_test.py @@ -2,61 +2,66 @@ """ ID: issue-1444 -ISSUE: 1444 -TITLE: ad plan in outer joins with IS NULL clauses (dependent on order of predicates) +ISSUE: https://github.com/FirebirdSQL/firebird/issues/1444 +TITLE: Bad plan in outer joins with IS NULL clauses (dependent on order of predicates) DESCRIPTION: JIRA: CORE-1029 FBTEST: bugs.core_1029 +NOTES: + [24.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """create table tb1 (id int, col int) ; -create index tbi1 on tb1 (id) ; -create index tbi2 on tb1 (col) ; - -insert into tb1 values (1, 1) ; -insert into tb1 values (2, 2) ; -insert into tb1 values (1, null) ; - -commit; -""" +db = db_factory() -db = db_factory(init=init_script) +test_script = """ + create table tb1 (id int, col int) ; -test_script = """set plan on; + insert into tb1 values (1, 1) ; + insert into tb1 values (2, 2) ; + insert into tb1 values (1, null) ; + commit; + create index tbi1 on tb1 (id); + create index tbi2 on tb1 (col); + commit; -select * from tb1 a - left join tb1 b on a.id = b.id - where a.col is null and a.col+0 is null; + set planonly; + select * from tb1 a + left join tb1 b on a.id = b.id + where a.col is null and a.col+0 is null; -select * from tb1 a - left join tb1 b on a.id = b.id - where a.col+0 is null and a.col is null; + select * from tb1 a + left join tb1 b on a.id = b.id + where a.col+0 is null and a.col is null; """ -act = isql_act('db', test_script) - -expected_stdout = """PLAN JOIN (A INDEX (TBI2), B INDEX (TBI1)) - ID COL ID COL -============ ============ ============ ============ - 1 1 1 - 1 1 +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] -PLAN JOIN (A INDEX (TBI2), B INDEX (TBI1)) +substitutions = [('[ \t]+', ' ')] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) - ID COL ID COL -============ ============ ============ ============ - 1 1 1 - 1 1 +act = isql_act('db', test_script, substitutions = substitutions) +expected_stdout = """ + PLAN JOIN (A INDEX (TBI2), B INDEX (TBI1)) + PLAN JOIN (A INDEX (TBI2), B INDEX (TBI1)) """ @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_1056_test.py b/tests/bugs/core_1056_test.py index 03332c2f..9a79977f 100644 --- a/tests/bugs/core_1056_test.py +++ b/tests/bugs/core_1056_test.py @@ -7,50 +7,58 @@ DESCRIPTION: JIRA: CORE-1056 FBTEST: bugs.core_1056 +NOTES: + [24.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.858; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """create table t (c varchar(10) character set win1250 collate pxw_csy); -insert into t values ('ch'); -commit; -""" +db = db_factory() -db = db_factory(init=init_script) +test_script = """ -test_script = """set plan on; + set list on; + create table t (c varchar(10) character set win1250 collate pxw_csy); + insert into t values ('ch'); + commit; -select * from t where c starting with 'c'; -commit; + set plan on; + select * from t where c starting with 'c'; + commit; -create index t_c on t (c); -commit; + create index t_c on t (c); + commit; -select * from t where c starting with 'c'; + select * from t where c starting with 'c'; """ -act = isql_act('db', test_script) - -expected_stdout = """ -PLAN (T NATURAL) - -C -========== -ch - +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] -PLAN (T INDEX (T_C)) +substitutions = [('[ \t]+', ' ')] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) -C -========== -ch +act = isql_act('db', test_script, substitutions = substitutions) +expected_stdout = """ + PLAN (T NATURAL) + C ch + PLAN (T INDEX (T_C)) + C ch """ @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_1058_addi_test.py b/tests/bugs/core_1058_addi_test.py index 6a56985b..895bd36d 100644 --- a/tests/bugs/core_1058_addi_test.py +++ b/tests/bugs/core_1058_addi_test.py @@ -18,8 +18,14 @@ FLD_DOMAIN_DEFINED VARCHAR(10) CHARACTER SET UTF8 COLLATE UNICODE_CI_AI Nullable // why not win1250 ? FLD_EXPLICIT_TYPE1 VARCHAR(10) CHARACTER SET WIN1257 COLLATE WIN1257_EE Nullable // why not win1252 ? FLD_EXPLICIT_TYPE2 VARCHAR(10) CHARACTER SET UTF8 COLLATE UNICODE_CI Nullable // why not win1257 ? - Initially report was sent to Adriano, Dmitry et al, 05-oct-2023 08:18. - Fix 22-JAN-2024 17:42 in: https://github.com/FirebirdSQL/firebird/commit/11dec10f9fc079ed74d623211e01f465e45d6a7c + Initially report was sent to Adriano, Dmitry et al, 05-oct-2023 08:18. + Fix 22-JAN-2024 17:42 in: https://github.com/FirebirdSQL/firebird/commit/11dec10f9fc079ed74d623211e01f465e45d6a7c + + [25.06.2025] pzotov + Minimal snapshot number for 6.x: 6.0.0.863, see letter to Adriano, 24.06.2025 11:05. Fixed in commit: + https://github.com/FirebirdSQL/firebird/commit/cbbbf3b94e7508806142eea0cd330ed4eedbbcdc + + Checked on 6.0.0.863; 3.0.13.33813. """ import pytest diff --git a/tests/bugs/core_1058_test.py b/tests/bugs/core_1058_test.py index 4c7b2e05..7d011048 100644 --- a/tests/bugs/core_1058_test.py +++ b/tests/bugs/core_1058_test.py @@ -11,18 +11,16 @@ [05.10.2023] pzotov 1. Removed SHOW command for check result because its output often changes. It is enough for this test to verify just absense of any error messages. - 2. Changed test queries. - 3. ::: NB ::: Have a question about case when we change table fields type using - alter table test - alter column fld_domain_defined type varchar(10) character set win1250 - ,alter column fld_explicit_type1 type varchar(10) character set win1252 - ,alter column fld_explicit_type2 type varchar(10) character set win1257 - ; - Result shows that column types NOT changed in this case and remains previous - FLD_DOMAIN_DEFINED VARCHAR(10) CHARACTER SET UTF8 COLLATE UNICODE_CI_AI Nullable // why not win1250 ? - FLD_EXPLICIT_TYPE1 VARCHAR(10) CHARACTER SET WIN1257 COLLATE WIN1257_EE Nullable // why not win1252 ? - FLD_EXPLICIT_TYPE2 VARCHAR(10) CHARACTER SET UTF8 COLLATE UNICODE_CI Nullable // why not win1257 ? - Sent report to Adriano, Dmitry et al, 05-oct-2023 08:18. Waiting for reply. + 2. There was issue about wrong change of columns collation, see: + https://github.com/FirebirdSQL/firebird/issues/7924 + Fixed in 6.0.0.219, commit: + https://github.com/FirebirdSQL/firebird/commit/11dec10f9fc079ed74d623211e01f465e45d6a7c + + [25.06.2025] pzotov + Minimal snapshot number for 6.x: 6.0.0.863, see letter to Adriano, 24.06.2025 11:05. Fixed in commit: + https://github.com/FirebirdSQL/firebird/commit/cbbbf3b94e7508806142eea0cd330ed4eedbbcdc + + Checked on 6.0.0.863; 3.0.13.33813. """ import pytest @@ -105,29 +103,6 @@ select 'domain_info, point-2' as msg, v.* from v_domain_info v; select 'table info, point-2' as msg, v.* from v_fields_info v; - - --------------------------------------------------------------- - - /* - !! TEMPORARY DISABLED. - !! LETTER TO ADRIANO, DIMITR ET AL, 05-OCT-2023 08:18. - !! WAITING FOR REPLY. - - alter domain dm_test type varchar(10) character set win1253; - - alter table test - alter column fld_domain_defined type varchar(10) character set win1250 - ,alter column fld_explicit_type1 type varchar(10) character set win1252 - ,alter column fld_explicit_type2 type varchar(10) character set win1257 - ; - commit; - - connect '$(DSN)'; - - select 'domain_info, point-3' as msg, v.* from v_domain_info v; - select 'table info, point-3' as msg, v.* from v_fields_info v; - */ - """ act = isql_act('db', test_script) diff --git a/tests/bugs/core_1083_test.py b/tests/bugs/core_1083_test.py index 27c33436..3b7c18ef 100644 --- a/tests/bugs/core_1083_test.py +++ b/tests/bugs/core_1083_test.py @@ -2,78 +2,70 @@ """ ID: issue-1504 -ISSUE: 1504 -TITLE: User (not SYSDBA) what have privileges with grant option, can't revoke privileges, granted by other user or SYSDBA +ISSUE: https://github.com/FirebirdSQL/firebird/issues/1504 +TITLE: User (not SYSDBA) that have privileges with grant option, can't revoke privileges, granted by other user or SYSDBA DESCRIPTION: JIRA: CORE-1083 FBTEST: bugs.core_1083 +NOTES: + [23.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -substitutions = [('set echo .*', ''), - ('-TMP\\$C1083 is not grantor of (UPDATE|Update|update) on TAB2 to ROLE1.', - '-TMP$C1083 is not grantor of UPDATE on TAB2 to ROLE1.')] - db = db_factory() +tmp_user = user_factory('db', name='tmp$c1083', password='123') +tmp_role = role_factory('db', name='dba_helper') -test_script = """ - -- Refactored 05-JAN-2016: removed dependency on recource 'test_user'. - -- Checked on WI-V3.0.0.32266 (SS/SC/CS). - -- Checked 06.08.2018: added 'substitutions' because different case if some words in error message - -- ('Update' in 3.0.x vs 'UPDATE' in 4.0) - -- 3.0.4.33021: OK, 1.563s. - -- 4.0.0.1143: OK, 2.703s. +substitutions = [] - create or alter user tmp$c1083 password 'QweRtyUioP'; - commit; - recreate table tab1(col1 integer); - recreate table tab2(col2 integer); - commit; - create role role1; - grant update (col1) on tab1 to tmp$c1083 with grant option; - grant update (col2) on tab2 to role1; - commit; +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] - connect 'localhost:$(DATABASE_LOCATION)test.fdb' user 'TMP$C1083' password 'QweRtyUioP'; - --set bail on; - set echo on; - grant update(col1) on tab1 to role1; - revoke update(col1) on tab1 from role1; - revoke update(col2) on tab2 from role1; - set echo off; - commit; +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) - connect 'localhost:$(DATABASE_LOCATION)test.fdb' user 'SYSDBA' password 'masterkey'; - set echo on; - drop user tmp$c1083; - set echo off; - commit; - -- ('-TMP\\$C1083 is not grantor.*', '') -""" +act = isql_act('db', substitutions=substitutions) -act = isql_act('db', test_script, substitutions=substitutions) +@pytest.mark.version('>=3.0') +def test_1(act: Action, tmp_user: User, tmp_role: Role): -expected_stdout = """ - grant update(col1) on tab1 to role1; - revoke update(col1) on tab1 from role1; - revoke update(col2) on tab2 from role1; - drop user tmp$c1083; -""" + test_sql = f""" + recreate table tab1(col1 integer); + recreate table tab2(col2 integer); + commit; -expected_stderr = """ - Statement failed, SQLSTATE = 42000 - unsuccessful metadata update - -REVOKE failed - -TMP$C1083 is not grantor of UPDATE on TAB2 to ROLE1. -""" + grant update (col1) on tab1 to {tmp_user.name} with grant option; + grant update (col2) on tab2 to {tmp_role.name}; + commit; + + connect '{act.db.dsn}' user '{tmp_user.name}' password '{tmp_user.password}'; + set echo on; + grant update(col1) on tab1 to {tmp_role.name}; + revoke update(col1) on tab1 from {tmp_role.name}; + revoke update(col2) on tab2 from {tmp_role.name}; + """ + + expected_stdout = f""" + grant update(col1) on tab1 to {tmp_role.name.upper()}; + revoke update(col1) on tab1 from {tmp_role.name.upper()}; + revoke update(col2) on tab2 from {tmp_role.name.upper()}; + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -REVOKE failed + -{tmp_user.name} is not grantor of UPDATE on TAB2 to {tmp_role.name.upper()}. + """ -@pytest.mark.version('>=3.0') -def test_1(act: Action): act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + act.isql(switches = ['-q'], input = test_sql, combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1090_test.py b/tests/bugs/core_1090_test.py index fb302007..52201828 100644 --- a/tests/bugs/core_1090_test.py +++ b/tests/bugs/core_1090_test.py @@ -2,46 +2,55 @@ """ ID: issue-1511 -ISSUE: 1511 -TITLE: Error msg "Could not find UNIQUE INDEX" when in fact one is present +ISSUE: https://github.com/FirebirdSQL/firebird/issues/1511 +TITLE: FK-definition. Make error message more relevant when parent table has no appropriate PK/UK constraint. DESCRIPTION: JIRA: CORE-1090 FBTEST: bugs.core_1090 +NOTES: + [24.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Previous test title: Error msg "Could not find UNIQUE INDEX" when in fact one is present [CORE1090] + Checked on 6.0.0.858; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """create table t (i integer not null); -create unique index ti on t(i); -commit; -""" +db = db_factory() -db = db_factory(init=init_script) +test_script = """ + create table tmain(id int not null); + create unique index tmain_id_unq on tmain(id); + create table tdetl(pid int references tmain(id)); +""" -test_script = """show table t; -show index ti; +substitutions = [] -create table t2 (i integer references t(i)); -""" +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] -act = isql_act('db', test_script) +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) -expected_stdout = """I INTEGER Not Null -TI UNIQUE INDEX ON T(I) -""" +act = isql_act('db', test_script, substitutions = substitutions) -expected_stderr = """Statement failed, SQLSTATE = 42000 -unsuccessful metadata update --CREATE TABLE T2 failed --could not find UNIQUE or PRIMARY KEY constraint in table T with specified columns +expected_stdout = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -CREATE TABLE TDETL failed + -could not find UNIQUE or PRIMARY KEY constraint in table TMAIN with specified columns """ @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1095_test.py b/tests/bugs/core_1095_test.py index df5bc2b2..51059042 100644 --- a/tests/bugs/core_1095_test.py +++ b/tests/bugs/core_1095_test.py @@ -3,10 +3,18 @@ """ ID: issue-1517 ISSUE: 1517 -TITLE: Support BETWEEN predicate for select expressions +TITLE: Support `BETWEEN` predicate for select expressions DESCRIPTION: JIRA: CORE-1095 FBTEST: bugs.core_1095 +NOTES: + [24.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + Also, for this test 'schema:' in SQLDA output is suppressed because as not relevant to check. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.858; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -40,7 +48,19 @@ ; """ -act = isql_act('db', test_script) +# NB: 'schema:' presents in the SQLDA output for FB 6.x, we can suppress it for *this* test: +substitutions = [('[ \t]+', ' '), ('table: schema: owner:', 'table: owner:')] + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + +act = isql_act('db', test_script, substitutions=substitutions) expected_stdout = """ INPUT message field count: 2 diff --git a/tests/bugs/core_1130_test.py b/tests/bugs/core_1130_test.py index b44f990b..a947e6bd 100644 --- a/tests/bugs/core_1130_test.py +++ b/tests/bugs/core_1130_test.py @@ -12,42 +12,44 @@ import pytest from firebird.qa import * -init_script = """SET TERM ^; -create procedure p - returns (r int) -as -begin - r = 1; - suspend; -end -^ -SET TERM ;^ -COMMIT; +db = db_factory() + +test_script = """ + set term ^; + create procedure sp_test returns (r int) as + begin + r = 1; + suspend; + end + ^ + set term ;^ + commit; + + set planonly; + select p.* + from sp_test p + left join (select rdb$relation_id from rdb$relations ) r on p.r = r.rdb$relation_id; """ -db = db_factory(init=init_script) +substitutions = [('RDB\\$INDEX_\\d+', 'RDB\\$INDEX')] -test_script = """SET PLAN ON; -select * -from p - left join ( select rdb$relation_id from rdb$relations ) r - on p.r = r.rdb$relation_id; +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] -""" - -act = isql_act('db', test_script) +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) -expected_stdout = """PLAN JOIN (P NATURAL, R RDB$RELATIONS INDEX (RDB$INDEX_1)) - - R RDB$RELATION_ID -============ =============== - 1 1 +act = isql_act('db', test_script, substitutions = substitutions) +expected_stdout = """ + PLAN JOIN (P NATURAL, R RDB$RELATIONS INDEX (RDB$INDEX_1)) """ @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_1142_test.py b/tests/bugs/core_1142_test.py index 4e434773..946ce1a3 100644 --- a/tests/bugs/core_1142_test.py +++ b/tests/bugs/core_1142_test.py @@ -6,37 +6,74 @@ TITLE: Cannot alter generator's comment to the same value DESCRIPTION: JIRA: CORE-1142 -FBTEST: bugs.core_1142 +NOTES: + [31.12.2024] pzotov + Removed 'SHOW' command because its output can change during intensive development. + Also, 'SHOW COMMENT ON ' is not valid in ISQL, see: + https://github.com/FirebirdSQL/firebird-qa/pull/33/files + + Parsing problem appeared on 6.0.0.0.570 after d6ad19aa07deeaac8107a25a9243c5699a3c4ea1 + ("Refactor ISQL creating FrontendParser class"). + + Checked on 6.0.0.570, 5.0.2.1583 """ import pytest from firebird.qa import * -init_script = """create generator T;""" +GEN_NAME = 'TEST_GEN' + +init_script = f""" + create generator {GEN_NAME}; + create view v_show_gen_descr as + select g.rdb$description as descr_blob_id + from rdb$generators g + where g.rdb$generator_name = '{GEN_NAME.upper()}'; + commit; +""" db = db_factory(init=init_script) -test_script = """comment on generator T is 'comment'; -commit; -show comment on generator T; -comment on generator T is 'comment'; -commit; -show comment on generator T; -comment on generator T is 'different comment'; -commit; -show comment on generator T; +test_script = f""" + set blob all; + set list on; + set count on; + comment on generator {GEN_NAME} is 'comment N1'; + commit; + select * from v_show_gen_descr; + + comment on generator {GEN_NAME} is 'comment N1'; + commit; + select * from v_show_gen_descr; + + comment on generator {GEN_NAME} is 'comment N11'; + commit; + select * from v_show_gen_descr; + + comment on generator {GEN_NAME} is 'comment N11'; + commit; + select * from v_show_gen_descr; """ -act = isql_act('db', test_script) +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' '), ('DESCR_BLOB_ID .*', '')]) + +expected_stdout = """ + comment N1 + Records affected: 1 + + comment N1 + Records affected: 1 + + comment N11 + Records affected: 1 -expected_stdout = """COMMENT ON GENERATOR T IS comment; -COMMENT ON GENERATOR T IS comment; -COMMENT ON GENERATOR T IS different comment; + comment N11 + Records affected: 1 """ @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1145_test.py b/tests/bugs/core_1145_test.py index f28d3e00..9efabd5c 100644 --- a/tests/bugs/core_1145_test.py +++ b/tests/bugs/core_1145_test.py @@ -7,77 +7,83 @@ DESCRIPTION: JIRA: CORE-1145 FBTEST: bugs.core_1145 +NOTES: + [24.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.858; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """create table expt1 (col1 int); -create table expt2 (col2 int); -commit; +db = db_factory() -insert into expt1 values (1); -insert into expt1 values (2); +test_script = """ + set list on; + create table expt1 (col1 int); + create table expt2 (col2 int); + commit; -insert into expt2 values (1); -insert into expt2 values (2); -commit; + insert into expt1 values (1); + insert into expt1 values (2); -create index iexpt1 on expt1 computed (col1 + 1); -create index iexpt2 on expt2 computed (col2 + 1); -commit; -""" + insert into expt2 values (1); + insert into expt2 values (2); + commit; -db = db_factory(init=init_script) + create index iexpt1 on expt1 computed (col1 + 1); + create index iexpt2 on expt2 computed (col2 + 1); + commit; -test_script = """set plan on; -select * from expt1 where col1 + 1 = 2; -select * from expt2 where col2 + 1 = 2; -commit; + set plan on; + select 'point-1' msg, e.* from expt1 e where col1 + 1 = 2; + select 'point-2' msg, e.* from expt2 e where col2 + 1 = 2; + commit; -drop index iexpt2; -commit; -- lockup + drop index iexpt2; + commit; -- lockup -select * from expt1 where col1 + 1 = 2; -select * from expt2 where col2 + 1 = 2; -commit; + select 'point-3' msg, e.* from expt1 e where col1 + 1 = 2; + select 'point-4' msg, e.* from expt2 e where col2 + 1 = 2; + commit; """ -act = isql_act('db', test_script) - -expected_stdout = """ -PLAN (EXPT1 INDEX (IEXPT1)) - - COL1 -============ - 1 +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] -PLAN (EXPT2 INDEX (IEXPT2)) +substitutions = [('[ \t]+', ' ')] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) - COL2 -============ - 1 - - -PLAN (EXPT1 INDEX (IEXPT1)) - - COL1 -============ - 1 +act = isql_act('db', test_script, substitutions = substitutions) +expected_stdout = """ + PLAN (E INDEX (IEXPT1)) + MSG point-1 + COL1 1 -PLAN (EXPT2 NATURAL) + PLAN (E INDEX (IEXPT2)) + MSG point-2 + COL2 1 - COL2 -============ - 1 + PLAN (E INDEX (IEXPT1)) + MSG point-3 + COL1 1 + PLAN (E NATURAL) + MSG point-4 + COL2 1 """ @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_1153_test.py b/tests/bugs/core_1153_test.py index d7d1cd0c..6d41d43c 100644 --- a/tests/bugs/core_1153_test.py +++ b/tests/bugs/core_1153_test.py @@ -7,98 +7,116 @@ DESCRIPTION: JIRA: CORE-1153 FBTEST: bugs.core_1153 +NOTES: + [24.06.2025] pzotov + Separated execution plans for FB major versions prior/since 6.x. + No substitutions are used to suppress schema name and quotes to enclosing object names. + Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.858; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """CREATE TABLE D ( - ID VARCHAR(40) -); - - -CREATE TABLE M ( - ID VARCHAR(40) -); - - -INSERT INTO D (ID) VALUES ('AAA'); -INSERT INTO D (ID) VALUES ('aaa'); -INSERT INTO D (ID) VALUES ('Aaa Aaa'); -INSERT INTO D (ID) VALUES ('BBB'); -INSERT INTO D (ID) VALUES ('BBB'); -INSERT INTO D (ID) VALUES ('CCC'); - -COMMIT WORK; - -INSERT INTO M (ID) VALUES ('AAA Aaa'); -INSERT INTO M (ID) VALUES ('AAA Bbb'); -INSERT INTO M (ID) VALUES ('DDD Ddd'); -INSERT INTO M (ID) VALUES ('Bbb Aaa'); -INSERT INTO M (ID) VALUES ('Bbb Bbb'); - -COMMIT WORK; - -CREATE INDEX D_IDX1 ON D COMPUTED BY (upper(id)); -CREATE INDEX M_IDX1 ON M COMPUTED BY (UPPER(ID)); - -COMMIT WORK; +db = db_factory() + +test_script = """ + create table tdetl ( + sid varchar(40) + ); + create table tmain ( + sid varchar(40) + ); + + insert into tdetl(sid) values('AAA'); + insert into tdetl(sid) values('aaa'); + insert into tdetl(sid) values('Aaa Aaa'); + insert into tdetl(sid) values('BBB'); + insert into tdetl(sid) values('BBB'); + insert into tdetl(sid) values('CCC'); + commit; + + insert into tmain(sid) values ('AAA Aaa'); + insert into tmain(sid) values ('AAA Bbb'); + insert into tmain(sid) values ('DDD Ddd'); + insert into tmain(sid) values ('Bbb Aaa'); + insert into tmain(sid) values ('Bbb Bbb'); + commit; + + create index d_idx1 on tdetl computed by (upper(sid)); + create index m_idx1 on tmain computed by (upper(sid)); + commit; + + set list on; + set plan on; + alter index d_idx1 inactive; + + select distinct m.sid as m_sid, d.sid as d_did + from tmain m + left outer join tdetl d + on upper(m.sid) starting upper(d.sid) + order by m.sid; + + alter index d_idx1 active; + + select distinct m.sid as m_sid, d.sid as d_sid + from tmain m + left outer join tdetl d + on upper(m.sid) starting upper(d.sid) + order by m.sid; """ -db = db_factory(init=init_script) - -test_script = """SET PLAN ON; - -ALTER INDEX D_IDX1 INACTIVE; - -select distinct mm.ID as MID, dd.ID as DID -from m mm -left outer join d dd - on upper(mm.id) starting upper(dd.id) -order by mm.id ; - -ALTER INDEX D_IDX1 ACTIVE; - -select distinct mm.ID as MID, dd.ID as DID -from m mm -left outer join d dd - on upper(mm.id) starting upper(dd.id) -order by mm.id ; -""" - -act = isql_act('db', test_script) - -expected_stdout = """PLAN SORT (JOIN (MM NATURAL, DD NATURAL)) - -MID DID -======================================== ======================================== -AAA Aaa AAA -AAA Aaa Aaa Aaa -AAA Aaa aaa -AAA Bbb AAA -AAA Bbb aaa -Bbb Aaa BBB -Bbb Bbb BBB -DDD Ddd - -PLAN SORT (JOIN (MM NATURAL, DD NATURAL)) - -MID DID -======================================== ======================================== -AAA Aaa AAA -AAA Aaa Aaa Aaa -AAA Aaa aaa -AAA Bbb AAA -AAA Bbb aaa -Bbb Aaa BBB -Bbb Bbb BBB -DDD Ddd - -""" +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) @pytest.mark.version('>=3') def test_1(act: Action): + + if act.is_version('<6'): + qry_plan = 'PLAN SORT (JOIN (M NATURAL, D NATURAL))' + else: + qry_plan = 'PLAN SORT (JOIN ("M" NATURAL, "D" NATURAL))' + + expected_stdout = f""" + {qry_plan} + M_SID AAA Aaa + D_DID AAA + M_SID AAA Aaa + D_DID Aaa Aaa + M_SID AAA Aaa + D_DID aaa + M_SID AAA Bbb + D_DID AAA + M_SID AAA Bbb + D_DID aaa + M_SID Bbb Aaa + D_DID BBB + M_SID Bbb Bbb + D_DID BBB + M_SID DDD Ddd + D_DID + + {qry_plan} + M_SID AAA Aaa + D_SID AAA + M_SID AAA Aaa + D_SID Aaa Aaa + M_SID AAA Aaa + D_SID aaa + M_SID AAA Bbb + D_SID AAA + M_SID AAA Bbb + D_SID aaa + M_SID Bbb Aaa + D_SID BBB + M_SID Bbb Bbb + D_SID BBB + M_SID DDD Ddd + D_SID + """ + act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1162_test.py b/tests/bugs/core_1162_test.py index eb7ff934..0ead1ab1 100644 --- a/tests/bugs/core_1162_test.py +++ b/tests/bugs/core_1162_test.py @@ -5,26 +5,15 @@ ISSUE: 1585 TITLE: Problem altering numeric field type DESCRIPTION: - create table tab (a numeric(4,2)); - insert into tab values (99.99); - select * from tab; - - A - ======= - 99.99 - - alter table tab alter a type numeric(4,3); - select * from tab; - - Statement failed, SQLCODE = -802 - arithmetic exception, numeric overflow, or string truncation - - BTW the database is not "corrupted" too badly - you can revert the change back by - alter table tab alter a type numeric(4,2); - and the engine is clever enough to convert data from stored format to requested one - directly, not through all intermediate format versions. JIRA: CORE-1162 FBTEST: bugs.core_1162 +NOTES: + [23.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -32,31 +21,36 @@ db = db_factory() -test_script = """create table tab ( a numeric(4,2) ); -insert into tab values (99.99); -alter table tab alter a type numeric(4,3); -select * from tab; +test_script = """ + create table tab ( a numeric(4,2) ); + insert into tab values (99.99); + alter table tab alter a type numeric(4,3); + set list on; + select * from tab; """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' ')] +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] -expected_stdout = """A -======= - 99.99 +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) -""" +act = isql_act('db', test_script, substitutions = substitutions) -expected_stderr = """Statement failed, SQLSTATE = 42000 -unsuccessful metadata update --ALTER TABLE TAB failed --New scale specified for column A must be at most 2. +expected_stdout = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -ALTER TABLE TAB failed + -New scale specified for column A must be at most 2. + A 99.99 """ @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1165_test.py b/tests/bugs/core_1165_test.py index b0fb5821..96c450bb 100644 --- a/tests/bugs/core_1165_test.py +++ b/tests/bugs/core_1165_test.py @@ -7,59 +7,81 @@ DESCRIPTION: JIRA: CORE-1165 FBTEST: bugs.core_1165 +NOTES: + [24.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.858; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """recreate exception e1 'e1' ; -recreate exception e2 'e2' ; +db = db_factory() + +test_script = """ + set list on; + recreate exception exc_1 'exc_1' ; + recreate exception exc_2 'exc_2' ; + + set term ^; -set term ^; + create procedure sp_test as + begin + begin end + when exception exc_1, exception exc_2 do + begin + end + end + ^ + set term ;^ + commit; -create procedure p as -begin - begin end - when exception e1, exception e2 do - begin - end -end^ + select rd.rdb$depended_on_name depends_on_name + from rdb$dependencies rd + where upper(rd.rdb$dependent_name) = upper('sp_test') + order by depends_on_name + ; + commit; -set term ;^ + recreate exception exc_1 'exc_1'; + recreate exception exc_2 'exc_2'; """ -db = db_factory(init=init_script) +substitutions = [('[ \t]+', ' ')] +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] -test_script = """show depend p; +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) -recreate exception e1 'e1'; -recreate exception e2 'e2'; -""" +act = isql_act('db', test_script, substitutions = substitutions) -act = isql_act('db', test_script) +expected_stdout = """ + DEPENDS_ON_NAME EXC_1 + DEPENDS_ON_NAME EXC_2 -expected_stdout = """[P:Procedure] -E2:Exception, E1:Exception -+++ -""" + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -cannot delete + -EXCEPTION EXC_1 + -there are 1 dependencies -expected_stderr = """Statement failed, SQLSTATE = 42000 -unsuccessful metadata update --cannot delete --EXCEPTION E1 --there are 1 dependencies -Statement failed, SQLSTATE = 42000 -unsuccessful metadata update --cannot delete --EXCEPTION E2 --there are 1 dependencies + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -cannot delete + -EXCEPTION EXC_2 + -there are 1 dependencies """ @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1173_test.py b/tests/bugs/core_1173_test.py index bae6cb5f..305aba09 100644 --- a/tests/bugs/core_1173_test.py +++ b/tests/bugs/core_1173_test.py @@ -7,6 +7,13 @@ DESCRIPTION: Index based on COMPUTED-BY column must be taken in account by optimizer. JIRA: CORE-1173 FBTEST: bugs.core_1173 +NOTES: + [24.06.2025] pzotov + Separated execution plans for FB major versions prior/since 6.x. + No substitutions are used to suppress schema name and quotes to enclosing object names. + Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.858; 3.0.13.33813. """ import pytest @@ -77,16 +84,23 @@ act = isql_act('db', test_script) -expected_stdout = """ +expected_stdout_5x = """ PLAN (TEST INDEX (TEST_ON_COMPUTED_FIELD_ASC)) PLAN (TEST INDEX (TEST_FDATE_FTIME_ASC)) PLAN (TEST INDEX (TEST_ON_COMPUTED_FIELD_DEC)) PLAN (TEST INDEX (TEST_FDATE_FTIME_DEC)) """ +expected_stdout_6x = """ + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_ON_COMPUTED_FIELD_ASC")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_FDATE_FTIME_ASC")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_ON_COMPUTED_FIELD_DEC")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_FDATE_FTIME_DEC")) +""" + @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1215_test.py b/tests/bugs/core_1215_test.py index ccbf7bfc..ffd60adf 100644 --- a/tests/bugs/core_1215_test.py +++ b/tests/bugs/core_1215_test.py @@ -7,73 +7,80 @@ DESCRIPTION: JIRA: CORE-1215 FBTEST: bugs.core_1215 +NOTES: + [24.06.2025] pzotov + Separated execution plans for FB major versions prior/since 6.x. + No substitutions are used to suppress schema name and quotes to enclosing object names. + Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.858; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """CREATE TABLE T (ID INT); -COMMIT; +init_script = """ + CREATE TABLE T (ID INT); + COMMIT; -set term ^; + set term ^; -EXECUTE BLOCK AS -DECLARE I INT = 0; -BEGIN - WHILE (I < 50000) DO - BEGIN - INSERT INTO T VALUES (1); - I = I + 1; - END -END^ + EXECUTE BLOCK AS + DECLARE I INT = 0; + BEGIN + WHILE (I < 50000) DO + BEGIN + INSERT INTO T VALUES (1); + I = I + 1; + END + END^ -set term ;^ -commit; + set term ;^ + commit; -CREATE INDEX IDX_T ON T (ID); -COMMIT; + CREATE INDEX IDX_T ON T (ID); + COMMIT; """ db = db_factory(init=init_script) -test_script = """set plan on; -SELECT COUNT(*) FROM T ; -SELECT COUNT(*) FROM T WHERE ID >= 1 ; -SELECT COUNT(*) FROM T WHERE ID = 1 ; -SELECT COUNT(*) FROM T WHERE ID <= 1 ; +test_script = """ + set list on; + set plan on; + SELECT COUNT(*) FROM T ; + SELECT COUNT(*) FROM T WHERE ID >= 1 ; + SELECT COUNT(*) FROM T WHERE ID = 1 ; + SELECT COUNT(*) FROM T WHERE ID <= 1 ; """ -act = isql_act('db', test_script) - -expected_stdout = """PLAN (T NATURAL) - - COUNT -===================== - 50000 - -PLAN (T INDEX (IDX_T)) - - COUNT -===================== - 50000 - -PLAN (T INDEX (IDX_T)) - - COUNT -===================== - 50000 - -PLAN (T INDEX (IDX_T)) - - COUNT -===================== - 50000 +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) + +expected_stdout_5x = """ + PLAN (T NATURAL) + COUNT 50000 + PLAN (T INDEX (IDX_T)) + COUNT 50000 + PLAN (T INDEX (IDX_T)) + COUNT 50000 + PLAN (T INDEX (IDX_T)) + COUNT 50000 +""" +expected_stdout_6x = """ + PLAN ("PUBLIC"."T" NATURAL) + COUNT 50000 + PLAN ("PUBLIC"."T" INDEX ("PUBLIC"."IDX_T")) + COUNT 50000 + PLAN ("PUBLIC"."T" INDEX ("PUBLIC"."IDX_T")) + COUNT 50000 + PLAN ("PUBLIC"."T" INDEX ("PUBLIC"."IDX_T")) + COUNT 50000 """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1254_test.py b/tests/bugs/core_1254_test.py index 3681445c..651e52ed 100644 --- a/tests/bugs/core_1254_test.py +++ b/tests/bugs/core_1254_test.py @@ -2,54 +2,106 @@ """ ID: issue-1678 -ISSUE: 1678 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/1678 TITLE: Problem with DISTINCT and insensitive collations -DESCRIPTION: +DESCRIPTION: See https://github.com/FirebirdSQL/firebird/issues/2965 JIRA: CORE-1254 FBTEST: bugs.core_1254 +NOTES: + 1. Confirmed problem on 2.1.3.18185 + Both queries: 'select ... group by ...' and 'select distinct ...' issued six rows: + GROUP_ID QUESTION + ======== ============ + a 1 + a 2 + a 3 + A 1 + A 2 + A 3 + (instead of expected three rows with 'a' or 'A' in the 1st column). + The only correct result issued when index was used. + + 2. Values in 1st column can vary if OptimizeForFirstRows = true (FB 5.x+). + Because of this, we have to check only COUNT of letters in this column + that are unique being compared using case SENSITIVE collation. + In all cases (for queries and with/without index) this count must be 1. """ import pytest from firebird.qa import * -init_script = """CREATE TABLE TEST -(GROUP_ID VARCHAR(1) CHARACTER SET UTF8 COLLATE UNICODE_CI, -QUESTION INTEGER, -SCORE INTEGER); -COMMIT; -INSERT INTO TEST (GROUP_ID,QUESTION,SCORE) VALUES ('a',1,1); -INSERT INTO TEST (GROUP_ID,QUESTION,SCORE) VALUES ('a',2,1); -INSERT INTO TEST (GROUP_ID,QUESTION,SCORE) VALUES ('a',3,1); -INSERT INTO TEST (GROUP_ID,QUESTION,SCORE) VALUES ('A',1,1); -INSERT INTO TEST (GROUP_ID,QUESTION,SCORE) VALUES ('A',2,1); -INSERT INTO TEST (GROUP_ID,QUESTION,SCORE) VALUES ('A',3,1); -COMMIT; - +init_script = """ + create table test( + group_id varchar(1) character set utf8 collate unicode_ci, + question integer, + score integer + ); + commit; + insert into test (group_id,question,score) values ('a',1,11); + insert into test (group_id,question,score) values ('a',3,13); + insert into test (group_id,question,score) values ('A',1,14); + insert into test (group_id,question,score) values ('a',2,12); + insert into test (group_id,question,score) values ('A',2,15); + insert into test (group_id,question,score) values ('A',3,16); + commit; + -- See https://github.com/FirebirdSQL/firebird/issues/2965#issue-866882047 + -- GROUP BY will use an index on multi-byte or insensitive collation only + -- when this index is: 1) UNIQUE and 2) ASCENDING. + create UNIQUE index test_gr_que_score on test(group_id, question, score); + commit; """ + db = db_factory(charset='UTF8', init=init_script) -test_script = """SELECT GROUP_ID, QUESTION, SUM(SCORE) FROM TEST GROUP BY 1,2; -SELECT DISTINCT GROUP_ID, QUESTION FROM TEST;""" +test_script = """ + --set explain on; + --set plan on; + set list on; + alter index test_gr_que_score inactive; + commit; + + select count( + distinct cast( group_id as varchar(1) + -- Check count of unique values in 1st column using + -- case SENSITIVE collation: + -- ######################### + character set ascii + ) + ) as "case_SENSITIVE_distinct_gr_1" + from ( + select group_id, question from test group by 1,2 + ); -act = isql_act('db', test_script) + select count( distinct cast(group_id as varchar(1) character set ascii)) as "case_SENSITIVE_distinct_gr_2" + from ( + select distinct group_id, question from test + ); -expected_stdout = """ -GROUP_ID QUESTION SUM -======== ============ ===================== -a 1 2 -a 2 2 -a 3 2 + alter index test_gr_que_score active; + commit; + select count( distinct cast(group_id as varchar(1) character set ascii)) as "case_SENSITIVE_distinct_gr_3" + from ( + select group_id, question from test group by 1,2 + ); -GROUP_ID QUESTION -======== ============ -a 1 -a 2 -a 3 + select count( distinct cast(group_id as varchar(1) character set ascii)) as "case_SENSITIVE_distinct_gr_4" + from ( + select distinct group_id, question from test + ); +""" + +act = isql_act('db', test_script) +expected_stdout = """ + case_SENSITIVE_distinct_gr_1 1 + case_SENSITIVE_distinct_gr_2 1 + case_SENSITIVE_distinct_gr_3 1 + case_SENSITIVE_distinct_gr_4 1 """ +@pytest.mark.intl @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_1271_test.py b/tests/bugs/core_1271_test.py index adcb81e6..71a17264 100644 --- a/tests/bugs/core_1271_test.py +++ b/tests/bugs/core_1271_test.py @@ -7,6 +7,15 @@ DESCRIPTION: JIRA: CORE-1271 FBTEST: bugs.core_1271 +NOTES: + [25.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Minimal snapshot number for 6.x: 6.0.0.863, see letter from Adriano, 24.06.2025 23:24, commit: + https://github.com/FirebirdSQL/firebird/commit/79ff650e5af7a0d6141e166b0cb8208ef211f0a7 + + Checked on 6.0.0.863; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -15,33 +24,41 @@ db = db_factory() test_script = """ + create table test(id int primary key using index test_id, f01 timestamp); + create index test_f01 on test(f01); + commit; set term ^; - create procedure p returns (out int) as + create procedure sp_test (a_id int) returns (o_f01 type of column test.f01) as begin - for - select rdb$relation_id - from rdb$relations - plan (rdb$relations order rdb$index_1) - order by rdb$description - into :out + for + select f01 from test where id = :a_id plan (test order test_f01) + into o_f01 do suspend; end ^ - commit^ + commit + ^ """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) + +expected_stdout_5x = """ + Statement failed, SQLSTATE = 2F000 + Error while parsing procedure SP_TEST's BLR + -index TEST_F01 cannot be used in the specified plan +""" -expected_stderr = """ +expected_stdout_6x = """ Statement failed, SQLSTATE = 2F000 - Error while parsing procedure P's BLR - -index RDB$INDEX_1 cannot be used in the specified plan + Error while parsing procedure "PUBLIC"."SP_TEST"'s BLR + -index "PUBLIC"."TEST_F01" cannot be used in the specified plan """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1291_test.py b/tests/bugs/core_1291_test.py index c1d36573..30dbc412 100644 --- a/tests/bugs/core_1291_test.py +++ b/tests/bugs/core_1291_test.py @@ -3,10 +3,14 @@ """ ID: issue-1712 ISSUE: 1712 -TITLE: Can't transliterate character set when look at procedure text in database just created from script (and thus in ODS 11.1) +TITLE: Can't transliterate character set when look at procedure text in database just created from script DESCRIPTION: JIRA: CORE-1291 FBTEST: bugs.core_1291 +NOTES: + [24.06.2025] pzotov + Added substitutions in orde to suppress output of procedure header info. This data irrelevant to the test. + Checked on 6.0.0.858; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -661,9 +665,10 @@ db = db_factory(do_not_create=True) -act = python_act('db') +substitutions = [ ('Procedure: \\S+.*', ''), ('Procedure text:', '') ] +act = python_act('db', substitutions = substitutions) -expected_stdout = """Procedure text: +expected_stdout = """ ============================================================================= DECLARE VARIABLE NDSDiv DOUBLE PRECISION; DECLARE VARIABLE ID_ExportFieldDoc Integer; @@ -881,8 +886,7 @@ @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout - act.isql(switches=[], - input=test_script % act.db.dsn, connect_db=False, charset='WIN1251') + act.isql(switches=['-q'], input = test_script % act.db.dsn, connect_db=False, charset='WIN1251', combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1292_test.py b/tests/bugs/core_1292_test.py index db9874a1..1384f6b4 100644 --- a/tests/bugs/core_1292_test.py +++ b/tests/bugs/core_1292_test.py @@ -7,56 +7,51 @@ DESCRIPTION: JIRA: CORE-1292 FBTEST: bugs.core_1292 +NOTES: + [23.08.2024] pzotov + 1. Removed LIST() from initial query because it displays tokens in unpredictable order. + This can cause fail if we change OptimizeForFirstRows = true config parameter. + 2. Found oddities when try to use non-ascii user name and substitute it using f-notation: + at least REVOKE and GRANT commands reflect this user name in the trace as encoded + in cp1251 instead of utf8. This causes: + 335544321 : arithmetic exception, numeric overflow, or string truncation + 335544565 : Cannot transliterate character between character sets + To be investigated further. """ - +import locale import pytest from firebird.qa import * -db = db_factory(charset='UTF8') - -test_script = """ - set wng off; - - -- Drop old account if it remains from prevoius run: - set term ^; - execute block as - begin - begin - execute statement 'drop user Nebuchadnezzar2_King_of_Babylon' with autonomous transaction; - when any do begin end - end - end^ - set term ;^ - commit; - - - create user Nebuchadnezzar2_King_of_Babylon password 'guinness'; -- revoke admin role; - -- 1234567890123456789012345678901 - -- 1 2 3 - commit; - revoke all on all from Nebuchadnezzar2_King_of_Babylon; - set term ^; - execute block as - begin - if ( rdb$get_context('SYSTEM', 'ENGINE_VERSION') not starting with '2.5' ) then - begin - execute statement 'grant create table to Nebuchadnezzar2_King_of_Babylon'; - end - end - ^ - set term ;^ - commit; - - connect '$(DSN)' user 'Nebuchadnezzar2_King_of_Babylon' password 'guinness'; - - create table test(n int); - commit; - - connect '$(DSN)' user 'SYSDBA' password 'masterkey'; - - set list on; - select usr_name, grantor, can_grant, tab_name,usr_type,obj_type, list(priv) priv_list - from ( +db = db_factory(charset = 'utf8') + +act = python_act('db', substitutions = [ ('[ \t]+', ' '), ]) +tmp_user = user_factory('db', name='Nebuchadnezzar_The_Babylon_Lord', password='123', plugin = 'Srp') +#tmp_user = user_factory('db', name='"НавохудоносорВластелинВавилона2"', password='123', plugin = 'Srp') + +@pytest.mark.version('>=3') +def test_1(act: Action, tmp_user: User, capsys): + + test_sql = f""" + set bail on; + set list on; + set wng off; + connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; + revoke all on all from {tmp_user.name}; + grant create table to {tmp_user.name}; + commit; + + connect '{act.db.dsn}' user {tmp_user.name} password '{tmp_user.password}'; + + select a.mon$user as who_am_i, c.rdb$character_set_name as my_connection_charset + from mon$attachments a + join rdb$character_sets c on a.mon$character_set_id = c.rdb$character_set_id + where a.mon$attachment_id = current_connection; + + create table test(n int); + commit; + + connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; + select p.rdb$user usr_name ,p.rdb$grantor grantor @@ -66,31 +61,58 @@ ,p.rdb$object_type obj_type ,trim(p.rdb$privilege) priv from rdb$user_privileges p - where upper(trim(p.rdb$relation_name)) = upper('test') - order by priv - ) - group by usr_name, grantor, can_grant, tab_name,usr_type,obj_type; - commit; - - drop user Nebuchadnezzar2_King_of_Babylon; - commit; -""" - -act = isql_act('db', test_script, substitutions=[('PRIV_LIST.*', '')]) + where + upper(trim(p.rdb$relation_name)) = upper('test') + and p.rdb$user = _utf8 '{tmp_user.name}' collate unicode_ci + order by priv; + commit; + """ + + expected_stdout = f""" + WHO_AM_I {tmp_user.name.upper()} + MY_CONNECTION_CHARSET UTF8 + + USR_NAME {tmp_user.name.upper()} + GRANTOR {tmp_user.name.upper()} + CAN_GRANT 1 + TAB_NAME TEST + USR_TYPE 8 + OBJ_TYPE 0 + PRIV D + + USR_NAME {tmp_user.name.upper()} + GRANTOR {tmp_user.name.upper()} + CAN_GRANT 1 + TAB_NAME TEST + USR_TYPE 8 + OBJ_TYPE 0 + PRIV I + + USR_NAME {tmp_user.name.upper()} + GRANTOR {tmp_user.name.upper()} + CAN_GRANT 1 + TAB_NAME TEST + USR_TYPE 8 + OBJ_TYPE 0 + PRIV R + + USR_NAME {tmp_user.name.upper()} + GRANTOR {tmp_user.name.upper()} + CAN_GRANT 1 + TAB_NAME TEST + USR_TYPE 8 + OBJ_TYPE 0 + PRIV S + + USR_NAME {tmp_user.name.upper()} + GRANTOR {tmp_user.name.upper()} + CAN_GRANT 1 + TAB_NAME TEST + USR_TYPE 8 + OBJ_TYPE 0 + PRIV U + """ -expected_stdout = """ - USR_NAME NEBUCHADNEZZAR2_KING_OF_BABYLON - GRANTOR NEBUCHADNEZZAR2_KING_OF_BABYLON - CAN_GRANT 1 - TAB_NAME TEST - USR_TYPE 8 - OBJ_TYPE 0 - D,I,R,S,U -""" - -@pytest.mark.version('>=3') -def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.isql(switches = ['-q'], input = test_sql, charset = 'utf8', connect_db=False, credentials = False, combine_output = True, io_enc = locale.getpreferredencoding()) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_1295_test.py b/tests/bugs/core_1295_test.py index 70e787be..e1f4d500 100644 --- a/tests/bugs/core_1295_test.py +++ b/tests/bugs/core_1295_test.py @@ -7,6 +7,13 @@ DESCRIPTION: JIRA: CORE-1295 FBTEST: bugs.core_1295 +NOTES: + [24.06.2025] pzotov + Separated execution plans for FB major versions prior/since 6.x. + No substitutions are used to suppress schema name and quotes to enclosing object names. + Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.858; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -14,19 +21,26 @@ db = db_factory() -test_script = """SET PLANONLY; -select * from rdb$relations where rdb$db_key = ? and rdb$relation_id = 0; -select * from rdb$relations where rdb$db_key = ? and rdb$relation_name = 'RDB$RELATIONS';""" +test_script = """ + SET PLANONLY; + select * from rdb$relations where rdb$db_key = ? and rdb$relation_id = 0; + select * from rdb$relations where rdb$db_key = ? and rdb$relation_name = 'RDB$RELATIONS'; +""" act = isql_act('db', test_script) -expected_stdout = """PLAN (RDB$RELATIONS INDEX ()) -PLAN (RDB$RELATIONS INDEX ()) +expected_stdout_5x = """ + PLAN (RDB$RELATIONS INDEX ()) + PLAN (RDB$RELATIONS INDEX ()) +""" + +expected_stdout_6x = """ + PLAN ("SYSTEM"."RDB$RELATIONS" INDEX ()) + PLAN ("SYSTEM"."RDB$RELATIONS" INDEX ()) """ @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_1306_test.py b/tests/bugs/core_1306_test.py index 70081183..7dd17cda 100644 --- a/tests/bugs/core_1306_test.py +++ b/tests/bugs/core_1306_test.py @@ -12,44 +12,42 @@ import pytest from firebird.qa import * -init_script = """CREATE TABLE "TABLE" (ID INTEGER NOT NULL PRIMARY KEY); - -COMMIT; - -INSERT INTO "TABLE" (ID) VALUES (1); -INSERT INTO "TABLE" (ID) VALUES (2); -INSERT INTO "TABLE" (ID) VALUES (3); - -COMMIT; - -CREATE VIEW "VIEW" AS SELECT * FROM "TABLE"; - -commit;""" - -db = db_factory(init=init_script) - -test_script = """set plan on; - -SELECT * FROM "TABLE" WHERE ID = 1 -UNION ALL -SELECT * FROM "VIEW" WHERE ID = 1 ; +db = db_factory() + +test_script = """ + set list on; + create table "TABLE" (id integer not null primary key); + commit; + insert into "TABLE" (id) values (1); + insert into "TABLE" (id) values (2); + insert into "TABLE" (id) values (3); + commit; + create view "VIEW" as select * from "TABLE"; + commit; + set plan on; + select * from "TABLE" where id = 1 + union all + select * from "VIEW" where id = 1 ; """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ -PLAN (TABLE INDEX (RDB$PRIMARY1), VIEW TABLE INDEX (RDB$PRIMARY1)) - - ID -============ - 1 - 1 +expected_stdout_5x = """ + PLAN (TABLE INDEX (RDB$PRIMARY1), VIEW TABLE INDEX (RDB$PRIMARY1)) + ID 1 + ID 1 +""" +expected_stdout_6x = """ + PLAN ("PUBLIC"."TABLE" INDEX ("PUBLIC"."RDB$PRIMARY1"), "PUBLIC"."VIEW" "PUBLIC"."TABLE" INDEX ("PUBLIC"."RDB$PRIMARY1")) + ID 1 + ID 1 """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1315_test.py b/tests/bugs/core_1315_test.py index 7b547cc2..a1800765 100644 --- a/tests/bugs/core_1315_test.py +++ b/tests/bugs/core_1315_test.py @@ -6,9 +6,17 @@ TITLE: Data type unknown DESCRIPTION: JIRA: CORE-1315 -FBTEST: bugs.core_1315 +NOTES: + [18.01.2025] pzotov + Resultset of cursor that executes using instance of selectable PreparedStatement must be stored + in some variable in order to have ability close it EXPLICITLY (before PS will be freed). + Otherwise access violation raises during Python GC and pytest hangs at final point (does not return control to OS). + This occurs at least for: Python 3.11.2 / pytest: 7.4.4 / firebird.driver: 1.10.6 / Firebird.Qa: 0.19.3 + The reason of that was explained by Vlad, 26.10.24 17:42 ("oddities when use instances of selective statements"). """ +from firebird.driver import DatabaseError + import pytest from firebird.qa import * @@ -16,26 +24,45 @@ act = python_act('db') -expected_stdout = """COALESCE ------------ -2 +expected_stdout = """ + COALESCE + ----------- + 2 -COALESCE ------------ -1 + COALESCE + ----------- + 1 """ @pytest.mark.version('>=3') def test_1(act: Action, capsys): with act.db.connect() as con: - c = con.cursor() - statement = c.prepare('select coalesce(?,1) from RDB$DATABASE') - c.execute(statement,[2]) - act.print_data(c) - c.execute(statement,[None]) - act.print_data(c) + cur = con.cursor() + ps, rs = None, None + try: + ps = cur.prepare('select coalesce(?,1) from RDB$DATABASE') + + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + rs = cur.execute(ps,[2]) + act.print_data(rs) + + rs = cur.execute(ps,[None]) + act.print_data(rs) + + except DatabaseError as e: + print( e.__str__() ) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() + act.stdout = capsys.readouterr().out act.expected_stdout = expected_stdout assert act.clean_stdout == act.clean_expected_stdout - - diff --git a/tests/bugs/core_1316_test.py b/tests/bugs/core_1316_test.py index 46eb8d44..a9d80ee2 100644 --- a/tests/bugs/core_1316_test.py +++ b/tests/bugs/core_1316_test.py @@ -7,6 +7,15 @@ DESCRIPTION: JIRA: CORE-1316 FBTEST: bugs.core_1316 +NOTES: + [24.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Adjusted explained plan in 6.x to actual. + + Checked on 6.0.0.858; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -14,26 +23,39 @@ db = db_factory() -test_script = """create procedure get_something(id integer not null) as begin end; -commit; -execute procedure get_something(NULL); -execute procedure get_something(1); -set term ^; -create procedure p0(inp int) as declare i int not null; begin i = inp; end^ -set term ;^ -commit; -execute procedure p0(null); -execute procedure p0(1); +test_script = """ + create procedure get_something(id integer not null) as begin end; + commit; + execute procedure get_something(NULL); + execute procedure get_something(1); + set term ^; + create procedure sp_test(inp int) as declare i int not null; begin i = inp; end^ + set term ;^ + commit; + execute procedure sp_test(null); + execute procedure sp_test(1); """ -act = isql_act('db', test_script, substitutions=[('line: \\d+, col: \\d+', '')]) +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('line: \\d+, col: \\d+', '')] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + +act = isql_act('db', test_script, substitutions = substitutions) + +expected_stderr = """ + Statement failed, SQLSTATE = 42000 + validation error for variable ID, value "*** null ***" + -At procedure 'GET_SOMETHING' -expected_stderr = """Statement failed, SQLSTATE = 42000 -validation error for variable ID, value "*** null ***" --At procedure 'GET_SOMETHING' -Statement failed, SQLSTATE = 42000 -validation error for variable I, value "*** null ***" --At procedure 'P0' line: 1, col: 63 + Statement failed, SQLSTATE = 42000 + validation error for variable I, value "*** null ***" + -At procedure 'SP_TEST' line: 1, col: 63 """ @pytest.mark.version('>=3') diff --git a/tests/bugs/core_1331_test.py b/tests/bugs/core_1331_test.py index fe9b6f3b..edc993b8 100644 --- a/tests/bugs/core_1331_test.py +++ b/tests/bugs/core_1331_test.py @@ -48,6 +48,7 @@ EXECUTE_STTM_SELECT милан """ +@pytest.mark.intl @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_1347_test.py b/tests/bugs/core_1347_test.py index fd1fecaf..2c7a00cb 100644 --- a/tests/bugs/core_1347_test.py +++ b/tests/bugs/core_1347_test.py @@ -57,6 +57,7 @@ and pr.rdb$procedure_name = upper('sp_test'); ''' +@pytest.mark.intl @pytest.mark.version('>=3') def test_1(act: Action, tmp_file: Path): tmp_file.write_bytes(sql_txt.encode('cp1251')) diff --git a/tests/bugs/core_1361_test.py b/tests/bugs/core_1361_test.py index ed8fe911..41ca10ae 100644 --- a/tests/bugs/core_1361_test.py +++ b/tests/bugs/core_1361_test.py @@ -146,6 +146,7 @@ USING_INDEX 1 """ +@pytest.mark.es_eds @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_1366_test.py b/tests/bugs/core_1366_test.py index c3788cb8..7fa1c8d8 100644 --- a/tests/bugs/core_1366_test.py +++ b/tests/bugs/core_1366_test.py @@ -1,117 +1,115 @@ -#coding:utf-8 - -""" -ID: issue-1784 -ISSUE: 1784 -TITLE: French insensitive collation FR_FR_CI_AI -DESCRIPTION: -JIRA: CORE-1366 -FBTEST: bugs.core_1366 -NOTES: - [06.10.2022] pzotov - Could not complete adjusting for LINUX in new-qa. - DEFERRED. -""" -import platform -import pytest -from firebird.qa import * - -init_script = """ - recreate table test(id int); - commit; - - set term ^; - execute block as - begin - begin execute statement 'drop collation coll_fr'; when any do begin end end - end - ^set term ;^ - commit; - - create collation coll_fr for iso8859_1 from external ('FR_FR') case insensitive accent insensitive; - commit; - - recreate table test(id int, cf varchar(10) collate coll_fr); - commit; - - recreate table noac(id int, nf varchar(10) collate coll_fr); - commit; - - -- http://french.about.com/od/pronunciation/a/accents.htm - - -- ### ONCE AGAIN ### - -- 1) for checking this under ISQL following must be encoded in ISO8859_1 - -- 2) for running under fbt_run utility following must be encoded in UTF8. - - -- (cedilla) is found only on the letter "C": - insert into test(id, cf) values( 1010, 'ç'); - - -- (acute accent) can only be on an "E" - insert into test(id, cf) values( 1020, 'é'); - - -- (grave accent) can be found on an "A", "E", "U" - insert into test(id, cf) values( 1030, 'à'); - insert into test(id, cf) values( 1040, 'è'); - insert into test(id, cf) values( 1050, 'ù'); - - -- (dieresis or umlaut) can be on an E, I and U - insert into test(id, cf) values( 1060, 'ë'); - insert into test(id, cf) values( 1070, 'ï'); - insert into test(id, cf) values( 1080, 'ü'); - - -- (circumflex) can be on an A, E, I, O and U - insert into test(id, cf) values( 1090, 'â'); - insert into test(id, cf) values( 1110, 'ê'); - insert into test(id, cf) values( 1120, 'î'); - insert into test(id, cf) values( 1130, 'û'); - insert into test(id, cf) values( 1140, 'ô'); - commit; - - -- ANSI letters that should be equal to diacritical - -- when doing comparison CI_AI: - insert into noac(id, nf) values( 1150, 'A'); - insert into noac(id, nf) values( 1160, 'C'); - insert into noac(id, nf) values( 1170, 'E'); - insert into noac(id, nf) values( 1180, 'I'); - insert into noac(id, nf) values( 1190, 'O'); - insert into noac(id, nf) values( 1200, 'U'); - commit; - -""" - -db = db_factory(charset='ISO8859_1', init=init_script) - -test_script = """ - select n.id n_id, n.nf, t.cf, t.id t_id - from noac n - left join test t on n.nf is not distinct from t.cf - order by n_id, t_id; -""" - -act = isql_act('db', test_script, substitutions=[('=.*', ''), ('[ \t]+', ' ')]) - -expected_stdout = """ - N_ID NF CF T_ID - ============ ========== ========== ============ - 1150 A à 1030 - 1150 A â 1090 - 1160 C ç 1010 - 1170 E é 1020 - 1170 E è 1040 - 1170 E ë 1060 - 1170 E ê 1110 - 1180 I ï 1070 - 1180 I î 1120 - 1190 O ô 1140 - 1200 U ù 1050 - 1200 U ü 1080 - 1200 U û 1130 -""" - -@pytest.mark.skipif(platform.system() != 'Windows', reason='FIXME: see notes') -@pytest.mark.version('>=3') -def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() - assert act.clean_stdout == act.clean_expected_stdout - +#coding:utf-8 + +""" +ID: issue-1784 +ISSUE: 1784 +TITLE: French insensitive collation FR_FR_CI_AI +DESCRIPTION: Add French case-/accent-insensitive collation. +JIRA: CORE-1366 +FBTEST: bugs.core_1366 +NOTES: + [31.10.2024] pzotov + Bug was fixed for too old FB (2.1.8), firebird-driver and/or QA-plugin + will not able to run on this version in order to reproduce problem. + + Checked on 6.0.0.511 (Windows/Linux); 5.0.2.1550; 4.0.6.3165; 3.0.2.32670, 3,0,1,32609 +""" +from pathlib import Path + +import pytest +from firebird.qa import * + +db = db_factory(charset='ISO8859_1') +act = isql_act('db', substitutions=[('=.*', ''), ('[ \\t]+', ' ')]) +tmp_sql = temp_file('tmp_core_1366.sql') + +@pytest.mark.intl +@pytest.mark.version('>=3.0.0') +def test_1(act: Action, tmp_sql: Path): + + test_script = f""" + recreate table test(id int); + commit; + + set term ^; + execute block as + begin + begin execute statement 'drop collation coll_fr'; when any do begin end end + end + ^set term ;^ + commit; + + create collation coll_fr for iso8859_1 from external ('FR_FR') case insensitive accent insensitive; + commit; + + recreate table test(id int, cf varchar(10) collate coll_fr); + commit; + + recreate table noac(id int, nf varchar(10) collate coll_fr); + commit; + + -- http://french.about.com/od/pronunciation/a/accents.htm + + -- (cedilla) is found only on the letter "C": + insert into test(id, cf) values( 1010, 'ç'); + + -- (acute accent) can only be on an "E" + insert into test(id, cf) values( 1020, 'é'); + + -- (grave accent) can be found on an "A", "E", "U" + insert into test(id, cf) values( 1030, 'à'); + insert into test(id, cf) values( 1040, 'è'); + insert into test(id, cf) values( 1050, 'ù'); + + -- (dieresis or umlaut) can be on an E, I and U + insert into test(id, cf) values( 1060, 'ë'); + insert into test(id, cf) values( 1070, 'ï'); + insert into test(id, cf) values( 1080, 'ü'); + + -- (circumflex) can be on an A, E, I, O and U + insert into test(id, cf) values( 1090, 'â'); + insert into test(id, cf) values( 1110, 'ê'); + insert into test(id, cf) values( 1120, 'î'); + insert into test(id, cf) values( 1130, 'û'); + insert into test(id, cf) values( 1140, 'ô'); + commit; + + -- ANSI letters that should be equal to diacritical + -- when doing comparison CI_AI: + insert into noac(id, nf) values( 1150, 'A'); + insert into noac(id, nf) values( 1160, 'C'); + insert into noac(id, nf) values( 1170, 'E'); + insert into noac(id, nf) values( 1180, 'I'); + insert into noac(id, nf) values( 1190, 'O'); + insert into noac(id, nf) values( 1200, 'U'); + commit; + select n.id n_id, n.nf, t.cf, t.id t_id + from noac n + left join test t on n.nf is not distinct from t.cf + order by n_id, t_id; + """ + + # https://github.com/FirebirdSQL/firebird/issues/1784#issuecomment-826188088 + # ::: NB ::: + # For proper output of test, input script must be encoded in ISO8859_1 rather than in UTF-8. + # + tmp_sql.write_text(test_script, encoding='iso8859_1') + act.expected_stdout = """ + N_ID NF CF T_ID + ============ ========== ========== ============ + 1150 A à 1030 + 1150 A â 1090 + 1160 C ç 1010 + 1170 E é 1020 + 1170 E è 1040 + 1170 E ë 1060 + 1170 E ê 1110 + 1180 I ï 1070 + 1180 I î 1120 + 1190 O ô 1140 + 1200 U ù 1050 + 1200 U ü 1080 + 1200 U û 1130 + """ + act.isql(switches = ['-q'], input_file = tmp_sql, charset = 'iso8859_1', combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1378_test.py b/tests/bugs/core_1378_test.py index db6559c8..58280003 100644 --- a/tests/bugs/core_1378_test.py +++ b/tests/bugs/core_1378_test.py @@ -88,6 +88,7 @@ RDB$COLLATION_NAME вид прописи """ +@pytest.mark.intl @pytest.mark.version('>=3') def test_1(act: Action, tmp_file: Path): tmp_file.write_bytes(sql_txt.encode('cp1251')) diff --git a/tests/bugs/core_1384_test.py b/tests/bugs/core_1384_test.py index 9e278d4f..5a37ebe0 100644 --- a/tests/bugs/core_1384_test.py +++ b/tests/bugs/core_1384_test.py @@ -34,15 +34,12 @@ test_script = """ set list on; -show collation; select * from v_test; """ act = isql_act('db', test_script) expected_stdout = """ - COLL_ES, CHARACTER SET ISO8859_1, FROM EXTERNAL ('ES_ES_CI_AI'), 'SPECIALS-FIRST=1' - COLL_FR, CHARACTER SET ISO8859_1, FROM EXTERNAL ('FR_FR'), CASE INSENSITIVE, ACCENT INSENSITIVE, 'SPECIALS-FIRST=1' RESULT_FOR_ES_CI_AI 0 RESULT_FOR_FR_CI_AI 0 RESULT_FOR_ES_CI_AI 0 diff --git a/tests/bugs/core_1386_test.py b/tests/bugs/core_1386_test.py index 67fd6f8b..def6f7ab 100644 --- a/tests/bugs/core_1386_test.py +++ b/tests/bugs/core_1386_test.py @@ -14,30 +14,27 @@ db = db_factory() -test_script = """CREATE TABLE TAB1 (COL1 INTEGER, COL2 GENERATED ALWAYS AS (COL1 +1), COL3 INTEGER GENERATED ALWAYS AS (COL1 +1)); -COMMIT; -SHOW TABLE TAB1; -INSERT INTO TAB1 (COL1) VALUES (1); -COMMIT; -SELECT * FROM TAB1; - +test_script = """ + set list on; + create table tab1 (col1 integer, col2 generated always as (col1 +1), col3 integer generated always as (col1 +1)); + commit; + insert into tab1 (col1) values (1); + commit; + select * from tab1; """ -act = isql_act('db', test_script) - -expected_stdout = """COL1 INTEGER Nullable -COL2 Computed by: (COL1 +1) -COL3 Computed by: (COL1 +1) - - COL1 COL2 COL3 -============ ===================== ============ - 1 2 2 +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) +expected_stdout = """ + COL1 1 + COL2 2 + COL3 2 """ @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1395_test.py b/tests/bugs/core_1395_test.py index 2b559250..30e0aee9 100644 --- a/tests/bugs/core_1395_test.py +++ b/tests/bugs/core_1395_test.py @@ -7,34 +7,54 @@ DESCRIPTION: JIRA: CORE-1395 FBTEST: bugs.core_1395 +NOTES: + [24.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.858; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """CREATE TABLE TEST ( ID INTEGER ); -CREATE DOMAIN TEST_DOMAIN AS INTEGER CHECK (EXISTS(SELECT * FROM TEST WHERE ID=VALUE)); +init_script = """ + create table test ( id integer ); + create domain test_domain as integer + check ( exists(select * from test where id=value) ); """ db = db_factory(init=init_script) -test_script = """DROP TABLE TEST; -COMMIT; - +test_script = """ + drop table test; + commit; """ -act = isql_act('db', test_script) +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [] +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + +act = isql_act('db', test_script, substitutions = substitutions) -expected_stderr = """Statement failed, SQLSTATE = 42000 -unsuccessful metadata update --cannot delete --COLUMN TEST.ID --there are 1 dependencies +expected_stdout = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -cannot delete + -COLUMN TEST.ID + -there are 1 dependencies """ @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1401_test.py b/tests/bugs/core_1401_test.py index e3dbf2d3..9b48882e 100644 --- a/tests/bugs/core_1401_test.py +++ b/tests/bugs/core_1401_test.py @@ -7,6 +7,15 @@ DESCRIPTION: JIRA: CORE-1401 FBTEST: bugs.core_1401 +NOTES: + [25.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Minimal snapshot number for 6.x: 6.0.0.863, see letter from Adriano, 24.06.2025 23:24, commit: + https://github.com/FirebirdSQL/firebird/commit/79ff650e5af7a0d6141e166b0cb8208ef211f0a7 + + Checked on 6.0.0.863; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -14,47 +23,56 @@ db = db_factory() -test_script = """create global temporary table t (f1 int, f2 int, f3 int); -create index idx1 on t (f1); -create index idx2 on t (f2); -create index idx3 on t (f3); -drop index idx2; - -set plan on; -insert into t values (1, 1, 1); -select * from t where f1 = 1; -select * from t where f2 = 1; -select * from t where f3 = 1; +test_script = """ + create global temporary table gtt_test (f1 int, f2 int, f3 int); + create index idx1 on gtt_test (f1); + create index idx2 on gtt_test (f2); + create index idx3 on gtt_test (f3); + drop index idx2; + set list on; + set plan on; + insert into gtt_test values (1, 1, 1); + select * from gtt_test where f1 = 1; + select * from gtt_test where f2 = 1; + select * from gtt_test where f3 = 1; """ -act = isql_act('db', test_script) - -expected_stdout = """ -PLAN (T INDEX (IDX1)) - - F1 F2 F3 -============ ============ ============ - 1 1 1 - - -PLAN (T NATURAL) - - F1 F2 F3 -============ ============ ============ - 1 1 1 - - -PLAN (T INDEX (IDX3)) - - F1 F2 F3 -============ ============ ============ - 1 1 1 +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) + +expected_stdout_5x = """ + PLAN (GTT_TEST INDEX (IDX1)) + F1 1 + F2 1 + F3 1 + PLAN (GTT_TEST NATURAL) + F1 1 + F2 1 + F3 1 + PLAN (GTT_TEST INDEX (IDX3)) + F1 1 + F2 1 + F3 1 +""" +expected_stdout_6x = """ + PLAN ("PUBLIC"."GTT_TEST" INDEX ("PUBLIC"."IDX1")) + F1 1 + F2 1 + F3 1 + PLAN ("PUBLIC"."GTT_TEST" NATURAL) + F1 1 + F2 1 + F3 1 + PLAN ("PUBLIC"."GTT_TEST" INDEX ("PUBLIC"."IDX3")) + F1 1 + F2 1 + F3 1 """ @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1453_test.py b/tests/bugs/core_1453_test.py index fb710faa..54f1a017 100644 --- a/tests/bugs/core_1453_test.py +++ b/tests/bugs/core_1453_test.py @@ -7,45 +7,45 @@ DESCRIPTION: JIRA: CORE-1443 FBTEST: bugs.core_1453 +NOTES: + [23.08.2024] pzotov + Reimplemented: we have to avoid to show result of LIST() call because unpredictable order of its tokens. + This can cause fail if we change OptimizeForFirstRows = true config parameter. + Instead, test apply char_len() to the result of list(<...>, ). """ import pytest from firebird.qa import * -init_script = """CREATE TABLE T1 (ID INTEGER, NAME CHAR(20)); -COMMIT; -INSERT INTO T1 (ID,NAME) VALUES (1,'ORANGE'); -INSERT INTO T1 (ID,NAME) VALUES (1,'APPLE'); -INSERT INTO T1 (ID,NAME) VALUES (1,'LEMON'); -INSERT INTO T1 (ID,NAME) VALUES (2,'ORANGE'); -INSERT INTO T1 (ID,NAME) VALUES (2,'APPLE'); -INSERT INTO T1 (ID,NAME) VALUES (2,'PEAR'); -COMMIT; +init_script = """ + create table t1 (id integer, name char(20)); + commit; + insert into t1 (id,name) values (1,'orange'); + insert into t1 (id,name) values (1,'apple'); + insert into t1 (id,name) values (1,'lemon'); + insert into t1 (id,name) values (2,'orange'); + insert into t1 (id,name) values (2,'apple'); + insert into t1 (id,name) values (2,'pear'); + commit; """ db = db_factory(init=init_script) -test_script = """select ID, LIST( trim(NAME), ASCII_CHAR(35) ) -from T1 -group by 1; +test_script = """ + set list on; + select id, char_length(list( trim(name), ascii_char(35) )) chr_len + from t1 + group by id + order by id; """ -act = isql_act('db', test_script) +act = isql_act('db', test_script, substitutions = [ ('[ \t]+', ' '), ]) expected_stdout = """ - ID LIST -============ ================= - 1 0:1 -============================================================================== -LIST: -ORANGE#LEMON#APPLE -============================================================================== - 2 0:2 -============================================================================== -LIST: -PEAR#ORANGE#APPLE -============================================================================== - + ID 1 + CHR_LEN 18 + ID 2 + CHR_LEN 17 """ @pytest.mark.version('>=2.5.0') diff --git a/tests/bugs/core_1482_test.py b/tests/bugs/core_1482_test.py index 804e0a42..746d5e39 100644 --- a/tests/bugs/core_1482_test.py +++ b/tests/bugs/core_1482_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-1482 FBTEST: bugs.core_1482 +NOTES: + [25.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.863; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -59,18 +65,24 @@ select * from tcolor m join thorses d on m.id = d.color_id order by d.id rows 1; """ -act = isql_act('db', test_script) -expected_stdout = """ - COLORS_CNT 50 - HORSES_CNT 50000 +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) +expected_stdout_5x = """ + COLORS_CNT 50 + HORSES_CNT 50000 PLAN JOIN (D ORDER THORSES_ID, M INDEX (TCOLOR_ID)) """ +expected_stdout_6x = """ + COLORS_CNT 50 + HORSES_CNT 50000 + PLAN JOIN ("D" ORDER "PUBLIC"."THORSES_ID", "M" INDEX ("PUBLIC"."TCOLOR_ID")) +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_1518_test.py b/tests/bugs/core_1518_test.py index 07bb3414..76dd78ad 100644 --- a/tests/bugs/core_1518_test.py +++ b/tests/bugs/core_1518_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-1518 FBTEST: bugs.core_1518 +NOTES: + [25.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.863; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -45,7 +51,7 @@ act = isql_act('db', test_script) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 22006 unsuccessful metadata update -Cannot make field C1 of table TEST NOT NULL because there are NULLs present @@ -63,9 +69,22 @@ -Cannot make field C3 of table TEST NOT NULL because there are NULLs present """ +expected_stdout_6x = """ + Statement failed, SQLSTATE = 22006 + unsuccessful metadata update + -Cannot make field "C1" of table "PUBLIC"."TEST" NOT NULL because there are NULLs present + Statement failed, SQLSTATE = 22006 + unsuccessful metadata update + -Cannot make field "C1" of table "PUBLIC"."TEST" NOT NULL because there are NULLs present + Statement failed, SQLSTATE = 22006 + unsuccessful metadata update + -Cannot make field "C2" of table "PUBLIC"."TEST" NOT NULL because there are NULLs present + Statement failed, SQLSTATE = 22006 + unsuccessful metadata update + -Cannot make field "C3" of table "PUBLIC"."TEST" NOT NULL because there are NULLs present +""" @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1525_test.py b/tests/bugs/core_1525_test.py index ebc87ede..0e85c85d 100644 --- a/tests/bugs/core_1525_test.py +++ b/tests/bugs/core_1525_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-1525 FBTEST: bugs.core_1525 +NOTES: + [25.06.2025] pzotov + Separated expected PLAN for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.863; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -41,29 +47,34 @@ where cast ('2007-09-09' as date) < comp_last_day; """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ - ID 1 - LAST_DAY 2007-10-10 - COMP_LAST_DAY 2007-10-10 - ID 2 - LAST_DAY - COMP_LAST_DAY 2999-12-31 +@pytest.mark.version('>=2.0.7') +def test_1(act: Action): - PLAN (TEST_1 INDEX (IDX_1)) + # 25.06.2025 Separate PLAN depending on major FB version: + ########################## + expected_plan = 'PLAN (TEST_1 INDEX (IDX_1))' if act.is_version('<6') else 'PLAN ("PUBLIC"."TEST_1" INDEX ("PUBLIC"."IDX_1"))' - ID 1 - LAST_DAY 2007-10-10 - COMP_LAST_DAY 2007-10-10 - ID 2 - LAST_DAY - COMP_LAST_DAY 2999-12-31 -""" + expected_stdout = f""" + ID 1 + LAST_DAY 2007-10-10 + COMP_LAST_DAY 2007-10-10 + ID 2 + LAST_DAY + COMP_LAST_DAY 2999-12-31 + + {expected_plan} + + ID 1 + LAST_DAY 2007-10-10 + COMP_LAST_DAY 2007-10-10 + ID 2 + LAST_DAY + COMP_LAST_DAY 2999-12-31 + """ -@pytest.mark.version('>=2.0.7') -def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_1539_test.py b/tests/bugs/core_1539_test.py index 8e3db6c4..5a9ed57a 100644 --- a/tests/bugs/core_1539_test.py +++ b/tests/bugs/core_1539_test.py @@ -2,9 +2,11 @@ """ ID: issue-1957 -ISSUE: 1957 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/1957 TITLE: select * from rdb$triggers where rdb$trigger_source like 'CHECK%' DESCRIPTION: + presence of '%' at the end of LIKE pattern causes + "arithmetic exception, numeric overflow, or string truncation. Cannot transliterate character between character sets" JIRA: CORE-1539 FBTEST: bugs.core_1539 """ @@ -13,9 +15,9 @@ from firebird.qa import * init_script = """ - -- ### ONCE AGAIN ### - -- 1) for checking this under ISQL following must be encoded in WIN1251 - -- 2) for running under fbt_run utility following must be encoded in UTF8. + -- note: + -- for checking this under ISQL following must be encoded in WIN1251 + -- for running under fbt_run utility following must be encoded in UTF8. recreate table test ( bugtype varchar(20), bugfrequency varchar(20), @@ -36,53 +38,51 @@ db = db_factory(charset='UTF8', init=init_script) test_script = """ -set blob all; -set list on; --- Ticket: --- select * from rdb$triggers where rdb$trigger_source like 'CHECK%%' ==> "Cannot transliterate character between character sets." --- select * from rdb$triggers where rdb$trigger_source starting 'CHECK' ==> works fine. -select rdb$trigger_name, rdb$trigger_source -from rdb$triggers -where rdb$trigger_source like 'check%%' -order by cast(replace(rdb$trigger_name, 'CHECK_', '') as int); + set blob all; + set list on; + -- Ticket: + -- select * from rdb$triggers where rdb$trigger_source like 'CHECK%%' ==> "Cannot transliterate character between character sets." + -- select * from rdb$triggers where rdb$trigger_source starting 'CHECK' ==> works fine. + select rdb$trigger_name, rdb$trigger_source as trg_source_blob_id + from rdb$triggers + where rdb$trigger_source like 'check%%' + order by rdb$trigger_name; """ -act = isql_act('db', test_script, - substitutions=[('RDB$TRIGGER_SOURCE.*', 'RDB$TRIGGER_SOURCE ')]) +act = isql_act('db', test_script, substitutions=[('TRG_SOURCE_BLOB_ID .*', 'TRG_SOURCE_BLOB_ID')]) expected_stdout = """ + RDB$TRIGGER_NAME CHECK_1 + TRG_SOURCE_BLOB_ID 0:b + check (bugtype in ('зрабіць', 'трэба зрабіць', 'недахоп', 'памылка', 'катастрофа')) -RDB$TRIGGER_NAME CHECK_1 -RDB$TRIGGER_SOURCE 0:b -check (bugtype in ('зрабіць', 'трэба зрабіць', 'недахоп', 'памылка', 'катастрофа')) + RDB$TRIGGER_NAME CHECK_2 + TRG_SOURCE_BLOB_ID 0:e + check (bugtype in ('зрабіць', 'трэба зрабіць', 'недахоп', 'памылка', 'катастрофа')) -RDB$TRIGGER_NAME CHECK_2 -RDB$TRIGGER_SOURCE 0:e -check (bugtype in ('зрабіць', 'трэба зрабіць', 'недахоп', 'памылка', 'катастрофа')) + RDB$TRIGGER_NAME CHECK_3 + TRG_SOURCE_BLOB_ID 0:11 + check (bugfrequency in ('ніколі', 'зрэдку', 'часам', 'часта', 'заўсёды', 'не прыкладаецца')) -RDB$TRIGGER_NAME CHECK_3 -RDB$TRIGGER_SOURCE 0:11 -check (bugfrequency in ('ніколі', 'зрэдку', 'часам', 'часта', 'заўсёды', 'не прыкладаецца')) + RDB$TRIGGER_NAME CHECK_4 + TRG_SOURCE_BLOB_ID 0:14 + check (bugfrequency in ('ніколі', 'зрэдку', 'часам', 'часта', 'заўсёды', 'не прыкладаецца')) -RDB$TRIGGER_NAME CHECK_4 -RDB$TRIGGER_SOURCE 0:14 -check (bugfrequency in ('ніколі', 'зрэдку', 'часам', 'часта', 'заўсёды', 'не прыкладаецца')) + RDB$TRIGGER_NAME CHECK_5 + TRG_SOURCE_BLOB_ID 0:17 + check (decision in ('адкрыта', 'зроблена', 'састарэла', 'адхілена', 'часткова', 'выдалена')) -RDB$TRIGGER_NAME CHECK_5 -RDB$TRIGGER_SOURCE 0:17 -check (decision in ('адкрыта', 'зроблена', 'састарэла', 'адхілена', 'часткова', 'выдалена')) + RDB$TRIGGER_NAME CHECK_6 + TRG_SOURCE_BLOB_ID 0:1a + check (decision in ('адкрыта', 'зроблена', 'састарэла', 'адхілена', 'часткова', 'выдалена')) -RDB$TRIGGER_NAME CHECK_6 -RDB$TRIGGER_SOURCE 0:1a -check (decision in ('адкрыта', 'зроблена', 'састарэла', 'адхілена', 'часткова', 'выдалена')) + RDB$TRIGGER_NAME CHECK_7 + TRG_SOURCE_BLOB_ID 0:1d + check ((decision = 'адкрыта' and fixerkey is null and decisiondate is null) or (decision <> 'адкрыта' and not fixerkey is null and not decisiondate is null)) -RDB$TRIGGER_NAME CHECK_7 -RDB$TRIGGER_SOURCE 0:1d -check ((decision = 'адкрыта' and fixerkey is null and decisiondate is null) or (decision <> 'адкрыта' and not fixerkey is null and not decisiondate is null)) - -RDB$TRIGGER_NAME CHECK_8 -RDB$TRIGGER_SOURCE 0:20 -check ((decision = 'адкрыта' and fixerkey is null and decisiondate is null) or (decision <> 'адкрыта' and not fixerkey is null and not decisiondate is null)) + RDB$TRIGGER_NAME CHECK_8 + TRG_SOURCE_BLOB_ID 0:20 + check ((decision = 'адкрыта' and fixerkey is null and decisiondate is null) or (decision <> 'адкрыта' and not fixerkey is null and not decisiondate is null)) """ @pytest.mark.version('>=3') diff --git a/tests/bugs/core_1549_test.py b/tests/bugs/core_1549_test.py index 29e0fece..00677906 100644 --- a/tests/bugs/core_1549_test.py +++ b/tests/bugs/core_1549_test.py @@ -7,157 +7,231 @@ DESCRIPTION: JIRA: CORE-1549 FBTEST: bugs.core_1549 +NOTES: + [25.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Re-implemented in order to preserve leading spaces in the explained plans output. + + Checked on 6.0.0.863; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -db = db_factory() - -test_script = """ +init_script = """ recreate table t(id int); commit; insert into t select row_number()over() from rdb$types a, (select 1 i from rdb$types rows 4) b rows 1000; commit; create index t_id on t(id); commit; - - -- Query-1: - set list on; - select '' as "EXISTS with ref. to 1st stream:" from rdb$database; - - set planonly; - set explain on; - - select a.id a_id, b.id b_id - from t a join t b on b.id >= a.id - where - not exists (select * from t x where x.id = a.id - 1) - and - not exists (select * from t z where z.id = b.id + 1); - - set planonly; - set plan off; - set explain off; - - select '' as "Two sep. DT and EXISTS inside:" from rdb$database; - - set planonly; - set explain on; - -- Query-2 - -- (serves as "etalone" -- how it should be in query-1): - select a.id a_id, b.id b_id - from ( - select t1.id - from t t1 - where - not exists (select * from t x where x.id = t1.id - 1) - ) a - join - ( - select t2.id - from t t2 - where - not exists (select * from t x where x.id = t2.id + 1) - ) b - on b.id >= a.id; """ +db = db_factory(init = init_script) -act = isql_act('db', test_script) +qry_map = { + 1000 : + ( + """ + select a.id a_id, b.id b_id + from t a join t b on b.id >= a.id + where + not exists (select * from t x where x.id = a.id - 1) + and + not exists (select * from t z where z.id = b.id + 1); + """ + , + "EXISTS() with reference to 1st stream" + ) + , + 2000 : + ( + """ + select a.id a_id, b.id b_id + from ( + select t1.id + from t t1 + where + not exists (select * from t x where x.id = t1.id - 1) + ) a + join + ( + select t2.id + from t t2 + where + not exists (select * from t x where x.id = t2.id + 1) + ) b + on b.id >= a.id + """ + , + 'Two separate derived tables and EXISTS() inside. Its plan must be considered as "etalone", i.e. how it should be in first the query' + ) +} + +act = python_act('db') + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- -fb3x_expected_out = """ - EXISTS with ref. to 1st stream: +@pytest.mark.version('>=3.0') +def test_1(act: Action, capsys): + with act.db.connect() as con: + cur = con.cursor() + for qry_idx, qry_data in qry_map.items(): + test_sql, qry_comment = qry_data[:2] + ps, rs = None, None + try: + cur = con.cursor() + ps = cur.prepare(test_sql) + + print(qry_comment) + # Print explained plan with padding eash line by dots in order to see indentations: + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + except DatabaseError as e: + print(e.__str__()) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() + + expected_out_3x = f""" + + {qry_map.get(1000)[1]} Select Expression - -> Filter - -> Table "T" as "X" Access By ID - -> Bitmap - -> Index "T_ID" Range Scan (full match) + ....-> Filter + ........-> Table "T" as "X" Access By ID + ............-> Bitmap + ................-> Index "T_ID" Range Scan (full match) Select Expression - -> Filter - -> Table "T" as "Z" Access By ID - -> Bitmap - -> Index "T_ID" Range Scan (full match) + ....-> Filter + ........-> Table "T" as "Z" Access By ID + ............-> Bitmap + ................-> Index "T_ID" Range Scan (full match) Select Expression - -> Nested Loop Join (inner) - -> Filter - -> Table "T" as "A" Full Scan - -> Filter - -> Table "T" as "B" Access By ID - -> Bitmap - -> Index "T_ID" Range Scan (lower bound: 1/1) - - - Two sep. DT and EXISTS inside: - + ....-> Nested Loop Join (inner) + ........-> Filter + ............-> Table "T" as "A" Full Scan + ........-> Filter + ............-> Table "T" as "B" Access By ID + ................-> Bitmap + ....................-> Index "T_ID" Range Scan (lower bound: 1/1) + + {qry_map.get(2000)[1]} Select Expression - -> Filter - -> Table "T" as "B X" Access By ID - -> Bitmap - -> Index "T_ID" Range Scan (full match) + ....-> Filter + ........-> Table "T" as "B X" Access By ID + ............-> Bitmap + ................-> Index "T_ID" Range Scan (full match) Select Expression - -> Filter - -> Table "T" as "A X" Access By ID - -> Bitmap - -> Index "T_ID" Range Scan (full match) + ....-> Filter + ........-> Table "T" as "A X" Access By ID + ............-> Bitmap + ................-> Index "T_ID" Range Scan (full match) Select Expression - -> Nested Loop Join (inner) - -> Filter - -> Table "T" as "A T1" Full Scan - -> Filter - -> Table "T" as "B T2" Access By ID - -> Bitmap - -> Index "T_ID" Range Scan (lower bound: 1/1) -""" - -fb5x_expected_out = """ - EXISTS with ref. to 1st stream: - - Sub-query - -> Filter - -> Table "T" as "X" Access By ID - -> Bitmap - -> Index "T_ID" Range Scan (full match) - Sub-query - -> Filter - -> Table "T" as "Z" Access By ID - -> Bitmap - -> Index "T_ID" Range Scan (full match) - Select Expression - -> Nested Loop Join (inner) - -> Filter - -> Table "T" as "A" Full Scan - -> Filter - -> Table "T" as "B" Access By ID - -> Bitmap - -> Index "T_ID" Range Scan (lower bound: 1/1) - - Two sep. DT and EXISTS inside: - - - - Sub-query - -> Filter - -> Table "T" as "B X" Access By ID - -> Bitmap - -> Index "T_ID" Range Scan (full match) - Sub-query - -> Filter - -> Table "T" as "A X" Access By ID - -> Bitmap - -> Index "T_ID" Range Scan (full match) - Select Expression - -> Nested Loop Join (inner) - -> Filter - -> Table "T" as "A T1" Full Scan - -> Filter - -> Table "T" as "B T2" Access By ID - -> Bitmap - -> Index "T_ID" Range Scan (lower bound: 1/1) -""" -@pytest.mark.version('>=3.0') -def test_1(act: Action): - act.expected_stdout = fb3x_expected_out if act.is_version('<5') else fb5x_expected_out - act.execute(combine_output = True) + ....-> Nested Loop Join (inner) + ........-> Filter + ............-> Table "T" as "A T1" Full Scan + ........-> Filter + ............-> Table "T" as "B T2" Access By ID + ................-> Bitmap + ....................-> Index "T_ID" Range Scan (lower bound: 1/1) + """ + + expected_out_5x = f""" + + {qry_map.get(1000)[1]} + Sub-query + ....-> Filter + ........-> Table "T" as "X" Access By ID + ............-> Bitmap + ................-> Index "T_ID" Range Scan (full match) + Sub-query + ....-> Filter + ........-> Table "T" as "Z" Access By ID + ............-> Bitmap + ................-> Index "T_ID" Range Scan (full match) + Select Expression + ....-> Nested Loop Join (inner) + ........-> Filter + ............-> Table "T" as "A" Full Scan + ........-> Filter + ............-> Table "T" as "B" Access By ID + ................-> Bitmap + ....................-> Index "T_ID" Range Scan (lower bound: 1/1) + + {qry_map.get(2000)[1]} + Sub-query + ....-> Filter + ........-> Table "T" as "B X" Access By ID + ............-> Bitmap + ................-> Index "T_ID" Range Scan (full match) + Sub-query + ....-> Filter + ........-> Table "T" as "A X" Access By ID + ............-> Bitmap + ................-> Index "T_ID" Range Scan (full match) + Select Expression + ....-> Nested Loop Join (inner) + ........-> Filter + ............-> Table "T" as "A T1" Full Scan + ........-> Filter + ............-> Table "T" as "B T2" Access By ID + ................-> Bitmap + ....................-> Index "T_ID" Range Scan (lower bound: 1/1) + """ + + expected_out_6x = f""" + + {qry_map.get(1000)[1]} + Sub-query + ....-> Filter + ........-> Table "PUBLIC"."T" as "X" Access By ID + ............-> Bitmap + ................-> Index "PUBLIC"."T_ID" Range Scan (full match) + Sub-query + ....-> Filter + ........-> Table "PUBLIC"."T" as "Z" Access By ID + ............-> Bitmap + ................-> Index "PUBLIC"."T_ID" Range Scan (full match) + Select Expression + ....-> Nested Loop Join (inner) + ........-> Filter + ............-> Table "PUBLIC"."T" as "A" Full Scan + ........-> Filter + ............-> Table "PUBLIC"."T" as "B" Access By ID + ................-> Bitmap + ....................-> Index "PUBLIC"."T_ID" Range Scan (lower bound: 1/1) + + {qry_map.get(2000)[1]} + Sub-query + ....-> Filter + ........-> Table "PUBLIC"."T" as "B" "X" Access By ID + ............-> Bitmap + ................-> Index "PUBLIC"."T_ID" Range Scan (full match) + Sub-query + ....-> Filter + ........-> Table "PUBLIC"."T" as "A" "X" Access By ID + ............-> Bitmap + ................-> Index "PUBLIC"."T_ID" Range Scan (full match) + Select Expression + ....-> Nested Loop Join (inner) + ........-> Filter + ............-> Table "PUBLIC"."T" as "A" "T1" Full Scan + ........-> Filter + ............-> Table "PUBLIC"."T" as "B" "T2" Access By ID + ................-> Bitmap + ....................-> Index "PUBLIC"."T_ID" Range Scan (lower bound: 1/1) + """ + + act.expected_stdout = expected_out_3x if act.is_version('<5') else expected_out_5x if act.is_version('<6') else expected_out_6x + + act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_1550_postfix_test.py b/tests/bugs/core_1550_postfix_test.py index 0ff0fb44..570e0154 100644 --- a/tests/bugs/core_1550_postfix_test.py +++ b/tests/bugs/core_1550_postfix_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-1550 FBTEST: bugs.core_1550_postfix +NOTES: + [23.03.2025] pzotov + Separated output because plans differ on 6.x vs previous versions since commit fc12c0ef + ("Unnest IN/ANY/EXISTS subqueries and optimize them using semi-join algorithm (#8061)"). + Checked on 6.0.0.687-730aa8f; 5.0.3.1633-25a0817 """ import pytest @@ -71,17 +76,23 @@ act = isql_act('db', test_script) -expected_stdout = """ - PLAN (D ORDER TD_PK) - PLAN (M NATURAL) - - PLAN (D ORDER TD_F01_F02_UNQ) - PLAN (M NATURAL) -""" - @pytest.mark.version('>=3.0') def test_1(act: Action): + if act.is_version('<6'): + expected_stdout = """ + PLAN (D ORDER TD_PK) + PLAN (M NATURAL) + + PLAN (D ORDER TD_F01_F02_UNQ) + PLAN (M NATURAL) + """ + else: + expected_stdout = """ + PLAN HASH ("M" NATURAL, "D" ORDER "PUBLIC"."TD_PK") + PLAN HASH ("M" NATURAL, "D" ORDER "PUBLIC"."TD_F01_F02_UNQ") + """ + act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1550_test.py b/tests/bugs/core_1550_test.py index d26bf98d..ec333164 100644 --- a/tests/bugs/core_1550_test.py +++ b/tests/bugs/core_1550_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-1550 FBTEST: bugs.core_1550 +NOTES: + [25.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.863; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -31,13 +37,16 @@ act = isql_act('db', test_script) -expected_stdout = """ +expected_stdout_5x = """ PLAN (TEST ORDER TEST_ID) """ +expected_stdout_6x = """ + PLAN ("PUBLIC"."TEST" ORDER "PUBLIC"."TEST_ID") +""" @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1559_test.py b/tests/bugs/core_1559_test.py index a9dd8ad3..92205677 100644 --- a/tests/bugs/core_1559_test.py +++ b/tests/bugs/core_1559_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-1559 FBTEST: bugs.core_1559 +NOTES: + [25.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.863; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -14,35 +20,33 @@ db = db_factory() -test_script = """create table t (n integer constraint c not null); -COMMIT; -insert into t values (null); -COMMIT; -alter table t drop constraint c; -COMMIT; -insert into t values (null); -COMMIT; -SELECT * FROM t; +test_script = """ + set list on; + create table test (n integer constraint explicit_check_for_nn not null); + insert into test values (null); + commit; + alter table test drop constraint explicit_check_for_nn; + insert into test values (null); + select * from test; """ -act = isql_act('db', test_script) - -expected_stdout = """ - N -============ - +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) +expected_stdout_5x = """ + Statement failed, SQLSTATE = 23000 + validation error for column "TEST"."N", value "*** null ***" + N """ -expected_stderr = """Statement failed, SQLSTATE = 23000 -validation error for column "T"."N", value "*** null ***" +expected_stdout_6x = """ + Statement failed, SQLSTATE = 23000 + validation error for column "PUBLIC"."TEST"."N", value "*** null ***" + N """ @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1592_test.py b/tests/bugs/core_1592_test.py index 05f838d0..dc5f62bd 100644 --- a/tests/bugs/core_1592_test.py +++ b/tests/bugs/core_1592_test.py @@ -2,11 +2,19 @@ """ ID: issue-2013 -ISSUE: 2013 -TITLE: Altering procedure parameters can lead to unrestorable database +ISSUE: https://github.com/FirebirdSQL/firebird/issues/2013 +TITLE: Altering procedure parameters must be prohibited if there are dependent data in RDB$DEPENDENCIES DESCRIPTION: + Firebird should not allow the ALTER PROCEDURE statements if there are records in RDB$DEPENDENCIES + that reference a parameter that is not going to exists after the statement. JIRA: CORE-1592 FBTEST: bugs.core_1592 +NOTES: + [25.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.863; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -18,21 +26,21 @@ set bail on; set list on; set term ^; - create or alter procedure p2 as begin end + create or alter procedure sp_caller as begin end ^ commit ^ - create or alter procedure p1 returns ( x1 integer ) as begin + create or alter procedure sp_worker returns ( x1 integer ) as begin x1 = 10; suspend; end ^ - create or alter procedure p2 returns ( x1 integer ) as begin - for select x1 from p1 into :x1 do suspend; + create or alter procedure sp_caller returns ( x1 integer ) as begin + for select x1 from sp_worker into :x1 do suspend; end ^ -- This should FAIL and terminate script execution: - alter procedure p1 returns ( x2 integer ) as begin + alter procedure sp_worker returns ( x2 integer ) as begin x2 = 10; suspend; end ^ @@ -43,17 +51,24 @@ act = isql_act('db', test_script) -expected_stderr = """ +expected_stdout_5x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -cannot delete + -PARAMETER SP_WORKER.X1 + -there are 1 dependencies +""" + +expected_stdout_6x = """ Statement failed, SQLSTATE = 42000 unsuccessful metadata update -cannot delete - -PARAMETER P1.X1 + -PARAMETER "PUBLIC"."SP_WORKER".X1 -there are 1 dependencies """ @pytest.mark.version('>=4.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1606_test.py b/tests/bugs/core_1606_test.py index aca29362..0c737e25 100644 --- a/tests/bugs/core_1606_test.py +++ b/tests/bugs/core_1606_test.py @@ -120,6 +120,7 @@ PID 1 """ +@pytest.mark.es_eds @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_1607_test.py b/tests/bugs/core_1607_test.py index 3876c2d3..8b6dea45 100644 --- a/tests/bugs/core_1607_test.py +++ b/tests/bugs/core_1607_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-1607 FBTEST: bugs.core_1607 +NOTES: + [23.03.2025] pzotov + Separated expected_out because plans differ on 6.x vs previous versions since commit fc12c0ef + ("Unnest IN/ANY/EXISTS subqueries and optimize them using semi-join algorithm (#8061)"). + Checked on 6.0.0.687-730aa8f; 5.0.3.1633-25a0817 """ import pytest @@ -14,44 +19,58 @@ db = db_factory() -test_script = """SET PLANONLY ON; -select 1 -from ( select rdb$relation_name, ( select 1 from rdb$database ) as c from rdb$relations ) r -where exists ( select * from rdb$relation_fields f where f.rdb$relation_name = r.rdb$relation_name ); -select 1 -from ( - select * from rdb$relations - union all - select * from rdb$relations -) r -where exists ( select * from rdb$relation_fields f where f.rdb$relation_name = r.rdb$relation_name ); -select ( select first 1 r.rdb$relation_name - from rdb$relations r - where r.rdb$relation_id = d.rdb$relation_id - 1 ) -from ( - select * from rdb$database - union all - select * from rdb$database -) d; -""" - -act = isql_act('db', test_script) +test_script = """ + set planonly; -expected_stdout = """ -PLAN (R RDB$DATABASE NATURAL) -PLAN (F INDEX (RDB$INDEX_4)) -PLAN (R RDB$RELATIONS NATURAL) + select 1 + from ( select rdb$relation_name, ( select 1 from rdb$database ) as c from rdb$relations ) r + where exists ( select * from rdb$relation_fields f where f.rdb$relation_name = r.rdb$relation_name ); -PLAN (F INDEX (RDB$INDEX_4)) -PLAN (R RDB$RELATIONS NATURAL, R RDB$RELATIONS NATURAL) + select 1 + from ( + select * from rdb$relations + union all + select * from rdb$relations + ) r + where exists ( select * from rdb$relation_fields f where f.rdb$relation_name = r.rdb$relation_name ); -PLAN (R INDEX (RDB$INDEX_1)) -PLAN (D RDB$DATABASE NATURAL, D RDB$DATABASE NATURAL) + select ( select first 1 r.rdb$relation_name + from rdb$relations r + where r.rdb$relation_id = d.rdb$relation_id - 1 ) + from ( + select * from rdb$database + union all + select * from rdb$database + ) d; """ +act = isql_act('db', test_script) + + @pytest.mark.version('>=3.0') def test_1(act: Action): + if act.is_version('<6'): + expected_stdout = """ + PLAN (R RDB$DATABASE NATURAL) + PLAN (F INDEX (RDB$INDEX_4)) + PLAN (R RDB$RELATIONS NATURAL) + + PLAN (F INDEX (RDB$INDEX_4)) + PLAN (R RDB$RELATIONS NATURAL, R RDB$RELATIONS NATURAL) + + PLAN (R INDEX (RDB$INDEX_1)) + PLAN (D RDB$DATABASE NATURAL, D RDB$DATABASE NATURAL) + """ + else: + expected_stdout = """ + PLAN ("R" "SYSTEM"."RDB$DATABASE" NATURAL) + PLAN HASH ("R" "SYSTEM"."RDB$RELATIONS" NATURAL, "F" NATURAL) + PLAN HASH ("R" "SYSTEM"."RDB$RELATIONS" NATURAL, "R" "SYSTEM"."RDB$RELATIONS" NATURAL, "F" NATURAL) + PLAN ("R" INDEX ("SYSTEM"."RDB$INDEX_1")) + PLAN ("D" "SYSTEM"."RDB$DATABASE" NATURAL, "D" "SYSTEM"."RDB$DATABASE" NATURAL) + """ + act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1620_test.py b/tests/bugs/core_1620_test.py index 3264150e..442cedca 100644 --- a/tests/bugs/core_1620_test.py +++ b/tests/bugs/core_1620_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-1620 FBTEST: bugs.core_1620 +NOTES: + [25.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.863; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -26,9 +32,15 @@ execute procedure test_es1; """ -act = isql_act('db', test_script, substitutions=[("-At procedure 'TEST_ES1' line:.*", '')]) +# ::: ACHTUNG ::: +# DO NOT use any substitutions here! +# We have to check EXACTLY that error message contains +# proper (adequate) column value in: +# "-Unexpected end of command - line 1, column 1" +# +act = isql_act('db', test_script) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 42000 Dynamic SQL Error -SQL error code = -104 @@ -36,9 +48,16 @@ -At procedure 'TEST_ES1' line: 3, col: 9 """ +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Unexpected end of command - line 1, column 1 + -At procedure "PUBLIC"."TEST_ES1" line: 3, col: 9 +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1689_test.py b/tests/bugs/core_1689_test.py index 389b38d2..bbc84d8b 100644 --- a/tests/bugs/core_1689_test.py +++ b/tests/bugs/core_1689_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-1689 FBTEST: bugs.core_1689 +NOTES: + [25.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.863; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -44,12 +50,15 @@ -there are 4 dependencies """ + @pytest.mark.version('>=3.0,<4.0') def test_1(act_1: Action): act_1.expected_stderr = expected_stderr_1 act_1.execute() assert act_1.clean_stderr == act_1.clean_expected_stderr +######################################################################################### + # version: 4.0 db_2 = db_factory() @@ -108,17 +117,26 @@ def test_1(act_1: Action): act_2 = isql_act('db_2', test_script_2) -expected_stderr_2 = """ - Statement failed, SQLSTATE = 38000 - unsuccessful metadata update - -cannot delete - -Function UDR40_GETEXACTTIMESTAMPUTC - -there are 6 dependencies -""" - @pytest.mark.version('>=4.0') def test_2(act_2: Action): - act_2.expected_stderr = expected_stderr_2 - act_2.execute() - assert act_2.clean_stderr == act_2.clean_expected_stderr + if act_2.is_version('<6'): + expected_stdout = """ + Statement failed, SQLSTATE = 38000 + unsuccessful metadata update + -cannot delete + -Function UDR40_GETEXACTTIMESTAMPUTC + -there are 6 dependencies + """ + else: + expected_stdout = """ + Statement failed, SQLSTATE = 38000 + unsuccessful metadata update + -cannot delete + -Function "PUBLIC"."UDR40_GETEXACTTIMESTAMPUTC" + -there are 6 dependencies + """ + + act_2.expected_stdout = expected_stdout + act_2.execute(combine_output = True) + assert act_2.clean_stdout == act_2.clean_expected_stdout diff --git a/tests/bugs/core_1690_test.py b/tests/bugs/core_1690_test.py index 9db71151..a6f08b16 100644 --- a/tests/bugs/core_1690_test.py +++ b/tests/bugs/core_1690_test.py @@ -7,29 +7,55 @@ DESCRIPTION: JIRA: CORE-1690 FBTEST: bugs.core_1690 -""" +NOTES: + [25.06.2025] pzotov + Re-implemented: non-ascii names are used for created table and its column, with max allowed length + for utf8 charset to prevent "Name longer than database column size" error. -import pytest -from firebird.qa import * + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. -init_script = """create table A (C1 INTEGER PRIMARY KEY); + Checked on 6.0.0.863; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ -db = db_factory(charset='UTF8', init=init_script) - -test_script = """show table A; -""" +import pytest +from firebird.qa import * -act = isql_act('db', test_script) +db = db_factory(charset='utf8') -expected_stdout = """C1 INTEGER Not Null -CONSTRAINT INTEG_2: - Primary key (C1) -""" +substitutions = [('[ \t]+', ' '), ('Table:.*', ''), ('CONSTRAINT INTEG_\\d+', 'CONSTRAINT INTEG')] +act = isql_act('db', substitutions = substitutions) @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + if act.is_version('<4'): + # We have ti limit names by 16 unicode characters otherwise get: + # "SQLSTATE = 42000 / -Name longer than database column size" + TABLE_NAME = 'àáâãäåæçèéêëìíî' + FIELD_NAME = 'ÐÑÒÓÔÕÖרÙÚÛÜÝÞ' + else: + TABLE_NAME = 'àáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ' + FIELD_NAME = 'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþ' + + test_script = f""" + create table "{TABLE_NAME}" ("{FIELD_NAME}" int primary key); + show table "{TABLE_NAME}"; + """ + + expected_stdout_5x = f""" + {FIELD_NAME} INTEGER Not Null + CONSTRAINT INTEG_2: + Primary key ({FIELD_NAME}) + """ + + # NB: Names of table and field are enclosed in double quotes in FB 6.x (since 6.0.0.834): + expected_stdout_6x = f""" + "{FIELD_NAME}" INTEGER Not Null + CONSTRAINT INTEG_2: + Primary key ("{FIELD_NAME}") + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + #act.execute(combine_output = True) + act.isql(switches = ['-q'], charset = 'utf8', input = test_script, combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_1694_test.py b/tests/bugs/core_1694_test.py index bffd5fc1..5dfbd77d 100644 --- a/tests/bugs/core_1694_test.py +++ b/tests/bugs/core_1694_test.py @@ -7,6 +7,15 @@ DESCRIPTION: JIRA: CORE-1694 FBTEST: bugs.core_1694 +NOTES: + [25.06.2025] pzotov + Re-implemented: use variables for storing non-ascii text and f-notation to sustitute them. + Use alternate string quoting to avoid duplicating of apostrophes inside statement passed to ES. + + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.863; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -14,33 +23,22 @@ db = db_factory(charset='UTF8') -test_script = """ -create domain varchar_domain as varchar(50) character set utf8 collate utf8; -create domain varchar_domain2 as varchar(50) character set utf8 collate utf8; -commit; - -set term ^; -execute block as -begin -execute statement ' -create or alter trigger trg_conn active on connect position 0 -as +HEAD_COMMENT_1 = """ /* — Eh bien, mon prince. Gênes et Lucques ne sont plus que des apanages, des поместья, de la famille Buonaparte. Non, je vous préviens que si vous ne me dites pas que nous avons la guerre, si vous vous permettez encore de pallier toutes les infamies, toutes - les atrocités de cet Antichrist (ma parole, j''y crois) — je ne vous connais plus, vous - n''êtes plus mon ami, vous n''êtes plus мой верный раб, comme vous dites. + les atrocités de cet Antichrist (ma parole, j'y crois) — je ne vous connais plus, vous + n'êtes plus mon ami, vous n'êtes plus мой верный раб, comme vous dites. Ну, здравствуйте, здравствуйте. Je vois que je vous fais peur, садитесь и рассказывайте. Так говорила в июле 1805 года известная Анна Павловна Шерер, фрейлина и приближенная императрицы Марии Феодоровны, встречая важного и чиновного князя Василия, первого приехавшего на ее вечер. Анна Павловна кашляла несколько дней, у нее был грипп, как она говорила (грипп был тогда новое слово, употреблявшееся только редкими). */ - declare u int; - declare variable new_var1 varchar(50) character set utf8 collate utf8 default ''Que voulez-vous ?''; - declare variable new_var3 type of varchar_domain default ''Что делать ?''; - declare variable new_var4 type of varchar_domain2 default ''Кто виноват ?''; +""" + +HEAD_COMMENT_2 = """ /* Dieu, quelle virulente sortie! 4 — отвечал, нисколько не смутясь такою встречей, вошедший князь, в придворном, шитом мундире, в чулках, башмаках и звездах, с светлым выражением @@ -49,105 +47,114 @@ состаревшемуся в свете и при дворе значительному человеку. Он подошел к Анне Павловне, поцеловал ее руку, подставив ей свою надушенную и сияющую лысину, и покойно уселся на диване. */ - declare v int = 2; +""" + +HEAD_COMMENT_3 = """ /* - — Ne me tourmentez pas. Eh bien, qu''a-t-on décidé par rapport à la dépêche de Novosilzoff? + — Ne me tourmentez pas. Eh bien, qu'a-t-on décidé par rapport à la dépêche de Novosilzoff? Vous savez tout. - — Как вам сказать? — сказал князь холодным, скучающим тоном. — Qu''a-t-on décidé? On a décidé + — Как вам сказать? — сказал князь холодным, скучающим тоном. — Qu'a-t-on décidé? On a décidé que Buonaparte a brûlé ses vaisseaux, et je crois que nous sommes en train de brûler les nôtres 8. Князь Василий говорил всегда лениво, как актер говорит роль старой пиесы. Анна Павловна Шерер, напротив, несмотря на свои сорок лет, была преисполнена оживления и порывов. */ - declare w int = 3; -begin - u = +""" + +BODY_COMMENT_1 = """ /* Ах, не говорите мне про Австрию! Я ничего не понимаю, может быть, но Австрия никогда не хотела и не хочет войны. Она предает нас. Россия одна должна быть спасительницей Европы. Наш благодетель знает свое высокое призвание и будет верен ему. Вот одно, во что я верю. Нашему доброму и чудному */ - v + +""" + +BODY_COMMENT_2 = """ /*государю предстоит величайшая роль в мире, и он так добродетелен и хорош, что Бог не оставит его, и он исполнит свое призвание задавить гидру революции, которая теперь еще ужаснее в лице этого убийцы и злодея. Мы одни должны искупить кровь праведника. На кого нам надеяться, я вас спрашиваю?.. */ - w; +""" + +BODY_COMMENT_3 = """ /* Англия с своим коммерческим духом не поймет и не может понять всю высоту души императора Александра */ -end' -; -end -^ -set term ;^ -commit; - -show trigger trg_conn; """ -act = isql_act('db', test_script, - substitutions=[('[+++].*', ''), ('[===].*', ''), ('Trigger text.*', '')]) +test_script = f""" +create domain varchar_domain as varchar(50) character set utf8 collate utf8; +create domain varchar_domain2 as varchar(50) character set utf8 collate utf8; +commit; -expected_stdout = """ -TRG_CONN, Sequence: 0, Type: ON CONNECT, Active +set term ^; +execute block as +begin +-- ############################################################################### +-- ### u s e `q' ` ### +-- ### see: $FB_HOME/doc/sql.extensions/README.alternate_string_quoting.txt ### +-- ############################################################################### +execute statement q'# +create or alter trigger trg_conn active on connect position 0 as - /* - — Eh bien, mon prince. Gênes et Lucques ne sont plus que des apanages, des поместья, - de la famille Buonaparte. Non, je vous préviens que si vous ne me dites pas que nous - avons la guerre, si vous vous permettez encore de pallier toutes les infamies, toutes - les atrocités de cet Antichrist (ma parole, j'y crois) — je ne vous connais plus, vous - n'êtes plus mon ami, vous n'êtes plus мой верный раб, comme vous dites. - Ну, здравствуйте, здравствуйте. Je vois que je vous fais peur, садитесь и рассказывайте. - Так говорила в июле 1805 года известная Анна Павловна Шерер, фрейлина и приближенная - императрицы Марии Феодоровны, встречая важного и чиновного князя Василия, первого - приехавшего на ее вечер. Анна Павловна кашляла несколько дней, у нее был грипп, как она - говорила (грипп был тогда новое слово, употреблявшееся только редкими). - */ + {HEAD_COMMENT_1} declare u int; declare variable new_var1 varchar(50) character set utf8 collate utf8 default 'Que voulez-vous ?'; declare variable new_var3 type of varchar_domain default 'Что делать ?'; declare variable new_var4 type of varchar_domain2 default 'Кто виноват ?'; - /* - Dieu, quelle virulente sortie! 4 — отвечал, нисколько не смутясь такою встречей, вошедший - князь, в придворном, шитом мундире, в чулках, башмаках и звездах, с светлым выражением - плоского лица. Он говорил на том изысканном французском языке, на котором не только говорили, - но и думали наши деды, и с теми, тихими, покровительственными интонациями, которые свойственны - состаревшемуся в свете и при дворе значительному человеку. Он подошел к Анне Павловне, - поцеловал ее руку, подставив ей свою надушенную и сияющую лысину, и покойно уселся на диване. - */ + {HEAD_COMMENT_2} declare v int = 2; - /* - — Ne me tourmentez pas. Eh bien, qu'a-t-on décidé par rapport à la dépêche de Novosilzoff? - Vous savez tout. - — Как вам сказать? — сказал князь холодным, скучающим тоном. — Qu'a-t-on décidé? On a décidé - que Buonaparte a brûlé ses vaisseaux, et je crois que nous sommes en train de brûler les nôtres 8. - Князь Василий говорил всегда лениво, как актер говорит роль старой пиесы. Анна Павловна Шерер, - напротив, несмотря на свои сорок лет, была преисполнена оживления и порывов. - */ + {HEAD_COMMENT_3} declare w int = 3; begin u = - /* - Ах, не говорите мне про Австрию! Я ничего не понимаю, может быть, но Австрия никогда не хотела - и не хочет войны. Она предает нас. Россия одна должна быть спасительницей Европы. Наш благодетель - знает свое высокое призвание и будет верен ему. Вот одно, во что я верю. Нашему доброму и чудному - */ + {BODY_COMMENT_1} v + - /*государю предстоит величайшая роль в мире, и он так добродетелен и хорош, что Бог не оставит его, - и он исполнит свое призвание задавить гидру революции, которая теперь еще ужаснее в лице этого - убийцы и злодея. Мы одни должны искупить кровь праведника. На кого нам надеяться, я вас спрашиваю?.. - */ + {BODY_COMMENT_2} w; - /* - Англия с своим коммерческим духом не поймет и не может понять всю высоту души императора Александра - */ + {BODY_COMMENT_3} +end#' +; end -+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +^ +set term ;^ +commit; + +show trigger trg_conn; """ +act = isql_act('db', test_script, + substitutions=[('[+++].*', ''), ('[===].*', ''), ('Trigger text.*', '')]) + @pytest.mark.version('>=3') def test_1(act: Action): + + # 25.06.2025: name of DB objects now have schema prefix (since 6.0.0.834): + # + TRG_NAME = 'TRG_CONN' if act.is_version('<6') else 'PUBLIC.TRG_CONN' + + expected_stdout = f""" + {TRG_NAME}, Sequence: 0, Type: ON CONNECT, Active + as + {HEAD_COMMENT_1} + declare u int; + declare variable new_var1 varchar(50) character set utf8 collate utf8 default 'Que voulez-vous ?'; + declare variable new_var3 type of varchar_domain default 'Что делать ?'; + declare variable new_var4 type of varchar_domain2 default 'Кто виноват ?'; + {HEAD_COMMENT_2} + declare v int = 2; + {HEAD_COMMENT_3} + declare w int = 3; + begin + u = + {BODY_COMMENT_1} + v + + {BODY_COMMENT_2} + w; + {BODY_COMMENT_3} + end + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + """ + act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_1715_test.py b/tests/bugs/core_1715_test.py index b33f0082..3e045be3 100644 --- a/tests/bugs/core_1715_test.py +++ b/tests/bugs/core_1715_test.py @@ -7,36 +7,74 @@ DESCRIPTION: JIRA: CORE-1715 FBTEST: bugs.core_1715 +NOTES: + [26.06.2025] pzotov + Re-implemented: use max allowed values of key lengths for indices when 4 and 6 bytes are used per character. + See: + https://firebirdsql.org/file/documentation/html/en/refdocs/fblangref50/firebird-50-language-reference.html#fblangref50-ddl-idx-limits + ("Table 38. Maximum indexable (VAR)CHAR length") + Bug existed on 6.x since '4fe307: Improvement #8406 - Increase MIN_PAGE_SIZE to 8192': only page_size = 8K was avaliable for usage. + Fixed in https://github.com/FirebirdSQL/firebird/commit/6f6d16831919c4fa279189f02b93346c4d5ac1bf + + Checked on 6.0.0.876-6f6d168 ; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ +from pathlib import Path +import locale import pytest from firebird.qa import * -init_script = """create table t1 ( - t1_id integer not null - , vc_50_utf8_utf8 varchar(253) character set utf8 collate utf8 - , vc_50_utf8_unicode varchar(169) character set utf8 collate unicode - , constraint pk_t1_id primary key (t1_id) -);""" +db = db_factory(charset='utf8') -db = db_factory(charset='UTF8', init=init_script) +substitutions = [('[ \t]+', ' ')] +act = python_act('db', substitutions = substitutions) -test_script = """create index i_vc_50_utf8_unicode on t1 (vc_50_utf8_unicode); -create index i_vc_50_utf8_utf8 on t1 (vc_50_utf8_utf8); -commit; -show index; -""" +tmp_fdb = temp_file('tmp_core_1715.fdb') -act = isql_act('db', test_script) +@pytest.mark.version('>=3') +def test_1(act: Action, tmp_fdb: Path, capsys): + + utf8_max_key_size_map = {4096 : (253, 169), 8192 : (509,339), 16384 : (1021,681)} + if act.is_version('>=6'): + del utf8_max_key_size_map[4096] + utf8_max_key_size_map[32768] = (2045, 1363) -expected_stdout = """I_VC_50_UTF8_UNICODE INDEX ON T1(VC_50_UTF8_UNICODE) -I_VC_50_UTF8_UTF8 INDEX ON T1(VC_50_UTF8_UTF8) -PK_T1_ID UNIQUE INDEX ON T1(T1_ID) -""" + expected_lst = [] + for pg_size, max_key_length_pair in utf8_max_key_size_map.items(): -@pytest.mark.version('>=3') -def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() - assert act.clean_stdout == act.clean_expected_stdout + tmp_fdb.unlink(missing_ok = True) + + max_key_4_bytes_per_char, max_key_6_bytes_per_char = max_key_length_pair[:2] + passed_msg = f'Passed for {max_key_length_pair=}' + + test_script = f""" + set bail on; + set list on; + create database 'localhost:{str(tmp_fdb)}' + page_size {pg_size} + default character set utf8 + ; + select mon$page_size from mon$database; + commit; + create table test ( + vc_utf8_utf8 varchar({max_key_4_bytes_per_char}) character set utf8 collate utf8 + ,vc_utf8_unic varchar({max_key_6_bytes_per_char}) character set utf8 collate unicode + ); + create index i_vc_utf8 on test (vc_utf8_utf8); + create index i_vc_unic on test (vc_utf8_unic); + commit; + set list off; + set heading off; + select '{passed_msg}' from rdb$database; + drop database; + """ + + act.isql(switches=['-q'], input = test_script, credentials = True, charset = 'utf8', connect_db = False, combine_output = True, io_enc = locale.getpreferredencoding()) + print(act.clean_stdout) + act.reset() + expected_lst.extend( [f'mon$page_size {pg_size}'.upper(), passed_msg] ) + + act.expected_stdout = '\n'.join(expected_lst) + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1725_test.py b/tests/bugs/core_1725_test.py index 52844ff5..2632bf7f 100644 --- a/tests/bugs/core_1725_test.py +++ b/tests/bugs/core_1725_test.py @@ -5,100 +5,125 @@ ISSUE: 2149 TITLE: Unable to restore a database with inactive indices if any SP/trigger contains an explicit plan DESCRIPTION: + We create table and indices for it. + Then we create trigger for this table, view, procedure, function and package - and all of them have DDL + which explicitly uses 'TEST ORDER ' in execution plan. + Such database then backed up and restored with command switch '-i(nactive)'. + Restore is logged and we check that this log does not contain 'gbak:error' message (and eventually completes OK). + Restored database must contain all created DB objects, i.e. we must have ability to explicitly specify them in SQL. + Table trigger (containing explicit PLAN clause in its DDL) also must exist and remain active. + + Before this bug was fixed: + 1) log of restore contained: + gbak: ERROR:Error while parsing function FN_WORKER's BLR + gbak: ERROR: index TEST_X cannot be used in the specified plan + 2) restored database had NO indices that were explicitly specified in any DDL and any attempt to use appropriate + DB object failed with SQLSTATE = 42S02/39000/42000 ('Table/Procedure/Function} unknown'). + JIRA: CORE-1725 FBTEST: bugs.core_1725 +NOTES: + [28.10.2024] pzotov + 1. Test fully re-implemented. + We do NOT extract metadata before and after restore (in order to compare it): + in FB 6.x 'gbak -i' leads to 'create INACTIVE index ...' statements in generated SQL + (see https://github.com/FirebirdSQL/firebird/issues/8091 - "Ability to create an inactive index"). + + Comparison of metadata that was before and after restore has no much sense. + Rather, we have to check SQL/DML that attempt to use DB object which DDL contain + explicitly specified execution plan. + All such actions must raise error related to invalid BLR, but *not* error about missing DB object. + + BTW: it looks strange that such messages contain "-there is no index TEST_X for table TEST". + Such index definitely DOES exist but it is inactive. + + 2. Bug existed up to 17-jan-2019. + It was fixed by commits related to other issues, namely: + 3.x: a74130019af89012cc1e04ba18bbc9c4a69e1a5d // 17.01.2019 + 4.x: fea7c61d9741dc142fa020bf3aa93af7e52e2002 // 17.01.2019 + 5.x: fea7c61d9741dc142fa020bf3aa93af7e52e2002 // 18.01.2019 + ("Attempted to fix CORE-2440, CORE-5118 and CORE-5900 together (expression indices contain NULL keys after restore).") + + Checked on: + 6.0.0.511-c4bc943; 5.0.2.1547-1e08f5e; 4.0.0.1384-fea7c61 (17-jan-2019, just after fix); 3.0.13.33793-3e62713 + + [30.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ -import pytest -from firebird.qa import * -from firebird.driver import SrvRestoreFlag, SrvRepairFlag -from io import BytesIO +import locale +import re +from collections import defaultdict from difflib import unified_diff +from pathlib import Path -substitutions_1 = [('[ \t]+', ' ')] +import pytest +from firebird.qa import * init_script = """ -set bail on; - -create or alter procedure sp_init as begin end; -create or alter procedure sp_main as begin end; -create or alter procedure sp_worker as begin end; - -create or alter function fn_init returns int as begin end; -create or alter function fn_main returns int as begin end; -create or alter function fn_worker returns int as begin end; - - -create table test(id int primary key, x int, y int); -create index test_x on test(x); -create descending index test_y on test(y); -commit; - -insert into test(id, x, y) select row_number()over(), rand()*5, rand()*100 from rdb$types; -commit; - -create or alter view v_init as - select count(*) as cnt from test group by x - rows 1 -; - -create or alter view v_worker as - select count(*) as cnt - from test - group by y - plan (TEST ORDER TEST_Y) - union all - select cnt from v_init -; -commit; - - -set term ^; -execute block as -begin - rdb$set_context('USER_SESSION','INITIAL_DDL', '1'); -end -^ - -create or alter procedure sp_init as - declare c int; -begin - select count(*) from test group by x - rows 1 - into c + set bail on; + + create or alter procedure sp_init as begin end; + create or alter procedure sp_main as begin end; + create or alter procedure sp_worker as begin end; + + create or alter function fn_init returns int as begin end; + create or alter function fn_main returns int as begin end; + create or alter function fn_worker returns int as begin end; + + create table test(id int primary key, x int, y int); + create index test_x on test(x); + create descending index test_y on test(y); + commit; + + insert into test(id, x, y) select row_number()over(), rand()*5, rand()*100 from rdb$types; + commit; + + create or alter view v_init as + select count(*) as cnt from test group by x + rows 1 ; -end -^ - -create or alter procedure sp_main as -begin - execute procedure sp_worker; -end -^ - -create or alter procedure sp_worker as - declare c int; -begin - select sum(cnt) - from ( + + create or alter view v_worker as select count(*) as cnt - from test group by x - plan (TEST ORDER TEST_X) + from test + group by y + plan (TEST ORDER TEST_Y) union all - select cnt from v_worker - ) - into c + select cnt from v_init ; -end -^ -create or alter function fn_init returns int as -begin - return ( select count(*) from test ); -end -^ -create or alter function fn_worker returns int as -begin - return ( + commit; + + + set term ^; + execute block as + begin + rdb$set_context('USER_SESSION','INITIAL_DDL', '1'); + end + ^ + + create or alter procedure sp_init as + declare c int; + begin + select count(*) from test group by x + rows 1 + into c + ; + end + ^ + + create or alter procedure sp_main as + begin + execute procedure sp_worker; + end + ^ + + create or alter procedure sp_worker as + declare c int; + begin select sum(cnt) from ( select count(*) as cnt @@ -107,24 +132,16 @@ union all select cnt from v_worker ) - ); -end -^ -create or alter function fn_main returns int as -begin - return fn_worker(); -end -^ - -create or alter package pg_test as -begin - function pg_fn_worker returns int; - procedure pg_sp_worker; -end -^ -recreate package body pg_test as -begin - function pg_fn_worker returns int as + into c + ; + end + ^ + create or alter function fn_init returns int as + begin + return ( select count(*) from test ); + end + ^ + create or alter function fn_worker returns int as begin return ( select sum(cnt) @@ -137,81 +154,311 @@ ) ); end + ^ + create or alter function fn_main returns int as + begin + return fn_worker(); + end + ^ - procedure pg_sp_worker as - declare c int; + create or alter package pg_test as begin - select sum(cnt) - from ( - select count(*) as cnt - from test group by x - plan (TEST ORDER TEST_X) - union all - select cnt from v_worker - ) - into c - ; + function pg_fn_worker returns int; + procedure pg_sp_worker; end + ^ + recreate package body pg_test as + begin + function pg_fn_worker returns int as + begin + return ( + select sum(cnt) + from ( + select count(*) as cnt + from test group by x + plan (TEST ORDER TEST_X) + union all + select cnt from v_worker + ) + ); + end -end -^ + procedure pg_sp_worker as + declare c int; + begin + select sum(cnt) + from ( + select count(*) as cnt + from test group by x + plan (TEST ORDER TEST_X) + union all + select cnt from v_worker + ) + into c + ; + end -create or alter trigger trg_attach active on connect position 0 as - declare c int; -begin - if ( rdb$get_context('USER_SESSION','INITIAL_DDL') is null ) then + end + ^ + create or alter trigger test_bi for test active before insert position 0 as + declare c int; begin - select sum(cnt) - from ( - select count(*) as cnt - from test group by x - plan (TEST ORDER TEST_X) - union all - select cnt from v_worker - ) - into c; + if ( rdb$get_context('USER_SESSION','INITIAL_DDL') is null ) then + begin + select sum(cnt) + from ( + select count(*) as cnt + from test group by x + plan (TEST ORDER TEST_X) + union all + select cnt from v_worker + ) + into c; + end end -end -^ -set term ;^ -commit; + ^ + set term ;^ + commit; """ -db = db_factory(init=init_script) +substitutions = [('[ \t]+', ' '), ('(-)?invalid request BLR at offset \\d+', 'invalid request BLR at offset')] -act = python_act('db') +db = db_factory(init = init_script) +act = python_act('db', substitutions = substitutions) + +tmp_fbk= temp_file('tmp_core_1725.fbk') +tmp_fdb = temp_file('tmp_core_1725.fdb') @pytest.mark.version('>=3.0.6') -def test_1(act: Action): - # Extract metadata from initial DB - act.isql(switches=['-nod', '-x']) - meta_1 = act.stdout +def test_1(act: Action, tmp_fbk: Path, tmp_fdb: Path, capsys): + + outcomes_map = defaultdict(str) + + act.gbak(switches=['-b', act.db.dsn, str(tmp_fbk)]) + + # restore _WITHOUT_ building indices: + act.gbak(switches=['-rep', '-i', '-v', str(tmp_fbk), str(tmp_fdb) ], combine_output = True, io_enc = locale.getpreferredencoding()) + + watching_patterns = [re.compile(x, re.IGNORECASE) for x in (r'gbak:\s?ERROR(:)?\s?', r'gbak:finis.*\s+going home', r'gbak:adjust.*\s+flags')] + + for line in act.clean_stdout.splitlines(): + for p in watching_patterns: + if p.search(line): + outcomes_map['restore_log'] += line+'\n' act.reset() - # backup + restore _WITHOUT_ building indices: - backup = BytesIO() - with act.connect_server() as srv: - srv.database.local_backup(database=act.db.db_path, backup_stream=backup) - backup.seek(0) - srv.database.local_restore(backup_stream=backup, database=act.db.db_path, - flags=SrvRestoreFlag.DEACTIVATE_IDX | SrvRestoreFlag.REPLACE) - # Get FB log before validation, run validation and get FB log after it: - log_before = act.get_firebird_log() - srv.database.repair(database=act.db.db_path, flags=SrvRepairFlag.CORRUPTION_CHECK) - #act.gfix(switches=['-v', '-full', act.db.dsn]) - log_after = act.get_firebird_log() - # Extract metadata from restored DB - act.isql(switches=['-nod', '-x']) - meta_2 = act.stdout + + ########################################################################### + + check_metadata = """ + set list on; + set count on; + + select ri.rdb$index_name, ri.rdb$index_inactive from rdb$indices ri where ri.rdb$relation_name = upper('test') and ri.rdb$index_name starting with upper('test'); + + select p.rdb$package_name, p.rdb$procedure_name as sp_name, p.rdb$valid_blr as sp_valid_blr + from rdb$procedures p + where p.rdb$system_flag is distinct from 1 + order by p.rdb$package_name, p.rdb$procedure_name + ; + + select f.rdb$package_name, f.rdb$function_name as fn_name, f.rdb$valid_blr as fn_valid_blr + from rdb$functions f + where f.rdb$system_flag is distinct from 1 + order by f.rdb$package_name, f.rdb$function_name + ; + + select rt.rdb$trigger_name, rt.rdb$trigger_inactive, rt.rdb$valid_blr as tg_valid_blr + from rdb$triggers rt + where + rt.rdb$system_flag is distinct from 1 and + rt.rdb$relation_name = upper('test') + ; + + set count off; + """ + act.isql(switches=['-nod', '-q', str(tmp_fdb)], input = check_metadata, credentials = True, charset = 'utf8', connect_db = False, combine_output = True, io_enc = locale.getpreferredencoding()) + for line in act.clean_stdout.splitlines(): + outcomes_map['check_metadata'] += line+'\n' act.reset() - # Restore with indices. This is necessary to drop the database safely otherwise connect - # to drop will fail in test treadown as connect trigger referes to index tat was not activated - with act.connect_server() as srv: - backup.seek(0) - srv.database.local_restore(backup_stream=backup, database=act.db.db_path, - flags=SrvRestoreFlag.REPLACE) - # - diff_meta = ''.join(unified_diff(meta_1.splitlines(), meta_2.splitlines())) - diff_log = [line for line in unified_diff(log_before, log_after) if line.startswith('+') and 'Validation finished:' in line] - # Checks - assert diff_meta == '' - assert diff_log == ['+\tValidation finished: 0 errors, 0 warnings, 0 fixed\n'] + + ########################################################################### + + check_avail_db_objects = """ + set list on; + set echo on; + select * from v_worker; + + execute procedure sp_main; + + select fn_main() from rdb$database; + + execute procedure pg_test.pg_sp_worker; + + select pg_test.pg_fn_worker() from rdb$database; + + insert into test(id, x, y) values(-1, -1, -1) returning id, x, y; + """ + act.isql(switches=['-nod', '-q', str(tmp_fdb)], input = check_avail_db_objects, credentials = True, charset = 'utf8', connect_db = False, combine_output = True, io_enc = locale.getpreferredencoding()) + + for line in act.clean_stdout.splitlines(): + outcomes_map['check_avail_db_objects'] += line+'\n' + act.reset() + + for k,v in outcomes_map.items(): + print(k) + for p in v.splitlines(): + print(p) + print('') + + ########################################################################### + + expected_stdout_5x = """ + restore_log + gbak:finishing, closing, and going home + gbak:adjusting the ONLINE and FORCED WRITES flags + check_metadata + RDB$INDEX_NAME TEST_X + RDB$INDEX_INACTIVE 1 + RDB$INDEX_NAME TEST_Y + RDB$INDEX_INACTIVE 1 + Records affected: 2 + RDB$PACKAGE_NAME + SP_NAME SP_INIT + SP_VALID_BLR 1 + RDB$PACKAGE_NAME + SP_NAME SP_MAIN + SP_VALID_BLR 1 + RDB$PACKAGE_NAME + SP_NAME SP_WORKER + SP_VALID_BLR 1 + RDB$PACKAGE_NAME PG_TEST + SP_NAME PG_SP_WORKER + SP_VALID_BLR 1 + Records affected: 4 + RDB$PACKAGE_NAME + FN_NAME FN_INIT + FN_VALID_BLR 1 + RDB$PACKAGE_NAME + FN_NAME FN_MAIN + FN_VALID_BLR 1 + RDB$PACKAGE_NAME + FN_NAME FN_WORKER + FN_VALID_BLR 1 + RDB$PACKAGE_NAME PG_TEST + FN_NAME PG_FN_WORKER + FN_VALID_BLR 1 + Records affected: 4 + RDB$TRIGGER_NAME TEST_BI + RDB$TRIGGER_INACTIVE 0 + TG_VALID_BLR 1 + Records affected: 1 + check_avail_db_objects + select * from v_worker; + Statement failed, SQLSTATE = 42000 + invalid request BLR at offset + -there is no index TEST_Y for table TEST + execute procedure sp_main; + Statement failed, SQLSTATE = 2F000 + Error while parsing procedure SP_MAIN's BLR + -Error while parsing procedure SP_WORKER's BLR + invalid request BLR at offset + -there is no index TEST_X for table TEST + select fn_main() from rdb$database; + Statement failed, SQLSTATE = 2F000 + Error while parsing function FN_MAIN's BLR + -Error while parsing function FN_WORKER's BLR + invalid request BLR at offset + -there is no index TEST_X for table TEST + execute procedure pg_test.pg_sp_worker; + Statement failed, SQLSTATE = 2F000 + Error while parsing procedure PG_TEST.PG_SP_WORKER's BLR + invalid request BLR at offset + -there is no index TEST_X for table TEST + select pg_test.pg_fn_worker() from rdb$database; + Statement failed, SQLSTATE = 2F000 + Error while parsing function PG_TEST.PG_FN_WORKER's BLR + invalid request BLR at offset + -there is no index TEST_X for table TEST + insert into test(id, x, y) values(-1, -1, -1) returning id, x, y; + Statement failed, SQLSTATE = 42000 + invalid request BLR at offset + -there is no index TEST_X for table TEST + """ + + expected_stdout_6x = """ + restore_log + gbak:finishing, closing, and going home + gbak:adjusting the ONLINE and FORCED WRITES flags + check_metadata + RDB$INDEX_NAME TEST_X + RDB$INDEX_INACTIVE 1 + RDB$INDEX_NAME TEST_Y + RDB$INDEX_INACTIVE 1 + Records affected: 2 + RDB$PACKAGE_NAME + SP_NAME SP_INIT + SP_VALID_BLR 1 + RDB$PACKAGE_NAME + SP_NAME SP_MAIN + SP_VALID_BLR 1 + RDB$PACKAGE_NAME + SP_NAME SP_WORKER + SP_VALID_BLR 1 + RDB$PACKAGE_NAME PG_TEST + SP_NAME PG_SP_WORKER + SP_VALID_BLR 1 + Records affected: 4 + RDB$PACKAGE_NAME + FN_NAME FN_INIT + FN_VALID_BLR 1 + RDB$PACKAGE_NAME + FN_NAME FN_MAIN + FN_VALID_BLR 1 + RDB$PACKAGE_NAME + FN_NAME FN_WORKER + FN_VALID_BLR 1 + RDB$PACKAGE_NAME PG_TEST + FN_NAME PG_FN_WORKER + FN_VALID_BLR 1 + Records affected: 4 + RDB$TRIGGER_NAME TEST_BI + RDB$TRIGGER_INACTIVE 0 + TG_VALID_BLR 1 + Records affected: 1 + check_avail_db_objects + select * from v_worker; + Statement failed, SQLSTATE = 42000 + invalid request BLR at offset + -there is no index "PUBLIC"."TEST_Y" for table "PUBLIC"."TEST" + execute procedure sp_main; + Statement failed, SQLSTATE = 2F000 + Error while parsing procedure "PUBLIC"."SP_MAIN"'s BLR + -Error while parsing procedure "PUBLIC"."SP_WORKER"'s BLR + invalid request BLR at offset + -there is no index "PUBLIC"."TEST_X" for table "PUBLIC"."TEST" + select fn_main() from rdb$database; + Statement failed, SQLSTATE = 2F000 + Error while parsing function "PUBLIC"."FN_MAIN"'s BLR + -Error while parsing function "PUBLIC"."FN_WORKER"'s BLR + invalid request BLR at offset + -there is no index "PUBLIC"."TEST_X" for table "PUBLIC"."TEST" + execute procedure pg_test.pg_sp_worker; + Statement failed, SQLSTATE = 2F000 + Error while parsing procedure "PUBLIC"."PG_TEST"."PG_SP_WORKER"'s BLR + invalid request BLR at offset + -there is no index "PUBLIC"."TEST_X" for table "PUBLIC"."TEST" + select pg_test.pg_fn_worker() from rdb$database; + Statement failed, SQLSTATE = 2F000 + Error while parsing function "PUBLIC"."PG_TEST"."PG_FN_WORKER"'s BLR + invalid request BLR at offset + -there is no index "PUBLIC"."TEST_X" for table "PUBLIC"."TEST" + insert into test(id, x, y) values(-1, -1, -1) returning id, x, y; + Statement failed, SQLSTATE = 42000 + invalid request BLR at offset + -there is no index "PUBLIC"."TEST_X" for table "PUBLIC"."TEST" + """ + + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1746_test.py b/tests/bugs/core_1746_test.py index 4e8a1b8c..826b3271 100644 --- a/tests/bugs/core_1746_test.py +++ b/tests/bugs/core_1746_test.py @@ -17,8 +17,10 @@ On 2.5.3.26780 and 3.0.0.32483 statement 'create index' will pass (and this must be considered as problem). On 2.5.27020 and 3.0.1 such attempt leads to exception "-901 / object ... in use" - and this is expected. See also core_4386_test.py. - - Checked on 3.0.8.33535 (SS/CS), 4.0.1.2692 (SS/CS), 5.0.0.730 (SS/CS) + Checked on 3.0.8.33535 (SS/CS), 4.0.1.2692 (SS/CS), 5.0.0.730 (SS/CS) + [25.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + Checked on 6.0.0.863; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -62,14 +64,22 @@ def test_1(act: Action, capsys): print(e.__str__()) print(e.gds_codes) - expected_fail = """ - lock conflict on no wait transaction - -unsuccessful metadata update - -object TABLE "TEST" is in use - (335544345, 335544351, 335544453) - """ + if act.is_version('<6'): + expected_out = """ + lock conflict on no wait transaction + -unsuccessful metadata update + -object TABLE "TEST" is in use + (335544345, 335544351, 335544453) + """ + else: + expected_out = """ + lock conflict on no wait transaction + -unsuccessful metadata update + -object TABLE "PUBLIC"."TEST" is in use + (335544345, 335544351, 335544453) + """ - act.expected_stdout = expected_fail + act.expected_stdout = expected_out act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout act.reset() diff --git a/tests/bugs/core_1760_test.py b/tests/bugs/core_1760_test.py index 9f558feb..3803ddeb 100644 --- a/tests/bugs/core_1760_test.py +++ b/tests/bugs/core_1760_test.py @@ -7,6 +7,12 @@ DESCRIPTION: See doc\\sql.extensions\\README.hex_literals.txt JIRA: CORE-1760 FBTEST: bugs.core_1760 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.863; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -72,107 +78,175 @@ act = isql_act('db', test_script, substitutions=[('.*At line.*', ''), ('-Token unknown.*', '-Token unknown')]) -expected_stdout = """ -CONSTANT 11 -CONSTANT 0123456789 -CONSTANT 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 -UUID_TO_CHAR BA1749B5-83BF-9146-B360-F54E25FE583E --1(a) -1 -+15 15 -32767 32767 -32768 32768 -65535 65535 -65536(a) 65536 -65536(b) 65536 --2147483648 -2147483648 -+2147483648(a) 2147483648 -+2147483648(b) 2147483648 --1(b) -1 -+4294967295 4294967295 -+4294967296(a) 4294967296 -+4294967296(b) 4294967296 -9223372036854775807 9223372036854775807 --9223372036854775808 -9223372036854775808 --9223372036854775807 -9223372036854775807 --9223372036854775806 -9223372036854775806 --1(c) -1 -INPUT message field count: 0 -OUTPUT message field count: 19 -01: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 - : name: -1(a) alias: -1(a) - : table: V_TEST owner: SYSDBA -02: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 - : name: +15 alias: +15 - : table: V_TEST owner: SYSDBA -03: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 - : name: 32767 alias: 32767 - : table: V_TEST owner: SYSDBA -04: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 - : name: 32768 alias: 32768 - : table: V_TEST owner: SYSDBA -05: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 - : name: 65535 alias: 65535 - : table: V_TEST owner: SYSDBA -06: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 - : name: 65536(a) alias: 65536(a) - : table: V_TEST owner: SYSDBA -07: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 - : name: 65536(b) alias: 65536(b) - : table: V_TEST owner: SYSDBA -08: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 - : name: -2147483648 alias: -2147483648 - : table: V_TEST owner: SYSDBA -09: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 - : name: +2147483648(a) alias: +2147483648(a) - : table: V_TEST owner: SYSDBA -10: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 - : name: +2147483648(b) alias: +2147483648(b) - : table: V_TEST owner: SYSDBA -11: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 - : name: -1(b) alias: -1(b) - : table: V_TEST owner: SYSDBA -12: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 - : name: +4294967295 alias: +4294967295 - : table: V_TEST owner: SYSDBA -13: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 - : name: +4294967296(a) alias: +4294967296(a) - : table: V_TEST owner: SYSDBA -14: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 - : name: +4294967296(b) alias: +4294967296(b) - : table: V_TEST owner: SYSDBA -15: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 - : name: 9223372036854775807 alias: 9223372036854775807 - : table: V_TEST owner: SYSDBA -16: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 - : name: -9223372036854775808 alias: -9223372036854775808 - : table: V_TEST owner: SYSDBA -17: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 - : name: -9223372036854775807 alias: -9223372036854775807 - : table: V_TEST owner: SYSDBA -18: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 - : name: -9223372036854775806 alias: -9223372036854775806 - : table: V_TEST owner: SYSDBA -19: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 - : name: -1(c) alias: -1(c) - : table: V_TEST owner: SYSDBA -""" - -expected_stderr = """ -Statement failed, SQLSTATE = 42000 -Dynamic SQL Error --SQL error code = -104 --Token unknown - line 1, column 9 --'1' - -Statement failed, SQLSTATE = 42000 -Dynamic SQL Error --SQL error code = -104 --Token unknown - line 1, column 9 --'0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678x' -""" - @pytest.mark.version('>=3.0') def test_1(act: Action): + if act.is_version('<6'): + expected_sqlda = """ + INPUT message field count: 0 + OUTPUT message field count: 19 + 01: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + : name: -1(a) alias: -1(a) + : table: V_TEST owner: SYSDBA + 02: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + : name: +15 alias: +15 + : table: V_TEST owner: SYSDBA + 03: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + : name: 32767 alias: 32767 + : table: V_TEST owner: SYSDBA + 04: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + : name: 32768 alias: 32768 + : table: V_TEST owner: SYSDBA + 05: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + : name: 65535 alias: 65535 + : table: V_TEST owner: SYSDBA + 06: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + : name: 65536(a) alias: 65536(a) + : table: V_TEST owner: SYSDBA + 07: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: 65536(b) alias: 65536(b) + : table: V_TEST owner: SYSDBA + 08: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + : name: -2147483648 alias: -2147483648 + : table: V_TEST owner: SYSDBA + 09: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: +2147483648(a) alias: +2147483648(a) + : table: V_TEST owner: SYSDBA + 10: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: +2147483648(b) alias: +2147483648(b) + : table: V_TEST owner: SYSDBA + 11: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + : name: -1(b) alias: -1(b) + : table: V_TEST owner: SYSDBA + 12: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: +4294967295 alias: +4294967295 + : table: V_TEST owner: SYSDBA + 13: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: +4294967296(a) alias: +4294967296(a) + : table: V_TEST owner: SYSDBA + 14: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: +4294967296(b) alias: +4294967296(b) + : table: V_TEST owner: SYSDBA + 15: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: 9223372036854775807 alias: 9223372036854775807 + : table: V_TEST owner: SYSDBA + 16: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: -9223372036854775808 alias: -9223372036854775808 + : table: V_TEST owner: SYSDBA + 17: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: -9223372036854775807 alias: -9223372036854775807 + : table: V_TEST owner: SYSDBA + 18: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: -9223372036854775806 alias: -9223372036854775806 + : table: V_TEST owner: SYSDBA + 19: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: -1(c) alias: -1(c) + : table: V_TEST owner: SYSDBA + """ + else: + expected_sqlda = """ + INPUT message field count: 0 + OUTPUT message field count: 19 + 01: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + : name: -1(a) alias: -1(a) + : table: V_TEST schema: PUBLIC owner: SYSDBA + 02: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + : name: +15 alias: +15 + : table: V_TEST schema: PUBLIC owner: SYSDBA + 03: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + : name: 32767 alias: 32767 + : table: V_TEST schema: PUBLIC owner: SYSDBA + 04: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + : name: 32768 alias: 32768 + : table: V_TEST schema: PUBLIC owner: SYSDBA + 05: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + : name: 65535 alias: 65535 + : table: V_TEST schema: PUBLIC owner: SYSDBA + 06: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + : name: 65536(a) alias: 65536(a) + : table: V_TEST schema: PUBLIC owner: SYSDBA + 07: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: 65536(b) alias: 65536(b) + : table: V_TEST schema: PUBLIC owner: SYSDBA + 08: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + : name: -2147483648 alias: -2147483648 + : table: V_TEST schema: PUBLIC owner: SYSDBA + 09: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: +2147483648(a) alias: +2147483648(a) + : table: V_TEST schema: PUBLIC owner: SYSDBA + 10: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: +2147483648(b) alias: +2147483648(b) + : table: V_TEST schema: PUBLIC owner: SYSDBA + 11: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + : name: -1(b) alias: -1(b) + : table: V_TEST schema: PUBLIC owner: SYSDBA + 12: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: +4294967295 alias: +4294967295 + : table: V_TEST schema: PUBLIC owner: SYSDBA + 13: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: +4294967296(a) alias: +4294967296(a) + : table: V_TEST schema: PUBLIC owner: SYSDBA + 14: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: +4294967296(b) alias: +4294967296(b) + : table: V_TEST schema: PUBLIC owner: SYSDBA + 15: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: 9223372036854775807 alias: 9223372036854775807 + : table: V_TEST schema: PUBLIC owner: SYSDBA + 16: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: -9223372036854775808 alias: -9223372036854775808 + : table: V_TEST schema: PUBLIC owner: SYSDBA + 17: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: -9223372036854775807 alias: -9223372036854775807 + : table: V_TEST schema: PUBLIC owner: SYSDBA + 18: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: -9223372036854775806 alias: -9223372036854775806 + : table: V_TEST schema: PUBLIC owner: SYSDBA + 19: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: -1(c) alias: -1(c) + : table: V_TEST schema: PUBLIC owner: SYSDBA + """ + + expected_stdout = f""" + CONSTANT 11 + CONSTANT 0123456789 + CONSTANT 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 + UUID_TO_CHAR BA1749B5-83BF-9146-B360-F54E25FE583E + -1(a) -1 + +15 15 + 32767 32767 + 32768 32768 + 65535 65535 + 65536(a) 65536 + 65536(b) 65536 + -2147483648 -2147483648 + +2147483648(a) 2147483648 + +2147483648(b) 2147483648 + -1(b) -1 + +4294967295 4294967295 + +4294967296(a) 4294967296 + +4294967296(b) 4294967296 + 9223372036854775807 9223372036854775807 + -9223372036854775808 -9223372036854775808 + -9223372036854775807 -9223372036854775807 + -9223372036854775806 -9223372036854775806 + -1(c) -1 + + {expected_sqlda} + """ + + expected_stderr = """ + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Token unknown - line 1, column 9 + -'1' + + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Token unknown - line 1, column 9 + -'0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678x' + """ + act.expected_stdout = expected_stdout act.expected_stderr = expected_stderr act.execute() diff --git a/tests/bugs/core_1793_test.py b/tests/bugs/core_1793_test.py index 151dbfd8..a29221f0 100644 --- a/tests/bugs/core_1793_test.py +++ b/tests/bugs/core_1793_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-1793 FBTEST: bugs.core_1793 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.863; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -24,26 +30,29 @@ set planonly; with x as (select x.x from test x), - y as (select y.x from test y) + y as (select y.x from test y), + z as (select x from test) select * from y; """ act = isql_act('db', test_script) -expected_stdout = """ +expected_stdout_5x = """ + SQL warning code = -104 + -CTE "X" is not used in query + -CTE "Z" is not used in query PLAN (Y Y NATURAL) """ -expected_stderr = """ +expected_stdout_6x = """ SQL warning code = -104 -CTE "X" is not used in query + -CTE "Z" is not used in query + PLAN ("Y" "Y" NATURAL) """ @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1802_test.py b/tests/bugs/core_1802_test.py index e266ba36..57f26ed9 100644 --- a/tests/bugs/core_1802_test.py +++ b/tests/bugs/core_1802_test.py @@ -7,29 +7,103 @@ DESCRIPTION: JIRA: CORE-1802 FBTEST: bugs.core_1802 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -db = db_factory() +db = db_factory(charset = 'win1250', page_size = 8192) + +test_script = """ + -- https://firebirdsql.org/file/documentation/html/en/refdocs/fblangref50/firebird-50-language-reference.html#fblangref50-datatypes-chartypes-charindxs + -- The following formula calculates the maximum length of an indexed string (in characters): + -- max_char_length = FLOOR((page_size / 4 - 9) / N) + -- where N is the number of bytes per character in the character set. + create table test( + id int generated by default as identity + ,f01 varchar(999) character set win1250 collate pxw_csy + ); + create index test_f01_unq on test(f01); + commit; + + -- https://www.generatormix.com/random-czech-words-generator?number=50 + insert into test(f01) values('všechno'); + insert into test(f01) values('ženy'); + insert into test(f01) values('takové'); + insert into test(f01) values('vaši'); + insert into test(f01) values('velké'); + insert into test(f01) values('přijít'); + insert into test(f01) values('našeho'); + insert into test(f01) values('běžte'); + insert into test(f01) values('zatraceně'); + insert into test(f01) values('straně'); + insert into test(f01) values('bolí'); + insert into test(f01) values('svém'); + insert into test(f01) values('místa'); + insert into test(f01) values('mrzí'); + insert into test(f01) values('příště'); + insert into test(f01) values('horší'); + insert into test(f01) values('mladý'); + insert into test(f01) values('dívka'); + insert into test(f01) values('lásku'); + insert into test(f01) values('cítit'); + insert into test(f01) values('nedá'); + insert into test(f01) values('rozhodnutí'); + insert into test(f01) values('dětí'); + insert into test(f01) values('náma'); + insert into test(f01) values('těžký'); + insert into test(f01) values('líbilo'); + insert into test(f01) values('píše'); + insert into test(f01) values('dívku'); + insert into test(f01) values('zbývá'); + insert into test(f01) values('dítě'); + insert into test(f01) values('haló'); + insert into test(f01) values('děkuju'); + insert into test(f01) values('jaké'); + insert into test(f01) values('potřebuji'); + insert into test(f01) values('půl'); + insert into test(f01) values('skutečně'); + insert into test(f01) values('chtěli'); + insert into test(f01) values('okamžitě'); + insert into test(f01) values('slyšela'); + insert into test(f01) values('krásná'); + insert into test(f01) values('mrzí'); + insert into test(f01) values('zapomeň'); + insert into test(f01) values('lásku'); + insert into test(f01) values('dávej'); + insert into test(f01) values('museli'); + insert into test(f01) values('zachránil'); + insert into test(f01) values('každej'); + insert into test(f01) values('milé'); + insert into test(f01) values('neznamená'); + insert into test(f01) values('polož'); + insert into test(f01) values('zbraní'); + insert into test(f01) values('tátu'); + insert into test(f01) values('několika'); + commit; -test_script = """CREATE TABLE TAB21( - ID INTEGER, - A VARCHAR(490) CHARACTER SET WIN1250 COLLATE PXW_CSY, - CONSTRAINT CU UNIQUE(A) ); -COMMIT; -SHOW INDEX CU; + set planonly; + select * from test where f01 = 'náma'; """ act = isql_act('db', test_script) -expected_stdout = """CU UNIQUE INDEX ON TAB21(A) +expected_stdout_5x = """ + PLAN (TEST INDEX (TEST_F01_UNQ)) +""" + +expected_stdout_6x = """ + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_F01_UNQ")) """ @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.isql(switches = ['-q'], charset = 'win1250', input = test_script, combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_1812_test.py b/tests/bugs/core_1812_test.py index 2c7cb5ed..9f25ec92 100644 --- a/tests/bugs/core_1812_test.py +++ b/tests/bugs/core_1812_test.py @@ -8,53 +8,19 @@ JIRA: CORE-1812 FBTEST: bugs.core_1812 NOTES: - [02.02.2019] pzotov - Added separate code for FB 4.0: statements like "SELECT TIMESTAMP 'now' FROM RDB$DATABASE;" can not - be used anymore (error: SQLSTATE = 22018 / conversion error from string "now"). - Details about timezone datatype see in: doc\\sql.extensions\\README.time_zone.md + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. - [01.09.2023] pzotov - Adjusted plan for FB 4.x+ to current versions after fixed - https://github.com/FirebirdSQL/firebird/issues/7727 + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -# version: 3.0 +db = db_factory(sql_dialect=1) -init_script_1 = """ - create table t (col timestamp) ; - create index it on t (col) ; - commit ; - """ - -db_1 = db_factory(sql_dialect=1, init=init_script_1) - -test_script_1 = """ - SET PLAN ON; - select * from t where col > timestamp 'now' - 7 ; - select * from t where col > 'now' - 7 ; -""" - -act_1 = isql_act('db_1', test_script_1) - -expected_stdout_1 = """ - PLAN (T INDEX (IT)) - PLAN (T INDEX (IT)) -""" - -@pytest.mark.version('>=3.0,<4.0') -def test_1(act_1: Action): - act_1.expected_stdout = expected_stdout_1 - act_1.execute() - assert act_1.clean_stdout == act_1.clean_expected_stdout - -# version: 4.0 - -db_2 = db_factory(sql_dialect=1) - -test_script_2 = """ +test_script = """ create table test (dts timestamp) ; commit; insert into test @@ -69,16 +35,20 @@ def test_1(act_1: Action): select * from test where dts = current_timestamp; """ -act_2 = isql_act('db_2', test_script_2) +act = isql_act('db', test_script) -expected_stdout_2 = """ +expected_stdout_5x = """ PLAN (TEST INDEX (TEST_DTS)) PLAN (TEST INDEX (TEST_DTS)) """ -@pytest.mark.version('>=4.0') -def test_2(act_2: Action): - act_2.expected_stdout = expected_stdout_2 - act_2.execute() - assert act_2.clean_stdout == act_2.clean_expected_stdout +expected_stdout_6x = """ + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_DTS")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_DTS")) +""" +@pytest.mark.version('>=3.0') +def test_1(act: Action): + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1815_test.py b/tests/bugs/core_1815_test.py index 7486c2a0..494fb594 100644 --- a/tests/bugs/core_1815_test.py +++ b/tests/bugs/core_1815_test.py @@ -5,402 +5,509 @@ ISSUE: 2245 TITLE: Ability to grant role to another role DESCRIPTION: - ### NB ### This test was NOT completed! - One need to check all nuances related to granting and revoking that is issued by NON sysdba user - which was granted admin option to manupulate appropriate DB objects. - Also, there is not clarity about issue 08/Aug/16 06:31 AM (when user Boss2 does grant-and-revoke - sequence with some role to other user Sales but this role already was granted by _other_ user, Boss1). - - We create two users (acnt and pdsk) and two roles for them (racnt and rpdsk). - Then we create two tables (tacnt & tpdsk) and grant access on these tables for acnt & pdsk. - Then we create user boss, role for him (rboss) and grant IMPLICITLY access on tables tacnt and tpdsk - to user boss via his role (rboss). - Check is made to ensure that user boss HAS ability to read from both tables (being connected with role Rboss). - After all, we IMPLICITLY revoke access from these tables and check again that user boss now has NO access - on tables tacnt and tpdsk. + ### NB ### This test was NOT completed! + One need to check all nuances related to granting and revoking that is issued by NON sysdba user + which was granted admin option to manupulate appropriate DB objects. + Also, there is not clarity about issue 08/Aug/16 06:31 AM (when user Boss2 does grant-and-revoke + sequence with some role to other user Sales but this role already was granted by _other_ user, Boss1). + + We create two users (acnt and pdsk) and two roles for them (racnt and rpdsk). + Then we create two tables (tacnt & tpdsk) and grant access on these tables for acnt & pdsk. + Then we create user boss, role for him (rboss) and grant IMPLICITLY access on tables tacnt and tpdsk + to user boss via his role (rboss). + Check is made to ensure that user boss HAS ability to read from both tables (being connected with role Rboss). + After all, we IMPLICITLY revoke access from these tables (by revoking ROLES from boss) and check again that + user boss now has NO access on tables tacnt and tpdsk. JIRA: CORE-1815 FBTEST: bugs.core_1815 +NOTES: + [26.06.2025] pzotov + Re-implemented: use fixture attributes and f-notation instead of hard-coding user/role names. + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214. """ import pytest from firebird.qa import * db = db_factory() + user_boss = user_factory('db', name='tmp$c1815_boss', password='boss') user_acnt = user_factory('db', name='tmp$c1815_acnt', password='acnt') user_pdsk = user_factory('db', name='tmp$c1815_pdsk', password='pdsk') -test_script = """ - set wng off; - --show version; - --set bail on; - --set echo on; - set list on; - - set width whoami 16; - set width my_role 7; - set width r_name 10; - set width r_owner 10; - - create or alter view v_role_info as - select - current_user as whoami, - current_role as my_role, - rdb$role_name as r_name, - rdb$owner_name r_owner, - rdb$role_in_use(rdb$role_name) r_in_use - from rdb$roles - where coalesce(rdb$system_flag,0) = 0 - ; - commit; - - create role rboss; - create role racnt; - create role rpdsk; - commit; - - grant select on v_role_info to user tmp$c1815_acnt; - grant select on v_role_info to user tmp$c1815_pdsk; - grant select on v_role_info to user tmp$c1815_boss; - commit; - - grant racnt to user tmp$c1815_acnt; - grant rpdsk to user tmp$c1815_pdsk; - commit; - - ------------------------------------------------------------------------ - - recreate table tacnt(id int, s varchar(5)); - recreate table tpdsk(id int, s varchar(5)); - commit; - - grant all on tacnt to role racnt; - grant all on tpdsk to role rpdsk; - commit; - - ------------------------------------------------------------------------ - - grant racnt to rboss; -- make RBOSS role able to do the same as role RACNT - grant rpdsk to rboss; -- make RBOSS role able to do the same as role RPDSK - - grant rboss to tmp$c1815_boss; -- let user BOSS to use role RBOSS - - commit; - --show grants; -- [ 1 ] - - ------------------------------------------------------------------------ - - insert into tacnt(id, s) values(1,'acnt'); - insert into tpdsk(id, s) values(2,'pdsk'); - commit; - - connect '$(DSN)' user tmp$c1815_acnt password 'acnt' role 'racnt'; - select * from v_role_info; - select current_user as whoami, a.* from tacnt a; - commit; - - connect '$(DSN)' user tmp$c1815_pdsk password 'pdsk' role 'rpdsk'; - select * from v_role_info; - select current_user as whoami, p.* from tpdsk p; - commit; - - - connect '$(DSN)' user tmp$c1815_boss password 'boss' role 'rboss'; - select * from v_role_info; - commit; - - select current_user as whoami, a.* from tacnt a; - select current_user as whoami, p.* from tpdsk p; - commit; - - --################################################################################################ - - connect '$(DSN)' user sysdba password 'masterkey'; - - revoke racnt from user tmp$c1815_acnt; -- REVOKE role from *other* USER; grant to RBoss should be reserved! - commit; +role_boss = role_factory('db', name='role_boss') +role_acnt = role_factory('db', name='role_acnt') +role_pdsk = role_factory('db', name='role_pdsk') - -- check that *role* RBoss still HAS grants to both RAcnt and RPdsk roles: - connect '$(DSN)' user tmp$c1815_boss password 'boss' role 'rboss'; - select * from v_role_info; -- should contain in all three rows +substitutions = [] # [('[ \t]+', ' ')] +act = isql_act('db', substitutions = substitutions) - -- should PASS because we revoked role from other USER (accountant) - -- rather than from current user (Boss) or its role (Rboss): - select current_user as whoami, a.* from tacnt a; - commit; - - --#################################################################################################### - - connect '$(DSN)' user sysdba password 'masterkey'; - - -- check that if we try to revoke role RPdsk from __USER__ Boss than - -- this action will not has effect because this USER got access through the ROLE (i.e. indirectly): - - revoke rpdsk from user tmp$c1815_boss; -- this is no-op action! We did NOT granted role to USER! - commit; - - connect '$(DSN)' user tmp$c1815_boss password 'boss' role 'rboss'; -- now check: is role Rboss really affected ? - - select * from v_role_info; -- should contain in all lines because we did not affect __role__ RBOSS - select current_user as whoami, p.* from tpdsk p; -- should PASS! - commit; - - --################################################################################################ - - connect '$(DSN)' user sysdba password 'masterkey'; - - -- check that if we revoke access to a table from ROLE RPdsk (and this role was granted to role RBoss) - -- then Rboss also will not be able to select from this table: - - revoke all on tpdsk from rpdsk; - commit; - - connect '$(DSN)' user tmp$c1815_boss password 'boss' role 'rboss'; - - select * from v_role_info; -- should contain in all lines because we did not affect __role__ RBOSS - select current_user as whoami, p.* from tpdsk p; -- should FAIL - commit; - - --################################################################################################ - - connect '$(DSN)' user sysdba password 'masterkey'; - - -- check that if we revoke ROLE 'racnt' which was granted before to ROLE 'rboss' - -- then user Boss will not be able to access table 'tacnt' (i.e. we revoke this access indirectly): - - revoke racnt from rboss; - commit; - - connect '$(DSN)' user tmp$c1815_boss password 'boss' role 'rboss'; - - select * from v_role_info; -- should contain for line with 'racnt' - select current_user as whoami, a.* from tacnt a; -- should FAIL - commit; +@pytest.mark.version('>=4.0') +def test_1(act: Action, user_boss: User, user_acnt: User, user_pdsk: User, role_boss: Role, role_acnt: Role, role_pdsk: Role): - -- ############################################################################################### + test_script = f""" + set wng off; + set list on; - connect '$(DSN)' user sysdba password 'masterkey'; + set width whoami 16; + set width my_role 7; + set width r_name 10; + set width r_owner 10; - -- check that if we GRANT again ROLE 'racnt' which was revoked before from ROLE 'rboss' - -- then user Boss WILL be able to access table 'tacnt' (we grant access indirectly after revoking): + create or alter view v_role_info as + select + current_user as whoami, + current_role as my_role, + rdb$role_name as r_name, + rdb$owner_name r_owner, + rdb$role_in_use(rdb$role_name) r_in_use + from rdb$roles + where coalesce(rdb$system_flag,0) = 0 + ; + commit; - grant racnt to rboss; -- RESTORE access for role Rboss - commit; + grant select on v_role_info to user {user_acnt.name}; + grant select on v_role_info to user {user_pdsk.name}; + grant select on v_role_info to user {user_boss.name}; + commit; + grant {role_acnt.name} to user {user_acnt.name}; + grant {role_pdsk.name} to user {user_pdsk.name}; + commit; - connect '$(DSN)' user tmp$c1815_boss password 'boss' role 'rboss'; + ------------------------------------------------------------------------ - select * from v_role_info; -- should contain for line with 'racnt' - select current_user as whoami, a.* from tacnt a; -- should PASS - commit; -""" + recreate table tacnt(id int, s varchar(15)); + recreate table tpdsk(id int, s varchar(15)); + commit; -act = isql_act('db', test_script) + grant all on tacnt to role {role_acnt.name}; + grant all on tpdsk to role {role_pdsk.name}; + commit; -expected_stdout = """ - WHOAMI TMP$C1815_ACNT - MY_ROLE RACNT - R_NAME RBOSS - R_OWNER SYSDBA - R_IN_USE + ------------------------------------------------------------------------ - WHOAMI TMP$C1815_ACNT - MY_ROLE RACNT - R_NAME RACNT - R_OWNER SYSDBA - R_IN_USE + grant {role_acnt.name} to {role_boss.name}; -- make {role_boss.name} role able to do the same as role {role_acnt.name} + grant {role_pdsk.name} to {role_boss.name}; -- make {role_boss.name} role able to do the same as role {role_pdsk.name} - WHOAMI TMP$C1815_ACNT - MY_ROLE RACNT - R_NAME RPDSK - R_OWNER SYSDBA - R_IN_USE + grant {role_boss.name} to {user_boss.name}; -- let user BOSS to use role {role_boss.name} + commit; + --show grants; -- [ 1 ] - WHOAMI TMP$C1815_ACNT - ID 1 - S acnt + ------------------------------------------------------------------------ + insert into tacnt(id, s) values(1, '{role_acnt.name.upper()}'); + insert into tpdsk(id, s) values(2, '{role_pdsk.name.upper()}'); + commit; - WHOAMI TMP$C1815_PDSK - MY_ROLE RPDSK - R_NAME RBOSS - R_OWNER SYSDBA - R_IN_USE + connect '{act.db.dsn}' user {user_acnt.name} password '{user_acnt.password}' role '{role_acnt.name}'; + select * from v_role_info; + select current_user as whoami, a.* from tacnt a; + commit; - WHOAMI TMP$C1815_PDSK - MY_ROLE RPDSK - R_NAME RACNT - R_OWNER SYSDBA - R_IN_USE + connect '{act.db.dsn}' user {user_pdsk.name} password '{user_pdsk.password}' role '{role_pdsk.name}'; + select * from v_role_info; + select current_user as whoami, p.* from tpdsk p; + commit; - WHOAMI TMP$C1815_PDSK - MY_ROLE RPDSK - R_NAME RPDSK - R_OWNER SYSDBA - R_IN_USE + connect '{act.db.dsn}' user {user_boss.name} password '{user_boss.password}' role '{role_boss.name}'; + select * from v_role_info; + commit; - WHOAMI TMP$C1815_PDSK - ID 2 - S pdsk + select current_user as whoami, a.* from tacnt a; + select current_user as whoami, p.* from tpdsk p; + commit; + --################################################################################################ - WHOAMI TMP$C1815_BOSS - MY_ROLE RBOSS - R_NAME RBOSS - R_OWNER SYSDBA - R_IN_USE - - WHOAMI TMP$C1815_BOSS - MY_ROLE RBOSS - R_NAME RACNT - R_OWNER SYSDBA - R_IN_USE - - WHOAMI TMP$C1815_BOSS - MY_ROLE RBOSS - R_NAME RPDSK - R_OWNER SYSDBA - R_IN_USE - - - WHOAMI TMP$C1815_BOSS - ID 1 - S acnt - - WHOAMI TMP$C1815_BOSS - ID 2 - S pdsk - - - WHOAMI TMP$C1815_BOSS - MY_ROLE RBOSS - R_NAME RBOSS - R_OWNER SYSDBA - R_IN_USE - - WHOAMI TMP$C1815_BOSS - MY_ROLE RBOSS - R_NAME RACNT - R_OWNER SYSDBA - R_IN_USE - - WHOAMI TMP$C1815_BOSS - MY_ROLE RBOSS - R_NAME RPDSK - R_OWNER SYSDBA - R_IN_USE - - - WHOAMI TMP$C1815_BOSS - ID 1 - S acnt - - - WHOAMI TMP$C1815_BOSS - MY_ROLE RBOSS - R_NAME RBOSS - R_OWNER SYSDBA - R_IN_USE - - WHOAMI TMP$C1815_BOSS - MY_ROLE RBOSS - R_NAME RACNT - R_OWNER SYSDBA - R_IN_USE + connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; - WHOAMI TMP$C1815_BOSS - MY_ROLE RBOSS - R_NAME RPDSK - R_OWNER SYSDBA - R_IN_USE + revoke {role_acnt.name} from user {user_acnt.name}; -- REVOKE role from *other* USER; grant to {role_boss.name} should be preserved! + commit; + -- check that *role* {role_boss.name} still HAS grants to both {role_acnt.name} and {role_pdsk.name} roles: + connect '{act.db.dsn}' user {user_boss.name} password '{user_boss.password}' role '{role_boss.name}'; - WHOAMI TMP$C1815_BOSS - ID 2 - S pdsk + select * from v_role_info; -- should contain in all three rows + -- should PASS because we revoked role from other USER (accountant) + -- rather than from current user (Boss) or its role ({role_boss.name}): + select current_user as whoami, a.* from tacnt a; + commit; - WHOAMI TMP$C1815_BOSS - MY_ROLE RBOSS - R_NAME RBOSS - R_OWNER SYSDBA - R_IN_USE - - WHOAMI TMP$C1815_BOSS - MY_ROLE RBOSS - R_NAME RACNT - R_OWNER SYSDBA - R_IN_USE - - WHOAMI TMP$C1815_BOSS - MY_ROLE RBOSS - R_NAME RPDSK - R_OWNER SYSDBA - R_IN_USE + --#################################################################################################### + connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; - WHOAMI TMP$C1815_BOSS - MY_ROLE RBOSS - R_NAME RBOSS - R_OWNER SYSDBA - R_IN_USE - - WHOAMI TMP$C1815_BOSS - MY_ROLE RBOSS - R_NAME RACNT - R_OWNER SYSDBA - R_IN_USE - - WHOAMI TMP$C1815_BOSS - MY_ROLE RBOSS - R_NAME RPDSK - R_OWNER SYSDBA - R_IN_USE - - - WHOAMI TMP$C1815_BOSS - MY_ROLE RBOSS - R_NAME RBOSS - R_OWNER SYSDBA - R_IN_USE - - WHOAMI TMP$C1815_BOSS - MY_ROLE RBOSS - R_NAME RACNT - R_OWNER SYSDBA - R_IN_USE - - WHOAMI TMP$C1815_BOSS - MY_ROLE RBOSS - R_NAME RPDSK - R_OWNER SYSDBA - R_IN_USE - - - - WHOAMI TMP$C1815_BOSS - ID 1 - S acnt -""" + -- check that if we try to revoke role {role_pdsk.name} from __USER__ Boss than + -- this action will not has effect because this USER got access through the ROLE (i.e. indirectly): -expected_stderr = """ - Statement failed, SQLSTATE = 28000 - no permission for SELECT access to TABLE TPDSK - -Effective user is TMP$C1815_BOSS + revoke {role_pdsk.name} from user {user_boss.name}; -- this is no-op action: we did NOT granted role to USER. + commit; - Statement failed, SQLSTATE = 28000 - no permission for SELECT access to TABLE TACNT - -Effective user is TMP$C1815_BOSS -""" + connect '{act.db.dsn}' user {user_boss.name} password '{user_boss.password}' role '{role_boss.name}'; -- now check: is role {role_boss.name} really affected ? -@pytest.mark.version('>=4.0') -def test_1(act: Action, user_boss: User, user_acnt: User, user_pdsk: User): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + select * from v_role_info; -- should contain in all lines because we did not affect __role__ {role_boss.name} + select current_user as whoami, p.* from tpdsk p; -- should PASS! + commit; + --################################################################################################ + + connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; + + -- check that if we revoke access to a table from ROLE {role_pdsk.name} (and this role was granted to role {role_boss.name}) + -- then {role_boss.name} also will not be able to select from this table: + + revoke all on tpdsk from {role_pdsk.name}; + commit; + + connect '{act.db.dsn}' user {user_boss.name} password '{user_boss.password}' role '{role_boss.name}'; + + select * from v_role_info; -- should contain in all lines because we did not affect __role__ {role_boss.name} + select current_user as whoami, p.* from tpdsk p; -- should FAIL + commit; + + --################################################################################################ + + connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; + + -- check that if we revoke ROLE '{role_acnt.name}' which was granted before to ROLE '{role_boss.name}' + -- then user Boss will not be able to access table 'tacnt' (i.e. we revoke this access indirectly): + + revoke {role_acnt.name} from {role_boss.name}; + commit; + + connect '{act.db.dsn}' user {user_boss.name} password '{user_boss.password}' role '{role_boss.name}'; + + select * from v_role_info; -- should contain for line with '{role_acnt.name}' + select current_user as whoami, a.* from tacnt a; -- should FAIL + commit; + + -- ############################################################################################### + + connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; + + -- check that if we GRANT again ROLE '{role_acnt.name}' which was revoked before from ROLE '{role_boss.name}' + -- then user Boss WILL be able to access table 'tacnt' (we grant access indirectly after revoking): + + grant {role_acnt.name} to {role_boss.name}; -- RESTORE access for role {role_boss.name} + commit; + + + connect '{act.db.dsn}' user {user_boss.name} password '{user_boss.password}' role '{role_boss.name}'; + + select * from v_role_info; -- should contain for line with '{role_acnt.name}' + select current_user as whoami, a.* from tacnt a; -- should PASS + commit; + """ + + + expected_stdout_5x = f""" + WHOAMI {user_acnt.name.upper()} + MY_ROLE {role_acnt.name.upper()} + R_NAME {role_boss.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_acnt.name.upper()} + MY_ROLE {role_acnt.name.upper()} + R_NAME {role_acnt.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_acnt.name.upper()} + MY_ROLE {role_acnt.name.upper()} + R_NAME {role_pdsk.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_acnt.name.upper()} + ID 1 + S {role_acnt.name.upper()} + WHOAMI {user_pdsk.name.upper()} + MY_ROLE {role_pdsk.name.upper()} + R_NAME {role_boss.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_pdsk.name.upper()} + MY_ROLE {role_pdsk.name.upper()} + R_NAME {role_acnt.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_pdsk.name.upper()} + MY_ROLE {role_pdsk.name.upper()} + R_NAME {role_pdsk.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_pdsk.name.upper()} + ID 2 + S {role_pdsk.name.upper()} + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_boss.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_acnt.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_pdsk.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + ID 1 + S {role_acnt.name.upper()} + WHOAMI {user_boss.name.upper()} + ID 2 + S {role_pdsk.name.upper()} + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_boss.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_acnt.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_pdsk.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + ID 1 + S {role_acnt.name.upper()} + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_boss.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_acnt.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_pdsk.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + ID 2 + S {role_pdsk.name.upper()} + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_boss.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_acnt.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_pdsk.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + Statement failed, SQLSTATE = 28000 + no permission for SELECT access to TABLE TPDSK + -Effective user is {user_boss.name.upper()} + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_boss.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_acnt.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_pdsk.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + Statement failed, SQLSTATE = 28000 + no permission for SELECT access to TABLE TACNT + -Effective user is {user_boss.name.upper()} + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_boss.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_acnt.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_pdsk.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + ID 1 + S {role_acnt.name.upper()} + """ + + expected_stdout_6x = f""" + WHOAMI {user_acnt.name.upper()} + MY_ROLE {role_acnt.name.upper()} + R_NAME {role_boss.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_acnt.name.upper()} + MY_ROLE {role_acnt.name.upper()} + R_NAME {role_acnt.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_acnt.name.upper()} + MY_ROLE {role_acnt.name.upper()} + R_NAME {role_pdsk.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_acnt.name.upper()} + ID 1 + S {role_acnt.name.upper()} + WHOAMI {user_pdsk.name.upper()} + MY_ROLE {role_pdsk.name.upper()} + R_NAME {role_boss.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_pdsk.name.upper()} + MY_ROLE {role_pdsk.name.upper()} + R_NAME {role_acnt.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_pdsk.name.upper()} + MY_ROLE {role_pdsk.name.upper()} + R_NAME {role_pdsk.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_pdsk.name.upper()} + ID 2 + S {role_pdsk.name.upper()} + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_boss.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_acnt.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_pdsk.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + ID 1 + S {role_acnt.name.upper()} + WHOAMI {user_boss.name.upper()} + ID 2 + S {role_pdsk.name.upper()} + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_boss.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_acnt.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_pdsk.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + ID 1 + S {role_acnt.name.upper()} + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_boss.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_acnt.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_pdsk.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + ID 2 + S {role_pdsk.name.upper()} + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_boss.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_acnt.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_pdsk.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + Statement failed, SQLSTATE = 28000 + no permission for SELECT access to TABLE "PUBLIC"."TPDSK" + -Effective user is {user_boss.name.upper()} + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_boss.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_acnt.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_pdsk.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + Statement failed, SQLSTATE = 28000 + no permission for SELECT access to TABLE "PUBLIC"."TACNT" + -Effective user is {user_boss.name.upper()} + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_boss.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_acnt.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + MY_ROLE {role_boss.name.upper()} + R_NAME {role_pdsk.name.upper()} + R_OWNER {act.db.user.upper()} + R_IN_USE + WHOAMI {user_boss.name.upper()} + ID 1 + S {role_acnt.name.upper()} + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.isql(switches = ['-q'], input = test_script, combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1830_test.py b/tests/bugs/core_1830_test.py index 9ea2765b..beb2d712 100644 --- a/tests/bugs/core_1830_test.py +++ b/tests/bugs/core_1830_test.py @@ -7,25 +7,29 @@ DESCRIPTION: JIRA: CORE-1830 FBTEST: bugs.core_1830 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """ +db = db_factory() + +test_script = """ create table a(id char(1), name varchar(255)); create index idx_a on a (id); - create exception ex_perm 'Something wrong occurs...'; + create exception exc_wrong 'Something wrong occurs...'; commit ; insert into a (id) values ('1'); commit; -""" -db = db_factory(init=init_script) - -test_script = """ set list on; select * from a where id = '1'; set term ^; @@ -33,46 +37,64 @@ begin update a set name = 'xxx'; update a set id = '2'; - exception ex_perm; + exception exc_wrong; end ^ set term ; ^ - select * from a ; - select * from a where id = '1' ; - + set count on; + select 'point-1' as msg, a.* from a ; + select 'point-2' as msg, a.* from a where id = '1' ; commit; - select * from a ; + select 'point-3' as msg, a.* from a ; """ -act = isql_act('db', test_script, - substitutions=[('column.*', 'column x'), ('[ \t]+', ' '), - ('-At block line: [\\d]+, col: [\\d]+', '')]) - -expected_stdout = """ - ID 1 - NAME - - ID 1 - NAME - - ID 1 - NAME - - ID 1 - NAME +substitutions = [ ('[ \t]+', ' '), ('column.*', 'column x'), ('-At block line: [\\d]+, col: [\\d]+', '')] +act = isql_act('db', test_script, substitutions = substitutions) + +expected_stdout_5x = """ + ID 1 + NAME + Statement failed, SQLSTATE = HY000 + exception 1 + -EXC_WRONG + -Something wrong occurs... + MSG point-1 + ID 1 + NAME + Records affected: 1 + + MSG point-2 + ID 1 + NAME + Records affected: 1 + + MSG point-3 + ID 1 + NAME + Records affected: 1 """ - -expected_stderr = """ - Statement failed, SQLSTATE = HY000 - exception 1 - -EX_PERM - -Something wrong occurs... +expected_stdout_6x = """ + ID 1 + NAME + Statement failed, SQLSTATE = HY000 + exception 1 + -"PUBLIC"."EXC_WRONG" + -Something wrong occurs... + MSG point-1 + ID 1 + NAME + Records affected: 1 + MSG point-2 + ID 1 + NAME + Records affected: 1 + MSG point-3 + ID 1 + NAME + Records affected: 1 """ @pytest.mark.version('>=2.5') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1841_test.py b/tests/bugs/core_1841_test.py index ce683d05..cbe9ed7a 100644 --- a/tests/bugs/core_1841_test.py +++ b/tests/bugs/core_1841_test.py @@ -5,10 +5,14 @@ ISSUE: 2270 TITLE: Possible overflow in RDB$VIEW_RELATIONS.RDB$CONTEXT_NAME DESCRIPTION: - Originale tite is: If some VIEW used derived tables and long table names/aliases, - It is possible to overflow RDB$VIEW_RELATIONS.RDB$CONTEXT_NAME + Original titte is: If some VIEW used derived tables and long table names/aliases, it is possible to overflow RDB$VIEW_RELATIONS.RDB$CONTEXT_NAME JIRA: CORE-1841 FBTEST: bugs.core_1841 +NOTES: + [26.06.2025] pzotov + Re-implemented: use f-notation in order to remove hard-coded DDL string from test_script and expected_out. + Removed 'SHOW VIEW'command because its output can change in any intensive developing FB version. + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -16,26 +20,28 @@ db = db_factory() -test_script = """create view x (id) as -select RDB$RELATION_ID - from (select * from RDB$DATABASE long_alias_long_alias_1) long_alias_long_alias_2; -COMMIT; -SHOW VIEW x; +VIEW_DDL = 'select rdb$relation_id from (select * from rdb$database long_alias_long_alias_1) long_alias_long_alias_2' +test_script = f""" + set list on; + set blob all; + set count on; + create view v_test (id) as + {VIEW_DDL}; + commit; + select r.rdb$view_source as blob_id from rdb$relations r where r.rdb$relation_name = upper('v_test'); """ -act = isql_act('db', test_script) +substitutions = [('BLOB_ID .*', '')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ID SMALLINT Expression -View Source: -==== ====== - -select RDB$RELATION_ID - from (select * from RDB$DATABASE long_alias_long_alias_1) long_alias_long_alias_2 +expected_stdout = f""" + {VIEW_DDL} + Records affected: 1 """ @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1846_test.py b/tests/bugs/core_1846_test.py index 47feb3f4..99ce92ef 100644 --- a/tests/bugs/core_1846_test.py +++ b/tests/bugs/core_1846_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-1846 FBTEST: bugs.core_1846 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -36,14 +42,18 @@ act = isql_act('db', test_script, substitutions=[('=.*', '')]) -expected_stdout = """ +expected_stdout_5x = """ PLAN (TEST ORDER TEST_N1_N2_ASC) PLAN (TEST ORDER TEST_N2_N1_DESC) """ +expected_stdout_6x = """ + PLAN ("PUBLIC"."TEST" ORDER "PUBLIC"."TEST_N1_N2_ASC") + PLAN ("PUBLIC"."TEST" ORDER "PUBLIC"."TEST_N2_N1_DESC") +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_1891_test.py b/tests/bugs/core_1891_test.py index 759d320e..9a92d0f1 100644 --- a/tests/bugs/core_1891_test.py +++ b/tests/bugs/core_1891_test.py @@ -7,32 +7,47 @@ DESCRIPTION: JIRA: CORE-1891 FBTEST: bugs.core_1891 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """create table test (n integer); -create view view_test (x, y) as select n, n * 2 from test; +db = db_factory() + +VIEW_DDL = 'select n, n * 2 from test' +test_script = f""" + create table test (n integer); + create view view_test (x, y) as + {VIEW_DDL}; + show view view_test; """ -db = db_factory(init=init_script) +substitutions = [ ('[ \t]+', ' '), ('=.*', '') ] +act = isql_act('db', test_script, substitutions = substitutions) -test_script = """show view view_test; +expected_stdout_5x = f""" + X INTEGER Nullable + Y BIGINT Expression + View Source: + {VIEW_DDL} """ -act = isql_act('db', test_script) - -expected_stdout = """X INTEGER Nullable -Y BIGINT Expression -View Source: -==== ====== - select n, n * 2 from test +expected_stdout_6x = f""" + View: PUBLIC.VIEW_TEST + X INTEGER Nullable + Y BIGINT Expression + View Source: + {VIEW_DDL} """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_1894_test.py b/tests/bugs/core_1894_test.py index 6589cbba..46726198 100644 --- a/tests/bugs/core_1894_test.py +++ b/tests/bugs/core_1894_test.py @@ -3,10 +3,16 @@ """ ID: issue-2325 ISSUE: 2325 -TITLE: Circular dependencies between computed fields crashs the engine +TITLE: Circular dependencies between computed fields crash the engine DESCRIPTION: JIRA: CORE-1894 FBTEST: bugs.core_1894 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -37,12 +43,11 @@ select * from t; select * from t2; -- THIS LEAD SERVER CRASH (checked on WI-T4.0.0.399) - """ act = isql_act('db', test_script) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 42000 unsuccessful metadata update -Cannot have circular dependencies with computed fields @@ -63,9 +68,28 @@ -there are 1 dependencies """ +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -Cannot have circular dependencies with computed fields + + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -cannot delete + -COLUMN "PUBLIC"."T2"."C1" + -there are 1 dependencies + + Statement failed, SQLSTATE = 42000 + Cannot have circular dependencies with computed fields + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -cannot delete + -COLUMN "PUBLIC"."T2"."C1" + -there are 1 dependencies +""" + @pytest.mark.version('>=3.0.2') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1930_test.py b/tests/bugs/core_1930_test.py index 9194d6c3..dc7f4610 100644 --- a/tests/bugs/core_1930_test.py +++ b/tests/bugs/core_1930_test.py @@ -7,122 +7,25 @@ DESCRIPTION: JIRA: CORE-1930 FBTEST: bugs.core_1930 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214. """ import pytest from firebird.qa import * -substitutions = [('Data source : Firebird::localhost:.*', 'Data source : Firebird::localhost:'), - ('-At block line: [\\d]+, col: [\\d]+', '-At block line')] +substitutions = [('Data source : Firebird::localhost:.*', 'Data source : Firebird::localhost:'), ('-At block line: [\\d]+, col: [\\d]+', '-At block line')] db = db_factory() -# version: 3.0 - -test_script_1 = """ - set term ^; - create or alter procedure sp1 returns (x int) as - begin - x=1; - suspend; - end - ^ - - create or alter procedure sp2 returns (x int) as - begin - select x from sp1 into :x; - suspend; - end - ^ - - create or alter procedure sp3 returns (x int) as - begin - select x from sp2 into :x; - suspend; - end - ^ - commit - ^ - - -- this is wrong but engine still didn't track procedure's fields dependencies - create or alter procedure sp1 as - begin - exit; - end - ^ - - set term ;^ - commit; - - -- Here we create new attachment using specification of some non-null data in ROLE clause: - set term ^; - execute block as - declare c int; - begin - begin - c = rdb$get_context('SYSTEM', 'EXT_CONN_POOL_SIZE'); - rdb$set_context('USER_SESSION', 'EXT_CONN_POOL_SUPPORT','1'); - when any do - begin - end - end - execute statement 'create or alter procedure sp3 as begin execute procedure sp2; end' - on external 'localhost:' || rdb$get_context('SYSTEM', 'DB_NAME') - as user 'sysdba' password 'masterkey' role 'R1930'; - end - ^ - commit - ^ - - -- |||||||||||||||||||||||||||| - -- ###################################||| HQBird 3.x SS/SC |||############################## - -- |||||||||||||||||||||||||||| - -- If we check SS or SC and ExtConnPoolLifeTime > 0 (avaliable in HQbird 3.x) then current - -- DB (bugs.core_NNNN.fdb) will be 'captured' by firebird.exe process and fbt_run utility - -- will not able to drop this database at the final point of test. - -- Moreover, DB file will be hold until all activity in firebird.exe completed and AFTER this - -- we have to wait for seconds after it (discussion and small test see - -- in the letter to hvlad and dimitr 13.10.2019 11:10). - -- This means that one need to kill all connections to prevent from exception on cleanup phase: - -- SQLCODE: -901 / lock time-out on wait transaction / object is in use - -- ############################################################################################# - execute block as - begin - if ( rdb$get_context('USER_SESSION', 'EXT_CONN_POOL_SUPPORT') = '1' ) then - begin - -- HQbird is tested now: - -- execute statement 'delete from mon$attachments where mon$attachment_id != current_connection'; - execute statement 'ALTER EXTERNAL CONNECTIONS POOL CLEAR ALL'; - end - end - ^ - commit - ^ -""" - -act_1 = isql_act('db', test_script_1, substitutions=substitutions) - -expected_stderr_1 = """ - Statement failed, SQLSTATE = 42000 - Execute statement error at isc_dsql_execute2 : - 335544351 : unsuccessful metadata update - 336397267 : CREATE OR ALTER PROCEDURE SP3 failed - 335544569 : Dynamic SQL Error - 335544850 : Output parameter mismatch for procedure SP2 - Statement : create or alter procedure sp3 as begin execute procedure sp2; end - Data source : Firebird::localhost:C:\\MIX\\FIREBIRD\\QA\\FBT-REPO\\TMP\\C1930.FDB - -At block line: 3, col: 9 -""" - -@pytest.mark.version('>=3.0,<4.0') -def test_1(act_1: Action): - act_1.expected_stderr = expected_stderr_1 - act_1.execute() - assert act_1.clean_stderr == act_1.clean_expected_stderr - -# version: 4.0 +# Statement that will be passed in ES/EDS (we have to check its presence in error message): +EDS_STATEMENT = 'create or alter procedure sp3 as begin execute procedure sp2; end' -test_script_2 = """ +test_script = f""" set term ^; create or alter procedure sp1 returns (x int) as begin @@ -164,34 +67,18 @@ def test_1(act_1: Action): set term ^; execute block as begin - execute statement 'create or alter procedure sp3 as begin execute procedure sp2; end' + execute statement '{EDS_STATEMENT}' on external 'localhost:' || rdb$get_context('SYSTEM', 'DB_NAME') as user 'sysdba' password 'masterkey' role 'R1930'; end ^ set term ;^ commit; - - -- |||||||||||||||||||||||||||| - -- ###################################||| FB 4.0+, SS and SC |||############################## - -- |||||||||||||||||||||||||||| - -- If we check SS or SC and ExtConnPoolLifeTime > 0 (config parameter FB 4.0+) then current - -- DB (bugs.core_NNNN.fdb) will be 'captured' by firebird.exe process and fbt_run utility - -- will not able to drop this database at the final point of test. - -- Moreover, DB file will be hold until all activity in firebird.exe completed and AFTER this - -- we have to wait for seconds after it (discussion and small test see - -- in the letter to hvlad and dimitr 13.10.2019 11:10). - -- This means that one need to kill all connections to prevent from exception on cleanup phase: - -- SQLCODE: -901 / lock time-out on wait transaction / object is in use - -- ############################################################################################# - - delete from mon$attachments where mon$attachment_id != current_connection; - commit; """ -act_2 = isql_act('db', test_script_2, substitutions=substitutions) +act = isql_act('db', test_script, substitutions = substitutions) -expected_stderr_2 = """ +expected_stdout_5x = f""" Statement failed, SQLSTATE = 42000 unsuccessful metadata update -cannot delete @@ -204,14 +91,31 @@ def test_1(act_1: Action): 336397267 : CREATE OR ALTER PROCEDURE SP3 failed 335544569 : Dynamic SQL Error 335544850 : Output parameter mismatch for procedure SP2 - Statement : create or alter procedure sp3 as begin execute procedure sp2; end + Statement : {EDS_STATEMENT} Data source : Firebird::localhost: -At block line """ -@pytest.mark.version('>=4.0') -def test_2(act_2: Action): - act_2.expected_stderr = expected_stderr_2 - act_2.execute() - assert act_2.clean_stderr == act_2.clean_expected_stderr +expected_stdout_6x = f""" + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -cannot delete + -PARAMETER "PUBLIC"."SP1".X + -there are 1 dependencies + Statement failed, SQLSTATE = 42000 + Execute statement error at isc_dsql_execute2 : + 335544351 : unsuccessful metadata update + 336397267 : CREATE OR ALTER PROCEDURE "PUBLIC"."SP3" failed + 335544569 : Dynamic SQL Error + 335544850 : Output parameter mismatch for procedure "PUBLIC"."SP2" + Statement : {EDS_STATEMENT} + Data source : Firebird::localhost: + -At block line +""" +@pytest.mark.es_eds +@pytest.mark.version('>=4.0') +def test_2(act: Action): + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1986_test.py b/tests/bugs/core_1986_test.py index 34430a30..74564e19 100644 --- a/tests/bugs/core_1986_test.py +++ b/tests/bugs/core_1986_test.py @@ -7,42 +7,55 @@ DESCRIPTION: JIRA: CORE-1986 FBTEST: bugs.core_1986 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214. """ import pytest from firebird.qa import * -init_script = """CREATE DOMAIN D_SOME AS INTEGER; - -CREATE OR ALTER PROCEDURE SP_SOME( - SOME_PARAM D_SOME) -AS -BEGIN -END; +db = db_factory() + +test_script = """ + create domain dm_int as int; + set term ^; + create or alter procedure sp_some(a_x dm_int) + as + begin + end + ^ + set term ;^ + alter domain dm_int to d_other; + execute procedure sp_some (1); + commit; + execute procedure sp_some (1); + commit; """ -db = db_factory(init=init_script) - -test_script = """ALTER DOMAIN D_SOME TO D_OTHER; +act = isql_act('db', test_script) -execute procedure SP_SOME (1); -commit; -execute procedure SP_SOME (1); -commit; +expected_stdout_5x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -cannot delete + -DOMAIN DM_INT + -there are 1 dependencies """ -act = isql_act('db', test_script) - -expected_stderr = """Statement failed, SQLSTATE = 42000 -unsuccessful metadata update --cannot delete --DOMAIN D_SOME --there are 1 dependencies +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -cannot delete + -DOMAIN "PUBLIC"."DM_INT" + -there are 1 dependencies """ @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_1997_test.py b/tests/bugs/core_1997_test.py index 9845f0df..acd764f6 100644 --- a/tests/bugs/core_1997_test.py +++ b/tests/bugs/core_1997_test.py @@ -3,48 +3,58 @@ """ ID: issue-2434 ISSUE: 2434 -TITLE: Broken foreign key handling for multi-segmented index using multi-level collations +TITLE: Broken foreign key handling for multi-segmented index using unicode_ci collation DESCRIPTION: JIRA: CORE-1997 FBTEST: bugs.core_1997 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """create table pk ( - c1 varchar (5) character set utf8 collate unicode_ci, - c2 varchar (5) character set utf8 collate unicode_ci, - primary key (c1, c2) -); -commit; -create table fk ( - c1 varchar (5) character set utf8 collate unicode_ci, - c2 varchar (5) character set utf8 collate unicode_ci, - foreign key (c1, c2) references pk -); -commit; -insert into pk values ('a', 'b'); -insert into fk values ('A', 'b'); -commit; +db = db_factory() + +test_script = """ + create table pk ( + c1 varchar (5) character set utf8 collate unicode_ci, + c2 varchar (5) character set utf8 collate unicode_ci, + primary key (c1, c2) + ); + create table fk ( + c1 varchar (5) character set utf8 collate unicode_ci, + c2 varchar (5) character set utf8 collate unicode_ci, + foreign key (c1, c2) references pk + ); + insert into pk values ('a', 'b'); + insert into fk values ('A', 'b'); + commit; + delete from pk; -- should not be allowed """ -db = db_factory(init=init_script) +act = isql_act('db', test_script) -test_script = """delete from pk; -- should not be allowed +expected_stdout_5x = """ + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint "INTEG_2" on table "FK" + -Foreign key references are present for the record + -Problematic key value is ("C1" = 'a', "C2" = 'b') """ -act = isql_act('db', test_script) - -expected_stderr = """Statement failed, SQLSTATE = 23000 -violation of FOREIGN KEY constraint "INTEG_2" on table "FK" --Foreign key references are present for the record --Problematic key value is ("C1" = 'a', "C2" = 'b') +expected_stdout_6x = """ + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint "INTEG_2" on table "PUBLIC"."FK" + -Foreign key references are present for the record + -Problematic key value is ("C1" = 'a', "C2" = 'b') """ @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_2018_test.py b/tests/bugs/core_2018_test.py index bd35e539..eeb1723e 100644 --- a/tests/bugs/core_2018_test.py +++ b/tests/bugs/core_2018_test.py @@ -119,6 +119,7 @@ ATTACHES_I_CAN_SEE 11 """ +@pytest.mark.es_eds @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_2051_test.py b/tests/bugs/core_2051_test.py index 50321f39..9a4bfcf1 100644 --- a/tests/bugs/core_2051_test.py +++ b/tests/bugs/core_2051_test.py @@ -7,6 +7,10 @@ DESCRIPTION: JIRA: CORE-2051 FBTEST: bugs.core_2051 +NOTES: + [12.09.2024] pzotov + Removed execution plan from expected output. + Requested by dimitr, letters with subj 'core_2051_test', since 11.09.2024 17:16. """ import pytest @@ -28,16 +32,13 @@ insert into test2 values(2); commit; - set plan on; set list on; select coalesce((select t2.id from test2 t2 where t2.id = t1.id), 0) id2 from test1 t1 order by t1.id; """ -act = isql_act('db', test_script) +act = isql_act('db', test_script, substitutions = [ ('[ \t]+',' ') ]) expected_stdout = """ - PLAN (T2 INDEX (TEST2_PK)) - PLAN (T1 ORDER TEST1_PK) ID2 1 ID2 2 ID2 0 @@ -46,6 +47,6 @@ @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_2053_test.py b/tests/bugs/core_2053_test.py index 3a09eb5f..03cc9f74 100644 --- a/tests/bugs/core_2053_test.py +++ b/tests/bugs/core_2053_test.py @@ -7,40 +7,48 @@ DESCRIPTION: JIRA: CORE-2053 FBTEST: bugs.core_2053 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """create table t1 (col1 int); -create index i1 on t1 (col1); -commit; -insert into t1 (col1) values (1); -commit; -create table t2 (col2 int); -commit; -""" - -db = db_factory(init=init_script) +db = db_factory() -test_script = """SET PLAN ON; -insert into t2 (col2) values (1) returning case when exists (select 1 from t1 where col1 = col2) then 1 else 0 end; -commit;""" +test_script = """ + set list on; + create table t1 (col1 int); + create index i1 on t1 (col1); + commit; + insert into t1 (col1) values (1); + commit; + create table t2 (col2 int); + commit; -act = isql_act('db', test_script) + SET PLAN ON; + insert into t2 (col2) values (1) returning case when exists (select 1 from t1 where col1 = col2) then 1 else 0 end as insert_outcome; +""" -expected_stdout = """ -PLAN (T1 INDEX (I1)) +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) - CASE -============ - 1 +expected_stdout_5x = """ + PLAN (T1 INDEX (I1)) + INSERT_OUTCOME 1 +""" +expected_stdout_6x = """ + PLAN ("PUBLIC"."T1" INDEX ("PUBLIC"."I1")) + INSERT_OUTCOME 1 """ @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_2073_test.py b/tests/bugs/core_2073_test.py index 766abfe6..3276eed2 100644 --- a/tests/bugs/core_2073_test.py +++ b/tests/bugs/core_2073_test.py @@ -7,64 +7,69 @@ DESCRIPTION: JIRA: CORE-1000 FBTEST: bugs.core_2073 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """CREATE TABLE TMP_DATE1 -( - DATE1 DATE, - DATE2 DATE -); -COMMIT; -SET TERM !!; -EXECUTE BLOCK -AS - DECLARE VARIABLE D DATE; -BEGIN - D = '01.01.2008'; - WHILE (D < '01.08.2008') DO BEGIN - INSERT INTO TMP_DATE1(DATE1, DATE2) - VALUES(:D, :D + 100); - D = D + 1; - END -END!! -SET TERM ;!! -COMMIT; -CREATE INDEX TMP_DATE1_IDX1 ON TMP_DATE1 COMPUTED BY (DATE1+0); -CREATE INDEX TMP_DATE1_IDX2 ON TMP_DATE1 (DATE1); -COMMIT; -""" - -db = db_factory(init=init_script) +db = db_factory() -test_script = """SET PLAN ON; -SELECT count(*) FROM TMP_DATE1 T WHERE '01.03.2008' BETWEEN T.DATE1+0 AND T.DATE2; -SELECT count(*) FROM TMP_DATE1 T WHERE '01.03.2008' >= T.DATE1; -""" +test_script = """ + set list on; + create table tmp_date1 ( + date1 date, + date2 date + ); + commit; + set term ^; + execute block as + declare variable d date; + begin + d = '01.01.2008'; + while (d < '01.08.2008') do begin + insert into tmp_date1(date1, date2) values(:d, :d + 100); + d = d + 1; + end + end^ + set term ;^ + commit; -act = isql_act('db', test_script) + create index tmp_date1_idx1 on tmp_date1 computed by (date1+0); + create index tmp_date1_idx2 on tmp_date1 (date1); + commit; -expected_stdout = """ -PLAN (T INDEX (TMP_DATE1_IDX1)) + set plan on; + select count(*) from tmp_date1 t where '01.03.2008' between t.date1+0 and t.date2; + select count(*) from tmp_date1 t where '01.03.2008' >= t.date1; +""" - COUNT -===================== - 61 +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) -PLAN (T INDEX (TMP_DATE1_IDX2)) +expected_stdout_5x = """ + PLAN (T INDEX (TMP_DATE1_IDX1)) + COUNT 61 + PLAN (T INDEX (TMP_DATE1_IDX2)) + COUNT 61 - COUNT -===================== - 61 +""" +expected_stdout_6x = """ + PLAN ("T" INDEX ("PUBLIC"."TMP_DATE1_IDX1")) + COUNT 61 + PLAN ("T" INDEX ("PUBLIC"."TMP_DATE1_IDX2")) + COUNT 61 """ -@pytest.mark.version('>=3.0') +@pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_2132_test.py b/tests/bugs/core_2132_test.py index a95ef0a6..060cbeb1 100644 --- a/tests/bugs/core_2132_test.py +++ b/tests/bugs/core_2132_test.py @@ -7,163 +7,182 @@ DESCRIPTION: JIRA: CORE-2132 FBTEST: bugs.core_2132 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """create table t1 (col int primary key); -set term ^ ; -create procedure p1 returns (ret int) as begin ret = 0; suspend; end ^ -create procedure p2 (prm int) returns (ret int) as begin ret = prm; suspend; end ^ -set term ; ^ -commit; -insert into t1 (col) values (0); -commit; +db = db_factory() + +test_script = """ + set list on; + create table t1 (col int primary key); + set term ^ ; + create procedure p1 returns (ret int) as begin ret = 0; suspend; end ^ + create procedure p2 (prm int) returns (ret int) as begin ret = prm; suspend; end ^ + set term ; ^ + commit; + insert into t1 (col) values (0); + commit; + + set plan on; + -- index + select 'point-01' msg, t1.* from t1 where col = 0; + + -- natural + select 'point-02' msg, t1.* from t1 where col = col; + + -- index + select 'point-03' msg, t1.* from t1 where col = ( select 0 from rdb$database ); + + -- natural + select 'point-04' msg, t1.* from t1 where col = ( select col from rdb$database ); + + -- index (currently natural) + select 'point-05' msg, t1.* from t1 where col = ( select 0 from p1 ); + + -- index (currently natural) + select 'point-06' msg, t1.* from t1 where col = ( select ret from p1 ); + + -- natural + select 'point-07' msg, t1.* from t1 where col = ( select col from p1 ); + + -- index (currently natural) + select 'point-08' msg, t1.* from t1 where col = ( select 0 from p2(0) ); + + -- index (currently natural) + select 'point-09' msg, t1.* from t1 where col = ( select ret from p2(0) ); + + -- natural + select 'point-10' msg, t1.* from t1 where col = ( select col from p2(0) ); + + -- natural + select 'point-11' msg, t1.* from t1 where col = ( select 0 from p2(col) ); + + -- natural + select 'point-12' msg, t1.* from t1 where col = ( select ret from p2(col) ); + + -- natural + select 'point-13' msg, t1.* from t1 where col = ( select col from p2(col) ); """ -db = db_factory(init=init_script) - -test_script = """set plan on ; - --- index -select * from t1 where col = 0; --- natural -select * from t1 where col = col; --- index -select * from t1 where col = ( select 0 from rdb$database ); --- natural -select * from t1 where col = ( select col from rdb$database ); --- index (currently natural) -select * from t1 where col = ( select 0 from p1 ); --- index (currently natural) -select * from t1 where col = ( select ret from p1 ); --- natural -select * from t1 where col = ( select col from p1 ); --- index (currently natural) -select * from t1 where col = ( select 0 from p2(0) ); --- index (currently natural) -select * from t1 where col = ( select ret from p2(0) ); --- natural -select * from t1 where col = ( select col from p2(0) ); --- natural -select * from t1 where col = ( select 0 from p2(col) ); --- natural -select * from t1 where col = ( select ret from p2(col) ); --- natural -select * from t1 where col = ( select col from p2(col) ); - +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) + +expected_stdout_5x = """ + PLAN (T1 INDEX (RDB$PRIMARY1)) + MSG point-01 + COL 0 + PLAN (T1 NATURAL) + MSG point-02 + COL 0 + PLAN (RDB$DATABASE NATURAL) + PLAN (T1 INDEX (RDB$PRIMARY1)) + MSG point-03 + COL 0 + PLAN (RDB$DATABASE NATURAL) + PLAN (T1 NATURAL) + MSG point-04 + COL 0 + PLAN (P1 NATURAL) + PLAN (T1 INDEX (RDB$PRIMARY1)) + MSG point-05 + COL 0 + PLAN (P1 NATURAL) + PLAN (T1 INDEX (RDB$PRIMARY1)) + MSG point-06 + COL 0 + PLAN (P1 NATURAL) + PLAN (T1 NATURAL) + MSG point-07 + COL 0 + PLAN (P2 NATURAL) + PLAN (T1 INDEX (RDB$PRIMARY1)) + MSG point-08 + COL 0 + PLAN (P2 NATURAL) + PLAN (T1 INDEX (RDB$PRIMARY1)) + MSG point-09 + COL 0 + PLAN (P2 NATURAL) + PLAN (T1 NATURAL) + MSG point-10 + COL 0 + PLAN (P2 NATURAL) + PLAN (T1 NATURAL) + MSG point-11 + COL 0 + PLAN (P2 NATURAL) + PLAN (T1 NATURAL) + MSG point-12 + COL 0 + PLAN (P2 NATURAL) + PLAN (T1 NATURAL) + MSG point-13 + COL 0 """ -act = isql_act('db', test_script) - -expected_stdout = """ -PLAN (T1 INDEX (RDB$PRIMARY1)) - - COL -============ - 0 - - -PLAN (T1 NATURAL) - - COL -============ - 0 - - -PLAN (RDB$DATABASE NATURAL) -PLAN (T1 INDEX (RDB$PRIMARY1)) - - COL -============ - 0 - - -PLAN (RDB$DATABASE NATURAL) -PLAN (T1 NATURAL) - - COL -============ - 0 - - -PLAN (P1 NATURAL) -PLAN (T1 INDEX (RDB$PRIMARY1)) - - COL -============ - 0 - - -PLAN (P1 NATURAL) -PLAN (T1 INDEX (RDB$PRIMARY1)) - - COL -============ - 0 - - -PLAN (P1 NATURAL) -PLAN (T1 NATURAL) - - COL -============ - 0 - - -PLAN (P2 NATURAL) -PLAN (T1 INDEX (RDB$PRIMARY1)) - - COL -============ - 0 - - -PLAN (P2 NATURAL) -PLAN (T1 INDEX (RDB$PRIMARY1)) - - COL -============ - 0 - - -PLAN (P2 NATURAL) -PLAN (T1 NATURAL) - - COL -============ - 0 - - -PLAN (P2 NATURAL) -PLAN (T1 NATURAL) - - COL -============ - 0 - - -PLAN (P2 NATURAL) -PLAN (T1 NATURAL) - - COL -============ - 0 - - -PLAN (P2 NATURAL) -PLAN (T1 NATURAL) - - COL -============ - 0 - +expected_stdout_6x = """ + PLAN ("PUBLIC"."T1" INDEX ("PUBLIC"."RDB$PRIMARY1")) + MSG point-01 + COL 0 + PLAN ("PUBLIC"."T1" NATURAL) + MSG point-02 + COL 0 + PLAN ("SYSTEM"."RDB$DATABASE" NATURAL) + PLAN ("PUBLIC"."T1" INDEX ("PUBLIC"."RDB$PRIMARY1")) + MSG point-03 + COL 0 + PLAN ("SYSTEM"."RDB$DATABASE" NATURAL) + PLAN ("PUBLIC"."T1" NATURAL) + MSG point-04 + COL 0 + PLAN ("PUBLIC"."P1" NATURAL) + PLAN ("PUBLIC"."T1" INDEX ("PUBLIC"."RDB$PRIMARY1")) + MSG point-05 + COL 0 + PLAN ("PUBLIC"."P1" NATURAL) + PLAN ("PUBLIC"."T1" INDEX ("PUBLIC"."RDB$PRIMARY1")) + MSG point-06 + COL 0 + PLAN ("PUBLIC"."P1" NATURAL) + PLAN ("PUBLIC"."T1" NATURAL) + MSG point-07 + COL 0 + PLAN ("PUBLIC"."P2" NATURAL) + PLAN ("PUBLIC"."T1" INDEX ("PUBLIC"."RDB$PRIMARY1")) + MSG point-08 + COL 0 + PLAN ("PUBLIC"."P2" NATURAL) + PLAN ("PUBLIC"."T1" INDEX ("PUBLIC"."RDB$PRIMARY1")) + MSG point-09 + COL 0 + PLAN ("PUBLIC"."P2" NATURAL) + PLAN ("PUBLIC"."T1" NATURAL) + MSG point-10 + COL 0 + PLAN ("PUBLIC"."P2" NATURAL) + PLAN ("PUBLIC"."T1" NATURAL) + MSG point-11 + COL 0 + PLAN ("PUBLIC"."P2" NATURAL) + PLAN ("PUBLIC"."T1" NATURAL) + MSG point-12 + COL 0 + PLAN ("PUBLIC"."P2" NATURAL) + PLAN ("PUBLIC"."T1" NATURAL) + MSG point-13 + COL 0 """ -@pytest.mark.version('>=3.0') +@pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_2140_test.py b/tests/bugs/core_2140_test.py index 84ae6c31..50d62b50 100644 --- a/tests/bugs/core_2140_test.py +++ b/tests/bugs/core_2140_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-2140 FBTEST: bugs.core_2140 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -14,36 +20,66 @@ db = db_factory() -test_script = """ - set list on; - set term ^ ; - execute block returns (y int) as - begin - for execute statement - ('select rdb$relation_id from rdb$database where rdb$relation_id = :x') (1) - with autonomous transaction - into y - do suspend; - end ^ -""" +substitutions = [ + ('Data source : Firebird::.*', 'Data source : Firebird::'), + ('col(umn)?(:)?.*', 'col x'), + ('-At block line: [\\d]+, col: [\\d]+', '') +] -act = isql_act('db', test_script, - substitutions=[('column.*', 'column x'), - ('-At block line: [\\d]+, col: [\\d]+', '')]) - -expected_stderr = """ - Statement failed, SQLSTATE = 42S22 - Dynamic SQL Error - -SQL error code = -206 - -Column unknown - -X - -At line 1, column 67 - -At block line: 5, col: 3 -""" +act = isql_act('db', substitutions = substitutions) +@pytest.mark.es_eds @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + # Statement that will be passed in ES/EDS (we have to check its presence in error message): + EDS_STATEMENT = 'select rdb$relation_id from rdb$database where rdb$relation_id = :x' + + test_script = f""" + set list on; + set term ^ ; + execute block returns (y int) as + begin + for + execute statement ('{EDS_STATEMENT}') (1) + on external 'localhost:' || rdb$get_context('SYSTEM', 'DB_NAME') + as user '{act.db.user}' password '{act.db.password}' + with autonomous transaction + into y + do suspend; + end + ^ + """ + + # BEFOREE fix output was: + # Statement : select rdb$relation_id from rdb$database where rdb$relation_id = :x\nData source : Internal::. + + expected_stdout_5x = f""" + Statement failed, SQLSTATE = 42000 + Execute statement error at isc_dsql_prepare : + 335544569 : Dynamic SQL Error + 335544436 : SQL error code = -206 + 335544578 : Column unknown + 335544382 : X + 336397208 : At line 1, column 66 + Statement : {EDS_STATEMENT} + Data source : Firebird:: + -At block line: 3, col: 11 + """ + + expected_stdout_6x = f""" + Statement failed, SQLSTATE = 42000 + Execute statement error at isc_dsql_prepare : + 335544569 : Dynamic SQL Error + 335544436 : SQL error code = -206 + 335544578 : Column unknown + 335544382 : "X" + 336397208 : At line 1, column 66 + Statement : {EDS_STATEMENT} + Data source : Firebird:: + -At block line: 3, col: 11 + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.isql(switches = ['-q'], input = test_script, combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_2155_test.py b/tests/bugs/core_2155_test.py index df2adbae..63a828e7 100644 --- a/tests/bugs/core_2155_test.py +++ b/tests/bugs/core_2155_test.py @@ -7,12 +7,20 @@ DESCRIPTION: JIRA: CORE-2155 FBTEST: bugs.core_2155 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """ +db = db_factory() + +test_script = """ set term ^; create or alter procedure sp_test(a_id int) returns (a_dup int) as begin @@ -30,11 +38,7 @@ select dummy_alias.rdb$relation_id, dummy_alias.rdb$field_id from rdb$relations as dummy_alias; commit; -""" - -db = db_factory(init=init_script) -test_script = """ set planonly; select v.rdb$relation_id, p.* @@ -46,16 +50,21 @@ INNER join sp_test(v.rdb$field_id) p on 1=1; """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ +expected_stdout_5x = """ PLAN JOIN (V RDB$RELATIONS NATURAL, P NATURAL) PLAN JOIN (V DUMMY_ALIAS NATURAL, P NATURAL) """ -@pytest.mark.version('>=3.0') +expected_stdout_6x = """ + PLAN JOIN ("V" "SYSTEM"."RDB$RELATIONS" NATURAL, "P" NATURAL) + PLAN JOIN ("V" "DUMMY_ALIAS" NATURAL, "P" NATURAL) +""" + +@pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_2200_test.py b/tests/bugs/core_2200_test.py index add0c0ee..0218b1cf 100644 --- a/tests/bugs/core_2200_test.py +++ b/tests/bugs/core_2200_test.py @@ -7,6 +7,20 @@ DESCRIPTION: JIRA: CORE-2200 FBTEST: bugs.core_2200 +NOTES: + [26.06.2025] pzotov + Re-implemented to be more relevant with: + https://github.com/FirebirdSQL/firebird/issues/2628#issuecomment-826197753 + (Improve the cross join algorithm to stop as soon as any of the involved streams is detected as empty). + No matter how many tables are involved in the cross join, its plan must always start from EMPTY table. + We create here six tables and fill five of them. Table 't6' remains empty. + We check that in all queries where this table present in different position execution plans starts + with name of this table. + + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -18,57 +32,93 @@ -- Confirmed: wrong plan on 2.0.0.12724 and 2.1.0.17798: 'T2' was choosen as drive source, -- absence of rows in T3 was ignored, used: PLAN JOIN (T2 NATURAL, T1 NATURAL, T3 NATURAL) - recreate table t0(id int); + set list on; recreate table t1(id int); recreate table t2(id int); recreate table t3(id int); + recreate table t4(id int); + recreate table t5(id int); + recreate table t6(id int); + set term ^; + create procedure sp_gen_rows( a_cnt int ) returns ( i int ) as + begin + i = 0; + while (i < a_cnt ) do + begin + suspend; + i = i + 1; + end + end + ^ + set term ;^ commit; - insert into t0 select 1 from rdb$types; -- ,rdb$types; - commit; - - - insert into t1 select * from t0; - insert into t2 select * from t0; - --------- ::: NB ::: we do NOT add any row to the table `t3`, it remains empty ----------- + insert into t1 select p.i from sp_gen_rows( 1000 + rand() * 9000 ) as p; + insert into t2 select p.i from sp_gen_rows( 1000 + rand() * 9000 ) as p; + insert into t3 select p.i from sp_gen_rows( 1000 + rand() * 9000 ) as p; + insert into t4 select p.i from sp_gen_rows( 1000 + rand() * 9000 ) as p; + insert into t5 select p.i from sp_gen_rows( 1000 + rand() * 9000 ) as p; + -- ::: NB ::: we do NOT add any row to the table `t6`, it remains empty commit; - set list on; - select case when count(*) > 100 then 'OK' else 'WRONG' end as t1_has_enough_rows from t1; - select case when count(*) > 100 then 'OK' else 'WRONG' end as t2_has_enough_rows from t2; - - set plan on; - set echo on; - - select count(*) from t1, t2, t3; - - select count(*) from t2, t3, t1; - - select count(*) from t3, t2, t1; + select + sign( (select count(*) from t1) - 1000 ) t1_big_enough + ,sign( (select count(*) from t2) - 1000 ) t2_big_enough + ,sign( (select count(*) from t3) - 1000 ) t3_big_enough + ,sign( (select count(*) from t4) - 1000 ) t4_big_enough + ,sign( (select count(*) from t5) - 1000 ) t5_big_enough + ,(select count(*) from t6) t6_rows_count + from rdb$database; + + set planonly; + select count(*) from t1, t2, t3, t4, t5, t6; + select count(*) from t1, t2, t3, t4, t6, t5; + select count(*) from t1, t2, t3, t6, t5, t4; + select count(*) from t1, t2, t6, t4, t5, t3; + select count(*) from t1, t6, t3, t4, t5, t2; + select count(*) from t6, t2, t3, t4, t5, t1; """ -act = isql_act('db', test_script) - -expected_stdout = """ - T1_HAS_ENOUGH_ROWS OK - T2_HAS_ENOUGH_ROWS OK - select count(*) from t1, t2, t3; - PLAN JOIN (T3 NATURAL, T1 NATURAL, T2 NATURAL) - COUNT 0 - - select count(*) from t2, t3, t1; - PLAN JOIN (T3 NATURAL, T2 NATURAL, T1 NATURAL) - COUNT 0 +substitutions = [ + ('[ \t]+', ' '), + (r'PLAN JOIN \(T6 NATURAL.*', 'PLAN JOIN (T6 NATURAL'), + (r'PLAN JOIN \("PUBLIC"."T6" NATURAL.*', 'PLAN JOIN ("PUBLIC"."T6" NATURAL') +] +act = isql_act('db', test_script, substitutions = substitutions) + +expected_stdout_5x = """ + T1_BIG_ENOUGH 1 + T2_BIG_ENOUGH 1 + T3_BIG_ENOUGH 1 + T4_BIG_ENOUGH 1 + T5_BIG_ENOUGH 1 + T6_ROWS_COUNT 0 + PLAN JOIN (T6 NATURAL, T2 NATURAL, T4 NATURAL, T5 NATURAL, T1 NATURAL, T3 NATURAL) + PLAN JOIN (T6 NATURAL, T2 NATURAL, T4 NATURAL, T5 NATURAL, T1 NATURAL, T3 NATURAL) + PLAN JOIN (T6 NATURAL, T2 NATURAL, T4 NATURAL, T5 NATURAL, T1 NATURAL, T3 NATURAL) + PLAN JOIN (T6 NATURAL, T2 NATURAL, T4 NATURAL, T5 NATURAL, T1 NATURAL, T3 NATURAL) + PLAN JOIN (T6 NATURAL, T2 NATURAL, T4 NATURAL, T5 NATURAL, T1 NATURAL, T3 NATURAL) + PLAN JOIN (T6 NATURAL, T2 NATURAL, T4 NATURAL, T5 NATURAL, T3 NATURAL, T1 NATURAL) +""" - select count(*) from t3, t2, t1; - PLAN JOIN (T3 NATURAL, T2 NATURAL, T1 NATURAL) - COUNT 0 +expected_stdout_6x = """ + T1_BIG_ENOUGH 1 + T2_BIG_ENOUGH 1 + T3_BIG_ENOUGH 1 + T4_BIG_ENOUGH 1 + T5_BIG_ENOUGH 1 + T6_ROWS_COUNT 0 + PLAN JOIN ("PUBLIC"."T6" NATURAL, "PUBLIC"."T5" NATURAL, "PUBLIC"."T4" NATURAL, "PUBLIC"."T1" NATURAL, "PUBLIC"."T3" NATURAL, "PUBLIC"."T2" NATURAL) + PLAN JOIN ("PUBLIC"."T6" NATURAL, "PUBLIC"."T5" NATURAL, "PUBLIC"."T4" NATURAL, "PUBLIC"."T1" NATURAL, "PUBLIC"."T3" NATURAL, "PUBLIC"."T2" NATURAL) + PLAN JOIN ("PUBLIC"."T6" NATURAL, "PUBLIC"."T5" NATURAL, "PUBLIC"."T4" NATURAL, "PUBLIC"."T1" NATURAL, "PUBLIC"."T3" NATURAL, "PUBLIC"."T2" NATURAL) + PLAN JOIN ("PUBLIC"."T6" NATURAL, "PUBLIC"."T5" NATURAL, "PUBLIC"."T4" NATURAL, "PUBLIC"."T1" NATURAL, "PUBLIC"."T3" NATURAL, "PUBLIC"."T2" NATURAL) + PLAN JOIN ("PUBLIC"."T6" NATURAL, "PUBLIC"."T5" NATURAL, "PUBLIC"."T4" NATURAL, "PUBLIC"."T1" NATURAL, "PUBLIC"."T3" NATURAL, "PUBLIC"."T2" NATURAL) + PLAN JOIN ("PUBLIC"."T6" NATURAL, "PUBLIC"."T5" NATURAL, "PUBLIC"."T4" NATURAL, "PUBLIC"."T3" NATURAL, "PUBLIC"."T1" NATURAL, "PUBLIC"."T2" NATURAL) """ @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_2202_test.py b/tests/bugs/core_2202_test.py index 9f0c609c..10f6dc8c 100644 --- a/tests/bugs/core_2202_test.py +++ b/tests/bugs/core_2202_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-2202 FBTEST: bugs.core_2202 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -46,18 +52,25 @@ from rdb$view_relations rv; """ -act = isql_act('db', test_script) +substitutions = [ ('[ \t]+', ' ') ] +act = isql_act('db', test_script, substitutions = substitutions) + +expected_stdout_5x = """ + VEW_NAME VW_TABLE + REL_NAME TABLE_3 + RDB$VIEW_CONTEXT 1 + CTX_NAME TABLE_3 +""" -expected_stdout = """ - VEW_NAME VW_TABLE - REL_NAME TABLE_3 - RDB$VIEW_CONTEXT 1 - CTX_NAME TABLE_3 +expected_stdout_6x = """ + VEW_NAME VW_TABLE + REL_NAME TABLE_3 + RDB$VIEW_CONTEXT 1 + CTX_NAME "PUBLIC"."TABLE_3" """ -@pytest.mark.version('>=3.0') +@pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_2216_test.py b/tests/bugs/core_2216_test.py index 9f180b1f..009e574e 100644 --- a/tests/bugs/core_2216_test.py +++ b/tests/bugs/core_2216_test.py @@ -15,6 +15,12 @@ 2. Run online validation of - it sould NOT produce any errors. JIRA: CORE-2216 FBTEST: bugs.core_2216 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -23,8 +29,8 @@ from firebird.driver import SrvNBackupFlag substitutions = [('BLOB_ID.*', ''), - ('[0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9]', ''), - ('Relation [0-9]{3,4}', 'Relation')] + ('\\d\\d:\\d\\d:\\d\\d.\\d\\d', ''), + ('Relation \\d{3,4}', 'Relation')] init_script = """ create table test(id int, s varchar(10) unique using index test_s, t timestamp, b blob); @@ -37,25 +43,36 @@ nbak_file_base = temp_file('nbak-file.fdb') nbak_file_add = temp_file('nbak-file.add') -expected_stdout_a = """ - ID 1 - S qwerty - T 2013-12-11 14:15:16.1780 - foo-rio-bar - Records affected: 1 -""" - -expected_stdout_b = """ - Validation started - Relation (TEST) - process pointer page 0 of 1 - Index 1 (TEST_S) - Relation (TEST) is ok - Validation finished -""" - @pytest.mark.version('>=4.0') def test_1(act: Action, nbak_file_base: Path, nbak_file_add: Path): + + expected_stdout_a = """ + ID 1 + S qwerty + T 2013-12-11 14:15:16.1780 + foo-rio-bar + Records affected: 1 + """ + + if act.is_version('<6'): + expected_stdout_b = """ + Validation started + Relation (TEST) + process pointer page 0 of 1 + Index 1 (TEST_S) + Relation (TEST) is ok + Validation finished + """ + else: + expected_stdout_b = """ + Validation started + Relation ("PUBLIC"."TEST") + process pointer page 0 of 1 + Index 1 ("PUBLIC"."TEST_S") + Relation ("PUBLIC"."TEST") is ok + Validation finished + """ + with act.connect_server() as srv, act.db.connect() as con: # Backup base database srv.database.nbackup(database=act.db.db_path, backup=nbak_file_base, diff --git a/tests/bugs/core_2227_test.py b/tests/bugs/core_2227_test.py index 817dfc74..da9e1b03 100644 --- a/tests/bugs/core_2227_test.py +++ b/tests/bugs/core_2227_test.py @@ -1,51 +1,83 @@ -#coding:utf-8 - -""" -ID: issue-2655 -ISSUE: 2655 -TITLE: Problem with column names with Accents and triggers -DESCRIPTION: -JIRA: CORE-2227 -FBTEST: bugs.core_2227 -NOTES: - [25.1.2022] pcisar - For yet unknown reason, ISQL gets malformed stdin from act.execute() although it was passed - correctly encoded in iso8859_1. Test changed to use script file writen in iso8859_1 - which works fine. - [06.10.2022] pzotov - Could not complete adjusting for LINUX in new-qa. - DEFERRED. -""" -import platform -import pytest -from pathlib import Path -from firebird.qa import * - -init_script = """ - RECREATE TABLE TESTING ( - "CÓDIGO" INTEGER - ); -""" - -db = db_factory(charset='ISO8859_1', init=init_script) - -test_script = """ - SET TERM ^; - CREATE TRIGGER TESTING_I FOR TESTING - ACTIVE BEFORE INSERT POSITION 0 - AS - BEGIN - NEW."CÓDIGO" = 1; - END - ^ -""" - -act = isql_act('db', test_script) - -script_file = temp_file('test_script.sql') - -@pytest.mark.skipif(platform.system() != 'Windows', reason='FIXME: see notes') -@pytest.mark.version('>=3') -def test_1(act: Action, script_file: Path): - script_file.write_text(test_script, encoding='iso8859_1') - act.isql(switches=[], input_file=script_file) +#coding:utf-8 + +""" +ID: issue-2655 +ISSUE: 2655 +TITLE: Problem with column names with Accents and triggers +DESCRIPTION: +JIRA: CORE-2227 +FBTEST: bugs.core_2227 +NOTES: + [31.10.2024] pzotov + Bug was fixed for too old FB (2.1.2; 2.5 Beta1) so firebird-driver and/or QA-plugin + will not able to run on this version in order to reproduce problem. + Source for this test was taken from ticket almost w/o changes. Only aux view has been added ('v_conn_cset') for + showing current connection protocol and character set - we make query to this view two twice: one for TCP and then + for local protocol. + + Checked on 6.0.0.511 (Windows/Linux); 5.0.2.1550; 4.0.6.3165; 3.0.2.32670, 3,0,1,32609 +""" +from pathlib import Path + +import pytest +from firebird.qa import * + +db = db_factory(charset='ISO8859_1') + +act = isql_act('db', substitutions = [ ('[ \\t]+', ' '), ('TCPv(4|6)', 'TCP') ]) + +tmp_sql = temp_file('tmp_core_2227.sql') + +@pytest.mark.intl +@pytest.mark.version('>=3.0.0') +def test_1(act: Action, tmp_sql: Path): + test_script = f""" + set bail on; + set list on; + recreate table testing ( + "CÓDIGO" integer + ); + commit; + set term ^; + create trigger testing_i for testing active before insert position 0 as + begin + new."CÓDIGO" = 1; + end + ^ + set term ;^ + commit; + + create view v_conn_cset as + select + rdb$get_context('SYSTEM', 'NETWORK_PROTOCOL') as conn_protocol + ,c.rdb$character_set_name as connection_cset + ,r.rdb$character_set_name as db_default_cset + from mon$attachments a + join rdb$character_sets c on a.mon$character_set_id = c.rdb$character_set_id + cross join rdb$database r where a.mon$attachment_id=current_connection; + commit; + + connect '{act.db.dsn}'; + select * from v_conn_cset; + insert into testing default values returning "CÓDIGO"; + rollback; + + connect '{act.db.db_path}'; + select * from v_conn_cset; + insert into testing default values returning "CÓDIGO"; + """ + + tmp_sql.write_text(test_script, encoding='iso8859_1') + act.expected_stdout = """ + CONN_PROTOCOL TCPv4 + CONNECTION_CSET ISO8859_1 + DB_DEFAULT_CSET ISO8859_1 + CÓDIGO 1 + + CONN_PROTOCOL + CONNECTION_CSET ISO8859_1 + DB_DEFAULT_CSET ISO8859_1 + CÓDIGO 1 + """ + act.isql(switches = ['-q'], input_file = tmp_sql, charset = 'iso8859_1', combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_2230_test.py b/tests/bugs/core_2230_test.py index 4e370f17..66b60c1b 100644 --- a/tests/bugs/core_2230_test.py +++ b/tests/bugs/core_2230_test.py @@ -7,34 +7,54 @@ DESCRIPTION: JIRA: CORE-2230 FBTEST: bugs.core_2230 +NOTES: + [26.06.2025] pzotov + Slightly refactored: made code more readable. + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * from firebird.driver import DatabaseError -init_script = """CREATE DOMAIN DOM1 AS INTEGER NOT NULL CHECK (value in (0, 1)); +init_script = """ + create domain dm_test as integer not null check (value in (0, 1)); """ db = db_factory(init=init_script) - act = python_act('db') -expected_stdout = """Y ------------ -1 -""" - @pytest.mark.version('>=3.0') def test_1(act: Action, capsys): with act.db.connect() as con: - c = con.cursor() - cmd = c.prepare('execute block (x DOM1 = ?) returns (y integer) as begin y = x; suspend; end') - c.execute(cmd, [1]) - act.print_data(c) - act.expected_stdout = expected_stdout - act.stdout = capsys.readouterr().out - assert act.clean_stdout == act.clean_expected_stdout - with pytest.raises(Exception, match='.*validation error for variable X, value "10"'): - c.execute(cmd, [10]) - act.print_data(c) + cur = con.cursor() + ps = cur.prepare('execute block (a_input_value dm_test = ?) returns (y integer) as begin y = a_input_value; suspend; end') + try: + for x in (1, 11): + cur.execute(ps, (x,)) + for r in cur: + print(r[0]) + except DatabaseError as e: + print(e.__str__()) + for x in e.gds_codes: + print(x) + + + expected_stdout_5x = """ + 1 + validation error for variable A_INPUT_VALUE, value "11" + 335544879 + """ + + expected_stdout_6x = """ + 1 + validation error for variable "A_INPUT_VALUE", value "11" + 335544879 + """ + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_2252_test.py b/tests/bugs/core_2252_test.py index a045fff2..61cd6660 100644 --- a/tests/bugs/core_2252_test.py +++ b/tests/bugs/core_2252_test.py @@ -77,6 +77,7 @@ -At block line: 11, col: 9 """ +@pytest.mark.es_eds @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_2258_test.py b/tests/bugs/core_2258_test.py index 0b5c3f34..23d6ed0c 100644 --- a/tests/bugs/core_2258_test.py +++ b/tests/bugs/core_2258_test.py @@ -2,7 +2,7 @@ """ ID: issue-2684 -ISSUE: 2684 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/2684 TITLE: Internal error when select upper() from union DESCRIPTION: JIRA: CORE-2258 @@ -14,53 +14,40 @@ db = db_factory() -test_script = """SELECT * FROM - ( - SELECT CAST('123' AS BLOB SUB_TYPE TEXT) FROM RDB$DATABASE - UNION ALL - SELECT CAST('123' AS BLOB SUB_TYPE TEXT) FROM RDB$DATABASE - ) AS R (BLOB_FIELD) -; - -SELECT UPPER(BLOB_FIELD) FROM - ( - SELECT CAST('123' AS BLOB SUB_TYPE TEXT) FROM RDB$DATABASE - UNION ALL - SELECT CAST('123' AS BLOB SUB_TYPE TEXT) FROM RDB$DATABASE - ) AS R (BLOB_FIELD) -; +test_script = """ + set list on; + set count on; + select * from + ( + select cast('123' as blob sub_type text) from rdb$database + union all + select cast('123' as blob sub_type text) from rdb$database + ) as r (blob_field_id) + ; + + select upper(blob_field) from + ( + select cast('123' as blob sub_type text) from rdb$database + union all + select cast('123' as blob sub_type text) from rdb$database + ) as r (blob_field) + ; """ -act = isql_act('db', test_script) +act = isql_act('db', test_script, substitutions = [('BLOB_FIELD_ID .*', 'BLOB_FIELD_ID'), ('UPPER.*', 'UPPER')]) expected_stdout = """ - BLOB_FIELD -================= - 0:1 -============================================================================== -BLOB_FIELD: -123 -============================================================================== - 0:2 -============================================================================== -BLOB_FIELD: -123 -============================================================================== - - - UPPER -================= - 0:7 -============================================================================== -UPPER: -123 -============================================================================== - 0:a -============================================================================== -UPPER: -123 -============================================================================== - + BLOB_FIELD_ID 0:1 + 123 + BLOB_FIELD_ID 0:3 + 123 + Records affected: 2 + + UPPER 0:7 + 123 + UPPER 0:b + 123 + Records affected: 2 """ @pytest.mark.version('>=3.0') diff --git a/tests/bugs/core_2289_test.py b/tests/bugs/core_2289_test.py index ee0a36c5..f41ccbc8 100644 --- a/tests/bugs/core_2289_test.py +++ b/tests/bugs/core_2289_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-2289 FBTEST: bugs.core_2289 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -32,16 +38,22 @@ act = isql_act('db', test_script) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 23000 violation of FOREIGN KEY constraint "PACKET_DETAIL_FK" on table "PACKET_DETAIL" -Foreign key reference target does not exist -Problematic key value is ("PACKET_ID" = 753) """ +expected_stdout_6x = """ + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint "PACKET_DETAIL_FK" on table "PUBLIC"."PACKET_DETAIL" + -Foreign key reference target does not exist + -Problematic key value is ("PACKET_ID" = 753) +""" + @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_2303_test.py b/tests/bugs/core_2303_test.py index a0969bf6..770f2f52 100644 --- a/tests/bugs/core_2303_test.py +++ b/tests/bugs/core_2303_test.py @@ -7,6 +7,13 @@ DESCRIPTION: JIRA: CORE-2303 FBTEST: bugs.core_2303 +NOTES: + [26.06.2025] pzotov + Slightly refactored: made code more readable. + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -16,11 +23,6 @@ act = python_act('db') TAG_TEXT = 'TAG_FOR_SEARCH' -expected_stdout = f""" - select 1 /* {TAG_TEXT} */ from rdb$database - Select Expression - -> Table "RDB$DATABASE" Full Scan -""" @pytest.mark.version('>=3.0') def test_1(act: Action, capsys): @@ -33,6 +35,20 @@ def test_1(act: Action, capsys): print(r[0]) print(r[1]) + + expected_stdout_5x = f""" + select 1 /* {TAG_TEXT} */ from rdb$database + Select Expression + -> Table "RDB$DATABASE" Full Scan + """ + + expected_stdout_6x = f""" + select 1 /* {TAG_TEXT} */ from rdb$database + Select Expression + -> Table "SYSTEM"."RDB$DATABASE" Full Scan + """ + expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.expected_stdout = expected_stdout act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_2305_test.py b/tests/bugs/core_2305_test.py index 013b16d3..522fad94 100644 --- a/tests/bugs/core_2305_test.py +++ b/tests/bugs/core_2305_test.py @@ -110,6 +110,7 @@ DISTINCT_STATEMENT_ID_THEY_SAW 1 """ +@pytest.mark.es_eds @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_2374_test.py b/tests/bugs/core_2374_test.py index 87851427..7bd3baa7 100644 --- a/tests/bugs/core_2374_test.py +++ b/tests/bugs/core_2374_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-2374 FBTEST: bugs.core_2374 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -33,20 +39,32 @@ act = isql_act('db', test_script) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 42000 unsuccessful metadata update -ALTER PROCEDURE TEST1 failed -Procedure TEST1 not found + Statement failed, SQLSTATE = 42000 unsuccessful metadata update -ALTER TRIGGER TRG1 failed -Trigger TRG1 not found """ -@pytest.mark.version('>=3.0') -def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -ALTER PROCEDURE "PUBLIC"."TEST1" failed + -Procedure "PUBLIC"."TEST1" not found + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -ALTER TRIGGER "PUBLIC"."TRG1" failed + -Trigger "PUBLIC"."TRG1" not found +""" + +@pytest.mark.version('>=3') +def test_1(act: Action): + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_2386_test.py b/tests/bugs/core_2386_test.py index 1d12a29f..93018147 100644 --- a/tests/bugs/core_2386_test.py +++ b/tests/bugs/core_2386_test.py @@ -7,48 +7,59 @@ DESCRIPTION: JIRA: CORE-2386 FBTEST: bugs.core_2386 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """SET TERM ^ ; - -CREATE VIEW V_TEST (F1, F2) -AS - SELECT 1, 2 FROM RDB$DATABASE -^ - -CREATE PROCEDURE SP_TEST -AS -DECLARE I INT; -BEGIN - SELECT F1, F2 FROM V_TEST - INTO :I, :I; -END -^ - -COMMIT -^ +init_script = """ """ db = db_factory(init=init_script) -test_script = """ALTER VIEW V_TEST (F1) AS - SELECT 1 FROM RDB$DATABASE ;""" +test_script = """ + set term ^ ; + create view v_test (f1, f2) as select 1, 2 from rdb$database + ^ + create procedure sp_test as + declare i int; + begin + select f1, f2 from v_test into :i, :i; + end + ^ + commit + ^ + alter view v_test (f1) as select 1 from rdb$database + ^ + +""" act = isql_act('db', test_script) -expected_stderr = """Statement failed, SQLSTATE = 42000 -unsuccessful metadata update --cannot delete --COLUMN V_TEST.F2 --there are 1 dependencies +expected_stdout_5x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -cannot delete + -COLUMN V_TEST.F2 + -there are 1 dependencies +""" + +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -cannot delete + -COLUMN "PUBLIC"."V_TEST"."F2" + -there are 1 dependencies """ @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_2424_test.py b/tests/bugs/core_2424_test.py index 3478ad94..5e91ef9b 100644 --- a/tests/bugs/core_2424_test.py +++ b/tests/bugs/core_2424_test.py @@ -7,6 +7,13 @@ DESCRIPTION: JIRA: CORE-2424 FBTEST: bugs.core_2424 +NOTES: + [26.06.2025] pzotov + Reimplemented. It is enough just to try to run command CREATE VIEW with check presence + of appropriate record in RDB$RELATIONS. + No need in 'SHOW VIEW'. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -14,27 +21,25 @@ db = db_factory() -test_script = """create view V as select d.rdb$relation_id from rdb$database d group by d.rdb$relation_id; -show view v; -recreate view V as select a from (select 1 a from rdb$database); -show view v; +test_script = """ + set list on; + create view v_test as + select d.rdb$relation_id + from rdb$database d + group by d.rdb$relation_id; + commit; + select count(*) as view_created from rdb$relations where rdb$relation_name = upper('v_test'); """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """RDB$RELATION_ID SMALLINT Expression -View Source: -==== ====== - select d.rdb$relation_id from rdb$database d group by d.rdb$relation_id -A INTEGER Expression -View Source: -==== ====== - select a from (select 1 a from rdb$database) +expected_stdout = """ + VIEW_CREATED 1 """ -@pytest.mark.version('>=3.0') +@pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_2430_test.py b/tests/bugs/core_2430_test.py index 3fe82320..a6053c50 100644 --- a/tests/bugs/core_2430_test.py +++ b/tests/bugs/core_2430_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-2430 FBTEST: bugs.core_2430 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -14,29 +20,39 @@ db = db_factory() -test_script = """CREATE TABLE T1 ( - F1 BIGINT NOT NULL, - F2 BIGINT NOT NULL, - F3 TIMESTAMP DEFAULT current_timestamp NOT NULL -); +test_script = """ + create table t1 ( + f1 bigint not null, + f2 bigint not null, + f3 timestamp default current_timestamp not null + ); -ALTER TABLE T1 ADD CONSTRAINT PK_T1 PRIMARY KEY (F1, F2); + alter table t1 add constraint pk_t1 primary key (f1, f2); -show table t1; + show table t1; """ act = isql_act('db', test_script) -expected_stdout = """F1 BIGINT Not Null -F2 BIGINT Not Null -F3 TIMESTAMP Not Null DEFAULT current_timestamp -CONSTRAINT PK_T1: - Primary key (F1, F2) +expected_stdout_5x = """ + F1 BIGINT Not Null + F2 BIGINT Not Null + F3 TIMESTAMP Not Null default current_timestamp + CONSTRAINT PK_T1: + Primary key (F1, F2) +""" + +expected_stdout_6x = """ + Table: PUBLIC.T1 + F1 BIGINT Not Null + F2 BIGINT Not Null + F3 TIMESTAMP Not Null default current_timestamp + CONSTRAINT PK_T1: + Primary key (F1, F2) """ @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_2431_test.py b/tests/bugs/core_2431_test.py index c0e17e2a..9cedd3b9 100644 --- a/tests/bugs/core_2431_test.py +++ b/tests/bugs/core_2431_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-2431 FBTEST: bugs.core_2431 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -31,26 +37,29 @@ commit; """ -act = isql_act('db', test_script, - substitutions=[('-At block line: [\\d]+, col: [\\d]+', '-At block line')]) +substitutions = [ ('[ \t]+', ' '), ('-At block line: [\\d]+, col: [\\d]+', '-At block line')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ - CONNECTION_CSET WIN1251 +expected_stdout_5x = """ + CONNECTION_CSET WIN1251 + Statement failed, SQLSTATE = HY000 + exception 1 + -EX_BAD_REMAINDER + -Новый остаток изделия будет отрицательным: -8 + -At block line """ -expected_stderr = """ +expected_stdout_6x = """ + CONNECTION_CSET WIN1251 Statement failed, SQLSTATE = HY000 exception 1 - -EX_BAD_REMAINDER + -"PUBLIC"."EX_BAD_REMAINDER" -Новый остаток изделия будет отрицательным: -8 - -At block line: 3, col: 7 + -At block line """ -@pytest.mark.version('>=3.0') +@pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute(charset='win1251') - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(charset='win1251', combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_2477_test.py b/tests/bugs/core_2477_test.py index ebaec7bc..adb14aad 100644 --- a/tests/bugs/core_2477_test.py +++ b/tests/bugs/core_2477_test.py @@ -21,20 +21,30 @@ JIRA: CORE-2477 FBTEST: bugs.core_2477 NOTES: -[16.11.2021] pcisar + [16.11.2021] pcisar This test is too complicated and fragile, and it's IMHO not worth to be implemented -[24.07.2022] pzotov + + [24.07.2022] pzotov Test was totally re-implemented. No async call of ISQL, waiting/killing etc. Checked on Windows: 3.0.8.33535 (SS/CS), 4.0.1.2692 (SS/CS), 5.0.0.591 Checked on Linux: 4.0.1.2692 (SS/CS) - needed to increase number of rows to be sorted. + + [18.01.2025] pzotov + Resultset of cursor that executes using instance of selectable PreparedStatement must be stored + in some variable in order to have ability close it EXPLICITLY (before PS will be freed). + Otherwise access violation raises during Python GC and pytest hangs at final point (does not return control to OS). + This occurs at least for: Python 3.11.2 / pytest: 7.4.4 / firebird.driver: 1.10.6 / Firebird.Qa: 0.19.3 + The reason of that was explained by Vlad, 26.10.24 17:42 ("oddities when use instances of selective statements"). """ import subprocess from pathlib import Path +from firebird.driver import DatabaseError +import time + import pytest from firebird.qa import * -import time init_script = """ create or alter view v_mon as @@ -108,27 +118,47 @@ def test_1(act: Action, capsys): with act.db.connect() as con_worker: cur_worker = con_worker.cursor() - cur_wrk_ps = cur_worker.prepare(heavy_sql_sttm) - - for m in ('beg','end'): - with act.db.connect() as con_monitor: - cur_monitor=con_monitor.cursor() - cur_monitor.execute('select * from v_gather_mon') - - for r in cur_monitor: - map_result[m] = (r[3], r[7]) # ('idle' | 'stalled', memo_used) - - assert map_result.get(m), f"No rows returned from v_gather_mon for m='{m}'" - - if m == 'beg': - cur_worker.execute(cur_wrk_ps) - for i in range(0, ROWS_TO_BE_SORTED): - r = cur_worker.fetchone() - - # After this loop statement with huge sort will remain in stalled state - # (its mon$statements.mon$state must be 2). - # We can now gather mon$ info second time (in a NEW connection) - # and then evaluate DIFFERENCE. + cur_wrk_ps, cur_wrk_rs = None, None + + try: + cur_wrk_ps = cur_worker.prepare(heavy_sql_sttm) + + for m in ('beg','end'): + with act.db.connect() as con_monitor: + cur_monitor=con_monitor.cursor() + cur_monitor.execute('select * from v_gather_mon') + + for r in cur_monitor: + map_result[m] = (r[3], r[7]) # ('idle' | 'stalled', memo_used) + + assert map_result.get(m), f"No rows returned from v_gather_mon for m='{m}'" + + if m == 'beg': + + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + + cur_wrk_rs = cur_worker.execute(cur_wrk_ps) + for i in range(0, ROWS_TO_BE_SORTED): + r = cur_worker.fetchone() + + # After this loop statement with huge sort will remain in stalled state + # (its mon$statements.mon$state must be 2). + # We can now gather mon$ info second time (in a NEW connection) + # and then evaluate DIFFERENCE. + + except DatabaseError as e: + print( e.__str__() ) + print(e.gds_codes) + finally: + if cur_wrk_rs: + cur_wrk_rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if cur_wrk_ps: + cur_wrk_ps.free() #------------------------------------------------------------------------------------------ diff --git a/tests/bugs/core_2508_test.py b/tests/bugs/core_2508_test.py index d150a8db..83c01d25 100644 --- a/tests/bugs/core_2508_test.py +++ b/tests/bugs/core_2508_test.py @@ -16,6 +16,12 @@ Old query that did use IN predicate no more applicable here: all occurences of the same index that works for mining data are now "collapsed" to the single one, i.e.: PLAN ( INDEX (, , )) ==> PLAN ( INDEX ()). + + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -42,14 +48,19 @@ -- PLAN (T INDEX (abc(, mod()) """ -act = isql_act('db', test_script) +substitutions = [ ('[ \t]+', ' '), ('-At block line: [\\d]+, col: [\\d]+', '-At block line')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ +expected_stdout_5x = """ PLAN (T INDEX (abc(, mod()) """ -@pytest.mark.version('>=3.0') +expected_stdout_6x = """ + PLAN ("PUBLIC"."T" INDEX ("PUBLIC"."abc(", "PUBLIC"."mod(")) +""" + +@pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_2516_test.py b/tests/bugs/core_2516_test.py index f9b93806..aa0a455f 100644 --- a/tests/bugs/core_2516_test.py +++ b/tests/bugs/core_2516_test.py @@ -3,36 +3,37 @@ """ ID: issue-2926 ISSUE: 2926 -TITLE: Wrong processing a SP parameters with arrays +TITLE: Wrong processing SP parameters with arrays DESCRIPTION: JIRA: CORE-2516 FBTEST: bugs.core_2516 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """ - create domain t_smallint_array as smallint [0:2]; -""" - -db = db_factory(init=init_script) +db = db_factory() test_script = """ + create domain t_smallint_array as smallint [0:2]; set term ^; - create procedure sp_smallint_array(x t_smallint_array) - returns (y t_smallint_array) - as + create procedure sp_smallint_array(x t_smallint_array) returns (y t_smallint_array) as begin - y=x; - suspend; + y=x; + suspend; end - ^ set term ;^ + ^ """ act = isql_act('db', test_script) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 0A000 CREATE PROCEDURE SP_SMALLINT_ARRAY failed -Dynamic SQL Error @@ -40,9 +41,16 @@ -Usage of domain or TYPE OF COLUMN of array type in PSQL """ -@pytest.mark.version('>=3.0') -def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr +expected_stdout_6x = """ + Statement failed, SQLSTATE = 0A000 + CREATE PROCEDURE "PUBLIC"."SP_SMALLINT_ARRAY" failed + -Dynamic SQL Error + -feature is not supported + -Usage of domain or TYPE OF COLUMN of array type in PSQL +""" +@pytest.mark.version('>=3') +def test_1(act: Action): + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_2531_test.py b/tests/bugs/core_2531_test.py index cbff7c60..4913c6d4 100644 --- a/tests/bugs/core_2531_test.py +++ b/tests/bugs/core_2531_test.py @@ -45,6 +45,7 @@ Records affected: 1 """ +@pytest.mark.intl @pytest.mark.version('>=3') def test_1(act: Action): non_ascii_query = "select 'gång' as non_ascii_literal from rdb$database" diff --git a/tests/bugs/core_2555_test.py b/tests/bugs/core_2555_test.py index 7cf4e7ca..94a7471b 100644 --- a/tests/bugs/core_2555_test.py +++ b/tests/bugs/core_2555_test.py @@ -61,7 +61,6 @@ commit; set list on; - set plan on; -- First, we check result of NATURAL scans: select rk from test group by ca,cb,rk; @@ -78,7 +77,6 @@ create unique index test_unq2 on test(cb, rk, ca, id); commit; - set plan on; select rk from test group by ca,cb,rk; select rk from test group by cb,rk,ca; """ @@ -87,7 +85,6 @@ expected_stdout = """ - PLAN SORT (TEST NATURAL) RK a RK c RK d @@ -97,7 +94,6 @@ RK s RK u - PLAN (TEST ORDER TEST_UNQ1) RK a RK c RK d @@ -107,7 +103,6 @@ RK s RK u - PLAN (TEST ORDER TEST_UNQ2) RK a RK c RK d diff --git a/tests/bugs/core_2606_test.py b/tests/bugs/core_2606_test.py index a85ab1f4..6b0dc212 100644 --- a/tests/bugs/core_2606_test.py +++ b/tests/bugs/core_2606_test.py @@ -50,6 +50,7 @@ VARC1_ASCII A. """ +@pytest.mark.intl @pytest.mark.version('>=3.0') def test_1(act: Action): act.script = test_script diff --git a/tests/bugs/core_2635_test.py b/tests/bugs/core_2635_test.py index 4db0edac..0ff116f4 100644 --- a/tests/bugs/core_2635_test.py +++ b/tests/bugs/core_2635_test.py @@ -7,70 +7,89 @@ DESCRIPTION: JIRA: CORE-2635 FBTEST: bugs.core_2635 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """set term ^; -recreate table t (id int, sss varchar(255)) ^ -create unique descending index t_id_desc on t (id) ^ -create unique ascending index t_id_asc on t (id) ^ -create unique descending index t_id_sss_desc on t (id, sss) ^ -create unique ascending index t_id_sss_asc on t (id, sss) ^ -commit ^ +init_script = """ + set term ^; + recreate table t (id int, sss varchar(255)) ^ + create unique descending index t_id_desc on t (id) ^ + create unique ascending index t_id_asc on t (id) ^ + create unique descending index t_id_sss_desc on t (id, sss) ^ + create unique ascending index t_id_sss_asc on t (id, sss) ^ + commit ^ -execute block as -declare n int = 0; -begin - while (n < 10000) do - begin - insert into t values (:n, :n); - n = n + 1; - end + execute block as + declare n int = 0; + begin + while (n < 10000) do + begin + insert into t values (:n, :n); + n = n + 1; + end - n = 0; - while (n < 10000) do - begin - insert into t values (null, null); - n = n + 1; - end -end ^ -commit ^ + n = 0; + while (n < 10000) do + begin + insert into t values (null, null); + n = n + 1; + end + end ^ + commit ^ -execute block as -declare n int = 5000; -begin - while (n > 0) do - begin - n = n - 1; - update t set id = null, sss = null where id = :n; - end -end ^ -commit ^ + execute block as + declare n int = 5000; + begin + while (n > 0) do + begin + n = n - 1; + update t set id = null, sss = null where id = :n; + end + end ^ + commit ^ """ db = db_factory(init=init_script) -act = python_act('db', substitutions=[('[0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9]', ''), - ('Relation [0-9]{3,4}', 'Relation')]) +act = python_act('db', substitutions=[ ('[ \t]+', ' '), ('\\d\\d:\\d\\d:\\d\\d.\\d\\d', ''), ('Relation \\d{3,4}', 'Relation')]) + +expected_stdout_5x = """ + Validation started + Relation (T) + process pointer page 0 of 1 + Index 1 (T_ID_DESC) + Index 2 (T_ID_ASC) + Index 3 (T_ID_SSS_DESC) + Index 4 (T_ID_SSS_ASC) + Relation (T) is ok + Validation finished +""" -expected_stdout = """ -Validation started -Relation (T) -process pointer page 0 of 1 -Index 1 (T_ID_DESC) -Index 2 (T_ID_ASC) -Index 3 (T_ID_SSS_DESC) -Index 4 (T_ID_SSS_ASC) -Relation (T) is ok -Validation finished +expected_stdout_6x = """ + Validation started + Relation ("PUBLIC"."T") + process pointer page 0 of 1 + Index 1 ("PUBLIC"."T_ID_DESC") + Index 2 ("PUBLIC"."T_ID_ASC") + Index 3 ("PUBLIC"."T_ID_SSS_DESC") + Index 4 ("PUBLIC"."T_ID_SSS_ASC") + Relation ("PUBLIC"."T") is ok + Validation finished """ -@pytest.mark.version('>=2.5') +@pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x with act.connect_server() as srv: srv.database.validate(database=act.db.db_path) act.stdout = '\n'.join(srv.readlines()) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_2650_test.py b/tests/bugs/core_2650_test.py index dfd57df7..45311ea7 100644 --- a/tests/bugs/core_2650_test.py +++ b/tests/bugs/core_2650_test.py @@ -5,26 +5,55 @@ ISSUE: 3057 TITLE: Improve sorting performance when long VARCHARs are involved DESCRIPTION: - Test verifies trivial queries with persistent and computed columns, predicates, views, - expressions without reference to any column and datatypes which have no symmetrical - transformation from value to a key (decfloat, time-with-timezone and varchar with non-default collation). + Test verifies trivial queries with persistent and computed columns, predicates, views, + expressions without reference to any column and datatypes which have no symmetrical + transformation from value to a key (decfloat, time-with-timezone and varchar with non-default collation). - It is supposed that default value of InlineSortThreshold parameter is 1000. - No changes in the firebird.conf reuired. + It is supposed that default value of InlineSortThreshold parameter is 1000. + No changes in the firebird.conf reuired. - This test most probably will be added by some new examples later. - - Thanks to dimitr for lot of explanations (e-mail discussion was 28.12.2020). + This test most probably will be added by some new examples later. + Thanks to dimitr for lot of explanations (e-mail discussion was 28.12.2020). JIRA: CORE-2650 FBTEST: bugs.core_2650 +NOTES: + [02.09.2024] pzotov + 1. Test was fully re-implemented in order to have ability to see both query and its comment in case of mismatch. + The 'f'-notation is used in expected output with substitution query text and comments to it, e.g.: + {query_map[1000][0]} // output will be compared with: "select txt_short from test a01 order by id" + {query_map[1000][1]} // output will be compared with: "Must NOT use refetch because length of non-key column is less than threshold" + 2. Explained plans for FB 4.x and 5.x/6.x not equal. + Because of this, expected output is stored differently (fb4x_expected_out, fb5x_expected_out). + 3. Indentation between 'Refetch' and subsequent 'Sort' was removed (only 'master' was affected). + This change was caused by (or related to) profiler, discussed with Adriano. + See letters since 24-aug-2023, subj: + "Test for gh-7687: can't understand the output for trivial .sql (from the ticket)" + See also: + https://groups.google.com/g/firebird-devel/c/dWIgSIemys4/m/TzUWYwmVAQAJ (18-JUL-2023) + https://github.com/FirebirdSQL/firebird/issues/7687 (28-JUL-2023) + 4 Need to preserve indentation was explained by dimitr 28-aug-2024 12:57, subj: + 'Explained plan, "Refetch": should the indentation be after it at line with subsequent "-> Sort (record ...)" ?' + 5. Indentation was restored 01-sep-2024 by fix https://github.com/FirebirdSQL/firebird/issues/8235 + https://github.com/FirebirdSQL/firebird/commit/901b4ced9a3615929e0027d42ebb2392e943b205 + + Checked on 6.0.0.447-901b4ce, 5.0.2.1487, 4.0.6.3142 + + [13.01.2025] pzotov + 6. Separated expected_out for FB 6.x after b2d03 2025.01.10 ("More correct plan output for subqueries generated during NOT IN transformation") + 7. Parameter OptimizeForFirstRows must have default value fcor this test (i.e. false). To prevent test fail in case of occasional changing of + this parameter, session-level command is used for FB 5.x+: 'set optimize for all rows'. + + [16.01.2025] pzotov + 8. Changed expected_out for FB 5.x after e24e0 2025.01.13 ("More correct plan output for subqueries generated during NOT IN transformation"). + Despite that now expected_out strings for 5.x and 6.x become equal, they remain separated in case of future changes in 6.x+ + + Checked 6.0.0.573-c20f37a; 5.0.2.1592-2d11769 """ import pytest from firebird.qa import * -db = db_factory() - -test_script = """ +init_sql = """ set bail on; recreate view v_unioned as select 1 id from rdb$database; commit; @@ -42,133 +71,6 @@ ,computed_ts_left computed by( left(txt_short,10) ) ,computed_tb_left computed by( left(txt_broad,10) ) ); - commit; - - set plan on; - set explain on; - -- set echo on; - - --########################## p e r s i s t e n t c o l u m n s ################################## - - -- Must not use refetch because length of non-key column is less than default threshold: - select txt_short from test a01 order by id; - - -- Must USE refetch because length of non-key column is greater than default threshold: - select txt_broad from test a02 order by id; - - -- MUST use refethc regardless on length of column because 'ROWS ' presents (!): - select txt_short from test a03 order by id rows 1; - - - -- ########################## c o m p u t e d c o l u m n s ##################################### - - -- does NOT use refetch because computed column is based on txt_short which has length < threshold: - select id, computed_ts_dup from test order by id; - - -- must use refetch because computed column is based on txt_broad which has length >= threshold: - select id, computed_tb_dup from test order by id; - - -- ###################### p r e d i c a t e s, e x c e p t E X I S T S ######################## - - select id from test a04 where '' in (select txt_short from test x04 where txt_short = '' order by id) ; - - select id from test a05 where '' in (select txt_broad from test x05 where txt_broad = '' order by id) ; - - - select id from test a06 where '' not in (select txt_short from test x06 where txt_short>'' order by id) ; - - select id from test a07 where '' not in (select txt_broad from test x07 where txt_broad>'' order by id) ; - - - select id from test a08 where '' > all (select id from test x08 where txt_short>'' order by id) ; - - select id from test a09 where '' > all (select id from test x09 where txt_broad>'' order by id) ; - - - select id from test a10 where '' <> any (select id from test x10 where txt_short>'' order by id) ; - - select id from test a11 where '' <> any (select id from test x11 where txt_broad>'' order by id) ; - - -- ######################################## e x i s t s ########################################### - - -- Predicate "EXISTS" must turn on refetching regardless of record length, but only when "WHERE" has column which not present in "ORDER BY" - select id,txt_short from test a12 where exists(select 1 from test x12 where txt_short>'' order by id) ; -- MUST use refetch - - -- does NOT use refetch: "order by" list contains the single element: ID, and it is the same field that 'computed_id_dup' relies on. - select id,txt_short from test a13 where exists(select 1 from test x13 where computed_id_dup > 0 order by id) ; - - -- ### NB ### Must use refetch! See letter from dimitr 28.12.2020 14:49, reply for: - -- "select id,txt_short from test a14 where exists(select 1 from test x14 where computed_ts_dup > '' order by computed_ts_left);" - -- Sort procedure will get: - -- a KEY = result of evaluating 'computed_id_dup'; - -- a VAL = value of the field 'ID' which is base for computing 'computed_id_dup' - -- Thus sorter will have a field which not equals to a key, which leads to refetch. - select id,txt_short from test a14 where exists(select 1 from test x14 where computed_id_dup > 0 order by computed_id_dup ) ; - - -- does NOT use refetch: all persistent columns from "WHERE" expr (f01, f02) belongs to "order by" list: - select id,txt_short from test a15 where exists(select 1 from test x15 where f02>0 and f01>0 order by f01, f02); - - -- must use refetch: one of coulmns from "where" expr (id) does not belong to "order by" list: - select id,txt_short from test a16 where exists(select 1 from test x16 where id>0 and f01>0 order by f01, f02); - - -- must use refetch: computed column in "where" expr does not belong to "order by" list: - select id,txt_short from test a17 where exists(select 1 from test x17 where computed_id_dup > 0 order by f01); - - -- does NOT use refetch: computed column "computed_guid" does not rely on any other columns in the table: - select id,txt_short from test a18 where exists(select 1 from test x18 where computed_guid > '' order by f01); - - - -- must use refetch both in anchor and recursive parts: - with recursive - r as ( - select a19.id, a19.txt_short - from test a19 - where not exists(select * from test x where x.txt_short < a19.txt_short order by id) - UNION ALL - select i.id, i.txt_short - from test i - join r on i.id > r.id - and not exists( select * from test x where x.txt_short between r.txt_short and i.txt_short order by id ) - ) - select * from r; - commit; - - - -- ###################################### v i e w s ########################################### - - recreate view v_unioned as - select id, txt_broad from test - union all - select -1, 'qwerty' - from rdb$database rows 0; - - -- does NOT use refetch because view is based on UNION: - select txt_broad from v_unioned v01 order by id; - commit; - - -- ################################# e x p r e s s i o n s ##################################### - - -- must use refetch because expression is based on column which has length >= threshold - -- (even if final length of expression result is much less than threshold): - select left(txt_broad, 50) as txt from test a21 order by id; - - -- does NOT use refetch because expression is based on column which has length < threshold - -- (even if final length of expression result is much bigger than threshold): - select left( txt_short || txt_short, 2000) as txt from test a22 order by id; - commit; - - - -- ########### n o n - s y m m e t r i c a l k e y - v a l u e d a t a t y p e s ######### - - -- Following data types in common case have no ability to get column value from a key: - -- * International type text has a computed key - -- * Different decimal float values sometimes have same keys - -- * Date/time with time zones too. - -- Because of this, a field of any such datatype that is specified in "order by" list, must also be involved - -- in the non-key fields and sort will deal with such "concatenated" list. - -- If total length of such list not exceeds InlineSortThreshold then sort will be done without refetch. - -- Otherwise refetch will occur. - -- See src\\jrd\\opt.cpp, OPT_gen_sort() and explanation fro dimitr: letter 28.12.2020 16:44. recreate table test_ns_01( id decfloat @@ -180,13 +82,6 @@ ,txt_short varchar(982) ); - select * from test_ns_01 a23 order by id; -- must use refetch - - select * from test_ns_02 a24 order by id; -- must NOT use refetch - - commit; - - ------------------------------------------ recreate table test_ns_03( id time with time zone ,txt_short varchar(991) @@ -197,11 +92,6 @@ ,txt_short varchar(990) ); - select * from test_ns_03 order by id; -- must use refetch - - select * from test_ns_04 order by id; -- must NOT use refetch - ------------------------------------------ - recreate table test_ns_05( id varchar(1) character set utf8 collate unicode_ci_ai ,txt_short varchar(993) @@ -211,478 +101,1082 @@ id varchar(1) character set utf8 collate unicode_ci_ai ,txt_short varchar(992) ); + commit; - select * from test_ns_05 order by id; -- must use refetch - - select * from test_ns_06 order by id; -- must NOT use refetch - - + recreate view v_unioned as + select id, txt_broad from test + union all + select -1, 'qwerty' + from rdb$database rows 0; + commit; """ - -act = isql_act('db', test_script) - -fb4x_expected_out = """ - Select Expression - -> Sort (record length: 1036, key length: 8) - -> Table "TEST" as "A01" Full Scan - - Select Expression - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Table "TEST" as "A02" Full Scan - - Select Expression - -> First N Records - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Table "TEST" as "A03" Full Scan - - Select Expression - -> Sort (record length: 1036, key length: 8) - -> Table "TEST" Full Scan - - Select Expression - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Table "TEST" Full Scan - - Select Expression - -> Filter - -> Sort (record length: 1036, key length: 8) - -> Filter - -> Table "TEST" as "X04" Full Scan - Select Expression - -> Filter - -> Table "TEST" as "A04" Full Scan - - Select Expression - -> Filter - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Filter - -> Table "TEST" as "X05" Full Scan - Select Expression - -> Filter - -> Table "TEST" as "A05" Full Scan - - Select Expression - -> Sort (record length: 1036, key length: 8) - -> Filter - -> Table "TEST" as "X06" Full Scan - Select Expression - -> Sort (record length: 1036, key length: 8) - -> Filter - -> Table "TEST" as "X06" Full Scan - Select Expression - -> Filter - -> Table "TEST" as "A06" Full Scan - - Select Expression - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Filter - -> Table "TEST" as "X07" Full Scan - Select Expression - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Filter - -> Table "TEST" as "X07" Full Scan - Select Expression - -> Filter - -> Table "TEST" as "A07" Full Scan - - Select Expression - -> Filter - -> Sort (record length: 1036, key length: 8) - -> Filter - -> Table "TEST" as "X08" Full Scan - Select Expression - -> Filter - -> Table "TEST" as "A08" Full Scan - - Select Expression - -> Filter - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Filter - -> Table "TEST" as "X09" Full Scan +db = db_factory(init = init_sql) + +query_map = { + ########################## p e r s i s t e n t c o l u m n s ################################## + + 1000 : ( 'select txt_short from test a01 order by id' , 'Must NOT use refetch because length of non-key column is less than threshold' ) + ,1010 : ( 'select txt_broad from test a02 order by id' , 'MUST use refetch because length of non-key column is greater than threshold' ) + ,1020 : ( 'select txt_short from test a03 order by id rows 1' , 'MUST use refetch regardless on length of column because ROWS presents' ) + + ########################## c o m p u t e d c o l u m n s ##################################### + + ,2000 : ( 'select id, computed_ts_dup from test order by id' , 'Must NOT use refetch because computed column is based on txt_short with length < threshold' ) + ,2010 : ( 'select id, computed_tb_dup from test order by id' , 'MUST use refetch because computed column is based on txt_broad which has length >= threshold' ) + + ###################### p r e d i c a t e s [N O T] I N, A L L, A N Y ######################## + + ,3000 : ( "select id from test a04 where '' in (select txt_short from test x04 where txt_short = '' order by id)" , '*** not [yet] commented ***' ) + ,3010 : ( "select id from test a05 where '' in (select txt_broad from test x05 where txt_broad = '' order by id)" , '*** not [yet] commented ***' ) + ,3020 : ( "select id from test a06 where '' not in (select txt_short from test x06 where txt_short>'' order by id)" , '*** not [yet] commented ***' ) + ,3030 : ( "select id from test a07 where '' not in (select txt_broad from test x07 where txt_broad>'' order by id)" , '*** not [yet] commented ***' ) + ,3040 : ( "select id from test a08 where '' > all (select id from test x08 where txt_short>'' order by id)" , '*** not [yet] commented ***' ) + ,3050 : ( "select id from test a09 where '' > all (select id from test x09 where txt_broad>'' order by id)" , '*** not [yet] commented ***' ) + ,3060 : ( "select id from test a10 where '' <> any (select id from test x10 where txt_short>'' order by id)" , '*** not [yet] commented ***' ) + ,3070 : ( "select id from test a11 where '' <> any (select id from test x11 where txt_broad>'' order by id)" , '*** not [yet] commented ***' ) + + ######################################## e x i s t s ########################################### + # Predicate "EXISTS" must turn on refetching regardless of record length + # but only when "WHERE" has column which not present in "ORDER BY" + + ,4000 : ( "select id,txt_short from test a12 where exists(select 1 from test x12 where txt_short>'' order by id)" + ,"MUST use refetch: column x12.txt_short not present in order by" + ) + ,4010 : ( "select id,txt_short from test a13 where exists(select 1 from test x13 where computed_id_dup > 0 order by id)" + ,"Must NOT use refetch: ORDER BY list contains the single element: ID, and it is base for x13.computed_id_dup column" + ) + ,4020 : ( "select id,txt_short from test a14 where exists(select 1 from test x14 where computed_id_dup > 0 order by computed_id_dup)" + ,""" + MUST use refetch! See letter from dimitr 28.12.2020 14:49 + Sort procedure will get: + a KEY = result of evaluating 'computed_id_dup'; + a VAL = value of the field 'ID' which is base for computing 'computed_id_dup' + Thus sorter will have a field which not equals to a key, which leads to refetch. + """ + ) + ,4030 : ( "select id,txt_short from test a15 where exists(select 1 from test x15 where f02>0 and f01>0 order by f01, f02)" + ,"Must NOT use refetch: all persistent columns from WHERE expression (f01, f02) belong to ORDER BY list" + ) + ,4040 : ( "select id,txt_short from test a16 where exists(select 1 from test x16 where id>0 and f01>0 order by f01, f02)" + ,"Must use refetch: one of columns from WHERE expr (id) does not belong to ORDER BY list" + ) + ,4050 : ( "select id,txt_short from test a17 where exists(select 1 from test x17 where computed_id_dup > 0 order by f01)" + ,"Must use refetch: computed column in WHERE expr does not belong to ORDER BY list" + ) + ,4060 : ( "select id,txt_short from test a18 where exists(select 1 from test x18 where computed_guid > '' order by f01)" + ,"Must NOT use refetch: computed column x18.computed_guid does is evaluated via GUID and does not refer to any columns" + ) + ,4070 : ( + """ + with recursive + r as ( + select a19.id, a19.txt_short + from test a19 + where not exists(select * from test x where x.txt_short < a19.txt_short order by id) + UNION ALL + select i.id, i.txt_short + from test i + join r on i.id > r.id + and not exists( select * from test x where x.txt_short between r.txt_short and i.txt_short order by id ) + ) + select * from r + """ + ,"MUST use refetch both in anchor and recursive parts" + ) + + ###################################### v i e w s ########################################### + + ,5000 : ( 'select txt_broad from v_unioned v01 order by id' , 'Must NOT use refetch because view DDL includes UNION' ) + + ################################# e x p r e s s i o n s ##################################### + + ,6000 : ( 'select left(txt_broad, 50) as txt from test a21 order by id' + ,""" + MUST use refetch because expression is based on column which has length >= threshold + (even if final length of expression result is much less than threshold) + """ + ) + ,6010 : ( 'select left( txt_short || txt_short, 2000) as txt from test a22 order by id' + ,""" + Must NOT use refetch because expression is based on column which has length < threshold + (even if final length of expression result is much bigger than threshold) + """ + ) + + ########### n o n - s y m m e t r i c a l k e y - v a l u e d a t a t y p e s ######### + + # Following data types in common case have no ability to get column value from a key: + # * International type text has a computed key + # * Different decimal float values sometimes have same keys + # * Date/time with time zones too. + # Because of this, a field of any such datatype that is specified in "order by" list, must also be involved + # in the non-key fields and sort will deal with such "concatenated" list. + # If total length of such list not exceeds InlineSortThreshold then sort will be done without refetch. + # Otherwise refetch will occur. + # See src/jrd/opt.cpp, OPT_gen_sort() and explanation from dimitr, letter 28.12.2020 16:44 + ,7000 : ( 'select * from test_ns_01 a23 order by id' , 'MUST use refetch' ) + ,7010 : ( 'select * from test_ns_02 a24 order by id' , 'Must NOT refetch' ) + ,7020 : ( 'select * from test_ns_03 order by id' , 'MUST use refetch' ) + ,7030 : ( 'select * from test_ns_04 order by id' , 'Must NOT use refetch' ) + ,7040 : ( 'select * from test_ns_05 order by id' , 'MUST use refetch' ) + ,7050 : ( 'select * from test_ns_06 order by id' , 'Must NOT use refetch' ) +} + + +############################################################################### + +fb4x_expected_out = f""" + 1000 + {query_map[1000][0]} + {query_map[1000][1]} + Select Expression + ....-> Sort (record length: 1036, key length: 8) + ........-> Table "TEST" as "A01" Full Scan + + 1010 + {query_map[1010][0]} + {query_map[1010][1]} + Select Expression + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Table "TEST" as "A02" Full Scan + + 1020 + {query_map[1020][0]} + {query_map[1020][1]} + Select Expression + ....-> First N Records + ........-> Refetch + ............-> Sort (record length: 28, key length: 8) + ................-> Table "TEST" as "A03" Full Scan + + 2000 + {query_map[2000][0]} + {query_map[2000][1]} + Select Expression + ....-> Sort (record length: 1036, key length: 8) + ........-> Table "TEST" Full Scan + + 2010 + {query_map[2010][0]} + {query_map[2010][1]} + Select Expression + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Table "TEST" Full Scan + + 3000 + {query_map[3000][0]} + {query_map[3000][1]} + Select Expression + ....-> Filter + ........-> Sort (record length: 1036, key length: 8) + ............-> Filter + ................-> Table "TEST" as "X04" Full Scan + Select Expression + ....-> Filter + ........-> Table "TEST" as "A04" Full Scan + + 3010 + {query_map[3010][0]} + {query_map[3010][1]} + Select Expression + ....-> Filter + ........-> Refetch + ............-> Sort (record length: 28, key length: 8) + ................-> Filter + ....................-> Table "TEST" as "X05" Full Scan + Select Expression + ....-> Filter + ........-> Table "TEST" as "A05" Full Scan + + 3020 + {query_map[3020][0]} + {query_map[3020][1]} + Select Expression + ....-> Sort (record length: 1036, key length: 8) + ........-> Filter + ............-> Table "TEST" as "X06" Full Scan + Select Expression + ....-> Sort (record length: 1036, key length: 8) + ........-> Filter + ............-> Table "TEST" as "X06" Full Scan + Select Expression + ....-> Filter + ........-> Table "TEST" as "A06" Full Scan + + 3030 + {query_map[3030][0]} + {query_map[3030][1]} + Select Expression + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Filter + ................-> Table "TEST" as "X07" Full Scan + Select Expression + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Filter + ................-> Table "TEST" as "X07" Full Scan Select Expression - -> Filter - -> Table "TEST" as "A09" Full Scan + ....-> Filter + ........-> Table "TEST" as "A07" Full Scan + 3040 + {query_map[3040][0]} + {query_map[3040][1]} + Select Expression + ....-> Filter + ........-> Sort (record length: 1036, key length: 8) + ............-> Filter + ................-> Table "TEST" as "X08" Full Scan Select Expression - -> Filter - -> Sort (record length: 1036, key length: 8) - -> Filter - -> Table "TEST" as "X10" Full Scan - Select Expression - -> Filter - -> Table "TEST" as "A10" Full Scan + ....-> Filter + ........-> Table "TEST" as "A08" Full Scan + 3050 + {query_map[3050][0]} + {query_map[3050][1]} + Select Expression + ....-> Filter + ........-> Refetch + ............-> Sort (record length: 28, key length: 8) + ................-> Filter + ....................-> Table "TEST" as "X09" Full Scan Select Expression - -> Filter - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Filter - -> Table "TEST" as "X11" Full Scan - Select Expression - -> Filter - -> Table "TEST" as "A11" Full Scan + ....-> Filter + ........-> Table "TEST" as "A09" Full Scan + 3060 + {query_map[3060][0]} + {query_map[3060][1]} Select Expression - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Filter - -> Table "TEST" as "X12" Full Scan + ....-> Filter + ........-> Sort (record length: 1036, key length: 8) + ............-> Filter + ................-> Table "TEST" as "X10" Full Scan Select Expression - -> Filter - -> Table "TEST" as "A12" Full Scan + ....-> Filter + ........-> Table "TEST" as "A10" Full Scan + 3070 + {query_map[3070][0]} + {query_map[3070][1]} Select Expression - -> Sort (record length: 28, key length: 8) - -> Filter - -> Table "TEST" as "X13" Full Scan + ....-> Filter + ........-> Refetch + ............-> Sort (record length: 28, key length: 8) + ................-> Filter + ....................-> Table "TEST" as "X11" Full Scan Select Expression - -> Filter - -> Table "TEST" as "A13" Full Scan + ....-> Filter + ........-> Table "TEST" as "A11" Full Scan + + 4000 + {query_map[4000][0]} + {query_map[4000][1]} + Select Expression + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Filter + ................-> Table "TEST" as "X12" Full Scan + Select Expression + ....-> Filter + ........-> Table "TEST" as "A12" Full Scan + + 4010 + {query_map[4010][0]} + {query_map[4010][1]} + Select Expression + ....-> Sort (record length: 28, key length: 8) + ........-> Filter + ............-> Table "TEST" as "X13" Full Scan + Select Expression + ....-> Filter + ........-> Table "TEST" as "A13" Full Scan + + 4020 + {query_map[4020][0]} + {query_map[4020][1]} + Select Expression + ....-> Refetch + ........-> Sort (record length: 36, key length: 12) + ............-> Filter + ................-> Table "TEST" as "X14" Full Scan + Select Expression + ....-> Filter + ........-> Table "TEST" as "A14" Full Scan + + 4030 + {query_map[4030][0]} + {query_map[4030][1]} + Select Expression + ....-> Sort (record length: 36, key length: 16) + ........-> Filter + ............-> Table "TEST" as "X15" Full Scan + Select Expression + ....-> Filter + ........-> Table "TEST" as "A15" Full Scan + 4040 + {query_map[4040][0]} + {query_map[4040][1]} Select Expression - -> Refetch - -> Sort (record length: 36, key length: 12) - -> Filter - -> Table "TEST" as "X14" Full Scan - Select Expression - -> Filter - -> Table "TEST" as "A14" Full Scan + ....-> Refetch + ........-> Sort (record length: 36, key length: 16) + ............-> Filter + ................-> Table "TEST" as "X16" Full Scan + Select Expression + ....-> Filter + ........-> Table "TEST" as "A16" Full Scan + 4050 + {query_map[4050][0]} + {query_map[4050][1]} Select Expression - -> Sort (record length: 36, key length: 16) - -> Filter - -> Table "TEST" as "X15" Full Scan - Select Expression - -> Filter - -> Table "TEST" as "A15" Full Scan + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Filter + ................-> Table "TEST" as "X17" Full Scan + Select Expression + ....-> Filter + ........-> Table "TEST" as "A17" Full Scan + 4060 + {query_map[4060][0]} + {query_map[4060][1]} Select Expression - -> Refetch - -> Sort (record length: 36, key length: 16) - -> Filter - -> Table "TEST" as "X16" Full Scan + ....-> Sort (record length: 28, key length: 8) + ........-> Filter + ............-> Table "TEST" as "X18" Full Scan Select Expression - -> Filter - -> Table "TEST" as "A16" Full Scan + ....-> Filter + ........-> Table "TEST" as "A18" Full Scan + 4070 + {query_map[4070][0]} + {query_map[4070][1]} Select Expression - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Filter - -> Table "TEST" as "X17" Full Scan + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Filter + ................-> Table "TEST" as "R X" Full Scan Select Expression - -> Filter - -> Table "TEST" as "A17" Full Scan - + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Filter + ................-> Table "TEST" as "R X" Full Scan Select Expression - -> Sort (record length: 28, key length: 8) - -> Filter - -> Table "TEST" as "X18" Full Scan - Select Expression - -> Filter - -> Table "TEST" as "A18" Full Scan - - Select Expression - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Filter - -> Table "TEST" as "R X" Full Scan - Select Expression - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Filter - -> Table "TEST" as "R X" Full Scan - Select Expression - -> Recursion - -> Filter - -> Table "TEST" as "R A19" Full Scan - -> Filter - -> Table "TEST" as "R I" Full Scan + ....-> Recursion + ........-> Filter + ............-> Table "TEST" as "R A19" Full Scan + ........-> Filter + ............-> Table "TEST" as "R I" Full Scan + 5000 + {query_map[5000][0]} + {query_map[5000][1]} Select Expression - -> Sort (record length: 1052, key length: 8) - -> First N Records - -> Union - -> Table "TEST" as "V01 TEST" Full Scan - -> Table "RDB$DATABASE" as "V01 RDB$DATABASE" Full Scan + ....-> Sort (record length: 4044, key length: 8) + ........-> First N Records + ............-> Union + ................-> Table "TEST" as "V01 TEST" Full Scan + ................-> Table "RDB$DATABASE" as "V01 RDB$DATABASE" Full Scan + 6000 + {query_map[6000][0]} + {query_map[6000][1]} Select Expression - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Table "TEST" as "A21" Full Scan + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Table "TEST" as "A21" Full Scan + 6010 + {query_map[6010][0]} + {query_map[6010][1]} Select Expression - -> Sort (record length: 1036, key length: 8) - -> Table "TEST" as "A22" Full Scan + ....-> Sort (record length: 1036, key length: 8) + ........-> Table "TEST" as "A22" Full Scan + 7000 + {query_map[7000][0]} + {query_map[7000][1]} Select Expression - -> Refetch - -> Sort (record length: 44, key length: 24) - -> Table "TEST_NS_01" as "A23" Full Scan + ....-> Refetch + ........-> Sort (record length: 44, key length: 24) + ............-> Table "TEST_NS_01" as "A23" Full Scan + 7010 + {query_map[7010][0]} + {query_map[7010][1]} Select Expression - -> Sort (record length: 1052, key length: 24) - -> Table "TEST_NS_02" as "A24" Full Scan + ....-> Sort (record length: 1052, key length: 24) + ........-> Table "TEST_NS_02" as "A24" Full Scan + 7020 + {query_map[7020][0]} + {query_map[7020][1]} Select Expression - -> Refetch - -> Sort (record length: 36, key length: 12) - -> Table "TEST_NS_03" Full Scan + ....-> Refetch + ........-> Sort (record length: 36, key length: 12) + ............-> Table "TEST_NS_03" Full Scan + 7030 + {query_map[7030][0]} + {query_map[7030][1]} Select Expression - -> Sort (record length: 1036, key length: 12) - -> Table "TEST_NS_04" Full Scan + ....-> Sort (record length: 1036, key length: 12) + ........-> Table "TEST_NS_04" Full Scan + 7040 + {query_map[7040][0]} + {query_map[7040][1]} Select Expression - -> Refetch - -> Sort (record length: 36, key length: 12) - -> Table "TEST_NS_05" Full Scan + ....-> Refetch + ........-> Sort (record length: 36, key length: 12) + ............-> Table "TEST_NS_05" Full Scan + 7050 + {query_map[7050][0]} + {query_map[7050][1]} Select Expression - -> Sort (record length: 1036, key length: 12) - -> Table "TEST_NS_06" Full Scan - + ....-> Sort (record length: 1036, key length: 12) + ........-> Table "TEST_NS_06" Full Scan """ -fb5x_expected_out = """ - Select Expression - -> Sort (record length: 1036, key length: 8) - -> Table "TEST" as "A01" Full Scan - - Select Expression - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Table "TEST" as "A02" Full Scan - - Select Expression - -> First N Records - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Table "TEST" as "A03" Full Scan - - Select Expression - -> Sort (record length: 1036, key length: 8) - -> Table "TEST" Full Scan - - Select Expression - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Table "TEST" Full Scan - +############################################################################### + +fb5x_expected_out = f""" + 1000 + select txt_short from test a01 order by id + Must NOT use refetch because length of non-key column is less than threshold + Select Expression + ....-> Sort (record length: 1036, key length: 8) + ........-> Table "TEST" as "A01" Full Scan + 1010 + select txt_broad from test a02 order by id + MUST use refetch because length of non-key column is greater than threshold + Select Expression + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Table "TEST" as "A02" Full Scan + 1020 + select txt_short from test a03 order by id rows 1 + MUST use refetch regardless on length of column because ROWS presents + Select Expression + ....-> First N Records + ........-> Refetch + ............-> Sort (record length: 28, key length: 8) + ................-> Table "TEST" as "A03" Full Scan + 2000 + select id, computed_ts_dup from test order by id + Must NOT use refetch because computed column is based on txt_short with length < threshold + Select Expression + ....-> Sort (record length: 1036, key length: 8) + ........-> Table "TEST" Full Scan + 2010 + select id, computed_tb_dup from test order by id + MUST use refetch because computed column is based on txt_broad which has length >= threshold + Select Expression + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Table "TEST" Full Scan + 3000 + select id from test a04 where '' in (select txt_short from test x04 where txt_short = '' order by id) + *** not [yet] commented *** Sub-query (invariant) - -> Filter - -> Sort (record length: 1036, key length: 8) - -> Filter - -> Table "TEST" as "X04" Full Scan - Select Expression - -> Filter (preliminary) - -> Table "TEST" as "A04" Full Scan - + ....-> Filter + ........-> Sort (record length: 1036, key length: 8) + ............-> Filter + ................-> Table "TEST" as "X04" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "TEST" as "A04" Full Scan + 3010 + select id from test a05 where '' in (select txt_broad from test x05 where txt_broad = '' order by id) + *** not [yet] commented *** Sub-query (invariant) - -> Filter - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Filter - -> Table "TEST" as "X05" Full Scan - Select Expression - -> Filter (preliminary) - -> Table "TEST" as "A05" Full Scan - - Select Expression - -> Sort (record length: 1036, key length: 8) - -> Filter - -> Table "TEST" as "X06" Full Scan - Select Expression - -> Sort (record length: 1036, key length: 8) - -> Filter - -> Table "TEST" as "X06" Full Scan - Select Expression - -> Filter (preliminary) - -> Table "TEST" as "A06" Full Scan - - Select Expression - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Filter - -> Table "TEST" as "X07" Full Scan - Select Expression - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Filter - -> Table "TEST" as "X07" Full Scan - Select Expression - -> Filter (preliminary) - -> Table "TEST" as "A07" Full Scan - + ....-> Filter + ........-> Refetch + ............-> Sort (record length: 28, key length: 8) + ................-> Filter + ....................-> Table "TEST" as "X05" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "TEST" as "A05" Full Scan + 3020 + select id from test a06 where '' not in (select txt_short from test x06 where txt_short>'' order by id) + *** not [yet] commented *** Sub-query (invariant) - -> Filter - -> Sort (record length: 1036, key length: 8) - -> Filter - -> Table "TEST" as "X08" Full Scan - Select Expression - -> Filter (preliminary) - -> Table "TEST" as "A08" Full Scan - + ....-> Sort (record length: 1036, key length: 8) + ........-> Filter + ............-> Table "TEST" as "X06" Full Scan Sub-query (invariant) - -> Filter - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Filter - -> Table "TEST" as "X09" Full Scan - Select Expression - -> Filter (preliminary) - -> Table "TEST" as "A09" Full Scan - + ....-> Sort (record length: 1036, key length: 8) + ........-> Filter + ............-> Table "TEST" as "X06" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "TEST" as "A06" Full Scan + 3030 + select id from test a07 where '' not in (select txt_broad from test x07 where txt_broad>'' order by id) + *** not [yet] commented *** Sub-query (invariant) - -> Filter - -> Sort (record length: 1036, key length: 8) - -> Filter - -> Table "TEST" as "X10" Full Scan - Select Expression - -> Filter (preliminary) - -> Table "TEST" as "A10" Full Scan - + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Filter + ................-> Table "TEST" as "X07" Full Scan Sub-query (invariant) - -> Filter - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Filter - -> Table "TEST" as "X11" Full Scan - Select Expression - -> Filter (preliminary) - -> Table "TEST" as "A11" Full Scan - + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Filter + ................-> Table "TEST" as "X07" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "TEST" as "A07" Full Scan + 3040 + select id from test a08 where '' > all (select id from test x08 where txt_short>'' order by id) + *** not [yet] commented *** Sub-query (invariant) - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Filter - -> Table "TEST" as "X12" Full Scan - Select Expression - -> Filter (preliminary) - -> Table "TEST" as "A12" Full Scan - + ....-> Filter + ........-> Sort (record length: 1036, key length: 8) + ............-> Filter + ................-> Table "TEST" as "X08" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "TEST" as "A08" Full Scan + 3050 + select id from test a09 where '' > all (select id from test x09 where txt_broad>'' order by id) + *** not [yet] commented *** Sub-query (invariant) - -> Sort (record length: 28, key length: 8) - -> Filter - -> Table "TEST" as "X13" Full Scan - Select Expression - -> Filter (preliminary) - -> Table "TEST" as "A13" Full Scan - + ....-> Filter + ........-> Refetch + ............-> Sort (record length: 28, key length: 8) + ................-> Filter + ....................-> Table "TEST" as "X09" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "TEST" as "A09" Full Scan + 3060 + select id from test a10 where '' <> any (select id from test x10 where txt_short>'' order by id) + *** not [yet] commented *** Sub-query (invariant) - -> Refetch - -> Sort (record length: 36, key length: 12) - -> Filter - -> Table "TEST" as "X14" Full Scan - Select Expression - -> Filter (preliminary) - -> Table "TEST" as "A14" Full Scan - + ....-> Filter + ........-> Sort (record length: 1036, key length: 8) + ............-> Filter + ................-> Table "TEST" as "X10" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "TEST" as "A10" Full Scan + 3070 + select id from test a11 where '' <> any (select id from test x11 where txt_broad>'' order by id) + *** not [yet] commented *** Sub-query (invariant) - -> Sort (record length: 36, key length: 16) - -> Filter - -> Table "TEST" as "X15" Full Scan - Select Expression - -> Filter (preliminary) - -> Table "TEST" as "A15" Full Scan - + ....-> Filter + ........-> Refetch + ............-> Sort (record length: 28, key length: 8) + ................-> Filter + ....................-> Table "TEST" as "X11" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "TEST" as "A11" Full Scan + 4000 + select id,txt_short from test a12 where exists(select 1 from test x12 where txt_short>'' order by id) + MUST use refetch: column x12.txt_short not present in order by Sub-query (invariant) - -> Refetch - -> Sort (record length: 36, key length: 16) - -> Filter - -> Table "TEST" as "X16" Full Scan - Select Expression - -> Filter (preliminary) - -> Table "TEST" as "A16" Full Scan - + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Filter + ................-> Table "TEST" as "X12" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "TEST" as "A12" Full Scan + 4010 + select id,txt_short from test a13 where exists(select 1 from test x13 where computed_id_dup > 0 order by id) + Must NOT use refetch: ORDER BY list contains the single element: ID, and it is base for x13.computed_id_dup column Sub-query (invariant) - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Filter - -> Table "TEST" as "X17" Full Scan - Select Expression - -> Filter (preliminary) - -> Table "TEST" as "A17" Full Scan - + ....-> Sort (record length: 28, key length: 8) + ........-> Filter + ............-> Table "TEST" as "X13" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "TEST" as "A13" Full Scan + 4020 + select id,txt_short from test a14 where exists(select 1 from test x14 where computed_id_dup > 0 order by computed_id_dup) + MUST use refetch! See letter from dimitr 28.12.2020 14:49 + Sort procedure will get: + a KEY = result of evaluating 'computed_id_dup'; + a VAL = value of the field 'ID' which is base for computing 'computed_id_dup' + Thus sorter will have a field which not equals to a key, which leads to refetch. + Sub-query (invariant) + ....-> Refetch + ........-> Sort (record length: 36, key length: 12) + ............-> Filter + ................-> Table "TEST" as "X14" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "TEST" as "A14" Full Scan + 4030 + select id,txt_short from test a15 where exists(select 1 from test x15 where f02>0 and f01>0 order by f01, f02) + Must NOT use refetch: all persistent columns from WHERE expression (f01, f02) belong to ORDER BY list + Sub-query (invariant) + ....-> Sort (record length: 36, key length: 16) + ........-> Filter + ............-> Table "TEST" as "X15" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "TEST" as "A15" Full Scan + 4040 + select id,txt_short from test a16 where exists(select 1 from test x16 where id>0 and f01>0 order by f01, f02) + Must use refetch: one of columns from WHERE expr (id) does not belong to ORDER BY list Sub-query (invariant) - -> Sort (record length: 28, key length: 8) - -> Filter - -> Table "TEST" as "X18" Full Scan + ....-> Refetch + ........-> Sort (record length: 36, key length: 16) + ............-> Filter + ................-> Table "TEST" as "X16" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "TEST" as "A16" Full Scan + 4050 + select id,txt_short from test a17 where exists(select 1 from test x17 where computed_id_dup > 0 order by f01) + Must use refetch: computed column in WHERE expr does not belong to ORDER BY list + Sub-query (invariant) + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Filter + ................-> Table "TEST" as "X17" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "TEST" as "A17" Full Scan + 4060 + select id,txt_short from test a18 where exists(select 1 from test x18 where computed_guid > '' order by f01) + Must NOT use refetch: computed column x18.computed_guid does is evaluated via GUID and does not refer to any columns + Sub-query (invariant) + ....-> Sort (record length: 28, key length: 8) + ........-> Filter + ............-> Table "TEST" as "X18" Full Scan Select Expression - -> Filter (preliminary) - -> Table "TEST" as "A18" Full Scan - + ....-> Filter (preliminary) + ........-> Table "TEST" as "A18" Full Scan + 4070 + with recursive + r as ( + select a19.id, a19.txt_short + from test a19 + where not exists(select * from test x where x.txt_short < a19.txt_short order by id) + UNION ALL + select i.id, i.txt_short + from test i + join r on i.id > r.id + and not exists( select * from test x where x.txt_short between r.txt_short and i.txt_short order by id ) + ) + select * from r + MUST use refetch both in anchor and recursive parts Sub-query - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Filter - -> Table "TEST" as "R X" Full Scan + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Filter + ................-> Table "TEST" as "R X" Full Scan Sub-query - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Filter - -> Table "TEST" as "R X" Full Scan - Select Expression - -> Recursion - -> Filter - -> Table "TEST" as "R A19" Full Scan - -> Filter - -> Table "TEST" as "R I" Full Scan - - Select Expression - -> Sort (record length: 1052, key length: 8) - -> First N Records - -> Union - -> Table "TEST" as "V01 TEST" Full Scan - -> Table "RDB$DATABASE" as "V01 RDB$DATABASE" Full Scan - - Select Expression - -> Refetch - -> Sort (record length: 28, key length: 8) - -> Table "TEST" as "A21" Full Scan - - Select Expression - -> Sort (record length: 1036, key length: 8) - -> Table "TEST" as "A22" Full Scan + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Filter + ................-> Table "TEST" as "R X" Full Scan + Select Expression + ....-> Recursion + ........-> Filter + ............-> Table "TEST" as "R A19" Full Scan + ........-> Filter + ............-> Table "TEST" as "R I" Full Scan + 5000 + select txt_broad from v_unioned v01 order by id + Must NOT use refetch because view DDL includes UNION + Select Expression + ....-> Sort (record length: 4044, key length: 8) + ........-> First N Records + ............-> Union + ................-> Table "TEST" as "V01 TEST" Full Scan + ................-> Table "RDB$DATABASE" as "V01 RDB$DATABASE" Full Scan + 6000 + select left(txt_broad, 50) as txt from test a21 order by id + MUST use refetch because expression is based on column which has length >= threshold + (even if final length of expression result is much less than threshold) + Select Expression + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Table "TEST" as "A21" Full Scan + 6010 + select left( txt_short || txt_short, 2000) as txt from test a22 order by id + Must NOT use refetch because expression is based on column which has length < threshold + (even if final length of expression result is much bigger than threshold) + Select Expression + ....-> Sort (record length: 1036, key length: 8) + ........-> Table "TEST" as "A22" Full Scan + 7000 + select * from test_ns_01 a23 order by id + MUST use refetch + Select Expression + ....-> Refetch + ........-> Sort (record length: 44, key length: 24) + ............-> Table "TEST_NS_01" as "A23" Full Scan + 7010 + select * from test_ns_02 a24 order by id + Must NOT refetch + Select Expression + ....-> Sort (record length: 1052, key length: 24) + ........-> Table "TEST_NS_02" as "A24" Full Scan + 7020 + select * from test_ns_03 order by id + MUST use refetch + Select Expression + ....-> Refetch + ........-> Sort (record length: 36, key length: 12) + ............-> Table "TEST_NS_03" Full Scan + 7030 + select * from test_ns_04 order by id + Must NOT use refetch + Select Expression + ....-> Sort (record length: 1036, key length: 12) + ........-> Table "TEST_NS_04" Full Scan + 7040 + select * from test_ns_05 order by id + MUST use refetch + Select Expression + ....-> Refetch + ........-> Sort (record length: 36, key length: 12) + ............-> Table "TEST_NS_05" Full Scan + 7050 + select * from test_ns_06 order by id + Must NOT use refetch + Select Expression + ....-> Sort (record length: 1036, key length: 12) + ........-> Table "TEST_NS_06" Full Scan +""" - Select Expression - -> Refetch - -> Sort (record length: 44, key length: 24) - -> Table "TEST_NS_01" as "A23" Full Scan - Select Expression - -> Sort (record length: 1052, key length: 24) - -> Table "TEST_NS_02" as "A24" Full Scan +############################################################################### + +fb6x_expected_out = f""" + 1000 + {query_map[1000][0]} + {query_map[1000][1]} + Select Expression + ....-> Sort (record length: 1036, key length: 8) + ........-> Table "PUBLIC"."TEST" as "A01" Full Scan + 1010 + {query_map[1010][0]} + {query_map[1010][1]} + Select Expression + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Table "PUBLIC"."TEST" as "A02" Full Scan + 1020 + {query_map[1020][0]} + {query_map[1020][1]} + Select Expression + ....-> First N Records + ........-> Refetch + ............-> Sort (record length: 28, key length: 8) + ................-> Table "PUBLIC"."TEST" as "A03" Full Scan + 2000 + {query_map[2000][0]} + {query_map[2000][1]} + Select Expression + ....-> Sort (record length: 1036, key length: 8) + ........-> Table "PUBLIC"."TEST" Full Scan + 2010 + {query_map[2010][0]} + {query_map[2010][1]} + Select Expression + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Table "PUBLIC"."TEST" Full Scan + 3000 + {query_map[3000][0]} + {query_map[3000][1]} + Sub-query (invariant) + ....-> Filter + ........-> Sort (record length: 1036, key length: 8) + ............-> Filter + ................-> Table "PUBLIC"."TEST" as "X04" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "PUBLIC"."TEST" as "A04" Full Scan + 3010 + {query_map[3010][0]} + {query_map[3010][1]} + Sub-query (invariant) + ....-> Filter + ........-> Refetch + ............-> Sort (record length: 28, key length: 8) + ................-> Filter + ....................-> Table "PUBLIC"."TEST" as "X05" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "PUBLIC"."TEST" as "A05" Full Scan + 3020 + {query_map[3020][0]} + {query_map[3020][1]} + Sub-query (invariant) + ....-> Sort (record length: 1036, key length: 8) + ........-> Filter + ............-> Table "PUBLIC"."TEST" as "X06" Full Scan + Sub-query (invariant) + ....-> Sort (record length: 1036, key length: 8) + ........-> Filter + ............-> Table "PUBLIC"."TEST" as "X06" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "PUBLIC"."TEST" as "A06" Full Scan + 3030 + {query_map[3030][0]} + {query_map[3030][1]} + Sub-query (invariant) + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Filter + ................-> Table "PUBLIC"."TEST" as "X07" Full Scan + Sub-query (invariant) + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Filter + ................-> Table "PUBLIC"."TEST" as "X07" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "PUBLIC"."TEST" as "A07" Full Scan + 3040 + {query_map[3040][0]} + {query_map[3040][1]} + Sub-query (invariant) + ....-> Filter + ........-> Sort (record length: 1036, key length: 8) + ............-> Filter + ................-> Table "PUBLIC"."TEST" as "X08" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "PUBLIC"."TEST" as "A08" Full Scan + 3050 + {query_map[3050][0]} + {query_map[3050][1]} + Sub-query (invariant) + ....-> Filter + ........-> Refetch + ............-> Sort (record length: 28, key length: 8) + ................-> Filter + ....................-> Table "PUBLIC"."TEST" as "X09" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "PUBLIC"."TEST" as "A09" Full Scan + 3060 + {query_map[3060][0]} + {query_map[3060][1]} + Sub-query (invariant) + ....-> Filter + ........-> Sort (record length: 1036, key length: 8) + ............-> Filter + ................-> Table "PUBLIC"."TEST" as "X10" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "PUBLIC"."TEST" as "A10" Full Scan + 3070 + {query_map[3070][0]} + {query_map[3070][1]} + Sub-query (invariant) + ....-> Filter + ........-> Refetch + ............-> Sort (record length: 28, key length: 8) + ................-> Filter + ....................-> Table "PUBLIC"."TEST" as "X11" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "PUBLIC"."TEST" as "A11" Full Scan + 4000 + {query_map[4000][0]} + {query_map[4000][1]} + Sub-query (invariant) + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Filter + ................-> Table "PUBLIC"."TEST" as "X12" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "PUBLIC"."TEST" as "A12" Full Scan + 4010 + {query_map[4010][0]} + {query_map[4010][1]} + Sub-query (invariant) + ....-> Sort (record length: 28, key length: 8) + ........-> Filter + ............-> Table "PUBLIC"."TEST" as "X13" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "PUBLIC"."TEST" as "A13" Full Scan + 4020 + {query_map[4020][0]} + {query_map[4020][1]} + Sub-query (invariant) + ....-> Refetch + ........-> Sort (record length: 36, key length: 12) + ............-> Filter + ................-> Table "PUBLIC"."TEST" as "X14" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "PUBLIC"."TEST" as "A14" Full Scan + 4030 + {query_map[4030][0]} + {query_map[4030][1]} + Sub-query (invariant) + ....-> Sort (record length: 36, key length: 16) + ........-> Filter + ............-> Table "PUBLIC"."TEST" as "X15" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "PUBLIC"."TEST" as "A15" Full Scan + 4040 + {query_map[4040][0]} + {query_map[4040][1]} + Sub-query (invariant) + ....-> Refetch + ........-> Sort (record length: 36, key length: 16) + ............-> Filter + ................-> Table "PUBLIC"."TEST" as "X16" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "PUBLIC"."TEST" as "A16" Full Scan + 4050 + {query_map[4050][0]} + {query_map[4050][1]} + Sub-query (invariant) + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Filter + ................-> Table "PUBLIC"."TEST" as "X17" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "PUBLIC"."TEST" as "A17" Full Scan + 4060 + {query_map[4060][0]} + {query_map[4060][1]} + Sub-query (invariant) + ....-> Sort (record length: 28, key length: 8) + ........-> Filter + ............-> Table "PUBLIC"."TEST" as "X18" Full Scan + Select Expression + ....-> Filter (preliminary) + ........-> Table "PUBLIC"."TEST" as "A18" Full Scan + 4070 + {query_map[4070][0]} + {query_map[4070][1]} + Sub-query + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Filter + ................-> Table "PUBLIC"."TEST" as "R" "X" Full Scan + Sub-query + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Filter + ................-> Table "PUBLIC"."TEST" as "R" "X" Full Scan + Select Expression + ....-> Recursion + ........-> Filter + ............-> Table "PUBLIC"."TEST" as "R" "A19" Full Scan + ........-> Filter + ............-> Table "PUBLIC"."TEST" as "R" "I" Full Scan + 5000 + {query_map[5000][0]} + {query_map[5000][1]} + Select Expression + ....-> Sort (record length: 4044, key length: 8) + ........-> First N Records + ............-> Union + ................-> Table "PUBLIC"."TEST" as "V01" "PUBLIC"."TEST" Full Scan + ................-> Table "SYSTEM"."RDB$DATABASE" as "V01" "SYSTEM"."RDB$DATABASE" Full Scan + 6000 + {query_map[6000][0]} + {query_map[6000][1]} + Select Expression + ....-> Refetch + ........-> Sort (record length: 28, key length: 8) + ............-> Table "PUBLIC"."TEST" as "A21" Full Scan + 6010 + {query_map[6010][0]} + {query_map[6010][1]} + Select Expression + ....-> Sort (record length: 1036, key length: 8) + ........-> Table "PUBLIC"."TEST" as "A22" Full Scan + 7000 + {query_map[7000][0]} + {query_map[7000][1]} + Select Expression + ....-> Refetch + ........-> Sort (record length: 44, key length: 24) + ............-> Table "PUBLIC"."TEST_NS_01" as "A23" Full Scan + 7010 + {query_map[7010][0]} + {query_map[7010][1]} + Select Expression + ....-> Sort (record length: 1052, key length: 24) + ........-> Table "PUBLIC"."TEST_NS_02" as "A24" Full Scan + 7020 + {query_map[7020][0]} + {query_map[7020][1]} + Select Expression + ....-> Refetch + ........-> Sort (record length: 36, key length: 12) + ............-> Table "PUBLIC"."TEST_NS_03" Full Scan + 7030 + {query_map[7030][0]} + {query_map[7030][1]} + Select Expression + ....-> Sort (record length: 1036, key length: 12) + ........-> Table "PUBLIC"."TEST_NS_04" Full Scan + 7040 + {query_map[7040][0]} + {query_map[7040][1]} + Select Expression + ....-> Refetch + ........-> Sort (record length: 36, key length: 12) + ............-> Table "PUBLIC"."TEST_NS_05" Full Scan + 7050 + {query_map[7050][0]} + {query_map[7050][1]} + Select Expression + ....-> Sort (record length: 1036, key length: 12) + ........-> Table "PUBLIC"."TEST_NS_06" Full Scan +""" - Select Expression - -> Refetch - -> Sort (record length: 36, key length: 12) - -> Table "TEST_NS_03" Full Scan +act = python_act('db') - Select Expression - -> Sort (record length: 1036, key length: 12) - -> Table "TEST_NS_04" Full Scan +#----------------------------------------------------------- - Select Expression - -> Refetch - -> Sort (record length: 36, key length: 12) - -> Table "TEST_NS_05" Full Scan +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped - Select Expression - -> Sort (record length: 1036, key length: 12) - -> Table "TEST_NS_06" Full Scan -""" +#----------------------------------------------------------- @pytest.mark.version('>=4.0') -def test_1(act: Action): - act.expected_stdout = fb4x_expected_out if act.is_version('<5') else fb5x_expected_out - act.execute() +def test_1(act: Action, capsys): + with act.db.connect() as con: + + # 13.01.2025: test will FAIL if config parameter OptimizeForFirstRows differs from default value (i.e. is set to true). + # To prevent this, we have to explicitly change appropriate session-level value: + if act.is_version('<5'): + pass + else: + con.execute_immediate('set optimize for all rows') + + cur = con.cursor() + for q_idx, q_tuple in query_map.items(): + test_sql, qry_comment = q_tuple[:2] + ps = cur.prepare(test_sql) + print(q_idx) + print(test_sql) + print(qry_comment) + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + ps.free() + + act.expected_stdout = fb4x_expected_out if act.is_version('<5') else fb5x_expected_out if act.is_version('<6') else fb6x_expected_out + act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_2651_test.py b/tests/bugs/core_2651_test.py index 05acc294..61c0a329 100644 --- a/tests/bugs/core_2651_test.py +++ b/tests/bugs/core_2651_test.py @@ -22,6 +22,7 @@ act = python_act('db') +@pytest.mark.intl @pytest.mark.version('>=3') def test_1(act: Action): with act.db.connect(charset='CP943C') as con: diff --git a/tests/bugs/core_2668_test.py b/tests/bugs/core_2668_test.py index f922eaac..e98a8921 100644 --- a/tests/bugs/core_2668_test.py +++ b/tests/bugs/core_2668_test.py @@ -63,6 +63,7 @@ tmp_script = temp_file('work_script.sql') +@pytest.mark.es_eds @pytest.mark.version('>=3') def test_1(act: Action, tmp_script: Path): tmp_script.write_text(test_script) diff --git a/tests/bugs/core_2678_test.py b/tests/bugs/core_2678_test.py index 73d08c6d..f0364495 100644 --- a/tests/bugs/core_2678_test.py +++ b/tests/bugs/core_2678_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-2678 FBTEST: bugs.core_2678 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -52,15 +58,19 @@ d1.c3, d2.c3; """ + act = isql_act('db', test_script) -expected_stdout = """ +expected_stdout_5x = """ PLAN SORT (JOIN (JOIN (D2 NATURAL, D1 INDEX (IDX_TD_DATA1)), JOIN (D1 NATURAL, D2 INDEX (IDX_TD_DATA2)))) """ -@pytest.mark.version('>=3.0') +expected_stdout_6x = """ + PLAN SORT (JOIN (JOIN ("D2" NATURAL, "D1" INDEX ("PUBLIC"."IDX_TD_DATA1")), JOIN ("D1" NATURAL, "D2" INDEX ("PUBLIC"."IDX_TD_DATA2")))) +""" + +@pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_2699_test.py b/tests/bugs/core_2699_test.py index a741bbc5..8c8979cf 100644 --- a/tests/bugs/core_2699_test.py +++ b/tests/bugs/core_2699_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-2699 FBTEST: bugs.core_2699 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -15,26 +21,33 @@ db_1 = db_factory() test_script = """ - with x as ( + with cte_alias as ( select 1 n from rdb$database ) - select * from x(10); + select * from cte_alias(10); """ act = isql_act('db_1', test_script, substitutions=[('-At line.*', '')]) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 42000 Dynamic SQL Error -SQL error code = -204 -Procedure unknown - -X + -CTE_ALIAS -At line 4, column 15 """ -@pytest.mark.version('>=3.0') -def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -204 + -Procedure unknown + -"CTE_ALIAS" +""" +@pytest.mark.version('>=3') +def test_1(act: Action): + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_2712_test.py b/tests/bugs/core_2712_test.py index 75a20cae..f86a1f2e 100644 --- a/tests/bugs/core_2712_test.py +++ b/tests/bugs/core_2712_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-2712 FBTEST: bugs.core_2712 +NOTES: + [26.06.2025] pzotov + Re-implemented via try/except and check show exception data. + Suppressing quotes around `id <...>` as irrelevant to this test. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -15,21 +21,32 @@ db = db_factory() -act = python_act('db', substitutions=[('table id [0-9]+ is not defined', 'table is not defined')]) +act = python_act('db', substitutions=[('table (")?id \\d+(")? is not defined', 'table is not defined')]) @pytest.mark.version('>=3.0') def test_1(act: Action, capsys): - with act.db.connect() as att1: - cur1 = att1.cursor() + with act.db.connect() as con: + cur1 = con.cursor() cur1.execute("recreate table test(x int)") - att1.commit() + con.commit() cur1.execute("insert into test values(1)") - att1.commit() - with act.db.connect() as att2: - cur2 = att2.cursor() - cur2.execute("select 1 from rdb$database") - - cur1.execute("drop table test") - with pytest.raises(DatabaseError, match='.*table id [0-9]+ is not defined.*'): + con.commit() + try: + with act.db.connect() as con2: + cur2 = con2.cursor() + cur2.execute("select 1 from rdb$database") + cur1.execute("drop table test") cur2.prepare("update test set x=-x") - att2.commit() + con2.commit() + except Exception as e: + print(e.__str__()) + for x in e.gds_codes: + print(x) + + act.expected_stdout = """ + table is not defined + 335544395 + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() diff --git a/tests/bugs/core_2731_test.py b/tests/bugs/core_2731_test.py index e0fd9efb..091e369c 100644 --- a/tests/bugs/core_2731_test.py +++ b/tests/bugs/core_2731_test.py @@ -85,6 +85,7 @@ -At block line: 5, col: 5 """ +@pytest.mark.es_eds @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stderr = expected_stderr diff --git a/tests/bugs/core_2735_test.py b/tests/bugs/core_2735_test.py index 84cba4f0..7e301297 100644 --- a/tests/bugs/core_2735_test.py +++ b/tests/bugs/core_2735_test.py @@ -7,33 +7,50 @@ DESCRIPTION: JIRA: CORE-2735 FBTEST: bugs.core_2735 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """set term ^; -create procedure p returns(a int) as begin a = 9; suspend; end^ -create view vp1 as select a from p^ -set term ;^ -COMMIT;""" +init_script = """ + set term ^; + create procedure sp_test returns(o_result int) as begin o_result = 9; suspend; end^ + create view vp1 as select o_result from sp_test^ + set term ;^ + commit; +""" db = db_factory(charset='UTF8', init=init_script) -test_script = """show view vp1; +test_script = """ + show view vp1; """ -act = isql_act('db', test_script) +substitutions = [('==.*', ''), ('[ \t]+', ' ')] + +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """A INTEGER Nullable -View Source: -==== ====== - select a from p +expected_stdout_5x = """ + O_RESULT INTEGER Nullable + View Source: + select o_result from sp_test """ -@pytest.mark.version('>=2.5.0') +expected_stdout_6x = """ + View: PUBLIC.VP1 + O_RESULT INTEGER Nullable + View Source: + select o_result from sp_test +""" + +@pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_2741_test.py b/tests/bugs/core_2741_test.py index 4a622855..ec590604 100644 --- a/tests/bugs/core_2741_test.py +++ b/tests/bugs/core_2741_test.py @@ -22,18 +22,26 @@ show domain dm_dts; """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' ')] -expected_stdout = """ - DM_INT INTEGER Nullable - cHeCk(vAlUE<>0) - DM_DTS TIMESTAMP Nullable - cHeCk(valUe<>cUrrent_timEstamp) +act = isql_act('db', test_script, substitutions = substitutions) + +expected_stdout_5x = """ + DM_INT INTEGER Nullable + cHeCk(vAlUE<>0) + DM_DTS TIMESTAMP Nullable + cHeCk(valUe<>cUrrent_timEstamp) +""" + +expected_stdout_6x = """ + PUBLIC.DM_INT INTEGER Nullable + cHeCk(vAlUE<>0) + PUBLIC.DM_DTS TIMESTAMP Nullable + cHeCk(valUe<>cUrrent_timEstamp) """ @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_2765_test.py b/tests/bugs/core_2765_test.py index bfcc96a5..c095b322 100644 --- a/tests/bugs/core_2765_test.py +++ b/tests/bugs/core_2765_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-2765 FBTEST: bugs.core_2765 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -111,6 +117,8 @@ def test_1(act_1: Action, user_admin: User, user_work1: User, user_work2: User): assert (act_1.clean_stderr == act_1.clean_expected_stderr and act_1.clean_stdout == act_1.clean_expected_stdout) +################################################################################# + # version: 4.0 test_script_2 = """ @@ -162,44 +170,62 @@ def test_1(act_1: Action, user_admin: User, user_work1: User, user_work2: User): act_2 = isql_act('db', test_script_2, substitutions=substitutions) -expected_stdout_2 = """ - WHO_AM_I TMP$C2765_ADMIN - WHAT_IS_MY_ROLE RDB$ADMIN - - WHO_AM_I TMP$C2765_WORKER1 - WHAT_IS_MY_ROLE NONE - ID 1 - X 1000 - - Records affected: 1 - Records affected: 1 - Records affected: 1 - Records affected: 1 - - WHO_AM_I TMP$C2765_ADMIN - WHAT_IS_MY_ROLE NONE - - WHO_AM_I TMP$C2765_WORKER2 - WHAT_IS_MY_ROLE NONE - -""" - -expected_stderr_2 = """ - Statement failed, SQLSTATE = 42000 - unsuccessful metadata update - -GRANT failed - -no SELECT privilege with grant option on table/view TEST - Statement failed, SQLSTATE = 28000 - no permission for SELECT access to TABLE TEST - -Effective user is TMP$C2765_WORKER2 -""" - - @pytest.mark.version('>=4.0') def test_2(act_2: Action, user_admin: User, user_work1: User, user_work2: User): - act_2.expected_stdout = expected_stdout_2 - act_2.expected_stderr = expected_stderr_2 - act_2.execute() - assert (act_2.clean_stderr == act_2.clean_expected_stderr and - act_2.clean_stdout == act_2.clean_expected_stdout) + + expected_stdout_5x = f""" + WHO_AM_I {user_admin.name.upper()} + WHAT_IS_MY_ROLE RDB$ADMIN + WHO_AM_I {user_work1.name.upper()} + WHAT_IS_MY_ROLE NONE + ID 1 + X 1000 + Records affected: 1 + Records affected: 1 + Records affected: 1 + Records affected: 1 + WHO_AM_I {user_admin.name.upper()} + WHAT_IS_MY_ROLE NONE + + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -GRANT failed + -no SELECT privilege with grant option on table/view TEST + + WHO_AM_I {user_work2.name.upper()} + WHAT_IS_MY_ROLE NONE + Statement failed, SQLSTATE = 28000 + no permission for SELECT access to TABLE TEST + -Effective user is {user_work2.name.upper()} + """ + + expected_stdout_6x = f""" + WHO_AM_I {user_admin.name.upper()} + WHAT_IS_MY_ROLE RDB$ADMIN + WHO_AM_I {user_work1.name.upper()} + WHAT_IS_MY_ROLE NONE + ID 1 + X 1000 + Records affected: 1 + Records affected: 1 + Records affected: 1 + Records affected: 1 + WHO_AM_I {user_admin.name.upper()} + WHAT_IS_MY_ROLE NONE + + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -GRANT failed + -no SELECT privilege with grant option on table/view "PUBLIC"."TEST" + + WHO_AM_I {user_work2.name.upper()} + WHAT_IS_MY_ROLE NONE + Statement failed, SQLSTATE = 28000 + no permission for SELECT access to TABLE "PUBLIC"."TEST" + -Effective user is {user_work2.name.upper()} + """ + + act_2.expected_stdout = expected_stdout_5x if act_2.is_version('<6') else expected_stdout_6x + act_2.execute(combine_output = True) + assert act_2.clean_stdout == act_2.clean_expected_stdout diff --git a/tests/bugs/core_2780_test.py b/tests/bugs/core_2780_test.py index 3cf60680..0db50555 100644 --- a/tests/bugs/core_2780_test.py +++ b/tests/bugs/core_2780_test.py @@ -30,7 +30,7 @@ -- Sufixes: 'Firebird' followed by space and at least one digit. ,iif( cast(mon$client_version as varchar(255) character set utf8) collate unicode_ci similar to - '(WI|LI|UI|UP|SI|SO|HU)[-](T|V){0,1}[0-9]+.[0-9]+.[0-9]+((.?[0-9]+)*)[[:WHITESPACE:]]+firebird[[:WHITESPACE:]]+[0-9]+((.?[0-9]+)*)%', 1, 0) is_client_version_valid + '(WI|LI|UI|UP|SI|SO|HU)[-](T|V){0,1}[0-9]+.[0-9]+.[0-9]+((.?[0-9]+)*)([-]dev)?[[:WHITESPACE:]]+firebird[[:WHITESPACE:]]+[0-9]+((.?[0-9]+)*)%', 1, 0) is_client_version_valid from mon$attachments where mon$attachment_id = current_connection; """ diff --git a/tests/bugs/core_2798_test.py b/tests/bugs/core_2798_test.py index 916dc2d2..f4668c14 100644 --- a/tests/bugs/core_2798_test.py +++ b/tests/bugs/core_2798_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-2798 FBTEST: bugs.core_2798 +NOTES: + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -31,13 +37,16 @@ act = isql_act('db', test_script) -expected_stdout = """ - PLAN JOIN (V P1 NATURAL, V P2 NATURAL, V T1 NATURAL, V T2 NATURAL) +expected_stdout_5x = """ + PLAN JOIN (V P1 NATURAL, V P2 NATURAL, V T1 NATURAL, V T2 NATURAL) +""" + +expected_stdout_6x = """ + PLAN JOIN ("PUBLIC"."V" "PUBLIC"."P1" NATURAL, "PUBLIC"."V" "P2" NATURAL, "PUBLIC"."V" "PUBLIC"."T1" NATURAL, "PUBLIC"."V" "T2" NATURAL) """ @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_2806_test.py b/tests/bugs/core_2806_test.py index 1c43f3ea..7b1e7cf9 100644 --- a/tests/bugs/core_2806_test.py +++ b/tests/bugs/core_2806_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-2806 FBTEST: bugs.core_2806 +NOTES: + [27.06.2025] pzotov + Reimplemented. No need to use 'SHOW VIEW' in this test. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -14,24 +19,34 @@ db = db_factory(charset='UTF8') -test_script = """set term ^; -create procedure p returns(rc int) as begin rc = 1; suspend; end^ -create view v2(dosrc) as select rc * 2 from p^ -commit ^ -show view v2^ +VIEW_DDL = 'select rc * 2 from sp_test' +test_script = f""" + set list on; + set blob all; + set term ^; + create procedure sp_test returns(rc int) as + begin + rc = 1; + suspend; + end^ + set term ;^ + create view v_test(double_rc) as {VIEW_DDL}; + commit; + select r.rdb$view_source as blob_id from rdb$relations r where r.rdb$relation_name = upper('v_test'); + select * from v_test; """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' '), ('BLOB_ID.*', '')] + +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """DOSRC BIGINT Expression -View Source: -==== ====== - select rc * 2 from p +expected_stdout = f""" + {VIEW_DDL} + DOUBLE_RC 2 """ -@pytest.mark.version('>=3.0') +@pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_2812_test.py b/tests/bugs/core_2812_test.py index c1108c82..1f079129 100644 --- a/tests/bugs/core_2812_test.py +++ b/tests/bugs/core_2812_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-2812 FBTEST: bugs.core_2812 +NOTES: + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -36,8 +42,6 @@ recreate table test(x int); insert into test values(1); commit; - - """ db = db_factory(init=init_script) @@ -121,53 +125,80 @@ """ -act = isql_act('db', test_script, substitutions=[('=.*', ''), ('-At line.*', '')]) +substitutions = [('[ \t]+', ' '), ('-At line.*', '')] -expected_stdout = """ +act = isql_act('db', test_script, substitutions = substitutions) + +expected_stdout_5x = """ + Statement failed, SQLSTATE = 42S22 + Dynamic SQL Error + -SQL error code = -206 + -Column unknown + -L.ID + -At line 11, column 33 MSG case-2 A_ID 111 MID_ID 1 B_ID 111 VAL 0 - MSG case-2 A_ID 999 MID_ID 1 B_ID 999 VAL 123456789 - + Statement failed, SQLSTATE = 42S22 + Dynamic SQL Error + -SQL error code = -206 + -Column unknown + -A.ID + -At line 4, column 24 MSG case-4 ID 1 - Z1 1 - + Statement failed, SQLSTATE = 42S22 + Dynamic SQL Error + -SQL error code = -206 + -Column unknown + -S.X + -At line 11, column 29 """ -expected_stderr = """ +expected_stdout_6x = """ Statement failed, SQLSTATE = 42S22 Dynamic SQL Error -SQL error code = -206 -Column unknown - -L.ID - + -"L"."ID" + -At line 11, column 33 + MSG case-2 + A_ID 111 + MID_ID 1 + B_ID 111 + VAL 0 + MSG case-2 + A_ID 999 + MID_ID 1 + B_ID 999 + VAL 123456789 Statement failed, SQLSTATE = 42S22 Dynamic SQL Error -SQL error code = -206 -Column unknown - -A.ID - + -"A"."ID" + -At line 5, column 24 + MSG case-4 + ID 1 + Z1 1 Statement failed, SQLSTATE = 42S22 Dynamic SQL Error - -SQL error code + -SQL error code = -206 -Column unknown - -S.X + -"S"."X" + -At line 12, column 29 """ -@pytest.mark.version('>=3.0') +@pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_2826_test.py b/tests/bugs/core_2826_test.py index 6786ac8c..e1fa29e9 100644 --- a/tests/bugs/core_2826_test.py +++ b/tests/bugs/core_2826_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-2826 FBTEST: bugs.core_2826 +NOTES: + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -51,23 +57,34 @@ -- 'show table' was removed, see CORE-4782 ("Command `SHOW TABLE` fails..." - reproduced on Windows builds 2.5 and 3.0 only) """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' '), ('-At line.*', '')] + +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ +expected_stdout_5x = """ PLAN (T1 NATURAL) - K1 ap - K2 123 + K1 ap + K2 123 K3 - PLAN (T1 INDEX (TXT1_NOPAD_PK)) - K1 ap - K2 123 + K1 ap + K2 123 + K3 +""" + +expected_stdout_6x = """ + PLAN ("T1" NATURAL) + K1 ap + K2 123 + K3 + PLAN ("T1" INDEX ("PUBLIC"."TXT1_NOPAD_PK")) + K1 ap + K2 123 K3 """ @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_2835_test.py b/tests/bugs/core_2835_test.py index 40fcefb5..da3afd46 100644 --- a/tests/bugs/core_2835_test.py +++ b/tests/bugs/core_2835_test.py @@ -7,12 +7,18 @@ DESCRIPTION: JIRA: CORE-2835 FBTEST: bugs.core_2835 +NOTES: + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -db_1 = db_factory() +db = db_factory() test_script = """ create table net_net_device( @@ -75,15 +81,19 @@ and t0_dep.id=t3_nd_dependantdevices_relation.secondary_devid; """ -act = isql_act('db_1', test_script) -expected_stdout = """ +act = isql_act('db', test_script) + +expected_stdout_5x = """ PLAN SORT (JOIN (T1_ND INDEX (PK_NET_NET_DEVICE), T3_ND_DEPENDANTDEVICES_RELATION INDEX (FK_NET_DEV_INTERCONNECTION_001), T0_DEP INDEX (PK_NET_NET_DEVICE))) """ +expected_stdout_6x = """ + PLAN SORT (JOIN ("T1_ND" INDEX ("PUBLIC"."PK_NET_NET_DEVICE"), "T3_ND_DEPENDANTDEVICES_RELATION" INDEX ("PUBLIC"."FK_NET_DEV_INTERCONNECTION_001"), "T0_DEP" INDEX ("PUBLIC"."PK_NET_NET_DEVICE"))) +""" + @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_2881_test.py b/tests/bugs/core_2881_test.py index e071b53b..37ecb728 100644 --- a/tests/bugs/core_2881_test.py +++ b/tests/bugs/core_2881_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-2881 FBTEST: bugs.core_2881 +NOTES: + [27.06.2025] pzotov + Re-implemented: use variables to store scema prefix (since 6.0.0.834), packages header and body. + Use f-notations to substitute variable values in the test script and expected_out. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -14,104 +20,90 @@ db = db_factory() -test_script = """ - set term ^; - create or alter package p1 - as - begin - function f(x int) returns int; - procedure p(x int) returns(y int); - end - ^ - create package body p1 - as - begin - function f(x int) returns int as +act = isql_act('db') + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'PUBLIC.' + PKG_HEAD_1 = """ begin - return 22*x; + function f(x int) returns int; + procedure p(x int) returns(y int); end - procedure p(x int) returns(y int) as + """ + PKG_BODY_1 = """ begin - y = 33*x; - suspend; + function f(x int) returns int as + begin + return 22*x; + end + procedure p(x int) returns(y int) as + begin + y = 33*x; + suspend; + end end - end - ^ + """ - create or alter package p2 - as - begin - function f(x int) returns int; - procedure p(x int) returns(y int); - end - ^ - create package body p2 - as - begin - function f(x int) returns int as + PKG_HEAD_2 = """ begin - return 222*x; + function f(x int) returns int; + procedure p(x int) returns(y int); end - procedure p(x int) returns(y int) as + """ + PKG_BODY_2 = """ begin - y = 333*x; - suspend; + function f(x int) returns int as + begin + return 222*x; + end + procedure p(x int) returns(y int) as + begin + y = 333*x; + suspend; + end end - end - ^ - set term ;^ - commit; + """ - show package p1; - show package p2; -""" + test_script = f""" + set term ^; + ^ + create package p1 as + {PKG_HEAD_1} + ^ + create package body p1 as + {PKG_BODY_1} + ^ + create package p2 as + {PKG_HEAD_2} + ^ + create package body p2 as + {PKG_BODY_2} + ^ + set term ;^ + commit; -act = isql_act('db', test_script) + show package p1; + show package p2; + """ -expected_stdout = """ - P1 - Header source: - begin - function f(x int) returns int; - procedure p(x int) returns(y int); - end + expected_stdout = f""" + {SQL_SCHEMA_PREFIX}P1 + Header source: + {PKG_HEAD_1} - Body source: - begin - function f(x int) returns int as - begin - return 22*x; - end - procedure p(x int) returns(y int) as - begin - y = 33*x; - suspend; - end - end - P2 - Header source: - begin - function f(x int) returns int; - procedure p(x int) returns(y int); - end + Body source: + {PKG_BODY_1} - Body source: - begin - function f(x int) returns int as - begin - return 222*x; - end - procedure p(x int) returns(y int) as - begin - y = 333*x; - suspend; - end - end -""" + {SQL_SCHEMA_PREFIX}P2 + Header source: + {PKG_HEAD_2} + + Body source: + {PKG_BODY_2} + """ -@pytest.mark.version('>=3.0') -def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.isql(switches = ['-q'], input = test_script, combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_2910_test.py b/tests/bugs/core_2910_test.py index 840e7393..f41ecbbc 100644 --- a/tests/bugs/core_2910_test.py +++ b/tests/bugs/core_2910_test.py @@ -7,46 +7,55 @@ DESCRIPTION: JIRA: CORE-2910 FBTEST: bugs.core_2910 +NOTES: + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """CREATE TABLE R$TMP ( - POSTING_ID INTEGER -); - -CREATE TABLE TMP ( - POSTING_ID INTEGER NOT NULL -); - -ALTER TABLE TMP ADD CONSTRAINT PK_TMP PRIMARY KEY (POSTING_ID); -commit; -""" - -db = db_factory(charset='UTF8', init=init_script) - -test_script = """SET PLAN ON; -select r.POSTING_ID as r$POSTING_ID, t.POSTING_ID from ( - SELECT POSTING_ID - FROM r$tmp - ) r left join ( - select POSTING_ID from - (select - posting_id - from tmp) - ) t on r.POSTING_ID = t.POSTING_ID; +db = db_factory() + +test_script = """ + create table r$tmp ( + posting_id integer + ); + + create table tmp ( + posting_id integer not null + ); + + alter table tmp add constraint pk_tmp primary key (posting_id); + commit; + + set plan on; + select r.posting_id as r$posting_id, t.posting_id from ( + select posting_id + from r$tmp + ) r left join ( + select posting_id from + (select + posting_id + from tmp) + ) t on r.posting_id = t.posting_id; """ act = isql_act('db', test_script) -expected_stdout = """ -PLAN JOIN (R R$TMP NATURAL, T TMP INDEX (PK_TMP)) +expected_stdout_5x = """ + PLAN JOIN (R R$TMP NATURAL, T TMP INDEX (PK_TMP)) +""" + +expected_stdout_6x = """ + PLAN JOIN ("R" "PUBLIC"."R$TMP" NATURAL, "T" "PUBLIC"."TMP" INDEX ("PUBLIC"."PK_TMP")) """ -@pytest.mark.version('>=3.0') +@pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_2912_test.py b/tests/bugs/core_2912_test.py index 47b96410..2bd5882e 100644 --- a/tests/bugs/core_2912_test.py +++ b/tests/bugs/core_2912_test.py @@ -1,82 +1,76 @@ -#coding:utf-8 - -""" -ID: issue-3296 -ISSUE: 3296 -TITLE: Exception when upper casing string with lowercase y trema (code 0xFF in ISO8859_1) -DESCRIPTION: - Test creates table and fills it with non-ascii characters in init_script, using charset = UTF8. - Then it generates .sql script for running it in separae ISQL process. - This script makes connection to test DB using charset = ISO8859_1 and perform several queries. - Result will be redirected to .log and .err files (they will be encoded, of course, also in ISO8859_1). - Finally, we open .log file (using codecs package), convert its content to UTF8 and show in expected_stdout. -JIRA: CORE-2912 -FBTEST: bugs.core_2912 -NOTES: - [16.11.2021] pcisar - This test fails as UPPER('ÿ') does not work properly - [16.09.2022] pzotov - Trouble with 'ÿ' raises only on LINUX. All fine on Windows. - Mark for running on Windows was *temporary* added to this test. Problem will be investigated. - - Checked on Windows: 3.0.8.33535, 4.0.1.2692, 5.0.0.730 -""" - -import pytest -from firebird.qa import * - -init_script = """ - create table test(c varchar(10)); - commit; - insert into test(c) values('ÿ'); - insert into test(c) values('Faÿ'); - commit; - create index test_cu on test computed by (upper (c collate iso8859_1)); - commit; -""" - -db = db_factory(charset='ISO8859_1', init=init_script) - -act = python_act('db') - -test_script = """set names ISO8859_1; - set list on; - select upper('aÿb') au from rdb$database; - select c, upper(c) cu from test where c starting with upper('ÿ'); - select c, upper(c) cu from test where c containing 'Faÿ'; - select c, upper(c) cu from test where c starting with 'Faÿ'; - select c, upper(c) cu from test where c like 'Faÿ%'; - -- ### ACHTUNG ### - -- As of WI-V2.5.4.26857, following will FAILS if character class "alpha" - -- will be specified not in UPPER case (see note in CORE-4740 08/Apr/15 05:48 PM): - select c, upper(c) cu from test where c similar to '[[:ALPHA:]]{1,}ÿ%'; - set plan on; - select c from test where upper (c collate iso8859_1) = upper('ÿ'); - select c, upper(c) cu from test where upper (c collate iso8859_1) starting with upper('Faÿ'); -""" - -expected_stdout = """ - AU AÿB - C ÿ - CU ÿ - C Faÿ - CU FAÿ - C Faÿ - CU FAÿ - C Faÿ - CU FAÿ - C Faÿ - CU FAÿ - PLAN (TEST INDEX (TEST_CU)) - C ÿ - PLAN (TEST INDEX (TEST_CU)) - C Faÿ - CU FAÿ -""" - -@pytest.mark.platform('Windows') -@pytest.mark.version('>=3') -def test_1(act: Action): - act.expected_stdout = expected_stdout - act.isql(switches=['-q'], charset='ISO8859_1', input=test_script) - assert act.clean_stdout == act.clean_expected_stdout +#coding:utf-8 + +""" +ID: issue-3296 +ISSUE: 3296 +TITLE: Exception when upper casing string with 'ÿ' (lowercase y trema, code 0xFF in ISO8859_1) +DESCRIPTION: +JIRA: CORE-2912 +FBTEST: bugs.core_2912 +NOTES: + [31.10.2024] pzotov + Bug was fixed for too old FB (2.1.6; 2.5.3; 3.0 Alpha 1), firebird-driver and/or QA-plugin + will not able to run on this version in order to reproduce problem. + Checked on 6.0.0.511 (Windows/Linux); 5.0.2.1551; 4.0.6.3165; 3.0.13.33794 + + [27.06.2025] pzotov + Removed 'set plan on' as irrelevant to this test. +""" +from pathlib import Path + +import pytest +from firebird.qa import * + +db = db_factory(charset='ISO8859_1') + +act = isql_act('db', substitutions=[('[ \\t]+', ' ')]) +tmp_sql = temp_file('tmp_core_2912.sql') + +@pytest.mark.intl +@pytest.mark.version('>=3.0.0') +def test_1(act: Action, tmp_sql: Path): + + test_script = """ + create table test(c varchar(10)); + commit; + insert into test(c) values('ÿ'); + insert into test(c) values('Faÿ'); + commit; + create index test_cu on test computed by (upper (c collate iso8859_1)); + commit; + set list on; + select upper('aÿb') au from rdb$database; + select c, upper(c) cu from test where c starting with upper('ÿ'); + select c, upper(c) cu from test where c containing 'Faÿ'; + select c, upper(c) cu from test where c starting with 'Faÿ'; + select c, upper(c) cu from test where c like 'Faÿ%'; + select c, upper(c) cu from test where c similar to '[[:alpha:]]{1,}ÿ%'; + select c from test where upper (c collate iso8859_1) = upper('ÿ'); + select c, upper(c) cu from test where upper (c collate iso8859_1) starting with upper('Faÿ'); + """ + + # ::: NB ::: + # For proper output of test, input script must be encoded in iso8859_1. + # + tmp_sql.write_text(test_script, encoding = 'iso8859_1') + + act.expected_stdout = """ + AU AÿB + C ÿ + CU ÿ + C Faÿ + CU FAÿ + C Faÿ + CU FAÿ + C Faÿ + CU FAÿ + C Faÿ + CU FAÿ + C ÿ + C Faÿ + CU FAÿ + """ + + act.isql(switches = ['-q'], input_file = tmp_sql, charset = 'iso8859_1', combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout + diff --git a/tests/bugs/core_2916_test.py b/tests/bugs/core_2916_test.py index f3bcf457..25b881ec 100644 --- a/tests/bugs/core_2916_test.py +++ b/tests/bugs/core_2916_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-2916 FBTEST: bugs.core_2916 +NOTES: + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -14,70 +20,38 @@ # version: 3.0 -init_script_1 = """create table tab (col date); -insert into tab (col) values (current_date); -commit; -""" - -db_1 = db_factory(init=init_script_1) - -test_script_1 = """create index itab on tab computed (cast(col as int)); -commit; -select * from tab where cast(col as int) is null;""" - -act_1 = isql_act('db_1', test_script_1, - substitutions=[('[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]', '2011-05-03')]) - -expected_stdout_1 = """ - COL -=========== -""" - -expected_stderr_1 = """Statement failed, SQLSTATE = 22018 - -conversion error from string "2011-05-03" - -Statement failed, SQLSTATE = 22018 - -conversion error from string "2011-05-03" -""" - -@pytest.mark.version('>=3.0,<4.0') -def test_1(act_1: Action): - act_1.expected_stdout = expected_stdout_1 - act_1.expected_stderr = expected_stderr_1 - act_1.execute() - assert (act_1.clean_stderr == act_1.clean_expected_stderr and - act_1.clean_stdout == act_1.clean_expected_stdout) - -# version: 4.0 +db = db_factory() -db_2 = db_factory() - -test_script_2 = """ +test_script = """ recreate table tab (col date); - insert into tab (col) values ( date '29.02.2004' ); + insert into tab (col) values ( current_date ); commit; - create index itab on tab computed (cast(col as int)); commit; - set list on; - select * from tab where cast(col as int) is null; """ -act_2 = isql_act('db_2', test_script_2) +substitutions = [('(-)?conversion error from string.*', 'conversion error from string')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stderr_2 = """ +expected_stdout_3x = """ Statement failed, SQLSTATE = 22018 - Expression evaluation error for index "***unknown***" on table "TAB" - -conversion error from string "2004-02-29" + conversion error from string "2021-09-27" +""" + +expected_stdout_5x = """ Statement failed, SQLSTATE = 22018 - conversion error from string "2004-02-29" + Expression evaluation error for index "***unknown***" on table "TAB" + conversion error from string "2021-09-27" """ -@pytest.mark.version('>=4.0') -def test_2(act_2: Action): - act_2.expected_stderr = expected_stderr_2 - act_2.execute() - assert act_2.clean_stderr == act_2.clean_expected_stderr +expected_stdout_6x = """ + Statement failed, SQLSTATE = 22018 + Expression evaluation error for index "***unknown***" on table "PUBLIC"."TAB" + conversion error from string "2021-09-27" +""" +@pytest.mark.version('>=3') +def test_1(act: Action): + act.expected_stdout = expected_stdout_3x if act.is_version('<4') else expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_2922_test.py b/tests/bugs/core_2922_test.py index 5b1424ad..469d0cc1 100644 --- a/tests/bugs/core_2922_test.py +++ b/tests/bugs/core_2922_test.py @@ -41,24 +41,22 @@ join rdb$types rt on rd.rdb$depended_on_type = rt.rdb$type and rt.rdb$type_name containing upper('COLLATION') - order by 1; - + order by dep_name, dep_on + ; """ act = isql_act('db', test_script) expected_stdout = """ -DEP_NAME P1 -DEP_ON WIN1250 -DEP_ON_TYPE COLLATION - -DEP_NAME P1 -DEP_ON UTF8 -DEP_ON_TYPE COLLATION - -DEP_NAME P2 -DEP_ON WIN1250 -DEP_ON_TYPE COLLATION + DEP_NAME P1 + DEP_ON UTF8 + DEP_ON_TYPE COLLATION + DEP_NAME P1 + DEP_ON WIN1250 + DEP_ON_TYPE COLLATION + DEP_NAME P2 + DEP_ON WIN1250 + DEP_ON_TYPE COLLATION """ @pytest.mark.version('>=3.0') diff --git a/tests/bugs/core_2930_test.py b/tests/bugs/core_2930_test.py index ce3d32f3..ae994b3a 100644 --- a/tests/bugs/core_2930_test.py +++ b/tests/bugs/core_2930_test.py @@ -7,39 +7,45 @@ DESCRIPTION: JIRA: CORE-2930 FBTEST: bugs.core_2930 +NOTES: + [27.06.2025] pzotov + Removed 'SHOW' commands from test as they can be replaced with query to rdb$procedure_parameters. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """set term !; -create procedure p1 returns (n integer) as begin suspend; end! -create view v1 as select * from p1! -commit! -set term ;! -""" - -db = db_factory(charset='UTF8', init=init_script) - -test_script = """show procedure p1; -drop view v1; -show procedure p1; +db = db_factory() + +test_script = """ + set list on; + set term ^; + create procedure sp_test returns (o_result integer) as + begin + o_result = 1; + suspend; + end ^ + set term ;^ + create view v1 as select * from sp_test; + commit; + drop view v1; + select + pp.rdb$parameter_name + ,pp.rdb$parameter_number + ,pp.rdb$parameter_type + from rdb$procedure_parameters pp where pp.rdb$procedure_name = upper('sp_test'); + select * from sp_test; """ act = isql_act('db', test_script) -expected_stdout = """Procedure text: -============================================================================= - begin suspend; end -============================================================================= -Parameters: -N OUTPUT INTEGER -Procedure text: -============================================================================= - begin suspend; end -============================================================================= -Parameters: -N OUTPUT INTEGER +expected_stdout = """ + RDB$PARAMETER_NAME O_RESULT + RDB$PARAMETER_NUMBER 0 + RDB$PARAMETER_TYPE 1 + O_RESULT 1 """ @pytest.mark.version('>=3.0') diff --git a/tests/bugs/core_2940_test.py b/tests/bugs/core_2940_test.py index 110f6521..b204ba4d 100644 --- a/tests/bugs/core_2940_test.py +++ b/tests/bugs/core_2940_test.py @@ -69,6 +69,7 @@ ] +@pytest.mark.trace @pytest.mark.version('>=3.0') def test_1(act: Action): with act.trace(db_events=trace): diff --git a/tests/bugs/core_2952_test.py b/tests/bugs/core_2952_test.py index 87d10423..3574b29c 100644 --- a/tests/bugs/core_2952_test.py +++ b/tests/bugs/core_2952_test.py @@ -2267,6 +2267,7 @@ def test_1(act_1: Action): S_WHITE_SPACE 0 """ +@pytest.mark.intl @pytest.mark.version('>=4.0') def test_2(act_2: Action): act_2.expected_stdout = expected_stdout_2 diff --git a/tests/bugs/core_2966_test.py b/tests/bugs/core_2966_test.py index 826849a9..a8a4213c 100644 --- a/tests/bugs/core_2966_test.py +++ b/tests/bugs/core_2966_test.py @@ -2,73 +2,89 @@ """ ID: issue-3348 -ISSUE: 3348 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/3348 TITLE: Wrong results or unexpected errors while sorting a large data set DESCRIPTION: JIRA: CORE-2966 FBTEST: bugs.core_2966 +NOTES: + [07.02.2025] pzotov + 1. Test runs for ~55-60s (checked on Win-10, Intel(R) Core(TM) i3 CPU 540 3.07GHz, FIREBIRD_TMP pointed to RAM drive. + 2. Test assumes that FIREBIRD_TMP points to the resource with enough space! + Otherwise it fails with one of following outcomes: + case-1 (if there is no folder that is specified in FIREBIRD_TMP): + INTERNALERROR> firebird.driver.types.DatabaseError: I/O error during "CreateFile (create)" operation for file "" + INTERNALERROR> -Error while trying to create file + INTERNALERROR> -- LOCALIZED message here + case-2 (if folder specified by FIREBIRD_TMP *exists* but no free space encountered during sort): + Statement failed, SQLSTATE = HY000 + sort error + -No free space found in temporary directories + -operating system directive WriteFile failed + - -- LOCALIZED message here + Because of presence of localized messages, we have to use 'io_enc = locale.getpreferredencoding()' in act.execute(). + Also, 'combine_output = True' must be used in order to see both STDOUT and STDERR in the same log. """ +import locale import pytest from firebird.qa import * -init_script = """create table t (col varchar(32000)); -commit; -set term !!; -execute block -as - declare variable i integer; -begin - i=0; - while (i < 200000) do begin - insert into t (col) values(mod(:i, 10)); - i= i+1; - end -end!! -set term ;!! -commit;""" +init_script = """ + create table t (col varchar(32000)); + commit; + set term ^; + execute block + as + declare variable i integer; + begin + i=0; + while (i < 200000) do begin + insert into t (col) values(mod(:i, 10)); + i= i+1; + end + end^ + set term ;^ + commit; +""" -db = db_factory(init=init_script) +db = db_factory(init = init_script) -test_script = """select col from t group by 1; -select cast(col as integer) from t group by 1; +test_script = """ + set list on; + select col as col_as_txt from t group by 1; + select cast(col as integer) as col_as_int from t group by 1; """ -act = isql_act('db', test_script, substitutions=[('=.*', '=')]) +substitutions = [('[ \t]+', ' '), ] +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ -COL -=============================================================================== -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 - - - CAST -============ - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - + COL_AS_TXT 0 + COL_AS_TXT 1 + COL_AS_TXT 2 + COL_AS_TXT 3 + COL_AS_TXT 4 + COL_AS_TXT 5 + COL_AS_TXT 6 + COL_AS_TXT 7 + COL_AS_TXT 8 + COL_AS_TXT 9 + COL_AS_INT 0 + COL_AS_INT 1 + COL_AS_INT 2 + COL_AS_INT 3 + COL_AS_INT 4 + COL_AS_INT 5 + COL_AS_INT 6 + COL_AS_INT 7 + COL_AS_INT 8 + COL_AS_INT 9 """ @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True, io_enc = locale.getpreferredencoding()) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_2981_test.py b/tests/bugs/core_2981_test.py index c8ad0936..fe0b2406 100644 --- a/tests/bugs/core_2981_test.py +++ b/tests/bugs/core_2981_test.py @@ -127,6 +127,7 @@ 'max_arg_count = 30', ] +@pytest.mark.trace @pytest.mark.version('>=3') def test_1(act: Action): # Get content of firebird.log BEFORE test diff --git a/tests/bugs/core_2985_test.py b/tests/bugs/core_2985_test.py index b42f8674..ebafaddf 100644 --- a/tests/bugs/core_2985_test.py +++ b/tests/bugs/core_2985_test.py @@ -7,31 +7,39 @@ DESCRIPTION: JIRA: CORE-2985 FBTEST: bugs.core_2985 +NOTES: + [27.06.2025] pzotov + Replaced 'SHOW' command with query to 't_dependant'. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """create table test (id numeric, f1 varchar(20)); -create table test1(id1 numeric, ff computed((select f1 from test where id=id1))); -commit; -""" - -db = db_factory(init=init_script) - -test_script = """show table test1; -alter table test1 alter ff computed(cast(null as varchar(20))); -drop table test; -commit; -show table test1; +db = db_factory() + +test_script = """ + set list on; + create table t_source (id numeric, f1 varchar(20)); + create table t_dependant(id1 numeric, ff computed( (select s.f1 from t_source s where s.id = id1) ) ); + commit; + insert into t_dependant(id1) values(1); + commit; + alter table t_dependant alter ff computed(cast(null as varchar(20))); + drop table t_source; + commit; + set count on; + select * from t_dependant; """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ID1 NUMERIC(9, 0) Nullable -FF Computed by: ((select f1 from test where id=id1)) -ID1 NUMERIC(9, 0) Nullable -FF Computed by: (cast(null as varchar(20))) +expected_stdout = """ + ID1 1 + FF + Records affected: 1 """ @pytest.mark.version('>=3.0') diff --git a/tests/bugs/core_2987_test.py b/tests/bugs/core_2987_test.py index e0e26b99..d6cf5907 100644 --- a/tests/bugs/core_2987_test.py +++ b/tests/bugs/core_2987_test.py @@ -152,6 +152,7 @@ MEASURE_RESULT WINS >= 3.8x """ +@pytest.mark.es_eds @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_3008_test.py b/tests/bugs/core_3008_test.py index f78f1580..7d328982 100644 --- a/tests/bugs/core_3008_test.py +++ b/tests/bugs/core_3008_test.py @@ -28,6 +28,7 @@ 'time_threshold = 0', ] +@pytest.mark.trace @pytest.mark.version('>=3.0') def test_1(act: Action): with act.trace(db_events=trace): diff --git a/tests/bugs/core_3029_test.py b/tests/bugs/core_3029_test.py index 6a6bb087..ca5272e3 100644 --- a/tests/bugs/core_3029_test.py +++ b/tests/bugs/core_3029_test.py @@ -7,110 +7,142 @@ DESCRIPTION: JIRA: CORE-3029 FBTEST: bugs.core_3029 +NOTES: + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """create sequence test_gen; - -recreate table test_row -(id int not null, - did int not null, - pid int not null, - dep int not null -); -alter table test_row add constraint pk_test_row primary key(id); -create unique index ix_test_row1 on test_row(did, pid, dep); -commit; - -insert into test_row(id, did, pid,dep) values(1, 2, 3, 4); -commit; -""" - -db = db_factory(init=init_script) - -test_script = """set term !!; -execute block returns(id int, did int, dep int, pid int) -as -declare variable xid int; -begin - select id,did, pid,dep - from test_row - where id=(select min(id) from test_row) - into :xid, :did, pid, :dep; - - while (1=1) do - begin - delete from test_row r where r.id = :xid; - - insert into test_row(id, did, dep, pid) - values (gen_id(test_gen, 1), :did, :dep, :pid); - - suspend; - - when any do - exception; - end -end !! -rollback !! -create or alter procedure sp_test - returns(id int, did int, dep int, pid int) -as -declare variable xid int; -begin - select id,did, pid,dep - from test_row - where id=(select min(id) from test_row) - into :xid, :did, pid, :dep; - - while (1=1) do - begin - delete from test_row r where r.id = :xid; - - insert into test_row(id, did, dep, pid) - values (gen_id(test_gen, 1), :did, :dep, :pid); - - suspend; - - when any do - exception; - end -end !! -select * from sp_test !! -rollback !! +db = db_factory() + +test_script = """ + set list on; + create sequence test_gen; + recreate table test_row + (id int not null, + did int not null, + pid int not null, + dep int not null + ); + alter table test_row add constraint pk_test_row primary key(id); + create unique index ix_test_row1 on test_row(did, pid, dep); + commit; + + insert into test_row(id, did, pid,dep) values(1, 2, 3, 4); + commit; + + set term ^; + execute block returns(id int, did int, dep int, pid int) + as + declare variable xid int; + begin + select id,did, pid,dep + from test_row + where id=(select min(id) from test_row) + into :xid, :did, pid, :dep; + + while (1=1) do + begin + delete from test_row r where r.id = :xid; + + insert into test_row(id, did, dep, pid) + values (gen_id(test_gen, 1), :did, :dep, :pid); + + suspend; + + when any do + exception; + end + end ^ + rollback ^ + create or alter procedure sp_test + returns(id int, did int, dep int, pid int) + as + declare variable xid int; + begin + select id,did, pid,dep + from test_row + where id=(select min(id) from test_row) + into :xid, :did, pid, :dep; + + while (1=1) do + begin + delete from test_row r where r.id = :xid; + + insert into test_row(id, did, dep, pid) + values (gen_id(test_gen, 1), :did, :dep, :pid); + + suspend; + + when any do + exception; + end + end ^ + select * from sp_test ^ + rollback ^ """ -act = isql_act('db', test_script, substitutions=[('line.*', ''), ('col.*', '')]) - -expected_stdout = """ - ID DID DEP PID -============ ============ ============ ============ - 2 4 3 - 2 4 3 - - ID DID DEP PID -============ ============ ============ ============ - 2 4 3 +substitutions = [ ('[ \t]+', ' '), ('line.*', ''), ('col.*', '')] + +act = isql_act('db', test_script, substitutions = substitutions) + +expected_stdout_5x = """ + ID + DID 2 + DEP 4 + PID 3 + ID + DID 2 + DEP 4 + PID 3 + Statement failed, SQLSTATE = 23000 + attempt to store duplicate value (visible to active transactions) in unique index "IX_TEST_ROW1" + -Problematic key value is ("DID" = 2, "PID" = 3, "DEP" = 4) + -At block + -At block + ID + DID 2 + DEP 4 + PID 3 + Statement failed, SQLSTATE = 23000 + attempt to store duplicate value (visible to active transactions) in unique index "IX_TEST_ROW1" + -Problematic key value is ("DID" = 2, "PID" = 3, "DEP" = 4) + -At procedure 'SP_TEST' + -At procedure 'SP_TEST' """ -expected_stderr = """Statement failed, SQLSTATE = 23000 -attempt to store duplicate value (visible to active transactions) in unique index "IX_TEST_ROW1" --Problematic key value is ("DID" = 2, "PID" = 3, "DEP" = 4) --At block --At block -Statement failed, SQLSTATE = 23000 -attempt to store duplicate value (visible to active transactions) in unique index "IX_TEST_ROW1" --Problematic key value is ("DID" = 2, "PID" = 3, "DEP" = 4) --At procedure 'SP_TEST' line: 15, col: 5 --At procedure 'SP_TEST' line: 20, col: 12 +expected_stdout_6x = """ + ID + DID 2 + DEP 4 + PID 3 + ID + DID 2 + DEP 4 + PID 3 + Statement failed, SQLSTATE = 23000 + attempt to store duplicate value (visible to active transactions) in unique index "PUBLIC"."IX_TEST_ROW1" + -Problematic key value is ("DID" = 2, "PID" = 3, "DEP" = 4) + -At block + -At block + ID + DID 2 + DEP 4 + PID 3 + Statement failed, SQLSTATE = 23000 + attempt to store duplicate value (visible to active transactions) in unique index "PUBLIC"."IX_TEST_ROW1" + -Problematic key value is ("DID" = 2, "PID" = 3, "DEP" = 4) + -At procedure "PUBLIC"."SP_TEST" + -At procedure "PUBLIC"."SP_TEST" """ @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3055_test.py b/tests/bugs/core_3055_test.py index 8e6dc606..54fe9304 100644 --- a/tests/bugs/core_3055_test.py +++ b/tests/bugs/core_3055_test.py @@ -3,11 +3,16 @@ """ ID: issue-3435 ISSUE: 3435 -TITLE: Variable/argument name could be absent or be wrong in error messages when more - than 256 variables are used +TITLE: Variable/argument name could be absent or be wrong in error messages when more than 256 variables are used DESCRIPTION: JIRA: CORE-3055 FBTEST: bugs.core_3055 +NOTES: + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -2033,15 +2038,20 @@ act = isql_act('db', test_script, substitutions=[('-At block line: [\\d]+, col: [\\d]+', '-At block line')]) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 42000 validation error for variable V_NN, value "*** null ***" - -At block line: 2004, col: 9 + -At block line """ -@pytest.mark.version('>=3.0') -def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + validation error for variable "V_NN", value "*** null ***" + -At block line +""" +@pytest.mark.version('>=3') +def test_1(act: Action): + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3056_test.py b/tests/bugs/core_3056_test.py index 174f8cd4..6f36197b 100644 --- a/tests/bugs/core_3056_test.py +++ b/tests/bugs/core_3056_test.py @@ -3,11 +3,19 @@ """ ID: issue-3436 ISSUE: 3436 -TITLE: Problems may happen when issuing DDL commands in the same transaction after - CREATE COLLATION was issued +TITLE: Problems may happen when issuing DDL commands in the same transaction after CREATE COLLATION was issued DESCRIPTION: JIRA: CORE-3056 FBTEST: bugs.core_3056 +NOTES: + [27.06.2025] pzotov + Uncommented lines "--,constraint test_pk1 primary key" and added "alter table drop constraint " + because core-4783 has been fixed long ago. + + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -16,14 +24,6 @@ db = db_factory() test_script = """ - -- NOTES. - -- 1. Results are identical on: LI-T3.0.0.31827 (64 bit) and WI-T3.0.0.31827 (32 bit). - -- 2. Despite of ticket issue that it was fixed only in 3.0, following script works OK on also oon 2.5 - -- (tested on WI-V2.5.5.26861; differences are only in stderr). - -- 3. ## TODO ### - -- Uncomment lines "--,constraint test_pk1 primary key" after CORE-4783 will be fixed, and add - -- statement 'alter table drop constraint " before each DROP TABLE statements. - create or alter view v_test_fields_ddl as select rf.rdb$field_name fld_name @@ -36,18 +36,6 @@ where rf.rdb$relation_name = 'TEST' order by rf.rdb$field_position; - /* - -- This works only in 3.0 and does NOT in 2.5 (rdb$collation_id present there only in rdb$relation_fields and NOT in rdb$fields): - create or alter view v_test_fields_ddl as - select rf.rdb$field_name fld_name, cs.rdb$character_set_name cset_name, co.rdb$base_collation_name base_coll, co.rdb$collation_attributes - from rdb$relation_fields rf - join rdb$fields ff on rf.rdb$field_source = ff.rdb$field_name - join rdb$character_sets cs on ff.rdb$character_set_id = cs.rdb$character_set_id - join rdb$collations co on ff.rdb$collation_id = co.rdb$collation_id - where rf.rdb$relation_name = 'TEST' - order by rf.rdb$field_position; - */ - recreate table test(id int); commit; set term ^; @@ -128,11 +116,14 @@ ,f10 varchar(2) character set win1251 collate coll_10 ,f11 varchar(2) character set win1251 collate coll_11 ,f12 varchar(2) character set win1251 collate coll_12 - --,constraint test_pk1 primary key (f01, f02, f03, f04, f05, f06, f07, f08, f09, f10, f11, f12) + -- 27.06.2025: uncommented PK because core-4783 ( https://github.com/FirebirdSQL/firebird/issues/5082 ) + -- has been fixed in 3.0.8 / 4.0.1 / 5.0 Beta1: + ,constraint test_pk1 primary key (f01, f02, f03, f04, f05, f06, f07, f08, f09, f10, f11, f12) ); select * from v_test_fields_ddl; + alter table test drop constraint test_pk1; drop table test; drop collation coll_01; @@ -174,10 +165,13 @@ ,f10 varchar(2) character set utf8 collate coll_10 ,f11 varchar(2) character set utf8 collate coll_11 ,f12 varchar(2) character set utf8 collate coll_12 - --,constraint test_pk2 primary key (f01, f02, f03, f04, f05, f06, f07, f08, f09, f10, f11, f12) + -- 27.06.2025: uncommented PK because core-4783 ( https://github.com/FirebirdSQL/firebird/issues/5082 ) + -- has been fixed in 3.0.8 / 4.0.1 / 5.0 Beta1: + ,constraint test_pk2 primary key (f01, f02, f03, f04, f05, f06, f07, f08, f09, f10, f11, f12) ); select * from v_test_fields_ddl; + alter table test drop constraint test_pk2; drop table test; drop collation coll_01; @@ -221,7 +215,9 @@ ,f10 varchar(2) character set iso8859_1 collate coll_10 ,f11 varchar(2) character set iso8859_1 collate coll_11 ,f12 varchar(2) character set iso8859_1 collate coll_12 - --,constraint test_pk2 primary key (f01, f02, f03, f04, f05, f06, f07, f08, f09, f10, f11, f12) + -- 27.06.2025: uncommented PK because core-4783 ( https://github.com/FirebirdSQL/firebird/issues/5082 ) + -- has been fixed in 3.0.8 / 4.0.1 / 5.0 Beta1: + ,constraint test_pk2 primary key (f01, f02, f03, f04, f05, f06, f07, f08, f09, f10, f11, f12) ); select * from v_test_fields_ddl; @@ -237,181 +233,258 @@ act = isql_act('db', test_script) -expected_stdout = """ - select * from v_test_fields_ddl; - Records affected: 0 - drop collation coll_01; - set echo off; - - FLD_NAME F01 - CSET_NAME WIN1251 - BASE_COLL PXW_CYRL - - FLD_NAME F02 - CSET_NAME WIN1251 - BASE_COLL PXW_CYRL - - FLD_NAME F03 - CSET_NAME WIN1251 - BASE_COLL PXW_CYRL - - FLD_NAME F04 - CSET_NAME WIN1251 - BASE_COLL PXW_CYRL - - FLD_NAME F05 - CSET_NAME WIN1251 - BASE_COLL PXW_CYRL - - FLD_NAME F06 - CSET_NAME WIN1251 - BASE_COLL PXW_CYRL - - FLD_NAME F07 - CSET_NAME WIN1251 - BASE_COLL PXW_CYRL - - FLD_NAME F08 - CSET_NAME WIN1251 - BASE_COLL PXW_CYRL - - FLD_NAME F09 - CSET_NAME WIN1251 - BASE_COLL PXW_CYRL - - FLD_NAME F10 - CSET_NAME WIN1251 - BASE_COLL PXW_CYRL - - FLD_NAME F11 - CSET_NAME WIN1251 - BASE_COLL PXW_CYRL - - FLD_NAME F12 - CSET_NAME WIN1251 - BASE_COLL PXW_CYRL - - - - FLD_NAME F01 - CSET_NAME UTF8 - BASE_COLL UNICODE - - FLD_NAME F02 - CSET_NAME UTF8 - BASE_COLL UNICODE - - FLD_NAME F03 - CSET_NAME UTF8 - BASE_COLL UNICODE - - FLD_NAME F04 - CSET_NAME UTF8 - BASE_COLL UNICODE - - FLD_NAME F05 - CSET_NAME UTF8 - BASE_COLL UNICODE - - FLD_NAME F06 - CSET_NAME UTF8 - BASE_COLL UNICODE - - FLD_NAME F07 - CSET_NAME UTF8 - BASE_COLL UNICODE - - FLD_NAME F08 - CSET_NAME UTF8 - BASE_COLL UNICODE - - FLD_NAME F09 - CSET_NAME UTF8 - BASE_COLL UNICODE - - FLD_NAME F10 - CSET_NAME UTF8 - BASE_COLL UNICODE - - FLD_NAME F11 - CSET_NAME UTF8 - BASE_COLL UNICODE - - FLD_NAME F12 - CSET_NAME UTF8 - BASE_COLL UNICODE - - - - FLD_NAME F01 - CSET_NAME ISO8859_1 - BASE_COLL DA_DA - - FLD_NAME F02 - CSET_NAME ISO8859_1 - BASE_COLL DE_DE - - FLD_NAME F03 - CSET_NAME ISO8859_1 - BASE_COLL DU_NL - - FLD_NAME F04 - CSET_NAME ISO8859_1 - BASE_COLL EN_UK - - FLD_NAME F05 - CSET_NAME ISO8859_1 - BASE_COLL EN_US - - FLD_NAME F06 - CSET_NAME ISO8859_1 - BASE_COLL ES_ES - - FLD_NAME F07 - CSET_NAME ISO8859_1 - BASE_COLL ES_ES_CI_AI - - FLD_NAME F08 - CSET_NAME ISO8859_1 - BASE_COLL FI_FI - - FLD_NAME F09 - CSET_NAME ISO8859_1 - BASE_COLL FR_CA - - FLD_NAME F10 - CSET_NAME ISO8859_1 - BASE_COLL FR_FR - - FLD_NAME F11 - CSET_NAME ISO8859_1 - BASE_COLL IS_IS - - FLD_NAME F12 - CSET_NAME ISO8859_1 - BASE_COLL IT_IT - - - select * from v_test_fields_ddl; - Records affected: 0 - select * from rdb$collations co where co.rdb$collation_name starting with 'COLL_'; - Records affected: 0 +expected_stdout_5x = """ + Statement failed, SQLSTATE = 22021 + unsuccessful metadata update + -CREATE TABLE TEST failed + -Dynamic SQL Error + -SQL error code = -204 + -COLLATION COLL_01 for CHARACTER SET NONE is not defined + select * from v_test_fields_ddl; + Records affected: 0 + drop collation coll_01; + set echo off; + FLD_NAME F01 + CSET_NAME WIN1251 + BASE_COLL PXW_CYRL + FLD_NAME F02 + CSET_NAME WIN1251 + BASE_COLL PXW_CYRL + FLD_NAME F03 + CSET_NAME WIN1251 + BASE_COLL PXW_CYRL + FLD_NAME F04 + CSET_NAME WIN1251 + BASE_COLL PXW_CYRL + FLD_NAME F05 + CSET_NAME WIN1251 + BASE_COLL PXW_CYRL + FLD_NAME F06 + CSET_NAME WIN1251 + BASE_COLL PXW_CYRL + FLD_NAME F07 + CSET_NAME WIN1251 + BASE_COLL PXW_CYRL + FLD_NAME F08 + CSET_NAME WIN1251 + BASE_COLL PXW_CYRL + FLD_NAME F09 + CSET_NAME WIN1251 + BASE_COLL PXW_CYRL + FLD_NAME F10 + CSET_NAME WIN1251 + BASE_COLL PXW_CYRL + FLD_NAME F11 + CSET_NAME WIN1251 + BASE_COLL PXW_CYRL + FLD_NAME F12 + CSET_NAME WIN1251 + BASE_COLL PXW_CYRL + FLD_NAME F01 + CSET_NAME UTF8 + BASE_COLL UNICODE + FLD_NAME F02 + CSET_NAME UTF8 + BASE_COLL UNICODE + FLD_NAME F03 + CSET_NAME UTF8 + BASE_COLL UNICODE + FLD_NAME F04 + CSET_NAME UTF8 + BASE_COLL UNICODE + FLD_NAME F05 + CSET_NAME UTF8 + BASE_COLL UNICODE + FLD_NAME F06 + CSET_NAME UTF8 + BASE_COLL UNICODE + FLD_NAME F07 + CSET_NAME UTF8 + BASE_COLL UNICODE + FLD_NAME F08 + CSET_NAME UTF8 + BASE_COLL UNICODE + FLD_NAME F09 + CSET_NAME UTF8 + BASE_COLL UNICODE + FLD_NAME F10 + CSET_NAME UTF8 + BASE_COLL UNICODE + FLD_NAME F11 + CSET_NAME UTF8 + BASE_COLL UNICODE + FLD_NAME F12 + CSET_NAME UTF8 + BASE_COLL UNICODE + FLD_NAME F01 + CSET_NAME ISO8859_1 + BASE_COLL DA_DA + FLD_NAME F02 + CSET_NAME ISO8859_1 + BASE_COLL DE_DE + FLD_NAME F03 + CSET_NAME ISO8859_1 + BASE_COLL DU_NL + FLD_NAME F04 + CSET_NAME ISO8859_1 + BASE_COLL EN_UK + FLD_NAME F05 + CSET_NAME ISO8859_1 + BASE_COLL EN_US + FLD_NAME F06 + CSET_NAME ISO8859_1 + BASE_COLL ES_ES + FLD_NAME F07 + CSET_NAME ISO8859_1 + BASE_COLL ES_ES_CI_AI + FLD_NAME F08 + CSET_NAME ISO8859_1 + BASE_COLL FI_FI + FLD_NAME F09 + CSET_NAME ISO8859_1 + BASE_COLL FR_CA + FLD_NAME F10 + CSET_NAME ISO8859_1 + BASE_COLL FR_FR + FLD_NAME F11 + CSET_NAME ISO8859_1 + BASE_COLL IS_IS + FLD_NAME F12 + CSET_NAME ISO8859_1 + BASE_COLL IT_IT + select * from v_test_fields_ddl; + Records affected: 0 + select * from rdb$collations co where co.rdb$collation_name starting with 'COLL_'; + Records affected: 0 """ -expected_stderr = """ - Statement failed, SQLSTATE = 22021 - unsuccessful metadata update - -CREATE TABLE TEST failed - -Dynamic SQL Error - -SQL error code = -204 - -COLLATION COLL_01 for CHARACTER SET NONE is not defined +expected_stdout_6x = """ + Statement failed, SQLSTATE = 22021 + unsuccessful metadata update + -CREATE TABLE "PUBLIC"."TEST" failed + -Dynamic SQL Error + -SQL error code = -204 + -COLLATION "PUBLIC"."COLL_01" for CHARACTER SET "SYSTEM"."NONE" is not defined + select * from v_test_fields_ddl; + Records affected: 0 + drop collation coll_01; + set echo off; + FLD_NAME F01 + CSET_NAME WIN1251 + BASE_COLL PXW_CYRL + FLD_NAME F02 + CSET_NAME WIN1251 + BASE_COLL PXW_CYRL + FLD_NAME F03 + CSET_NAME WIN1251 + BASE_COLL PXW_CYRL + FLD_NAME F04 + CSET_NAME WIN1251 + BASE_COLL PXW_CYRL + FLD_NAME F05 + CSET_NAME WIN1251 + BASE_COLL PXW_CYRL + FLD_NAME F06 + CSET_NAME WIN1251 + BASE_COLL PXW_CYRL + FLD_NAME F07 + CSET_NAME WIN1251 + BASE_COLL PXW_CYRL + FLD_NAME F08 + CSET_NAME WIN1251 + BASE_COLL PXW_CYRL + FLD_NAME F09 + CSET_NAME WIN1251 + BASE_COLL PXW_CYRL + FLD_NAME F10 + CSET_NAME WIN1251 + BASE_COLL PXW_CYRL + FLD_NAME F11 + CSET_NAME WIN1251 + BASE_COLL PXW_CYRL + FLD_NAME F12 + CSET_NAME WIN1251 + BASE_COLL PXW_CYRL + FLD_NAME F01 + CSET_NAME UTF8 + BASE_COLL UNICODE + FLD_NAME F02 + CSET_NAME UTF8 + BASE_COLL UNICODE + FLD_NAME F03 + CSET_NAME UTF8 + BASE_COLL UNICODE + FLD_NAME F04 + CSET_NAME UTF8 + BASE_COLL UNICODE + FLD_NAME F05 + CSET_NAME UTF8 + BASE_COLL UNICODE + FLD_NAME F06 + CSET_NAME UTF8 + BASE_COLL UNICODE + FLD_NAME F07 + CSET_NAME UTF8 + BASE_COLL UNICODE + FLD_NAME F08 + CSET_NAME UTF8 + BASE_COLL UNICODE + FLD_NAME F09 + CSET_NAME UTF8 + BASE_COLL UNICODE + FLD_NAME F10 + CSET_NAME UTF8 + BASE_COLL UNICODE + FLD_NAME F11 + CSET_NAME UTF8 + BASE_COLL UNICODE + FLD_NAME F12 + CSET_NAME UTF8 + BASE_COLL UNICODE + FLD_NAME F01 + CSET_NAME ISO8859_1 + BASE_COLL DA_DA + FLD_NAME F02 + CSET_NAME ISO8859_1 + BASE_COLL DE_DE + FLD_NAME F03 + CSET_NAME ISO8859_1 + BASE_COLL DU_NL + FLD_NAME F04 + CSET_NAME ISO8859_1 + BASE_COLL EN_UK + FLD_NAME F05 + CSET_NAME ISO8859_1 + BASE_COLL EN_US + FLD_NAME F06 + CSET_NAME ISO8859_1 + BASE_COLL ES_ES + FLD_NAME F07 + CSET_NAME ISO8859_1 + BASE_COLL ES_ES_CI_AI + FLD_NAME F08 + CSET_NAME ISO8859_1 + BASE_COLL FI_FI + FLD_NAME F09 + CSET_NAME ISO8859_1 + BASE_COLL FR_CA + FLD_NAME F10 + CSET_NAME ISO8859_1 + BASE_COLL FR_FR + FLD_NAME F11 + CSET_NAME ISO8859_1 + BASE_COLL IS_IS + FLD_NAME F12 + CSET_NAME ISO8859_1 + BASE_COLL IT_IT + select * from v_test_fields_ddl; + Records affected: 0 + select * from rdb$collations co where co.rdb$collation_name starting with 'COLL_'; + Records affected: 0 """ -@pytest.mark.version('>=3.0') +@pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3064_test.py b/tests/bugs/core_3064_test.py index bb59d940..cbbe76b4 100644 --- a/tests/bugs/core_3064_test.py +++ b/tests/bugs/core_3064_test.py @@ -10,12 +10,20 @@ NOTES: [04.03.2023] pzotov Expected output was splitted because FB 5.x now *allows* execution w/o error. + + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """ +db = db_factory() + +test_script = """ set term ^ ; create or alter procedure get_dates ( adate_from date, @@ -26,7 +34,7 @@ declare variable d date; begin d = adate_from; - while (d<=adate_to) do + while (d <= adate_to) do begin out_date = d; suspend; @@ -35,11 +43,7 @@ end^ set term ; ^ commit; -""" -db = db_factory(init=init_script) - -test_script = """ set planonly; select * from get_dates( 'yesterday', 'today' ) PLAN (GET_DATES NATURAL); select * from get_dates( 'yesterday', 'today' ) p PLAN (P NATURAL); @@ -47,7 +51,7 @@ act = isql_act('db', test_script, substitutions=[('offset .*', 'offset')]) -fb3x_expected_out = """ +expected_out_4x = """ Statement failed, SQLSTATE = 42S02 Dynamic SQL Error -SQL error code = -104 @@ -59,13 +63,18 @@ -BLR syntax error: expected TABLE at offset 51, encountered 132 """ -fb5x_expected_out = """ +expected_out_5x = """ PLAN (GET_DATES NATURAL) PLAN (P NATURAL) """ +expected_out_6x = """ + PLAN ("PUBLIC"."GET_DATES" NATURAL) + PLAN ("P" NATURAL) +""" + @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = fb3x_expected_out if act.is_version('<5') else fb5x_expected_out + act.expected_stdout = expected_out_4x if act.is_version('<5') else expected_out_5x if act.is_version('<6') else expected_out_6x act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3085_test.py b/tests/bugs/core_3085_test.py index add533f4..c6046701 100644 --- a/tests/bugs/core_3085_test.py +++ b/tests/bugs/core_3085_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-3085 FBTEST: bugs.core_3085 +NOTES: + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -17,22 +23,22 @@ test_script = """ create domain dm_int int; commit; - create table t(x dm_int); + create table test(x dm_int); commit; set term ^; - create procedure p(a dm_int) returns(msg varchar(30)) as + create procedure sp_test(a dm_int) returns(msg varchar(30)) as begin - msg='intro proc p: a=' || coalesce(a, 'null'); + msg='intro proc sp_test: a=' || coalesce(a, 'null'); suspend; end ^ set term ;^ commit; - insert into t values(1); - insert into t values(2); - insert into t values(3); + insert into test values(1); + insert into test values(2); + insert into test values(3); commit; set list on; @@ -40,43 +46,56 @@ alter domain dm_int set not null; commit; - select msg from p(null); - update t set x=null where x=2; + select msg from sp_test(null); + update test set x=null where x=2; commit; alter domain dm_int drop not null; commit; - select msg from p(null); - update t set x=null where x=2 returning x; + select msg from sp_test(null); + update test set x=null where x=2 returning x; commit; alter domain dm_int set not null; """ -act = isql_act('db', test_script) +substitutions = [ ('[ \t]+', ' ') ] +act = isql_act('db', test_script, substitutions = substitutions) + -expected_stdout = """ - MSG intro proc p: a=null +expected_out_5x = """ + Statement failed, SQLSTATE = 42000 + validation error for variable A, value "*** null ***" + -At procedure 'SP_TEST' + + Statement failed, SQLSTATE = 23000 + validation error for column "TEST"."X", value "*** null ***" + MSG intro proc sp_test: a=null X + + Statement failed, SQLSTATE = 22006 + unsuccessful metadata update + -Cannot make field X of table TEST NOT NULL because there are NULLs present """ -expected_stderr = """ +expected_out_6x = """ Statement failed, SQLSTATE = 42000 - validation error for variable A, value "*** null ***" - -At procedure 'P' + validation error for variable "A", value "*** null ***" + -At procedure "PUBLIC"."SP_TEST" + Statement failed, SQLSTATE = 23000 - validation error for column "T"."X", value "*** null ***" + validation error for column "PUBLIC"."TEST"."X", value "*** null ***" + MSG intro proc sp_test: a=null + X + Statement failed, SQLSTATE = 22006 unsuccessful metadata update - -Cannot make field X of table T NOT NULL because there are NULLs present + -Cannot make field "X" of table "PUBLIC"."TEST" NOT NULL because there are NULLs present """ -@pytest.mark.version('>=3.0') +@pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.expected_stdout = expected_out_5x if act.is_version('<6') else expected_out_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3100_test.py b/tests/bugs/core_3100_test.py index 98a62c6b..39986d29 100644 --- a/tests/bugs/core_3100_test.py +++ b/tests/bugs/core_3100_test.py @@ -252,6 +252,7 @@ record not found for user: TMP$C3100B """ +@pytest.mark.es_eds @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_3101_test.py b/tests/bugs/core_3101_test.py index 9323f44d..d6327856 100644 --- a/tests/bugs/core_3101_test.py +++ b/tests/bugs/core_3101_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-3101 FBTEST: bugs.core_3101 +NOTES: + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -21,17 +27,23 @@ show domain state; """ -act = isql_act('db', test_script) +substitutions = [ ('[ \t]+', ' ') ] +act = isql_act('db', test_script, substitutions = substitutions) + +expected_out_5x = """ + STATE SMALLINT Nullable + STATE SMALLINT Nullable + default 0 +""" -expected_stdout = """ - STATE SMALLINT Nullable - STATE SMALLINT Nullable - default 0 +expected_out_6x = """ + PUBLIC.STATE SMALLINT Nullable + PUBLIC.STATE SMALLINT Nullable + default 0 """ @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_out_5x if act.is_version('<6') else expected_out_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_3103_test.py b/tests/bugs/core_3103_test.py index bdb8c564..7522d9c7 100644 --- a/tests/bugs/core_3103_test.py +++ b/tests/bugs/core_3103_test.py @@ -8,6 +8,12 @@ Ticket subj: Select statement with more non indexed reads in version 2.5RC3 as in version 2.1.3 JIRA: CORE-3103 FBTEST: bugs.core_3103 +NOTES: + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -81,14 +87,18 @@ act = isql_act('db', test_script) -expected_stdout = """ +expected_out_5x = """ PLAN JOIN (A INDEX (BSTAMMDATEN_MASKENKEY), B INDEX (FK_BAUF_BSTAMMDATEN_ID)) PLAN (BAUF INDEX (BAUF_PK)) """ +expected_out_6x = """ + PLAN JOIN ("A" INDEX ("PUBLIC"."BSTAMMDATEN_MASKENKEY"), "B" INDEX ("PUBLIC"."FK_BAUF_BSTAMMDATEN_ID")) + PLAN ("PUBLIC"."BAUF" INDEX ("PUBLIC"."BAUF_PK")) +""" + @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_out_5x if act.is_version('<6') else expected_out_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_3168_test.py b/tests/bugs/core_3168_test.py index 126fdc59..eee22ae4 100644 --- a/tests/bugs/core_3168_test.py +++ b/tests/bugs/core_3168_test.py @@ -29,6 +29,7 @@ temp_file = temp_file('test-file') +@pytest.mark.trace @pytest.mark.version('>=3.0') def test_1(act: Action, temp_file): with act.trace(svc_events=trace), act.connect_server() as srv: diff --git a/tests/bugs/core_3174_test.py b/tests/bugs/core_3174_test.py index b1f1b6b0..f107be3f 100644 --- a/tests/bugs/core_3174_test.py +++ b/tests/bugs/core_3174_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-3174 FBTEST: bugs.core_3174 +NOTES: + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -35,6 +41,7 @@ test_script = """ set plan on; + set list on; --set echo on; select '1.a' as test_no, id,'.' || c_pad || '.' as c_pad from t where trim(c_pad) = '123'; @@ -54,117 +61,240 @@ select '2.g' as test_no, id,'.' || c_nopad || '.' as c_nopad from t where trim(leading from c_nopad) starting with '123'; """ -act = isql_act('db', test_script, substitutions=[('=.*', '')]) - -expected_stdout = """ - PLAN (T NATURAL) - - TEST_NO ID C_PAD - ======= ============ ============ - 1.a 1 .123. - 1.a 2 . 123. - 1.a 3 .123 . - 1.a 4 . 123 . - - - PLAN (T NATURAL) - - TEST_NO ID C_PAD - ======= ============ ============ - 1.b 1 .123. - 1.b 3 .123 . - - - PLAN (T INDEX (T_C_PAD_TRIM_RIGHT)) - - TEST_NO ID C_PAD - ======= ============ ============ - 1.c 1 .123. - 1.c 3 .123 . - - - PLAN (T INDEX (T_C_PAD_TRIM_LEFT)) - - TEST_NO ID C_PAD - ======= ============ ============ - 1.d 1 .123. - 1.d 2 . 123. - 1.d 3 .123 . - 1.d 4 . 123 . - - - PLAN (T INDEX (T_C_PAD_TRIM_RIGHT)) - - TEST_NO ID C_PAD - ======= ============ ============ - 1.e 1 .123. - 1.e 3 .123 . - - - PLAN (T INDEX (T_C_PAD_TRIM_LEFT)) - - TEST_NO ID C_PAD - ======= ============ ============ - 1.f 1 .123. - 1.f 2 . 123. - 1.f 3 .123 . - 1.f 4 . 123 . - - - PLAN (T NATURAL) - - TEST_NO ID C_NOPAD - ======= ============ ================================================ - 2.a 1 .123. - 2.a 2 . 123. - 2.a 3 .123 . - 2.a 4 . 123 . - - - PLAN (T NATURAL) - - TEST_NO ID C_NOPAD - ======= ============ ================================================ - 2.b 1 .123. - - - PLAN (T INDEX (T_C_NOPAD_TRIM_RIGHT)) - - TEST_NO ID C_NOPAD - ======= ============ ================================================ - 2.c 1 .123. - 2.c 3 .123 . - - - PLAN (T INDEX (T_C_NOPAD_TRIM_LEFT)) - - TEST_NO ID C_NOPAD - ======= ============ ================================================ - 2.d 1 .123. - 2.d 2 . 123. - - - PLAN (T INDEX (T_C_NOPAD_TRIM_RIGHT)) - - TEST_NO ID C_NOPAD - ======= ============ ================================================ - 2.f 1 .123. - 2.f 3 .123 . - - - PLAN (T INDEX (T_C_NOPAD_TRIM_LEFT)) +substitutions = [ ('[ \t]+', ' ') ] +act = isql_act('db', test_script, substitutions = substitutions) + + +expected_out_5x = """ + PLAN (T NATURAL) + TEST_NO 1.a + ID 1 + C_PAD .123. + TEST_NO 1.a + ID 2 + C_PAD . 123. + TEST_NO 1.a + ID 3 + C_PAD .123 . + TEST_NO 1.a + ID 4 + C_PAD . 123 . + PLAN (T NATURAL) + TEST_NO 1.b + ID 1 + C_PAD .123. + TEST_NO 1.b + ID 3 + C_PAD .123 . + PLAN (T INDEX (T_C_PAD_TRIM_RIGHT)) + TEST_NO 1.c + ID 1 + C_PAD .123. + TEST_NO 1.c + ID 3 + C_PAD .123 . + PLAN (T INDEX (T_C_PAD_TRIM_LEFT)) + TEST_NO 1.d + ID 1 + C_PAD .123. + TEST_NO 1.d + ID 2 + C_PAD . 123. + TEST_NO 1.d + ID 3 + C_PAD .123 . + TEST_NO 1.d + ID 4 + C_PAD . 123 . + PLAN (T INDEX (T_C_PAD_TRIM_RIGHT)) + TEST_NO 1.e + ID 1 + C_PAD .123. + TEST_NO 1.e + ID 3 + C_PAD .123 . + PLAN (T INDEX (T_C_PAD_TRIM_LEFT)) + TEST_NO 1.f + ID 1 + C_PAD .123. + TEST_NO 1.f + ID 2 + C_PAD . 123. + TEST_NO 1.f + ID 3 + C_PAD .123 . + TEST_NO 1.f + ID 4 + C_PAD . 123 . + PLAN (T NATURAL) + TEST_NO 2.a + ID 1 + C_NOPAD .123. + TEST_NO 2.a + ID 2 + C_NOPAD . 123. + TEST_NO 2.a + ID 3 + C_NOPAD .123 . + TEST_NO 2.a + ID 4 + C_NOPAD . 123 . + PLAN (T NATURAL) + TEST_NO 2.b + ID 1 + C_NOPAD .123. + PLAN (T INDEX (T_C_NOPAD_TRIM_RIGHT)) + TEST_NO 2.c + ID 1 + C_NOPAD .123. + TEST_NO 2.c + ID 3 + C_NOPAD .123 . + PLAN (T INDEX (T_C_NOPAD_TRIM_LEFT)) + TEST_NO 2.d + ID 1 + C_NOPAD .123. + TEST_NO 2.d + ID 2 + C_NOPAD . 123. + PLAN (T INDEX (T_C_NOPAD_TRIM_RIGHT)) + TEST_NO 2.f + ID 1 + C_NOPAD .123. + TEST_NO 2.f + ID 3 + C_NOPAD .123 . + PLAN (T INDEX (T_C_NOPAD_TRIM_LEFT)) + TEST_NO 2.g + ID 1 + C_NOPAD .123. + TEST_NO 2.g + ID 2 + C_NOPAD . 123. + TEST_NO 2.g + ID 3 + C_NOPAD .123 . + TEST_NO 2.g + ID 4 + C_NOPAD . 123 . +""" - TEST_NO ID C_NOPAD - ======= ============ ================================================ - 2.g 1 .123. - 2.g 2 . 123. - 2.g 3 .123 . - 2.g 4 . 123 . +expected_out_6x = """ + PLAN ("PUBLIC"."T" NATURAL) + TEST_NO 1.a + ID 1 + C_PAD .123. + TEST_NO 1.a + ID 2 + C_PAD . 123. + TEST_NO 1.a + ID 3 + C_PAD .123 . + TEST_NO 1.a + ID 4 + C_PAD . 123 . + PLAN ("PUBLIC"."T" NATURAL) + TEST_NO 1.b + ID 1 + C_PAD .123. + TEST_NO 1.b + ID 3 + C_PAD .123 . + PLAN ("PUBLIC"."T" INDEX ("PUBLIC"."T_C_PAD_TRIM_RIGHT")) + TEST_NO 1.c + ID 1 + C_PAD .123. + TEST_NO 1.c + ID 3 + C_PAD .123 . + PLAN ("PUBLIC"."T" INDEX ("PUBLIC"."T_C_PAD_TRIM_LEFT")) + TEST_NO 1.d + ID 1 + C_PAD .123. + TEST_NO 1.d + ID 2 + C_PAD . 123. + TEST_NO 1.d + ID 3 + C_PAD .123 . + TEST_NO 1.d + ID 4 + C_PAD . 123 . + PLAN ("PUBLIC"."T" INDEX ("PUBLIC"."T_C_PAD_TRIM_RIGHT")) + TEST_NO 1.e + ID 1 + C_PAD .123. + TEST_NO 1.e + ID 3 + C_PAD .123 . + PLAN ("PUBLIC"."T" INDEX ("PUBLIC"."T_C_PAD_TRIM_LEFT")) + TEST_NO 1.f + ID 1 + C_PAD .123. + TEST_NO 1.f + ID 2 + C_PAD . 123. + TEST_NO 1.f + ID 3 + C_PAD .123 . + TEST_NO 1.f + ID 4 + C_PAD . 123 . + PLAN ("PUBLIC"."T" NATURAL) + TEST_NO 2.a + ID 1 + C_NOPAD .123. + TEST_NO 2.a + ID 2 + C_NOPAD . 123. + TEST_NO 2.a + ID 3 + C_NOPAD .123 . + TEST_NO 2.a + ID 4 + C_NOPAD . 123 . + PLAN ("PUBLIC"."T" NATURAL) + TEST_NO 2.b + ID 1 + C_NOPAD .123. + PLAN ("PUBLIC"."T" INDEX ("PUBLIC"."T_C_NOPAD_TRIM_RIGHT")) + TEST_NO 2.c + ID 1 + C_NOPAD .123. + TEST_NO 2.c + ID 3 + C_NOPAD .123 . + PLAN ("PUBLIC"."T" INDEX ("PUBLIC"."T_C_NOPAD_TRIM_LEFT")) + TEST_NO 2.d + ID 1 + C_NOPAD .123. + TEST_NO 2.d + ID 2 + C_NOPAD . 123. + PLAN ("PUBLIC"."T" INDEX ("PUBLIC"."T_C_NOPAD_TRIM_RIGHT")) + TEST_NO 2.f + ID 1 + C_NOPAD .123. + TEST_NO 2.f + ID 3 + C_NOPAD .123 . + PLAN ("PUBLIC"."T" INDEX ("PUBLIC"."T_C_NOPAD_TRIM_LEFT")) + TEST_NO 2.g + ID 1 + C_NOPAD .123. + TEST_NO 2.g + ID 2 + C_NOPAD . 123. + TEST_NO 2.g + ID 3 + C_NOPAD .123 . + TEST_NO 2.g + ID 4 + C_NOPAD . 123 . """ -@pytest.mark.version('>=3.0') +@pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_out_5x if act.is_version('<6') else expected_out_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_3176_test.py b/tests/bugs/core_3176_test.py index a2acc40f..1294bd1f 100644 --- a/tests/bugs/core_3176_test.py +++ b/tests/bugs/core_3176_test.py @@ -3,47 +3,52 @@ """ ID: issue-3550 ISSUE: 3550 -TITLE: View with "subselect" column join table and not use index +TITLE: Index is not used when view has "subselect" column DESCRIPTION: JIRA: CORE-3176 FBTEST: bugs.core_3176 +NOTES: + [30.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """CREATE TABLE TMP -( - ID Integer NOT NULL, - CONSTRAINT PK_TMP_1 PRIMARY KEY (ID) -); -COMMIT; -CREATE VIEW TMP_VIEW (ID1, ID2) -AS -SELECT 1,(SELECT 1 FROM RDB$DATABASE) FROM RDB$DATABASE; -COMMIT;""" - -db = db_factory(init=init_script) - -test_script = """SET PLAN ON; -SELECT * FROM tmp_view TV LEFT JOIN tmp T ON T.id=TV.id2; +db = db_factory() + +test_script = """ + create table tmp ( + id int not null, + constraint pk_tmp_1 primary key (id) + ); + create view tmp_view as + select 1 as id1, (select 1 from rdb$database) as id2 from rdb$database; + commit; + + set planonly; + select * from tmp_view tv left join tmp t on t.id=tv.id2; """ act = isql_act('db', test_script) -expected_stdout = """ -PLAN (TV RDB$DATABASE NATURAL) -PLAN (TV RDB$DATABASE NATURAL) -PLAN JOIN (TV RDB$DATABASE NATURAL, T INDEX (PK_TMP_1)) +expected_stdout_5x = """ + PLAN (TV RDB$DATABASE NATURAL) + PLAN (TV RDB$DATABASE NATURAL) + PLAN JOIN (TV RDB$DATABASE NATURAL, T INDEX (PK_TMP_1)) +""" - ID1 ID2 ID -============ ============ ============ - 1 1 +expected_stdout_6x = """ + PLAN ("TV" "SYSTEM"."RDB$DATABASE" NATURAL) + PLAN ("TV" "SYSTEM"."RDB$DATABASE" NATURAL) + PLAN JOIN ("TV" "SYSTEM"."RDB$DATABASE" NATURAL, "T" INDEX ("PUBLIC"."PK_TMP_1")) """ @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_3180_test.py b/tests/bugs/core_3180_test.py index 7d62839a..653a93ad 100644 --- a/tests/bugs/core_3180_test.py +++ b/tests/bugs/core_3180_test.py @@ -3,46 +3,42 @@ """ ID: issue-3554 ISSUE: 3554 -TITLE: ALTER VIEW with not matched columns in declaration and selection crashs the server +TITLE: ALTER VIEW with not matched columns in declaration and selection crashes the server DESCRIPTION: JIRA: CORE-3180 FBTEST: bugs.core_3180 +NOTES: + [27.06.2025] pzotov + Suppressed name of altered view in order to have same expected* text for versions prior/since 6.x + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """create view TEST_VIEW (ID) as select 1 from rdb$database; -commit;""" - -db = db_factory(init=init_script) +db = db_factory() -test_script = """alter view TEST_VIEW (ID) as select 1, 2 from rdb$database; -COMMIT; -SHOW VIEW TEST_VIEW; +test_script = """ + create view TEST_VIEW (ID) as select 1 from rdb$database; + alter view TEST_VIEW (ID) as select 1, 2 from rdb$database; + commit; """ -act = isql_act('db', test_script) +substitutions = [('(-)?ALTER VIEW \\S+ failed', 'ALTER VIEW failed')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ID INTEGER Expression -View Source: -==== ====== - select 1 from rdb$database -""" - -expected_stderr = """Statement failed, SQLSTATE = 07002 -unsuccessful metadata update --ALTER VIEW TEST_VIEW failed --SQL error code = -607 --Invalid command --number of columns does not match select list +expected_stdout = """ + Statement failed, SQLSTATE = 07002 + unsuccessful metadata update + ALTER VIEW failed + -SQL error code = -607 + -Invalid command + -number of columns does not match select list """ @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3222_test.py b/tests/bugs/core_3222_test.py index 9ff626a2..00477c2c 100644 --- a/tests/bugs/core_3222_test.py +++ b/tests/bugs/core_3222_test.py @@ -7,47 +7,53 @@ DESCRIPTION: JIRA: CORE-3222 FBTEST: bugs.core_3222 +NOTES: + [27.06.2025] pzotov + Re-implemented: use f-notation in order to remove hard-coded DDL string from test_script and expected_out. + Removed 'SHOW VIEW'command because its output can change in any intensive developing FB version. + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """CREATE TABLE Foo ( - Bar INTEGER, - Str CHAR(31) -); -COMMIT; +db = db_factory() + +VIEW_DDL = """ + select bar + from test + where(trim(str) = 'test') + with check option """ -db = db_factory(init=init_script) - -test_script = """CREATE VIEW VIEW_Foo ( - Bar -) AS SELECT - Bar - FROM Foo - WHERE(Trim(Str) = 'test') -WITH CHECK OPTION -; -COMMIT; -SHOW VIEW VIEW_Foo; +test_script = f""" + set list on; + set blob all; + set count on; + create table test ( + bar integer, + str char(31) + ); + commit; + + create view v_test (bar) as + {VIEW_DDL} + ; + commit; + select r.rdb$view_source as blob_id from rdb$relations r where r.rdb$relation_name = upper('v_test'); """ -act = isql_act('db', test_script) +substitutions = [('BLOB_ID .*', '')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """BAR INTEGER Nullable -View Source: -==== ====== - SELECT - Bar - FROM Foo - WHERE(Trim(Str) = 'test') -WITH CHECK OPTION +expected_stdout = f""" + {VIEW_DDL} + Records affected: 1 """ @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3227_test.py b/tests/bugs/core_3227_test.py index f3ea9b00..39f30fc7 100644 --- a/tests/bugs/core_3227_test.py +++ b/tests/bugs/core_3227_test.py @@ -32,6 +32,7 @@ """ +@pytest.mark.intl @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_3234_test.py b/tests/bugs/core_3234_test.py index 85f2d3ff..b8f5f182 100644 --- a/tests/bugs/core_3234_test.py +++ b/tests/bugs/core_3234_test.py @@ -203,6 +203,7 @@ TRIMMED_CHAR_LEN 11 """ +@pytest.mark.intl @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_3239_test.py b/tests/bugs/core_3239_test.py index 273a1645..c6f253f9 100644 --- a/tests/bugs/core_3239_test.py +++ b/tests/bugs/core_3239_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-3239 FBTEST: bugs.core_3239 +NOTES: + [27.06.2025] pzotov + Suppressed name of altered view in order to have same expected* text for versions prior/since 6.x + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -63,27 +68,39 @@ where a.bfield = true; """ -act = isql_act('db', test_script) +substitutions = [ ('[ \t]+', ' ') ] +act = isql_act('db', test_script, substitutions = substitutions) + -expected_stdout = """ +expected_out_5x = """ PLAN (TEST INDEX (TEST_BOOL_CIAI_CI)) RULE_ID 2 RULE_ID 3 - PLAN (TEST INDEX (TEST_INT_CI_CIAI)) RULE_ID 1 - PLAN (TEST INDEX (TEST_BOOL_CIAI_CI)) RULE_ID 2 - PLAN JOIN (A INDEX (TEST_BOOL_CIAI_CI), B INDEX (TEST_INT_CI_CIAI)) RULE_ID 1 RULE_ID 4 """ -@pytest.mark.version('>=3.0') +expected_out_6x = """ + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_BOOL_CIAI_CI")) + RULE_ID 2 + RULE_ID 3 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_INT_CI_CIAI")) + RULE_ID 1 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_BOOL_CIAI_CI")) + RULE_ID 2 + PLAN JOIN ("A" INDEX ("PUBLIC"."TEST_BOOL_CIAI_CI"), "B" INDEX ("PUBLIC"."TEST_INT_CI_CIAI")) + RULE_ID 1 + RULE_ID 4 +""" + +@pytest.mark.intl +@pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_out_5x if act.is_version('<6') else expected_out_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_3283_test.py b/tests/bugs/core_3283_test.py index 6253b908..c5b039ea 100644 --- a/tests/bugs/core_3283_test.py +++ b/tests/bugs/core_3283_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-3283 FBTEST: bugs.core_3283 +NOTES: + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -43,14 +49,18 @@ act = isql_act('db', test_script) -expected_stdout = """ +expected_out_5x = """ PLAN SORT (JOIN (T2 INDEX (TTT_ID), T3 INDEX (TTT_ID))) PLAN (T1 INDEX (TTT_ID)) """ +expected_out_6x = """ + PLAN SORT (JOIN ("T2" INDEX ("PUBLIC"."TTT_ID"), "T3" INDEX ("PUBLIC"."TTT_ID"))) + PLAN ("T1" INDEX ("PUBLIC"."TTT_ID")) +""" + @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_out_5x if act.is_version('<6') else expected_out_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_3305_test.py b/tests/bugs/core_3305_test.py index 3b485147..27f8cf5b 100644 --- a/tests/bugs/core_3305_test.py +++ b/tests/bugs/core_3305_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-3305 FBTEST: bugs.core_3305 +NOTES: + [27.06.2025] pzotov + Reimplemented: make single test function for the whole code, use different variables to store output on 3.x / 4.x+5.x / 6.x + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -14,67 +20,44 @@ db = db_factory() -# version: 3.0 - -test_script_1 = """ - recreate table t(v int); - commit; +test_script = """ + recreate table test(v int); set term ^; - create or alter trigger t_ai for t active after insert position 0 as + create or alter trigger test_ai for test active AFTER insert position 0 as begin new.v = 1; end ^ set term ;^ - commit; - insert into t(v) values(123); + insert into test(v) values(123); rollback; """ -act_1 = isql_act('db', test_script_1) +act = isql_act('db', test_script) -expected_stderr_1 = """ +expected_out_3x = """ Statement failed, SQLSTATE = 42000 attempted update of read-only column Statement failed, SQLSTATE = 42000 attempted update of read-only column """ -@pytest.mark.version('>=3,<4.0') -def test_1(act_1: Action): - act_1.expected_stderr = expected_stderr_1 - act_1.execute() - assert act_1.clean_stderr == act_1.clean_expected_stderr - -# version: 4.0 - -test_script_2 = """ - recreate table t(v int); - commit; - set term ^; - create or alter trigger t_ai for t active after insert position 0 as - begin - new.v = 1; - end - ^ - set term ;^ - commit; - insert into t(v) values(123); - rollback; +expected_out_5x = """ + Statement failed, SQLSTATE = 42000 + attempted update of read-only column TEST.V + Statement failed, SQLSTATE = 42000 + attempted update of read-only column TEST.V """ -act_2 = isql_act('db', test_script_2) - -expected_stderr_2 = """ +expected_out_6x = """ Statement failed, SQLSTATE = 42000 - attempted update of read-only column T.V + attempted update of read-only column "PUBLIC"."TEST"."V" Statement failed, SQLSTATE = 42000 - attempted update of read-only column T.V + attempted update of read-only column "PUBLIC"."TEST"."V" """ -@pytest.mark.version('>=4.0') -def test_2(act_2: Action): - act_2.expected_stderr = expected_stderr_2 - act_2.execute() - assert act_2.clean_stderr == act_2.clean_expected_stderr - +@pytest.mark.version('>=3') +def test_1(act: Action): + act.expected_stdout = expected_out_3x if act.is_version('<4') else expected_out_5x if act.is_version('<6') else expected_out_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3311_test.py b/tests/bugs/core_3311_test.py index 04d11e7d..f3569089 100644 --- a/tests/bugs/core_3311_test.py +++ b/tests/bugs/core_3311_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-3311 FBTEST: bugs.core_3311 +NOTES: + [27.06.2025] pzotov + Reimplemented: it is enough to check only STDERR in this test rather that compare issued execution plans. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -48,20 +53,12 @@ act = isql_act('db', test_script) -expected_stdout = """ - PLAN (TEST NATURAL) - PLAN (TEST ORDER TEST_ID) - PLAN (TEST NATURAL) - PLAN (TEST ORDER TEST_ID) - PLAN (TEST NATURAL) - PLAN (TEST ORDER TEST_ID) - PLAN JOIN (S TEST INDEX (TEST_ID), T INDEX (TEST_ID)) - PLAN JOIN (S TEST ORDER TEST_ID, T INDEX (TEST_ID)) +expected_stderr = """ """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() - assert act.clean_stdout == act.clean_expected_stdout + act.expected_stderr = expected_stderr + act.execute() # ::: do not use 'combine_outpt = True! We have to check here only STDERR ::: + assert act.clean_stderr == act.clean_expected_stderr diff --git a/tests/bugs/core_3312_test.py b/tests/bugs/core_3312_test.py index 9d32ae36..7dae875d 100644 --- a/tests/bugs/core_3312_test.py +++ b/tests/bugs/core_3312_test.py @@ -3,10 +3,17 @@ """ ID: issue-3679 ISSUE: 3679 -TITLE: Sub-optimal join plan when the slave table depends on the master one via the OR predicate +TITLE: Sub-optimal join plan when the slave table depends on the master via the "OR" predicate DESCRIPTION: JIRA: CORE-3312 FBTEST: bugs.core_3312 +NOTES: + [27.06.2025] pzotov + Added substitutions in order to suppress digital suffix of indices name (in 'RDB$INDEX_'). + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -14,7 +21,8 @@ db = db_factory() -test_script = """SET PLANONLY ON; +test_script = """ +SET PLANONLY ON; select * from rdb$relations r join rdb$security_classes sc @@ -27,16 +35,23 @@ or (r.rdb$default_class = sc.rdb$security_class and r.rdb$relation_id = 1); """ -act = isql_act('db', test_script) -expected_stdout = """ -PLAN JOIN (R NATURAL, SC INDEX (RDB$INDEX_7, RDB$INDEX_7)) -PLAN JOIN (R INDEX (RDB$INDEX_1, RDB$INDEX_1), SC INDEX (RDB$INDEX_7, RDB$INDEX_7)) +substitutions = [ ('[ \t]+', ' '), ('RDB\\$INDEX_\\d+', 'RDB$INDEX_*') ] +act = isql_act('db', test_script, substitutions = substitutions) + + +expected_out_5x = """ + PLAN JOIN (R NATURAL, SC INDEX (RDB$INDEX_*, RDB$INDEX_*)) + PLAN JOIN (R INDEX (RDB$INDEX_*, RDB$INDEX_*), SC INDEX (RDB$INDEX_*, RDB$INDEX_*)) +""" + +expected_out_6x = """ + PLAN JOIN ("R" NATURAL, "SC" INDEX ("SYSTEM"."RDB$INDEX_*", "SYSTEM"."RDB$INDEX_*")) + PLAN JOIN ("R" INDEX ("SYSTEM"."RDB$INDEX_*", "SYSTEM"."RDB$INDEX_*"), "SC" INDEX ("SYSTEM"."RDB$INDEX_*", "SYSTEM"."RDB$INDEX_*")) """ @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_out_5x if act.is_version('<6') else expected_out_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_3323_test.py b/tests/bugs/core_3323_test.py index 88d8f6ce..a66eda82 100644 --- a/tests/bugs/core_3323_test.py +++ b/tests/bugs/core_3323_test.py @@ -22,7 +22,6 @@ * every worker log must contain text 'SQLSTATE = 08003'; * no alive ISQL processes remain after issuing 'delete from mon$..' statements. JIRA: CORE-3323 -FBTEST: bugs.core_3323 NOTES: [17.11.2021] pcisar This test is too complicated and fragile (can screw the test environment) @@ -35,6 +34,13 @@ or some trouble occurs with deleting from mon$attachments. Checked on 3.0.8.33535 (SS/CS), 4.0.1.2692 (SS/CS), 5.0.0.730 (SS/CS) - both Linux and Windows. + + [18.01.2025] pzotov + Resultset of cursor that executes using instance of selectable PreparedStatement must be stored + in some variable in order to have ability close it EXPLICITLY (before PS will be freed). + Otherwise access violation raises during Python GC and pytest hangs at final point (does not return control to OS). + This occurs at least for: Python 3.11.2 / pytest: 7.4.4 / firebird.driver: 1.10.6 / Firebird.Qa: 0.19.3 + The reason of that was explained by Vlad, 26.10.24 17:42 ("oddities when use instances of selective statements"). """ import time import datetime as py_dt @@ -46,6 +52,7 @@ import pytest from firebird.qa import * +from firebird.driver import DatabaseError ########################### ### S E T T I N G S ### @@ -65,7 +72,7 @@ # because we kill its attachment (see 'p_isql.wait(...)'), seconds. # See also: https://docs.python.org/3/library/subprocess.html#subprocess.Popen.wait # -MAX_WAIT_FOR_ISQL_FINISH_S = 5 +MAX_WAIT_FOR_ISQL_FINISH_S = 10 init_ddl = f""" recreate table test(id int primary key using descending index test_id_desc); @@ -102,15 +109,16 @@ def test_1(act: Action, tmp_isql_cmds: List[Path], tmp_isql_logs: List[Path], ca where s.mon$attachment_id <> current_connection and s.mon$sql_text containing cast(? as varchar(20)) """ with con.cursor() as cur: - ps = cur.prepare(sql_check_appearance) - worker_att_list = [] - worker_log_list = [] - worker_pid_list = [] + ps, rs = None, None + try: + ps = cur.prepare(sql_check_appearance) + worker_att_list = [] + worker_log_list = [] + worker_pid_list = [] - for worker_i in range(0, CONCURRENT_ATT_CNT): - worker_log_list.append( open(tmp_isql_logs[worker_i], 'w') ) + for worker_i in range(0, CONCURRENT_ATT_CNT): + worker_log_list.append( open(tmp_isql_logs[worker_i], 'w') ) - try: for worker_i in range(0, CONCURRENT_ATT_CNT): if worker_i < CONCURRENT_ATT_CNT-1: @@ -147,7 +155,17 @@ def test_1(act: Action, tmp_isql_cmds: List[Path], tmp_isql_logs: List[Path], ca print(f'TIMEOUT EXPIRATION: waiting for ISQL process on iter {worker_i} took {dd} ms which exceeds limit = {MAX_WAIT_FOR_ISQL_START_MS} ms.') break - worker_att = cur.execute(ps, (f'TAG_{worker_i}',)).fetchone() + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + rs = cur.execute(ps, (f'TAG_{worker_i}',)) + worker_att = None + for r in rs: + worker_att = r + con.commit() if worker_att: @@ -159,23 +177,32 @@ def test_1(act: Action, tmp_isql_cmds: List[Path], tmp_isql_logs: List[Path], ca # result: all ISQLs are launched and their attachments are visible in mon$attachments (and can be traversed via worker_att_list) - ps = cur.prepare('delete from mon$attachments a where a.mon$attachment_id = ?') + kill_sttm = cur.prepare('delete from mon$attachments a where a.mon$attachment_id = ?') ################################################################### ### k i l l a t t a c h m e n t s o n e - b y - o n e ### ################################################################### for worker_id in reversed(worker_att_list): - cur.execute(ps, (worker_id,)) + cur.execute(kill_sttm, (worker_id,)) + + except DatabaseError as e: + print( e.__str__() ) + print(e.gds_codes) finally: - + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() + for i,p_isql in enumerate(worker_pid_list): p_isql.wait(MAX_WAIT_FOR_ISQL_FINISH_S) print(f'returncode for ISQL worker #{i}:',p.poll()) - for f in worker_log_list: f.close() + + # All worker logs must contain 'SQLSTATE = 08003' pattern (i.e. 'connection shutdown'): p_shutdown = re.compile('SQLSTATE\\s+=\\s+08003', re.IGNORECASE) for g in worker_log_list: @@ -185,8 +212,9 @@ def test_1(act: Action, tmp_isql_cmds: List[Path], tmp_isql_logs: List[Path], ca pass else: print('Pattern ',p_shutdown,' NOT FOUND in the log ',g.name,':') + print('=== beg of log ===') print(txt) - print('='*50) + print('=== end of log ===') con.commit() # NO any ISQL worker must be alive now: diff --git a/tests/bugs/core_3338_test.py b/tests/bugs/core_3338_test.py index 4242ad09..f47b0ae2 100644 --- a/tests/bugs/core_3338_test.py +++ b/tests/bugs/core_3338_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-3338 FBTEST: bugs.core_3338 +NOTES: + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -29,14 +35,18 @@ act = isql_act('db', test_script) -expected_stdout = """ +expected_out_5x = """ PLAN (T INDEX (T_N2_COALESCE)) PLAN (T INDEX (T_N2_DECODE)) """ -@pytest.mark.version('>=3.0') +expected_out_6x = """ + PLAN ("PUBLIC"."T" INDEX ("PUBLIC"."T_N2_COALESCE")) + PLAN ("PUBLIC"."T" INDEX ("PUBLIC"."T_N2_DECODE")) +""" + +@pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_out_5x if act.is_version('<6') else expected_out_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_3353_test.py b/tests/bugs/core_3353_test.py index 77380bfa..c784f1d1 100644 --- a/tests/bugs/core_3353_test.py +++ b/tests/bugs/core_3353_test.py @@ -17,6 +17,12 @@ Added 'SQLSTATE' in substitutions: runtime error must not be filtered out by '?!(...)' pattern ("negative lookahead assertion", see https://docs.python.org/3/library/re.html#regular-expression-syntax). Added 'combine_output = True' in order to see SQLSTATE if any error occurs. + + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -51,14 +57,19 @@ def test_1(act: Action): # version: 4.0 -expected_stdout_2 = """ +expected_out_5x = """ 01: sqltype: 520 BLOB Nullable scale: 0 subtype: 1 len: 8 charset: 4 UTF8 01: sqltype: 520 BLOB Nullable scale: 0 subtype: 1 len: 8 charset: 4 UTF8 """ +expected_out_6x = """ + 01: sqltype: 520 BLOB Nullable scale: 0 subtype: 1 len: 8 charset: 4 SYSTEM.UTF8 + 01: sqltype: 520 BLOB Nullable scale: 0 subtype: 1 len: 8 charset: 4 SYSTEM.UTF8 +""" + @pytest.mark.version('>=4.0') def test_2(act: Action): - act.expected_stdout = expected_stdout_2 + act.expected_stdout = expected_out_5x if act.is_version('<6') else expected_out_6x act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3357_test.py b/tests/bugs/core_3357_test.py index d592aac6..25c144b3 100644 --- a/tests/bugs/core_3357_test.py +++ b/tests/bugs/core_3357_test.py @@ -5,13 +5,20 @@ ISSUE: 3723 TITLE: Generators are set to 0 after restore DESCRIPTION: - FB 4.x has incompatible behaviour with all previous versions since build 4.0.0.2131 (06-aug-2020): - statement 'alter sequence restart with 0' changes rdb$generators.rdb$initial_value to -1 thus - next call of gen_id(,1) will return 0 (ZERO!) rather than 1. - See also CORE-6084 and its fix: https://github.com/FirebirdSQL/firebird/commit/23dc0c6297825b2e9006f4d5a2c488702091033d - This is considered as *expected* and is noted in doc/README.incompatibilities.3to4.txt + FB 4.x has incompatible behaviour with all previous versions since build 4.0.0.2131 (06-aug-2020): + statement 'alter sequence restart with 0' changes rdb$generators.rdb$initial_value to -1 thus + next call of gen_id(,1) will return 0 (ZERO!) rather than 1. + See also CORE-6084 and related commit: + https://github.com/FirebirdSQL/firebird/commit/23dc0c6297825b2e9006f4d5a2c488702091033d + This is considered as *expected* and is noted in doc/README.incompatibilities.3to4.txt JIRA: CORE-3357 FBTEST: bugs.core_3357 +NOTES: + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -29,20 +36,21 @@ act = python_act('db') -# version: 3.0 - -expected_stdout_3 = """ +expected_out_3x = """ Generator G1, current value: 9223372036854775807, initial value: 9223372036854775807, increment: -2147483647 Generator G2, current value: -9223372036854775808, initial value: -9223372036854775808, increment: 2147483647 """ -# version: 4.0 - -expected_stdout_4 = """ +expected_out_5x = """ Generator G1, current value: -9223372034707292162, initial value: 9223372036854775807, increment: -2147483647 Generator G2, current value: 9223372034707292161, initial value: -9223372036854775808, increment: 2147483647 """ +expected_out_6x = """ + Generator PUBLIC.G1, current value: -9223372034707292162, initial value: 9223372036854775807, increment: -2147483647 + Generator PUBLIC.G2, current value: 9223372034707292161, initial value: -9223372036854775808, increment: 2147483647 +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): with act.connect_server() as srv: @@ -51,6 +59,7 @@ def test_1(act: Action): backup.seek(0) srv.database.local_restore(backup_stream=backup, database=act.db.db_path, flags=SrvRestoreFlag.REPLACE) - act.expected_stdout = expected_stdout_4 if act.is_version('>=4') else expected_stdout_3 - act.isql(switches=[], input="show sequ g1; show sequ g2;") + + act.expected_stdout = expected_out_3x if act.is_version('<4') else expected_out_5x if act.is_version('<6') else expected_out_6x + act.isql(switches = ['-q'], input="show sequ g1; show sequ g2;", combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3364_test.py b/tests/bugs/core_3364_test.py index aed19b66..de8b74ff 100644 --- a/tests/bugs/core_3364_test.py +++ b/tests/bugs/core_3364_test.py @@ -35,33 +35,49 @@ test_script = """ set list on; set blob all; - select rdb$debug_info from rdb$procedures where upper(rdb$procedure_name) = upper('sp_test'); + select rdb$debug_info as blob_id from rdb$procedures where upper(rdb$procedure_name) = upper('sp_test'); """ -act = isql_act('db', test_script, substitutions=[('RDB\\$DEBUG_INFO', ''), ('-', ''), - ('[0-9]+[ ]+[0-9]+[ ]+[0-9]+', '')]) +''' +BLOB_ID 1a:f0 +Parameters: +Number Name Type +-------------------------------------------------- +0 A_N INPUT +0 N_FACT OUTPUT +Variables: +Number Name +------------------------------------------- +0 N_FACT +BLR to Source mapping: +BLR offset Line Column +-------------------------------- +36 2 5 +38 3 9 +73 5 9 +92 6 9 +94 7 11 +116 8 11 +142 10 9 +''' + +substitutions=[ ('BLOB_ID .*', ''), + ('[ \t]+', ' '), + ('-', ''), + ('\\d+[ ]+\\d+[ ]+\\d+', '') + ] +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ - RDB$DEBUG_INFO 1a:f0 Parameters: - Number Name Type - -------------------------------------------------- - 0 A_N INPUT - 0 N_FACT OUTPUT + Number Name Type + 0 A_N INPUT + 0 N_FACT OUTPUT Variables: Number Name - ------------------------------------------- 0 N_FACT BLR to Source mapping: - BLR offset Line Column - -------------------------------- - 36 2 5 - 38 3 9 - 73 5 9 - 92 6 9 - 94 7 11 - 116 8 11 - 142 10 9 + BLR offset Line Column """ @pytest.mark.version('>=3.0') diff --git a/tests/bugs/core_3394_test.py b/tests/bugs/core_3394_test.py index 7c452ef9..ca6d91a8 100644 --- a/tests/bugs/core_3394_test.py +++ b/tests/bugs/core_3394_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-3394 FBTEST: bugs.core_3394 +NOTES: + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -30,18 +36,25 @@ rollback; """ -act = isql_act('db', test_script, substitutions=[('-At block line: [\\d]+, col: [\\d]+', '-At block line')]) +act = isql_act('db', test_script, substitutions=[('(-)?At block line: [\\d]+, col: [\\d]+', 'At block line')]) -expected_stderr = """ +expected_out_5x = """ Statement failed, SQLSTATE = 23000 violation of PRIMARY or UNIQUE KEY constraint "T_PK" on table "T" -Problematic key value is ("ID" = 1) - -At block line: 5, col: 7 + At block line +""" + +expected_out_6x = """ + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "T_PK" on table "PUBLIC"."T" + -Problematic key value is ("ID" = 1) + At block line """ -@pytest.mark.version('>=3') -def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr +@pytest.mark.version('>=3.0') +def test_2(act: Action): + act.expected_stdout = expected_out_5x if act.is_version('<6') else expected_out_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3401_test.py b/tests/bugs/core_3401_test.py index 5d369048..dbadd0de 100644 --- a/tests/bugs/core_3401_test.py +++ b/tests/bugs/core_3401_test.py @@ -164,6 +164,7 @@ EQUAL 1 """ +@pytest.mark.intl @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_3413_test.py b/tests/bugs/core_3413_test.py index fccab271..eb44ee6b 100644 --- a/tests/bugs/core_3413_test.py +++ b/tests/bugs/core_3413_test.py @@ -30,6 +30,7 @@ 'log_statement_finish = foo' ] +@pytest.mark.trace @pytest.mark.version('>=3.0') def test_1(act: Action): with act.trace(db_events=trace): diff --git a/tests/bugs/core_3416_test.py b/tests/bugs/core_3416_test.py index 2a172095..9e315357 100644 --- a/tests/bugs/core_3416_test.py +++ b/tests/bugs/core_3416_test.py @@ -48,6 +48,7 @@ script_file = temp_file('test_script.sql') +@pytest.mark.intl @pytest.mark.version('>=3') def test_1(act: Action, script_file: Path): script_file.write_text(""" diff --git a/tests/bugs/core_3419_test.py b/tests/bugs/core_3419_test.py index c25a7bb7..18c6a798 100644 --- a/tests/bugs/core_3419_test.py +++ b/tests/bugs/core_3419_test.py @@ -7,13 +7,18 @@ DESCRIPTION: JIRA: CORE-3419 FBTEST: bugs.core_3419 +NOTES: + [27.06.2025] pzotov + Reimplemented: it is enought to check that first lines of error message appear, w/o name of trigger. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ +import locale import pytest from firebird.qa import * db = db_factory() - test_script = """ set autoddl off; commit; @@ -21,7 +26,7 @@ commit; set term ^; -- This trigger will fire 1001 times before exception raising: - create or alter trigger trg_trans_start + create or alter trigger tx_trg active on transaction start position 0 as begin @@ -34,39 +39,15 @@ set transaction; """ -act = isql_act('db', test_script, substitutions=[('line: [0-9]+, col: [0-9]+', 'line: , col: ')]) +act = isql_act('db', substitutions=[('(-)?At trigger .*', '')]) -expected_stderr = """ +expected_out = f""" Statement failed, SQLSTATE = 54001 Too many concurrent executions of the same request - -At trigger 'TRG_TRANS_START' line: 5, col: 9 - At trigger 'TRG_TRANS_START' line: 5, col: 9 - At trigger 'TRG_TRANS_START' line: 5, col: 9 - At trigger 'TRG_TRANS_START' line: 5, col: 9 - At trigger 'TRG_TRANS_START' line: 5, col: 9 - At trigger 'TRG_TRANS_START' line: 5, col: 9 - At trigger 'TRG_TRANS_START' line: 5, col: 9 - At trigger 'TRG_TRANS_START' line: 5, col: 9 - At trigger 'TRG_TRANS_START' line: 5, col: 9 - At trigger 'TRG_TRANS_START' line: 5, col: 9 - At trigger 'TRG_TRANS_START' line: 5, col: 9 - At trigger 'TRG_TRANS_START' line: 5, col: 9 - At trigger 'TRG_TRANS_START' line: 5, col: 9 - At trigger 'TRG_TRANS_START' line: 5, col: 9 - At trigger 'TRG_TRANS_START' line: 5, col: 9 - At trigger 'TRG_TRANS_START' line: 5, col: 9 - At trigger 'TRG_TRANS_START' line: 5, col: 9 - At trigger 'TRG_TRANS_START' line: 5, col: 9 - At trigger 'TRG_TRANS_START' line: 5, col: 9 - At trigger 'TRG_TRANS_START' line: 5, col: 9 - At trigger 'TRG_TRANS_START' line: 5, col: 9 - At trigger 'TRG_TRANS_START' line: 5, col: 9 - At trigger 'TRG_TRANS_START' ... """ -@pytest.mark.version('>=3') -def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute(charset='utf8') - assert act.clean_stderr == act.clean_expected_stderr - +@pytest.mark.version('>=3.0') +def test_2(act: Action): + act.expected_stdout = expected_out + act.isql(switches=['-q'], input = test_script, combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3421_test.py b/tests/bugs/core_3421_test.py index 5de88dee..3e3338c2 100644 --- a/tests/bugs/core_3421_test.py +++ b/tests/bugs/core_3421_test.py @@ -12,41 +12,41 @@ Added 'Error reading/writing' in substitutions: runtime error must not be filtered out by '?!(...)' pattern ("negative lookahead assertion", see https://docs.python.org/3/library/re.html#regular-expression-syntax). Added 'combine_output = True' in order to see message related to any error. + + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """ - create or alter procedure sp_test as begin end; - commit; +db = db_factory() + +test_script = """ + set bail on; + set list on; + set sqlda_display on; + recreate table test( id bigint constraint test_id_pk primary key using index test_id_pk, s01 varchar(512) ); - commit; - set term ^; create or alter procedure sp_test( a_id type of column test.id, a_s01 type of column test.s01) - returns(o_id type of column test.id, o_s01 type of column test.s01) - as + returns(o_id type of column test.id, o_s01 type of column test.s01) as begin - execute statement ('update or insert into test(id, s01) values( :x, :y ) returning id, s01') - ( x := a_id, y := a_s01 ) - into o_id, o_s01; - suspend; + execute statement ('update or insert into test(id, s01) values( :x, :y ) returning id, s01') + ( x := a_id, y := a_s01 ) + into o_id, o_s01; + suspend; end ^ set term ;^ commit; -""" - -db = db_factory(init=init_script) -test_script = """ - set bail on; - set list on; - set sqlda_display on; set planonly; set term ^; execute block returns(o_id type of column test.id, o_s01 type of column test.s01) @@ -65,7 +65,7 @@ # act = isql_act('db', test_script, substitutions = [ ('^((?!(sqltype|(Error\\s+(reading|writing)) )).)*$', ''), ('[ \t]+', ' ') ] ) -expected_stdout = """ +expected_stdout_5x = """ 01: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 02: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 512 charset: 0 NONE 01: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 @@ -74,9 +74,17 @@ 02: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 512 charset: 0 NONE """ -@pytest.mark.version('>=3.0') +expected_stdout_6x = """ + 01: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + 02: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 512 charset: 0 SYSTEM.NONE + 01: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + 02: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 512 charset: 0 SYSTEM.NONE + 01: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + 02: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 512 charset: 0 SYSTEM.NONE +""" + +@pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_3450_test.py b/tests/bugs/core_3450_test.py index 4e5b8521..111a4541 100644 --- a/tests/bugs/core_3450_test.py +++ b/tests/bugs/core_3450_test.py @@ -8,16 +8,11 @@ JIRA: CORE-3450 FBTEST: bugs.core_3450 NOTES: - [30.11.2016] pzotov - It seems that we have regression in current 4.0 snapshots (elapsed time more than 10x comparing with 2.5). - Also, 4.0 has different plan comparing with 3.0. - After discuss with dimitr it was decided to commit this test into fbt-repo in order to have constant - reminder about this issue. - Currently this test should FAIL on 4.0. - - [22.09.2022] pzotov - Removed SKIP notation because test works fine on FB 4.0.1 - Checked on 3.0.8.33535, 4.0.1.2692, 5.0.0.736 + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -101,13 +96,16 @@ act = isql_act('db', test_script) -expected_stdout = """ +expected_stdout_5x = """ PLAN JOIN (JOIN (P1 NATURAL, T1 INDEX (TEST_1_F2)), T2 INDEX (TEST_2_F1), T3 INDEX (TEST_3_F1)) """ +expected_stdout_6x = """ + PLAN JOIN (JOIN ("P1" NATURAL, "T1" INDEX ("PUBLIC"."TEST_1_F2")), "T2" INDEX ("PUBLIC"."TEST_2_F1"), "T3" INDEX ("PUBLIC"."TEST_3_F1")) +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_3474_test.py b/tests/bugs/core_3474_test.py index 497f618f..bf2d522c 100644 --- a/tests/bugs/core_3474_test.py +++ b/tests/bugs/core_3474_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-3474 FBTEST: bugs.core_3474 +NOTES: + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -36,27 +42,32 @@ rows 1; """ -act = isql_act('db', test_script, substitutions=[('-At line.*', '')]) +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' '), ('(-)?At line.*', '')]) -expected_stdout = """ +expected_stdout_5x = """ + Statement failed, SQLSTATE = 42S22 + Dynamic SQL Error + -SQL error code = -206 + -Column unknown + -E.EMP_NO EMP_2 2 NAME_2 Nelson PROJ_2 """ -expected_stderr = """ +expected_stdout_6x = """ Statement failed, SQLSTATE = 42S22 Dynamic SQL Error -SQL error code = -206 -Column unknown - -E.EMP_NO + -"E"."EMP_NO" + EMP_2 2 + NAME_2 Nelson + PROJ_2 """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3489_test.py b/tests/bugs/core_3489_test.py index f959cf20..7cd10edf 100644 --- a/tests/bugs/core_3489_test.py +++ b/tests/bugs/core_3489_test.py @@ -1,66 +1,103 @@ -#coding:utf-8 - -""" -ID: issue-3848 -ISSUE: 3848 -TITLE: Blob transliteration may not happen inside the union -DESCRIPTION: -JIRA: CORE-3489 -FBTEST: bugs.core_3489 -NOTES: - [06.10.2022] pzotov - Could not complete adjusting for LINUX in new-qa. - DEFERRED. -""" -import platform -import pytest -from pathlib import Path -from firebird.qa import * - -init_script = """ - set term ^; - create or alter procedure sp_test - returns ( - msg_blob_id blob sub_type 1 segment size 80 character set unicode_fss) - AS - begin - msg_blob_id= 'Это проверка на вывод строки "Йцукёнг"'; -- text in cyrillic - suspend; - end - ^ - set term ;^ - commit; -""" - -db = db_factory(charset='WIN1251', init=init_script) - -act = python_act('db', substitutions=[('MSG_BLOB_ID.*', '')]) - -expected_stdout = """ - Это проверка на вывод строки "Йцукёнг" - Это проверка на вывод строки "Йцукёнг" - Records affected: 2 -""" - -script_file = temp_file('test_script.sql') - -@pytest.mark.skipif(platform.system() != 'Windows', reason='FIXME: see notes') -@pytest.mark.version('>=3') -def test_1(act: Action, script_file: Path): - script_file.write_text(""" - set list on; - set blob all; - set count on; - set list on; - - select msg_blob_id - from sp_test - union - select msg_blob_id - from sp_test; - """, encoding='cp1251') - act.expected_stdout = expected_stdout - act.isql(switches=[], input_file=script_file, charset='WIN1251') - assert act.clean_stdout == act.clean_expected_stdout - - +#coding:utf-8 + +""" +ID: issue-3848 +ISSUE: 3848 +TITLE: Blob transliteration may not happen inside the union +DESCRIPTION: +JIRA: CORE-3489 +FBTEST: bugs.core_3489 +NOTES: + [30.10.2024] pzotov + Bug was fixed for too old FB (3.0 Alpha 1), firebird-driver and/or QA-plugin + will not able to run on this version in order to reproduce problem. + Source for this test was taken from ticket almost w/o changes. Only aux view has been added ('v_conn_cset') for + showing current connection protocol and character set - we make query to this view two twice: one for TCP and then + for local protocol. + + Checked on 6.0.0.511 (Windows/Linux); 5.0.2.1550; 4.0.6.3165; 3.0.2.32670, 3,0,1,32609 +""" + +import locale +from pathlib import Path +import pytest +from firebird.qa import * + +db = db_factory(charset='WIN1251') + +act = python_act('db', substitutions=[('MSG_BLOB_ID.*', ''), ('TCPv(4|6)', 'TCP')]) + +expected_stdout = """ + CONN_PROTOCOL TCP + CONNECTION_CSET WIN1251 + DB_DEFAULT_CSET WIN1251 + Records affected: 1 + Это проверка на вывод строки "Йцукёнг" + Это проверка на вывод строки "Йцукёнг" + Records affected: 2 + + CONN_PROTOCOL + CONNECTION_CSET WIN1251 + DB_DEFAULT_CSET WIN1251 + Records affected: 1 + Это проверка на вывод строки "Йцукёнг" + Это проверка на вывод строки "Йцукёнг" + Records affected: 2 +""" + +tmp_sql = temp_file('tmp_core_3489.sql') + +@pytest.mark.intl +@pytest.mark.version('>=3.0.0') +def test_1(act: Action, tmp_sql: Path): + tmp_sql.write_text( + f""" + set bail on; + set list on; + set blob all; + set count on; + set names win1251; + connect '{act.db.dsn}'; + create view v_conn_cset as + select + rdb$get_context('SYSTEM', 'NETWORK_PROTOCOL') as conn_protocol + ,c.rdb$character_set_name as connection_cset + ,r.rdb$character_set_name as db_default_cset + from mon$attachments a + join rdb$character_sets c on a.mon$character_set_id = c.rdb$character_set_id + cross join rdb$database r where a.mon$attachment_id=current_connection; + + set term ^; + create or alter procedure sp_test + returns ( + msg_blob_id blob sub_type 1 segment size 80 character set unicode_fss) + AS + begin + msg_blob_id= 'Это проверка на вывод строки "Йцукёнг"'; -- text in cyrillic + suspend; + end + ^ + set term ;^ + commit; + -------------------------- + connect '{act.db.dsn}'; -- check TCP protocol + select * from v_conn_cset; + select msg_blob_id + from sp_test + union + select msg_blob_id + from sp_test; + commit; + -------------------------- + connect '{act.db.db_path}'; -- check local protocol + select * from v_conn_cset; + select msg_blob_id + from sp_test + union + select msg_blob_id + from sp_test; + """ + ,encoding='cp1251') + act.expected_stdout = expected_stdout + act.isql(switches = ['-q'], input_file = tmp_sql, charset = 'WIN1251', combine_output = True, connect_db = False) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3491_test.py b/tests/bugs/core_3491_test.py index 77d39214..c117a7d4 100644 --- a/tests/bugs/core_3491_test.py +++ b/tests/bugs/core_3491_test.py @@ -7,43 +7,65 @@ DESCRIPTION: JIRA: CORE-3491 FBTEST: bugs.core_3491 +NOTES: + [27.06.2025] pzotov + Removed 'SHOW' command. It is enough to check twise results of query to rdb$ tables - they must be same. + Test script was checked on 2.5.0.26074 - bug has been confirmed. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """create table aaa (a integer); -commit; -set term !!; -create or alter procedure bbb -returns (b type of column aaa.a) -as -begin - suspend; -end!! -set term ;!! -commit; -""" +db = db_factory() + +test_script = """ + set list on; + + create view v_info as + select rf.rdb$field_name fld_name, f.rdb$field_type fld_type, f.rdb$field_length fld_length, f.rdb$field_scale fld_scale + from rdb$relation_fields rf + left join rdb$fields f on rf.rdb$field_source = f.rdb$field_name + where rf.rdb$relation_name = upper('test'); -db = db_factory(init=init_script) - -test_script = """show table aaa; -set term !!; -create or alter procedure bbb -returns (b varchar(10)) -as -begin - suspend; -end!! -set term ;!! -commit; -show table aaa; + create table test (f01 integer); + commit; + set term ^; + create or alter procedure sp_test returns (o_result type of column test.f01) as + begin + suspend; + end^ + set term ;^ + commit; + + select 'point-1' as msg, v.* from v_info v; + + set term ^; + create or alter procedure sp_test returns (o_result varchar(10)) as + begin + suspend; + end^ + set term ;^ + commit; + + select 'point-2' as msg, v.* from v_info v; """ -act = isql_act('db', test_script) +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' '), ('Table: .*', '')]) + +expected_stdout = """ + MSG point-1 + FLD_NAME F01 + FLD_TYPE 8 + FLD_LENGTH 4 + FLD_SCALE 0 -expected_stdout = """A INTEGER Nullable -A INTEGER Nullable + MSG point-2 + FLD_NAME F01 + FLD_TYPE 8 + FLD_LENGTH 4 + FLD_SCALE 0 """ @pytest.mark.version('>=3') diff --git a/tests/bugs/core_3502_test.py b/tests/bugs/core_3502_test.py index 8f315753..1ea64193 100644 --- a/tests/bugs/core_3502_test.py +++ b/tests/bugs/core_3502_test.py @@ -7,17 +7,19 @@ DESCRIPTION: JIRA: CORE-3502 FBTEST: bugs.core_3502 +NOTES: + [27.06.2025] pzotov + Added subst to suppress output: it is enough to display error message w/o concrete coulmn name for this test. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """ - set autoddl on; - commit; - create or alter procedure p as begin end; - commit; +db = db_factory() +test_script = """ create or alter view v (id) as select rdb$relation_id from rdb$database; commit; @@ -29,29 +31,25 @@ end^ set term ;^ commit; -""" -db = db_factory(init=init_script) - -test_script = """ execute procedure p; commit; + drop view v; """ -act = isql_act('db', test_script) +act = isql_act('db', test_script, substitutions = [('(-)?COLUMN .*', 'COLUMN *')]) -expected_stderr = """ +expected_stdout = """ Statement failed, SQLSTATE = 42000 unsuccessful metadata update -cannot delete - -COLUMN V.ID + COLUMN * -there are 1 dependencies """ -@pytest.mark.version('>=3') +@pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr - + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3509_test.py b/tests/bugs/core_3509_test.py index c3b05241..a6b7d02f 100644 --- a/tests/bugs/core_3509_test.py +++ b/tests/bugs/core_3509_test.py @@ -51,10 +51,10 @@ expected_stdout_6x = """ Statement failed, SQLSTATE = 42000 - CREATE OR ALTER PROCEDURE DUPLICATE_OUTPUT_ARGS failed + CREATE OR ALTER PROCEDURE "PUBLIC"."DUPLICATE_OUTPUT_ARGS" failed -Dynamic SQL Error -SQL error code = -637 - -duplicate specification of A_DUP - not supported + -duplicate specification of "A_DUP" - not supported """ @pytest.mark.version('>=3.0') diff --git a/tests/bugs/core_3511_test.py b/tests/bugs/core_3511_test.py index 3dba7c29..8b4457cb 100644 --- a/tests/bugs/core_3511_test.py +++ b/tests/bugs/core_3511_test.py @@ -37,6 +37,7 @@ act = python_act('db') +@pytest.mark.intl @pytest.mark.version('>=3.0') def test_1(act: Action, tmp_user: User, tmp_role1: Role, tmp_role2: Role, capsys): init_script = f""" diff --git a/tests/bugs/core_3530_test.py b/tests/bugs/core_3530_test.py index a56e3795..3cf70cb6 100644 --- a/tests/bugs/core_3530_test.py +++ b/tests/bugs/core_3530_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-3530 FBTEST: bugs.core_3530 +NOTES: + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -55,7 +61,7 @@ act = isql_act('db', test_script) -expected_stdout = """ +expected_stdout_5x = """ PLAN (TEST NATURAL) PLAN (TEST NATURAL) PLAN (TEST NATURAL) @@ -63,9 +69,16 @@ Records affected: 0 """ +expected_stdout_6x = """ + PLAN ("PUBLIC"."TEST" NATURAL) + PLAN ("PUBLIC"."TEST" NATURAL) + PLAN ("PUBLIC"."TEST" NATURAL) + PLAN ("PUBLIC"."TEST2" NATURAL) + Records affected: 0 +""" + @pytest.mark.version('>=3.0.2') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_3537_test.py b/tests/bugs/core_3537_test.py index d19f7f19..a7cb9997 100644 --- a/tests/bugs/core_3537_test.py +++ b/tests/bugs/core_3537_test.py @@ -120,22 +120,12 @@ act = python_act('db') -expected_stdout = """ - Check ratio_fetches_to_datapages_for_GTT_DELETE_ROWS: OK - Check ratio_fetches_to_datapages_for_GTT_PRESERVE_ROWS: OK - Check ratio_fetches_to_row_count_for_GTT_DELETE_ROWS: OK - Check ratio_fetches_to_row_count_for_GTT_PRESERVE_ROWS: OK - Check ratio_marks_to_datapages_for_GTT_DELETE_ROWS: OK - Check ratio_marks_to_datapages_for_GTT_PRESERVE_ROWS: OK - Check ratio_marks_to_row_count_for_GTT_DELETE_ROWS: OK - Check ratio_marks_to_row_count_for_GTT_PRESERVE_ROWS: OK -""" - trace = ['log_transactions = true', 'print_perf = true', 'log_initfini = false', ] +@pytest.mark.trace @pytest.mark.version('>=3') def test_1(act: Action, capsys): NUM_ROWS_TO_BE_ADDED = 45000 @@ -218,6 +208,18 @@ def test_1(act: Action, capsys): if failed_flag: print('Trace for GTT PRESERVE rows: ' + gtt_sav_trace) print('Trace for GTT DELETE rows: ' + gtt_del_trace) + + expected_stdout = """ + Check ratio_fetches_to_datapages_for_GTT_DELETE_ROWS: OK + Check ratio_fetches_to_datapages_for_GTT_PRESERVE_ROWS: OK + Check ratio_fetches_to_row_count_for_GTT_DELETE_ROWS: OK + Check ratio_fetches_to_row_count_for_GTT_PRESERVE_ROWS: OK + Check ratio_marks_to_datapages_for_GTT_DELETE_ROWS: OK + Check ratio_marks_to_datapages_for_GTT_PRESERVE_ROWS: OK + Check ratio_marks_to_row_count_for_GTT_DELETE_ROWS: OK + Check ratio_marks_to_row_count_for_GTT_PRESERVE_ROWS: OK + """ + # Check act.expected_stdout = expected_stdout act.stdout = capsys.readouterr().out diff --git a/tests/bugs/core_3545_test.py b/tests/bugs/core_3545_test.py index 8fa80156..ecf31a6d 100644 --- a/tests/bugs/core_3545_test.py +++ b/tests/bugs/core_3545_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-3545 FBTEST: bugs.core_3545 +NOTES: + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -117,49 +123,62 @@ act = isql_act('db', test_script, substitutions=[('-At block line: [\\d]+, col: [\\d]+', '-At block line')]) -expected_stdout = """ - ID 1 - N 4 - - ID 3 - N 4 -""" - -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 23000 validation error for column "TEST"."N", value "399" - Statement failed, SQLSTATE = 23000 validation error for column "TEST"."N", value "50" - Statement failed, SQLSTATE = 23000 validation error for column "TEST"."N", value "399" - Statement failed, SQLSTATE = 23000 validation error for column "TEST"."N", value "50" - + ID 1 + N 4 + ID 3 + N 4 Statement failed, SQLSTATE = 42000 validation error for variable VAR2_ASSIGNMENT_WITHOUT_CAST, value "50" - -At block line: 4, col: 7 - + -At block line Statement failed, SQLSTATE = 42000 validation error for variable VAR2_ASSIGNMENT_WITHOUT_CAST, value "399" - -At block line: 4, col: 7 - + -At block line Statement failed, SQLSTATE = 42000 validation error for variable VAR2_CAST_INT_TO_DOMAIN, value "50" - -At block line: 4, col: 7 - + -At block line Statement failed, SQLSTATE = 42000 validation error for variable VAR2_CAST_INT_TO_DOMAIN, value "399" - -At block line: 4, col: 7 + -At block line +""" + +expected_stdout_6x = """ + Statement failed, SQLSTATE = 23000 + validation error for column "PUBLIC"."TEST"."N", value "399" + Statement failed, SQLSTATE = 23000 + validation error for column "PUBLIC"."TEST"."N", value "50" + Statement failed, SQLSTATE = 23000 + validation error for column "PUBLIC"."TEST"."N", value "399" + Statement failed, SQLSTATE = 23000 + validation error for column "PUBLIC"."TEST"."N", value "50" + ID 1 + N 4 + ID 3 + N 4 + Statement failed, SQLSTATE = 42000 + validation error for variable "VAR2_ASSIGNMENT_WITHOUT_CAST", value "50" + -At block line + Statement failed, SQLSTATE = 42000 + validation error for variable "VAR2_ASSIGNMENT_WITHOUT_CAST", value "399" + -At block line + Statement failed, SQLSTATE = 42000 + validation error for variable "VAR2_CAST_INT_TO_DOMAIN", value "50" + -At block line + Statement failed, SQLSTATE = 42000 + validation error for variable "VAR2_CAST_INT_TO_DOMAIN", value "399" + -At block line """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3547_test.py b/tests/bugs/core_3547_test.py index abd5a2d1..cbfea4e1 100644 --- a/tests/bugs/core_3547_test.py +++ b/tests/bugs/core_3547_test.py @@ -15,8 +15,13 @@ Added substitution in order to stop comparison after 15th digit ("COL = 0.000000000000000"). We have to ensure that one can not insert duplicate (-0e0). It is enough to show concrete value of problematic key with accuracy 15 digits. - Checked on 3.0.8.33535, 4.0.1.2692, 5.0.0.730 - both Linux and Windows + + [27.06.2025] pzotov + Output of constraint name is suppressed (not needed for this test). + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. + """ import pytest @@ -56,7 +61,8 @@ -- ^ """ -act = isql_act('db', test_script, substitutions = [(' = 0.0000000000000000', ' = 0.000000000000000')]) +substitutions = [ (' = 0.0000000000000000', ' = 0.000000000000000'), ('constraint .*', 'constraint') ] +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ where id = 0 1 diff --git a/tests/bugs/core_3549_test.py b/tests/bugs/core_3549_test.py index 3cfd8e92..e53163db 100644 --- a/tests/bugs/core_3549_test.py +++ b/tests/bugs/core_3549_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-3549 FBTEST: bugs.core_3549 +NOTES: + [27.06.2025] pzotov + *** DEFERRED *** + Test failed since 6.0.0.834. PROBABLY NEEDS TO BE REFACTORED / REIMPLEMENTED. + https://github.com/FirebirdSQL/firebird/issues/3905#issuecomment-826222236 """ import pytest diff --git a/tests/bugs/core_3553_test.py b/tests/bugs/core_3553_test.py index 5f9fe25b..98baf05d 100644 --- a/tests/bugs/core_3553_test.py +++ b/tests/bugs/core_3553_test.py @@ -13,6 +13,12 @@ FB 5.0.0.455 and later: data sources with equal cardinality now present in the HASH plan in order they are specified in the query. Reversed order was used before this build. Because of this, two cases of expected stdout must be taken in account, see variables 'fb3x_checked_stdout' and 'fb5x_checked_stdout'. + + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -29,17 +35,21 @@ act = isql_act('db', test_script) -fb3x_checked_stdout = """ +expected_stdout_4x = """ PLAN HASH (D2 NATURAL, D1 NATURAL) """ -fb5x_checked_stdout = """ +expected_stdout_5x = """ PLAN HASH (D1 NATURAL, D2 NATURAL) """ +expected_stdout_6x = """ + PLAN HASH ("D1" NATURAL, "D2" NATURAL) +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = fb3x_checked_stdout if act.is_version('<5') else fb5x_checked_stdout + act.expected_stdout = expected_stdout_4x if act.is_version('<5') else expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.execute() assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3554_test.py b/tests/bugs/core_3554_test.py index 4a25115d..4c7ac629 100644 --- a/tests/bugs/core_3554_test.py +++ b/tests/bugs/core_3554_test.py @@ -99,6 +99,7 @@ -At block line: 4, col: 7 """ +@pytest.mark.es_eds @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_3569_test.py b/tests/bugs/core_3569_test.py index 3b35d648..1e25496d 100644 --- a/tests/bugs/core_3569_test.py +++ b/tests/bugs/core_3569_test.py @@ -12,6 +12,12 @@ Added 'SQLSTATE' in substitutions: runtime error must not be filtered out by '?!(...)' pattern ("negative lookahead assertion", see https://docs.python.org/3/library/re.html#regular-expression-syntax). Added 'combine_output = True' in order to see SQLSTATE if any error occurs. + + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -42,14 +48,20 @@ # act = isql_act('db', test_script, substitutions = [ ('^((?!(SQLSTATE|exceeds|maximum|limit|sqltype|literal)).)*$', ''), ('[ \t]+', ' ') ] ) -expected_stdout = """ +expected_stdout_5x = """ 01: sqltype: 452 TEXT scale: 0 subtype: 0 len: 32765 charset: 0 NONE 01: sqltype: 452 TEXT scale: 0 subtype: 0 len: 32766 charset: 0 NONE 01: sqltype: 452 TEXT scale: 0 subtype: 0 len: 32767 charset: 0 NONE """ +expected_stdout_6x = """ + 01: sqltype: 452 TEXT scale: 0 subtype: 0 len: 32765 charset: 0 SYSTEM.NONE + 01: sqltype: 452 TEXT scale: 0 subtype: 0 len: 32766 charset: 0 SYSTEM.NONE + 01: sqltype: 452 TEXT scale: 0 subtype: 0 len: 32767 charset: 0 SYSTEM.NONE +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3594_test.py b/tests/bugs/core_3594_test.py index 662df2ba..de088ec8 100644 --- a/tests/bugs/core_3594_test.py +++ b/tests/bugs/core_3594_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-3594 FBTEST: bugs.core_3594 +NOTES: + [27.06.2025] pzotov + Suppressed output of procedure name (no needed for this test). + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -50,25 +55,26 @@ -- -string right truncation """ -act = isql_act('db', test_script, substitutions=[('line: .*', 'line'), ('col: .*', 'col')]) +substitutions=[('line: .*', 'line'), ('col: .*', 'col'), ('(-)?At procedure .*', 'At procedure')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stderr = """ +expected_stdout = """ Statement failed, SQLSTATE = 22001 arithmetic exception, numeric overflow, or string truncation -string right truncation -expected length 50, actual 60 - -At procedure 'SP_OVERFLOWED_1' line: 3, col: 5 + At procedure Statement failed, SQLSTATE = 22001 arithmetic exception, numeric overflow, or string truncation -string right truncation -expected length 59, actual 60 - -At procedure 'SP_OVERFLOWED_2' line: 3, col: 5 + At procedure """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3598_test.py b/tests/bugs/core_3598_test.py index c0648e8b..e82a0a60 100644 --- a/tests/bugs/core_3598_test.py +++ b/tests/bugs/core_3598_test.py @@ -17,32 +17,36 @@ Finally (after ISQL will finish), we stop trace and parse its log. For *each* table TWO lines with performance statristics must exist: both for COMMIT and ROLLBACK events. -[08.02.2022] pcisar - Fails on Windows 3.0.8 with unexpected additional output line: - + Statement statistics detected for ROLLBACK - Statement statistics detected for COMMIT - Statement statistics detected for COMMIT - Statement statistics detected for ROLLBACK - Found performance block header - Found table statistics for TFIX - Statement statistics detected for COMMIT - Statement statistics detected for ROLLBACK - Found performance block header - Found table statistics for GTT_SSN - Statement statistics detected for COMMIT - Statement statistics detected for ROLLBACK - -[04.03.2022] pzotov: RESOLVED. - Problem on Windows was caused by excessive query: - "select current_user, current_role from rdb$database" - -- which is done by ISQL 3.x when it gets commands from STDIN via PIPE mechanism. - Discussed with Alex et al, since 28-feb-2022 18:05 +0300. - Alex explanation: 28-feb-2022 19:52 +0300 - subj: "Firebird new-QA: weird result for trivial test (outcome depends on presence of... running trace session!)" - - JIRA: CORE-3598 FBTEST: bugs.core_3598 +NOTES: + [08.02.2022] pcisar + Fails on Windows 3.0.8 with unexpected additional output line: + + Statement statistics detected for ROLLBACK + Statement statistics detected for COMMIT + Statement statistics detected for COMMIT + Statement statistics detected for ROLLBACK + Found performance block header + Found table statistics for TFIX + Statement statistics detected for COMMIT + Statement statistics detected for ROLLBACK + Found performance block header + Found table statistics for GTT_SSN + Statement statistics detected for COMMIT + Statement statistics detected for ROLLBACK + + [04.03.2022] pzotov: RESOLVED. + Problem on Windows was caused by excessive query: + "select current_user, current_role from rdb$database" + -- which is done by ISQL 3.x when it gets commands from STDIN via PIPE mechanism. + Discussed with Alex et al, since 28-feb-2022 18:05 +0300. + Alex explanation: 28-feb-2022 19:52 +0300 + subj: "Firebird new-QA: weird result for trivial test (outcome depends on presence of... running trace session!)" + + [27.06.2025] pzotov + Added substitutions to suppress output of "PUBLIC" schema name in expected output (no needed for this test). + Added check for presence `"PUBLIC".` prefix in thetable statistics for table names that are used in this test. + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -57,7 +61,9 @@ db = db_factory(init=init_script) -act = python_act('db') +substitutions = [('for \"PUBLIC\".\"TFIX\"', 'for TFIX'), ('for \"PUBLIC\".\"GTT_SSN\"', 'for GTT_SSN'), ] + +act = python_act('db', substitutions = substitutions) test_script = """ set autoddl off; @@ -83,6 +89,7 @@ 'log_initfini = false', ] +@pytest.mark.trace @pytest.mark.version('>=3.0') def test_1(act: Action, capsys): expected_stdout = '' @@ -134,6 +141,11 @@ def test_1(act: Action, capsys): print('Found performance block header') if line.startswith('TFIX') or line.startswith('GTT_SSN') or line.startswith('GTT_TRA'): print(f'Found table statistics for {line.split()[0]}') + + # Added for FB 6.x: SCHEMA name presents since 6.0.0.834: + if line.startswith('"PUBLIC"."TFIX"') or line.startswith('"PUBLIC"."GTT_SSN"') or line.startswith('"PUBLIC"."GTT_TRA"'): + print(f'Found table statistics for {line.split()[0]}') + # Check act.expected_stdout = expected_stdout act.stdout = capsys.readouterr().out diff --git a/tests/bugs/core_3601_test.py b/tests/bugs/core_3601_test.py index a86d631a..eac82da4 100644 --- a/tests/bugs/core_3601_test.py +++ b/tests/bugs/core_3601_test.py @@ -2,7 +2,7 @@ """ ID: issue-3955 -ISSUE: 3955 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/3955 TITLE: Incorrect TEXT BLOB charset transliteration on VIEW with trigger DESCRIPTION: Test verifies that all OK when connection charset = UTF8 @@ -281,7 +281,7 @@ from v_t_test; """ -act = isql_act('db', test_script, substitutions=[('MEMO_UTF8.*', 'MEMO_UTF8')]) +act = isql_act('db', test_script, substitutions=[ ('MEMO_UTF8.*', 'MEMO_UTF8'), ('MEMO_WIN1250.*', 'MEMO_WIN1250') ]) expected_stdout = """ ID 1 diff --git a/tests/bugs/core_3610_test.py b/tests/bugs/core_3610_test.py index dde3b409..89553613 100644 --- a/tests/bugs/core_3610_test.py +++ b/tests/bugs/core_3610_test.py @@ -7,15 +7,17 @@ DESCRIPTION: JIRA: CORE-3610 FBTEST: bugs.core_3610 +NOTES: + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -substitutions = [('Data source : Firebird::localhost:.*', 'Data source : Firebird::localhost:'), - ('335544382 : Problematic key', '335545072 : Problematic key'), - ('-At block line: [\\d]+, col: [\\d]+', '-At block line')] - db = db_factory() test_script = """ @@ -40,28 +42,24 @@ set term ;^ rollback; - -- |||||||||||||||||||||||||||| - -- ###################################||| FB 4.0+, SS and SC |||############################## - -- |||||||||||||||||||||||||||| - -- If we check SS or SC and ExtConnPoolLifeTime > 0 (config parameter FB 4.0+) then current - -- DB (bugs.core_NNNN.fdb) will be 'captured' by firebird.exe process and fbt_run utility - -- will not able to drop this database at the final point of test. - -- Moreover, DB file will be hold until all activity in firebird.exe completed and AFTER this - -- we have to wait for seconds after it (discussion and small test see - -- in the letter to hvlad and dimitr 13.10.2019 11:10). - -- This means that one need to kill all connections to prevent from exception on cleanup phase: - -- SQLCODE: -901 / lock time-out on wait transaction / object is in use - -- ############################################################################################# - delete from mon$attachments where mon$attachment_id != current_connection; - commit; - set list on; select * from test; """ +substitutions = [('[ \t]+', ' '), + ('Data source : Firebird::localhost:.*', 'Data source : Firebird::localhost:'), + ('335544382 : Problematic key', '335545072 : Problematic key'), + ('-At block line: [\\d]+, col: [\\d]+', '-At block line')] act = isql_act('db', test_script, substitutions=substitutions) -expected_stdout = """ +expected_stdout_5x = """ + Statement failed, SQLSTATE = 42000 + Execute statement error at isc_dsql_execute2 : + 335544665 : violation of PRIMARY or UNIQUE KEY constraint "TEST_UNQ" on table "TEST" + 335545072 : Problematic key value is ("F01" = 1) + Statement : update test set f01 = ? where id = ? + Data source : Firebird::localhost: + -At block line ID 1 F01 1 ID 2 @@ -70,21 +68,25 @@ F01 """ -expected_stderr = """ +expected_stdout_6x = """ Statement failed, SQLSTATE = 42000 Execute statement error at isc_dsql_execute2 : - 335544665 : violation of PRIMARY or UNIQUE KEY constraint "TEST_UNQ" on table "TEST" + 335544665 : violation of PRIMARY or UNIQUE KEY constraint "TEST_UNQ" on table "PUBLIC"."TEST" 335545072 : Problematic key value is ("F01" = 1) Statement : update test set f01 = ? where id = ? Data source : Firebird::localhost: - -At block line: 3, col: 9 + -At block line + ID 1 + F01 1 + ID 2 + F01 + ID 3 + F01 """ +@pytest.mark.es_eds @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3627_test.py b/tests/bugs/core_3627_test.py index a4882841..c3949ae0 100644 --- a/tests/bugs/core_3627_test.py +++ b/tests/bugs/core_3627_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-3627 FBTEST: bugs.core_3627 +NOTES: + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -38,15 +44,20 @@ act = isql_act('db', test_script) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 23000 attempt to store duplicate value (visible to active transactions) in unique index "CLASSIDKSGIDX" -Problematic key value is ("CLASSID" = 1, "KSGFK" = NULL) """ +expected_stdout_6x = """ + Statement failed, SQLSTATE = 23000 + attempt to store duplicate value (visible to active transactions) in unique index "PUBLIC"."CLASSIDKSGIDX" + -Problematic key value is ("CLASSID" = 1, "KSGFK" = NULL) +""" + @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3658_test.py b/tests/bugs/core_3658_test.py index 033aa60d..f6097e0d 100644 --- a/tests/bugs/core_3658_test.py +++ b/tests/bugs/core_3658_test.py @@ -41,6 +41,7 @@ 'log_errors = true', ] +@pytest.mark.trace @pytest.mark.version('>=3') def test_1(act: Action, tmp_trace_cfg: Path, tmp_trace_log: Path, capsys): #print( os.environ.get('ISC_USER', 'UNKNOWN_ISC_USR') ) diff --git a/tests/bugs/core_3672_test.py b/tests/bugs/core_3672_test.py index 97f0c2d9..4cd76ffc 100644 --- a/tests/bugs/core_3672_test.py +++ b/tests/bugs/core_3672_test.py @@ -7,6 +7,10 @@ DESCRIPTION: JIRA: CORE-3672 FBTEST: bugs.core_3672 +NOTES: + [27.06.2025] pzotov + Added 'SCHEMA_PREFIX' to be substituted in expected_out on FB 6.x + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -14,7 +18,14 @@ db = db_factory() -test_script = """ +IDX_EXPR_1 = 'substring(col1 from 1 for 169)' +IDX_EXPR_2 = 'substring(trim( col2 from col1 ) from 1 for 169) ' +IDX_EXPR_3 = 'col2' +IDX_EXPR_4 = 'substring(col2 from 1 for 169)' +IDX_EXPR_5 = 'col3' +IDX_EXPR_6 = 'substring(col3 from 1 for 169)' + +test_script = f""" recreate table test( col1 varchar(8190) character set utf8 collate unicode_ci_ai ,col2 computed by ( substring(col1 from 1 for 169) ) @@ -27,12 +38,12 @@ -- max_key_length = floor( (page_size / 4 - 9) / N ) = 169 characters. -- Verify that we CAN do that w/o error: - create index test_col1_idx_a on test computed by ( substring(col1 from 1 for 169) ); - create index test_col1_idx_b on test computed by ( substring(trim( col2 from col1 ) from 1 for 169) ); - create index test_col2_idx_a on test computed by ( col2 ); - create index test_col2_idx_b on test computed by ( substring(col2 from 1 for 169) ); - create index test_col3_idx_a on test computed by ( col3 ); - create index test_col3_idx_b on test computed by ( substring(col3 from 1 for 169) ); + create index test_col1_idx_a on test computed by ( {IDX_EXPR_1} ); + create index test_col1_idx_b on test computed by ( {IDX_EXPR_2} ); + create index test_col2_idx_a on test computed by ( {IDX_EXPR_3} ); + create index test_col2_idx_b on test computed by ( {IDX_EXPR_4} ); + create index test_col3_idx_a on test computed by ( {IDX_EXPR_5} ); + create index test_col3_idx_b on test computed by ( {IDX_EXPR_6} ); commit; -- Confirmed for 2.5.5: "-key size exceeds implementation restriction" @@ -46,18 +57,20 @@ act = isql_act('db', test_script) -expected_stdout = """ - TEST_COL1_IDX_A INDEX ON TEST COMPUTED BY ( substring(col1 from 1 for 169) ) - TEST_COL1_IDX_B INDEX ON TEST COMPUTED BY ( substring(trim( col2 from col1 ) from 1 for 169) ) - TEST_COL2_IDX_A INDEX ON TEST COMPUTED BY ( col2 ) - TEST_COL2_IDX_B INDEX ON TEST COMPUTED BY ( substring(col2 from 1 for 169) ) - TEST_COL3_IDX_A INDEX ON TEST COMPUTED BY ( col3 ) - TEST_COL3_IDX_B INDEX ON TEST COMPUTED BY ( substring(col3 from 1 for 169) ) -""" - -@pytest.mark.version('>=3.0') +@pytest.mark.version('>=3') def test_1(act: Action): + + SCHEMA_PREFIX = '' if act.is_version('<6') else 'PUBLIC.' + + expected_stdout = f""" + {SCHEMA_PREFIX}TEST_COL1_IDX_A INDEX ON TEST COMPUTED BY ( {IDX_EXPR_1} ) + {SCHEMA_PREFIX}TEST_COL1_IDX_B INDEX ON TEST COMPUTED BY ( {IDX_EXPR_2} ) + {SCHEMA_PREFIX}TEST_COL2_IDX_A INDEX ON TEST COMPUTED BY ( {IDX_EXPR_3} ) + {SCHEMA_PREFIX}TEST_COL2_IDX_B INDEX ON TEST COMPUTED BY ( {IDX_EXPR_4} ) + {SCHEMA_PREFIX}TEST_COL3_IDX_A INDEX ON TEST COMPUTED BY ( {IDX_EXPR_5} ) + {SCHEMA_PREFIX}TEST_COL3_IDX_B INDEX ON TEST COMPUTED BY ( {IDX_EXPR_6} ) + """ + act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_3690_dialect_1_test.py b/tests/bugs/core_3690_dialect_1_test.py index 43691247..70b1171d 100644 --- a/tests/bugs/core_3690_dialect_1_test.py +++ b/tests/bugs/core_3690_dialect_1_test.py @@ -7,12 +7,18 @@ DESCRIPTION: SQL dialect 1 allows such queries for backward compatibility reasons JIRA: CORE-3690 FBTEST: bugs.core_3690 +NOTES: + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -db_1 = db_factory(sql_dialect=1) +db = db_factory(sql_dialect=1) test_script = """ set list on; @@ -20,20 +26,27 @@ select 0*rdb$relation_id as id from rdb$database,rdb$database; """ -act_1 = python_act('db_1') +substitutions = [('[ \t]+', ' ')] +act = python_act('db', substitutions = substitutions) -expected_stdout_1 = """ +expected_stdout_5x = """ MON$SQL_DIALECT 1 - SQL warning code = 204 -Ambiguous field name between table RDB$DATABASE and table RDB$DATABASE -RDB$RELATION_ID + ID 0 +""" +expected_stdout_6x = """ + MON$SQL_DIALECT 1 + SQL warning code = 204 + -Ambiguous field name between table "SYSTEM"."RDB$DATABASE" and table "SYSTEM"."RDB$DATABASE" + -RDB$RELATION_ID ID 0 """ @pytest.mark.version('>=3') -def test_dialect_1(act_1: Action): - act_1.expected_stdout = expected_stdout_1 - act_1.isql(switches=['-q', '-sql_dialect', '1'], input=test_script, combine_output=True) - assert act_1.clean_stdout == act_1.clean_expected_stdout +def test_1(act: Action): + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.isql(switches=['-q', '-sql_dialect', '1'], input=test_script, combine_output=True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3690_dialect_3_test.py b/tests/bugs/core_3690_dialect_3_test.py index dac2ac49..1374c264 100644 --- a/tests/bugs/core_3690_dialect_3_test.py +++ b/tests/bugs/core_3690_dialect_3_test.py @@ -2,17 +2,23 @@ """ ID: issue-4038 -ISSUE: https://github.com/FirebirdSQL/firebird/issues/4038 +ISSUE: 4038 TITLE: Wrong warning message for ambiguous query -DESCRIPTION: SQL dialect 1 allows such queries for backward compatibility reasons +DESCRIPTION: Check for dialect 3. JIRA: CORE-3690 FBTEST: bugs.core_3690 +NOTES: + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -db_3 = db_factory(sql_dialect=3) +db = db_factory() test_script = """ set list on; @@ -20,11 +26,11 @@ select 0*rdb$relation_id as id from rdb$database,rdb$database; """ -act_3 = python_act('db_3') - -expected_stdout_3 = """ - MON$SQL_DIALECT 3 +substitutions = [('[ \t]+', ' ')] +act = python_act('db', substitutions = substitutions) +expected_stdout_5x = """ + MON$SQL_DIALECT 3 Statement failed, SQLSTATE = 42702 Dynamic SQL Error -SQL error code = -204 @@ -32,8 +38,17 @@ -RDB$RELATION_ID """ +expected_stdout_6x = """ + MON$SQL_DIALECT 3 + Statement failed, SQLSTATE = 42702 + Dynamic SQL Error + -SQL error code = -204 + -Ambiguous field name between table "SYSTEM"."RDB$DATABASE" and table "SYSTEM"."RDB$DATABASE" + -RDB$RELATION_ID +""" + @pytest.mark.version('>=3') -def test_dialect_3(act_3: Action): - act_3.expected_stdout = expected_stdout_3 - act_3.isql(switches=['-q', '-sql_dialect', '3'], input=test_script, combine_output=True) - assert act_3.clean_stdout == act_3.clean_expected_stdout +def test_1(act: Action): + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.isql(switches=['-q'], input=test_script, combine_output=True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3691_test.py b/tests/bugs/core_3691_test.py index 2af895f9..680e16bd 100644 --- a/tests/bugs/core_3691_test.py +++ b/tests/bugs/core_3691_test.py @@ -7,6 +7,10 @@ DESCRIPTION: JIRA: CORE-3691 FBTEST: bugs.core_3691 +NOTES: + [27.06.2025] pzotov + Added 'SCHEMA_PREFIX' to be substituted in expected_out on FB 6.x + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -29,16 +33,19 @@ act = isql_act('db', test_script) -expected_stderr = """ - Statement failed, SQLSTATE = 23000 - violation of FOREIGN KEY constraint "TDETL_FK" on table "TDETL" - -Foreign key reference target does not exist - -Problematic key value is ("PID" = 2) -""" - @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + + SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + + expected_stdout = f""" + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint "TDETL_FK" on table {SCHEMA_PREFIX}"TDETL" + -Foreign key reference target does not exist + -Problematic key value is ("PID" = 2) + """ + + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3722_test.py b/tests/bugs/core_3722_test.py index a02a4a1c..e529a61a 100644 --- a/tests/bugs/core_3722_test.py +++ b/tests/bugs/core_3722_test.py @@ -7,36 +7,49 @@ DESCRIPTION: JIRA: CORE-3722 FBTEST: bugs.core_3722 +NOTES: + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """create table t (a varchar(5)); -create index t_a on t (a); +db = db_factory() + +test_script = """ + create table test (f01 int); + insert into test select iif(rand() < 0.5, null, rand()*1000) from rdb$types; + commit; + create index test_f01 on test (f01); + set planonly; + select * from test where f01 is null; + select * from test where f01 is not distinct from null; + select * from test where f01 is not distinct from null PLAN (test INDEX (test_f01)); + select * from test where f01 is not distinct from nullif('', ''); """ -db = db_factory(init=init_script) +act = isql_act('db', test_script) -test_script = """SET PLAN ON; -select * from t where a is null; -select * from t where a is not distinct from null; -select * from t where a is not distinct from null PLAN (T INDEX (T_A)); -select * from t where a is not distinct from nullif('', ''); +expected_stdout_5x = """ + PLAN (TEST INDEX (TEST_F01)) + PLAN (TEST INDEX (TEST_F01)) + PLAN (TEST INDEX (TEST_F01)) + PLAN (TEST INDEX (TEST_F01)) """ -act = isql_act('db', test_script) - -expected_stdout = """ -PLAN (T INDEX (T_A)) -PLAN (T INDEX (T_A)) -PLAN (T INDEX (T_A)) -PLAN (T INDEX (T_A)) +expected_stdout_6x = """ + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_F01")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_F01")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_F01")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_F01")) """ @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_3735_test.py b/tests/bugs/core_3735_test.py index a73a9566..dcd97243 100644 --- a/tests/bugs/core_3735_test.py +++ b/tests/bugs/core_3735_test.py @@ -7,173 +7,239 @@ DESCRIPTION: JIRA: CORE-3735 FBTEST: bugs.core_3735 +NOTES: + [28.06.2025] pzotov + Reimplemented: use variables to be used (via f-notations) in expected_out_* instead of hard-coding. + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -substitutions = [('-Effective user is.*', ''), ('Rolling back work.', ''), - ('Commit current transaction \\(y/n\\)\\?', '')] - db = db_factory() -test_user = user_factory('db', name='tmp$c3735', password='123') - -test_script = """ - -- See also more complex test in CORE-4731 // Prohibit an ability to issue DML or DDL statements on RDB$ tables - - connect '$(DSN)' user tmp$c3735 password '123'; - - set list on; - set blob all; - select current_user from rdb$database; - show grants; - set count on; - - insert into rdb$character_sets( - rdb$character_set_name - ,rdb$form_of_use - ,rdb$number_of_characters - ,rdb$default_collate_name - ,rdb$character_set_id - ,rdb$system_flag - ,rdb$description - ,rdb$function_name - ,rdb$bytes_per_character - )values ( - 'ISO-8859-15', - null, - null, - 'ISO-8859-15', - ( select max(rdb$character_set_id) from rdb$character_sets ) + 1, - 1, - null, - null, - 1 - ) returning - rdb$character_set_name, - rdb$character_set_id, - rdb$default_collate_name - ; - - insert into rdb$collations( - rdb$collation_name - ,rdb$collation_id - ,rdb$character_set_id - ,rdb$collation_attributes - ,rdb$system_flag - ,rdb$description - ,rdb$function_name - ,rdb$base_collation_name - ,rdb$specific_attributes - ) values( - 'SUPER_SMART_ORDER' - ,( select max(rdb$collation_id) from rdb$collations ) + 1 - ,( select rdb$character_set_id from rdb$character_sets where upper(rdb$character_set_name) = upper('ISO-8859-15') ) - ,1 - ,1 - ,null - ,null - ,null - ,null - ) returning - rdb$collation_name - ,rdb$collation_id - ,rdb$character_set_id - ; - - - insert into rdb$database( - rdb$description - ,rdb$relation_id - ,rdb$security_class - ,rdb$character_set_name - ) values ( - 'This is special record, do not delete it!' - ,( select max(rdb$relation_id) from rdb$relations ) + 1 - ,null - ,'ISO_HE_HE' - ) returning - rdb$description - ,rdb$relation_id - ,rdb$security_class - ,rdb$character_set_name - ; - - - update rdb$collations set rdb$description = null rows 1 - returning - rdb$collation_id - ; - - update rdb$character_sets set rdb$description = null rows 1 - returning - rdb$character_set_id - ; - - update rdb$database set rdb$character_set_name = 'ISO_HA_HA' - returning - rdb$relation_id - ; - - delete from rdb$collations order by rdb$collation_id desc rows 1 - returning - rdb$collation_name - ,rdb$collation_id - ,rdb$character_set_id - ; - - delete from rdb$character_sets order by rdb$character_set_id desc rows 1 - returning - rdb$character_set_name, - rdb$character_set_id, - rdb$default_collate_name - ; - - delete from rdb$database order by rdb$relation_id desc rows 1 - returning - rdb$description - ,rdb$relation_id - ,rdb$security_class - ,rdb$character_set_name - ; - - commit; -""" - -act = isql_act('db', test_script, substitutions=substitutions) - -expected_stdout = """ - USER TMP$C3735 -""" - -expected_stderr = """ - There is no privilege granted in this database - Statement failed, SQLSTATE = 28000 - no permission for INSERT access to TABLE RDB$CHARACTER_SETS - Statement failed, SQLSTATE = 28000 - no permission for INSERT access to TABLE RDB$COLLATIONS - Statement failed, SQLSTATE = 28000 - no permission for INSERT access to TABLE RDB$DATABASE - Statement failed, SQLSTATE = 28000 - no permission for UPDATE access to TABLE RDB$COLLATIONS - Statement failed, SQLSTATE = 28000 - no permission for UPDATE access to TABLE RDB$CHARACTER_SETS - Statement failed, SQLSTATE = 28000 - no permission for UPDATE access to TABLE RDB$DATABASE - Statement failed, SQLSTATE = 28000 - no permission for DELETE access to TABLE RDB$COLLATIONS - Statement failed, SQLSTATE = 28000 - no permission for DELETE access to TABLE RDB$CHARACTER_SETS - Statement failed, SQLSTATE = 28000 - no permission for DELETE access to TABLE RDB$DATABASE -""" - -@pytest.mark.version('>=3.0') -def test_1(act: Action, test_user: User): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - +tmp_user = user_factory('db', name='tmp$c3735', password='123') + +substitutions = [('[ \t]+', ' '), ('/\\* Grant permissions for.*','')] + +act = isql_act('db', substitutions=substitutions) + +@pytest.mark.version('>=3') +def test_1(act: Action, tmp_user: User): + + test_script = f""" + -- See also more complex test in CORE-4731 // Prohibit an ability to issue DML or DDL statements on RDB$ tables + + connect '{act.db.dsn}' user '{tmp_user.name}' password '{tmp_user.password}'; + + set list on; + set blob all; + select current_user from rdb$database; + show grants; + set count on; + + insert into rdb$character_sets( + rdb$character_set_name + ,rdb$form_of_use + ,rdb$number_of_characters + ,rdb$default_collate_name + ,rdb$character_set_id + ,rdb$system_flag + ,rdb$description + ,rdb$function_name + ,rdb$bytes_per_character + )values ( + 'ISO-8859-15', + null, + null, + 'ISO-8859-15', + ( select max(rdb$character_set_id) from rdb$character_sets ) + 1, + 1, + null, + null, + 1 + ) returning + rdb$character_set_name, + rdb$character_set_id, + rdb$default_collate_name + ; + + insert into rdb$collations( + rdb$collation_name + ,rdb$collation_id + ,rdb$character_set_id + ,rdb$collation_attributes + ,rdb$system_flag + ,rdb$description + ,rdb$function_name + ,rdb$base_collation_name + ,rdb$specific_attributes + ) values( + 'SUPER_SMART_ORDER' + ,( select max(rdb$collation_id) from rdb$collations ) + 1 + ,( select rdb$character_set_id from rdb$character_sets where upper(rdb$character_set_name) = upper('ISO-8859-15') ) + ,1 + ,1 + ,null + ,null + ,null + ,null + ) returning + rdb$collation_name + ,rdb$collation_id + ,rdb$character_set_id + ; + + + insert into rdb$database( + rdb$description + ,rdb$relation_id + ,rdb$security_class + ,rdb$character_set_name + ) values ( + 'This is special record, do not delete it!' + ,( select max(rdb$relation_id) from rdb$relations ) + 1 + ,null + ,'ISO_HE_HE' + ) returning + rdb$description + ,rdb$relation_id + ,rdb$security_class + ,rdb$character_set_name + ; + + + update rdb$collations set rdb$description = null rows 1 + returning + rdb$collation_id + ; + + update rdb$character_sets set rdb$description = null rows 1 + returning + rdb$character_set_id + ; + + update rdb$database set rdb$character_set_name = 'ISO_HA_HA' + returning + rdb$relation_id + ; + + delete from rdb$collations order by rdb$collation_id desc rows 1 + returning + rdb$collation_name + ,rdb$collation_id + ,rdb$character_set_id + ; + + delete from rdb$character_sets order by rdb$character_set_id desc rows 1 + returning + rdb$character_set_name, + rdb$character_set_id, + rdb$default_collate_name + ; + + delete from rdb$database order by rdb$relation_id desc rows 1 + returning + rdb$description + ,rdb$relation_id + ,rdb$security_class + ,rdb$character_set_name + ; + + commit; + """ + + expected_stdout_3x = f""" + USER {tmp_user.name.upper()} + There is no privilege granted in this database + Statement failed, SQLSTATE = 28000 + no permission for INSERT access to TABLE RDB$CHARACTER_SETS + Statement failed, SQLSTATE = 28000 + no permission for INSERT access to TABLE RDB$COLLATIONS + Statement failed, SQLSTATE = 28000 + no permission for INSERT access to TABLE RDB$DATABASE + Statement failed, SQLSTATE = 28000 + no permission for UPDATE access to TABLE RDB$COLLATIONS + Statement failed, SQLSTATE = 28000 + no permission for UPDATE access to TABLE RDB$CHARACTER_SETS + Statement failed, SQLSTATE = 28000 + no permission for UPDATE access to TABLE RDB$DATABASE + Statement failed, SQLSTATE = 28000 + no permission for DELETE access to TABLE RDB$COLLATIONS + Statement failed, SQLSTATE = 28000 + no permission for DELETE access to TABLE RDB$CHARACTER_SETS + Statement failed, SQLSTATE = 28000 + no permission for DELETE access to TABLE RDB$DATABASE + """ + + expected_stdout_5x = f""" + USER {tmp_user.name.upper()} + There is no privilege granted in this database + Statement failed, SQLSTATE = 28000 + no permission for INSERT access to TABLE RDB$CHARACTER_SETS + -Effective user is {tmp_user.name.upper()} + Statement failed, SQLSTATE = 28000 + no permission for INSERT access to TABLE RDB$COLLATIONS + -Effective user is {tmp_user.name.upper()} + Statement failed, SQLSTATE = 28000 + no permission for INSERT access to TABLE RDB$DATABASE + -Effective user is {tmp_user.name.upper()} + Statement failed, SQLSTATE = 28000 + no permission for UPDATE access to TABLE RDB$COLLATIONS + -Effective user is {tmp_user.name.upper()} + Statement failed, SQLSTATE = 28000 + no permission for UPDATE access to TABLE RDB$CHARACTER_SETS + -Effective user is {tmp_user.name.upper()} + Statement failed, SQLSTATE = 28000 + no permission for UPDATE access to TABLE RDB$DATABASE + -Effective user is {tmp_user.name.upper()} + Statement failed, SQLSTATE = 28000 + no permission for DELETE access to TABLE RDB$COLLATIONS + -Effective user is {tmp_user.name.upper()} + Statement failed, SQLSTATE = 28000 + no permission for DELETE access to TABLE RDB$CHARACTER_SETS + -Effective user is {tmp_user.name.upper()} + Statement failed, SQLSTATE = 28000 + no permission for DELETE access to TABLE RDB$DATABASE + -Effective user is {tmp_user.name.upper()} + """ + + expected_stdout_6x = f""" + USER {tmp_user.name.upper()} + + GRANT USAGE ON SCHEMA PUBLIC TO PUBLIC + Statement failed, SQLSTATE = 28000 + no permission for INSERT access to TABLE "SYSTEM"."RDB$CHARACTER_SETS" + -Effective user is {tmp_user.name.upper()} + Statement failed, SQLSTATE = 28000 + no permission for INSERT access to TABLE "SYSTEM"."RDB$COLLATIONS" + -Effective user is {tmp_user.name.upper()} + Statement failed, SQLSTATE = 28000 + no permission for INSERT access to TABLE "SYSTEM"."RDB$DATABASE" + -Effective user is {tmp_user.name.upper()} + Statement failed, SQLSTATE = 28000 + no permission for UPDATE access to TABLE "SYSTEM"."RDB$COLLATIONS" + -Effective user is {tmp_user.name.upper()} + Statement failed, SQLSTATE = 28000 + no permission for UPDATE access to TABLE "SYSTEM"."RDB$CHARACTER_SETS" + -Effective user is {tmp_user.name.upper()} + Statement failed, SQLSTATE = 28000 + no permission for UPDATE access to TABLE "SYSTEM"."RDB$DATABASE" + -Effective user is {tmp_user.name.upper()} + Statement failed, SQLSTATE = 28000 + no permission for DELETE access to TABLE "SYSTEM"."RDB$COLLATIONS" + -Effective user is {tmp_user.name.upper()} + Statement failed, SQLSTATE = 28000 + no permission for DELETE access to TABLE "SYSTEM"."RDB$CHARACTER_SETS" + -Effective user is {tmp_user.name.upper()} + Statement failed, SQLSTATE = 28000 + no permission for DELETE access to TABLE "SYSTEM"."RDB$DATABASE" + -Effective user is {tmp_user.name.upper()} + """ + + act.expected_stdout = expected_stdout_3x if act.is_version('<4') else expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.isql(switches = ['-q'], connect_db = False, input = test_script, combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3736_test.py b/tests/bugs/core_3736_test.py index a9918177..787c2a58 100644 --- a/tests/bugs/core_3736_test.py +++ b/tests/bugs/core_3736_test.py @@ -8,6 +8,15 @@ DESCRIPTION: JIRA: CORE-3736 FBTEST: bugs.core_3736 +NOTES: + [28.06.2025] pzotov + Reimplemented: use variables to be used (via f-notations) in expected_out_* instead of hard-coding. + Only error messages are checked in this test (rather than both stdout and stderr). + + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -18,8 +27,12 @@ user_1_ro = user_factory('db', name='tmp$c3736_ro', password='tmp$c3736_ro') user_1_ud = user_factory('db', name='tmp$c3736_ud', password='tmp$c3736_ud') -test_script = """ +READ_ONLY_STTM = "select current_user, 'select_read_only', id from t_read_only_for_non_sys" +WITH_LOCK_STTM = "select current_user, 'select_with_lock', id from t_read_only_for_non_sys for update with lock" + +test_script = f""" set wng off; + set list on; recreate table t_read_only_for_non_sys(id int); commit; @@ -52,7 +65,7 @@ into v_dbname; for - execute statement 'select current_user, ''select_read_only'', id from t_read_only_for_non_sys' -- for update with lock' + execute statement q'#{READ_ONLY_STTM}#' on external (v_dbname) as user (v_usr_ro) password (v_pwd_ro) into who_am_i, my_action, what_i_see @@ -60,7 +73,7 @@ suspend; for - execute statement 'select current_user, ''select_with_lock'', id from t_read_only_for_non_sys for update with lock' + execute statement q'#{WITH_LOCK_STTM}#' on external (v_dbname) as user (v_usr_ro) password (v_pwd_ro) into who_am_i, my_action, what_i_see @@ -90,7 +103,7 @@ into v_dbname; for - execute statement 'select current_user, ''select_with_lock'', id from t_read_only_for_non_sys for update with lock' + execute statement q'#{WITH_LOCK_STTM}#' on external (v_dbname) as user (v_usr_ud) password (v_pwd_ud) into who_am_i, my_action, what_i_see @@ -101,53 +114,70 @@ ^ set term ;^ rollback; - - -- |||||||||||||||||||||||||||| - -- ###################################||| FB 4.0+, SS and SC |||############################## - -- |||||||||||||||||||||||||||| - -- If we check SS or SC and ExtConnPoolLifeTime > 0 (config parameter FB 4.0+) then current - -- DB (bugs.core_NNNN.fdb) will be 'captured' by firebird.exe process and fbt_run utility - -- will not able to drop this database at the final point of test. - -- Moreover, DB file will be hold until all activity in firebird.exe completed and AFTER this - -- we have to wait for seconds after it (discussion and small test see - -- in the letter to hvlad and dimitr 13.10.2019 11:10). - -- This means that one need to kill all connections to prevent from exception on cleanup phase: - -- SQLCODE: -901 / lock time-out on wait transaction / object is in use - -- ############################################################################################# - delete from mon$attachments where mon$attachment_id != current_connection; - commit; """ -act = isql_act('db', test_script, - substitutions=[('^((?!335544352|335544878).)*$', ''), ('number is.*', '')]) +# Only error messages are checked in this test: +substitutions = [ ('(-)?At block line(:)?\\s+\\d+.*', ''), + ('Data source : Firebird::.*', 'Data source : Firebird::'), + ('335544878 : concurrent transaction number.*', '335544878 : concurrent transaction number'), + ] -expected_stdout = """ - WHO_AM_I MY_ACTION WHAT_I_SEE - =============================== ==================== ============ - TMP$C3736_RO select_read_only 1 - TMP$C3736_RO select_read_only 2 - TMP$C3736_RO select_read_only 3 - TMP$C3736_RO select_read_only 4 - TMP$C3736_RO select_read_only 5 - - - WHO_AM_I MY_ACTION WHAT_I_SEE - =============================== ==================== ============ - TMP$C3736_UD select_with_lock 1 - TMP$C3736_UD select_with_lock 2 - TMP$C3736_UD select_with_lock 3 -""" - -expected_stderr = """ - 335544352 : no permission for UPDATE access to TABLE T_READ_ONLY_FOR_NON_SYS - 335544878 : concurrent transaction number is 806 -""" +act = isql_act('db', test_script, substitutions = substitutions) +@pytest.mark.es_eds @pytest.mark.version('>=3.0') def test_1(act: Action, user_1_ro: User, user_1_ud: User): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + expected_stderr_3x = f""" + Statement failed, SQLSTATE = 42000 + Execute statement error at isc_dsql_prepare : + 335544352 : no permission for UPDATE access to TABLE T_READ_ONLY_FOR_NON_SYS + Statement : {WITH_LOCK_STTM} + Data source : Firebird:: + + Statement failed, SQLSTATE = 42000 + Execute statement error at isc_dsql_fetch : + 335544336 : deadlock + 335544451 : update conflicts with concurrent update + 335544878 : concurrent transaction number + Statement : {WITH_LOCK_STTM} + Data source : Firebird:: + """ + + expected_stderr_5x = f""" + Statement failed, SQLSTATE = 42000 + Execute statement error at isc_dsql_prepare : + 335544352 : no permission for UPDATE access to TABLE T_READ_ONLY_FOR_NON_SYS + 335545254 : Effective user is {user_1_ro.name.upper()} + Statement : {WITH_LOCK_STTM} + Data source : Firebird:: + + Statement failed, SQLSTATE = 42000 + Execute statement error at isc_dsql_fetch : + 335544336 : deadlock + 335544451 : update conflicts with concurrent update + 335544878 : concurrent transaction number + Statement : {WITH_LOCK_STTM} + Data source : Firebird:: + """ + + expected_stderr_6x = f""" + Statement failed, SQLSTATE = 42000 + Execute statement error at isc_dsql_prepare : + 335544352 : no permission for UPDATE access to TABLE "PUBLIC"."T_READ_ONLY_FOR_NON_SYS" + 335545254 : Effective user is {user_1_ro.name.upper()} + Statement : {WITH_LOCK_STTM} + Data source : Firebird:: + + Statement failed, SQLSTATE = 42000 + Execute statement error at isc_dsql_fetch : + 335544336 : deadlock + 335544451 : update conflicts with concurrent update + 335544878 : concurrent transaction number + Statement : {WITH_LOCK_STTM} + Data source : Firebird:: + """ + + act.expected_stderr = expected_stderr_3x if act.is_version('<4') else expected_stderr_5x if act.is_version('<6') else expected_stderr_6x + act.execute(combine_output = False) # ::: NB ::: we need to parse only error messages. + assert act.clean_stderr == act.clean_expected_stderr diff --git a/tests/bugs/core_3761_test.py b/tests/bugs/core_3761_test.py index 0482e531..d72511ee 100644 --- a/tests/bugs/core_3761_test.py +++ b/tests/bugs/core_3761_test.py @@ -9,20 +9,24 @@ FBTEST: bugs.core_3761 NOTES: [25.11.2023] pzotov - Writing code requires more care since 6.0.0.150: ISQL does not allow specifying duplicate delimiters without any statements between them (two semicolon, two carets etc). + Writing code requires more care since 6.0.0.150: ISQL does not allow specifying duplicate + delimiters without any statements between them (two semicolon, two carets etc). + + [28.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """ - create exception check_exception 'check exception'; - commit; -""" - -db = db_factory(init=init_script) +db = db_factory() test_script = """ + create exception check_exception 'check exception'; + commit; set term ^; execute block as begin @@ -31,19 +35,24 @@ set term ;^ """ -act = isql_act('db', test_script, substitutions=[('-At block line: [\\d]+, col: [\\d]+', '-At block line')]) +act = isql_act('db', test_script, substitutions=[('(-)?At block line(:)?\\s+\\d+.*', '')]) -expected_stdout = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = HY000 exception 1 -CHECK_EXCEPTION -word - -At block line: 4, col: 2 """ -@pytest.mark.version('>=3') +expected_stdout_6x = """ + Statement failed, SQLSTATE = HY000 + exception 1 + -"PUBLIC"."CHECK_EXCEPTION" + -word +""" + +@pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_3834_test.py b/tests/bugs/core_3834_test.py index 2668ec10..581abd8c 100644 --- a/tests/bugs/core_3834_test.py +++ b/tests/bugs/core_3834_test.py @@ -14,6 +14,11 @@ 2. FB 5.0.0.455 and later: data sources with equal cardinality now present in the HASH plan in order they are specified in the query. Reversed order was used before this build. Because of this, two cases of expected stdout must be taken in account, see variables 'fb3x_checked_stdout' and 'fb5x_checked_stdout'. + [28.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -25,22 +30,29 @@ set list on; set planonly; -- This query caused FB 2.5.0 to crash: - select * from (select * from mon$attachments a) a natural join mon$statements s where mon$stat_id = ?; + select * + from (select * from mon$attachments a_mon_att_inner) + a_mon_att_outer natural join mon$statements a_mon_sttm + where a_mon_sttm.mon$stat_id = ?; quit; """ act = isql_act('db', test_script) fb3x_checked_stdout = """ - PLAN HASH (S NATURAL, A A NATURAL) + PLAN HASH (A_MON_STTM NATURAL, A_MON_ATT_OUTER A_MON_ATT_INNER NATURAL) """ fb5x_checked_stdout = """ - PLAN HASH (A A NATURAL, S NATURAL) + PLAN HASH (A_MON_ATT_OUTER A_MON_ATT_INNER NATURAL, A_MON_STTM NATURAL) +""" + +fb6x_checked_stdout = """ + PLAN HASH ("A_MON_ATT_OUTER" "A_MON_ATT_INNER" NATURAL, "A_MON_STTM" NATURAL) """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = fb3x_checked_stdout if act.is_version('<5') else fb5x_checked_stdout + act.expected_stdout = fb3x_checked_stdout if act.is_version('<5') else fb5x_checked_stdout if act.is_version('<6') else fb6x_checked_stdout act.execute() assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3881_test.py b/tests/bugs/core_3881_test.py index cf7bd6e1..adf83589 100644 --- a/tests/bugs/core_3881_test.py +++ b/tests/bugs/core_3881_test.py @@ -7,6 +7,10 @@ DESCRIPTION: JIRA: CORE-3881 FBTEST: bugs.core_3881 +NOTES: + [27.06.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -52,31 +56,35 @@ act = isql_act('db', test_script, substitutions=[('-At trigger.*', '')]) -expected_stderr = """ - Statement failed, SQLSTATE = 23000 - violation of PRIMARY or UNIQUE KEY constraint "TMAIN_PK" on table "TMAIN" - -Problematic key value is ("ID" = 300) - Statement failed, SQLSTATE = 23000 - violation of PRIMARY or UNIQUE KEY constraint "TMAIN_MKA_MKB_CONSTRAINT_UNQ" on table "TMAIN" - -Problematic key value is ("MKA" = NULL, "MKB" = 200) +@pytest.mark.version('>=3') +def test_1(act: Action): + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' - Statement failed, SQLSTATE = 23000 - attempt to store duplicate value (visible to active transactions) in unique index "TMAIN_DIFFERENCE_UNQ_IDX" - -Problematic key value is ( = 0) + expected_stderr = f""" + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TMAIN_PK" on table {SQL_SCHEMA_PREFIX}"TMAIN" + -Problematic key value is ("ID" = 300) - Statement failed, SQLSTATE = 23000 - violation of PRIMARY or UNIQUE KEY constraint "TMAIN_MKA_MKB_CONSTRAINT_UNQ" on table "TMAIN" - -Problematic key value is ("MKA" = 200, "MKB" = 200) + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TMAIN_MKA_MKB_CONSTRAINT_UNQ" on table {SQL_SCHEMA_PREFIX}"TMAIN" + -Problematic key value is ("MKA" = NULL, "MKB" = 200) - Statement failed, SQLSTATE = 23000 - attempt to store duplicate value (visible to active transactions) in unique index "TMAIN_DIFFERENCE_UNQ_IDX" - -Problematic key value is ( = 0) - -At trigger 'CHECK_2' -""" + Statement failed, SQLSTATE = 23000 + attempt to store duplicate value (visible to active transactions) in unique index {SQL_SCHEMA_PREFIX}"TMAIN_DIFFERENCE_UNQ_IDX" + -Problematic key value is ( = 0) + + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TMAIN_MKA_MKB_CONSTRAINT_UNQ" on table {SQL_SCHEMA_PREFIX}"TMAIN" + -Problematic key value is ("MKA" = 200, "MKB" = 200) + + Statement failed, SQLSTATE = 23000 + attempt to store duplicate value (visible to active transactions) in unique index {SQL_SCHEMA_PREFIX}"TMAIN_DIFFERENCE_UNQ_IDX" + -Problematic key value is ( = 0) + -At trigger 'CHECK_2' + """ -@pytest.mark.version('>=3') -def test_1(act: Action): act.expected_stderr = expected_stderr act.execute() assert act.clean_stderr == act.clean_expected_stderr diff --git a/tests/bugs/core_3884_test.py b/tests/bugs/core_3884_test.py index d25b6364..ae06320f 100644 --- a/tests/bugs/core_3884_test.py +++ b/tests/bugs/core_3884_test.py @@ -44,6 +44,7 @@ -At block line: 3, col: 13 """ +@pytest.mark.trace @pytest.mark.version('>=3') def test_1(act: Action, capsys): diff --git a/tests/bugs/core_3894_test.py b/tests/bugs/core_3894_test.py index bb933014..6aed46b2 100644 --- a/tests/bugs/core_3894_test.py +++ b/tests/bugs/core_3894_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-3894 FBTEST: bugs.core_3894 +NOTES: + [28.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -30,25 +36,29 @@ show table test; """ -act = isql_act('db', test_script) +substitutions=[('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ +expected_stdout_5x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -ALTER TABLE TEST failed + -New size specified for column S01 must be at least 8190 characters. ID INTEGER Nullable S01 VARCHAR(8190) CHARACTER SET UTF8 Nullable """ - -expected_stderr = """ +expected_stdout_6x = """ Statement failed, SQLSTATE = 42000 unsuccessful metadata update - -ALTER TABLE TEST failed - -New size specified for column S01 must be at least 8190 characters. + -ALTER TABLE "PUBLIC"."TEST" failed + -New size specified for column "S01" must be at least 8190 characters. + Table: PUBLIC.TEST + ID INTEGER Nullable + S01 VARCHAR(8190) CHARACTER SET SYSTEM.UTF8 Nullable """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3902_test.py b/tests/bugs/core_3902_test.py index 86b95304..8f3372d1 100644 --- a/tests/bugs/core_3902_test.py +++ b/tests/bugs/core_3902_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-3902 FBTEST: bugs.core_3902 +NOTES: + [28.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -28,16 +34,20 @@ """ -act = isql_act('db', test_script) +substitutions = [(r'RDB\$INDEX_\d+', 'RDB$INDEX_*')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ -PLAN JOIN (RDB$DATABASE NATURAL, TEMP RDB$RELATIONS INDEX (RDB$INDEX_1)) -PLAN JOIN (RDB$DATABASE NATURAL, TEMP RDB$RELATIONS INDEX (RDB$INDEX_1)) +expected_stdout_5x = """ + PLAN JOIN (RDB$DATABASE NATURAL, TEMP RDB$RELATIONS INDEX (RDB$INDEX_1)) + PLAN JOIN (RDB$DATABASE NATURAL, TEMP RDB$RELATIONS INDEX (RDB$INDEX_1)) +""" +expected_stdout_6x = """ + PLAN JOIN ("SYSTEM"."RDB$DATABASE" NATURAL, "TEMP" "SYSTEM"."RDB$RELATIONS" INDEX ("SYSTEM"."RDB$INDEX_1")) + PLAN JOIN ("SYSTEM"."RDB$DATABASE" NATURAL, "TEMP" "SYSTEM"."RDB$RELATIONS" INDEX ("SYSTEM"."RDB$INDEX_1")) """ -@pytest.mark.version('>=3') +@pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_3925_test.py b/tests/bugs/core_3925_test.py index c93dd342..1eba387c 100644 --- a/tests/bugs/core_3925_test.py +++ b/tests/bugs/core_3925_test.py @@ -10,6 +10,10 @@ DELETE statement does not raise error. JIRA: CORE-3925 FBTEST: bugs.core_3925 +NOTES: + [27.06.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -28,16 +32,18 @@ act = isql_act('db', test_script) -expected_stderr = """ - Statement failed, SQLSTATE = 23000 - violation of FOREIGN KEY constraint "FK_KEY_REF" on table "TEST" - -Foreign key reference target does not exist - -Problematic key value is ("REF" = -1) -""" - @pytest.mark.version('>=3.0.5') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + expected_stdout = f""" + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint "FK_KEY_REF" on table {SQL_SCHEMA_PREFIX}"TEST" + -Foreign key reference target does not exist + -Problematic key value is ("REF" = -1) + """ + + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_3934_test.py b/tests/bugs/core_3934_test.py index 7f14f311..c7753d0a 100644 --- a/tests/bugs/core_3934_test.py +++ b/tests/bugs/core_3934_test.py @@ -38,6 +38,7 @@ def check_sweep(act: Action, log_sweep: bool): with act.trace(db_events=cfg), act.connect_server() as srv: srv.database.sweep(database=act.db.db_path) +@pytest.mark.trace @pytest.mark.version('>=3.0') def test_1(act: Action): # Case 1 - sweep logged diff --git a/tests/bugs/core_3947_test.py b/tests/bugs/core_3947_test.py index 8afd2661..b79e9ba3 100644 --- a/tests/bugs/core_3947_test.py +++ b/tests/bugs/core_3947_test.py @@ -9,6 +9,12 @@ not be usable for ORDER, only for lookups. JIRA: CORE-3947 FBTEST: bugs.core_3947 +NOTES: + [27.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -45,15 +51,20 @@ act = isql_act('db', test_script) -expected_stdout = """ +expected_stdout_5x = """ PLAN (T ORDER T_S1_NON_UNQ) PLAN SORT (T NATURAL) PLAN SORT (T NATURAL) """ +expected_stdout_6x = """ + PLAN ("PUBLIC"."T" ORDER "PUBLIC"."T_S1_NON_UNQ") + PLAN SORT ("PUBLIC"."T" NATURAL) + PLAN SORT ("PUBLIC"."T" NATURAL) +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_3963_test.py b/tests/bugs/core_3963_test.py index 4eae7d47..f82d7a6c 100644 --- a/tests/bugs/core_3963_test.py +++ b/tests/bugs/core_3963_test.py @@ -17,6 +17,12 @@ in UDR library "udf_compat", see it in folder: ../plugins/udr/ JIRA: CORE-3963 FBTEST: bugs.core_3963 +NOTES: + [28.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -106,6 +112,7 @@ def test_1(act_1: Action): assert (act_1.clean_stderr == act_1.clean_expected_stderr and act_1.clean_stdout == act_1.clean_expected_stdout) +############################################################################# # version: 4.0 test_script_2 = """ @@ -170,53 +177,64 @@ def test_1(act_1: Action): act_2 = isql_act('db', test_script_2) -expected_stdout_2 = """ - +expected_stdout_5x = """ INPUT message field count: 0 - OUTPUT message field count: 1 01: sqltype: 480 DOUBLE Nullable scale: 0 subtype: 0 len: 8 - : name: THE_FRAC alias: THE_FRAC - : table: owner: - + : name: THE_FRAC alias: THE_FRAC + : table: owner: THE_FRAC -0.1415926535897931 - - - + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -CREATE FUNCTION THE_FRAC failed + -Function THE_FRAC already exists + Statement failed, SQLSTATE = 39000 + Dynamic SQL Error + -SQL error code = -804 + -Function unknown + -THE_FRAC INPUT message field count: 0 - OUTPUT message field count: 1 01: sqltype: 480 DOUBLE Nullable scale: 0 subtype: 0 len: 8 - : name: THE_FRAC alias: THE_FRAC - : table: owner: - + : name: THE_FRAC alias: THE_FRAC + : table: owner: THE_FRAC -0.1415926535897931 - -""" - -expected_stderr_2 = """ Statement failed, SQLSTATE = 42000 unsuccessful metadata update -CREATE FUNCTION THE_FRAC failed -Function THE_FRAC already exists +""" +expected_stdout_6x = """ + INPUT message field count: 0 + OUTPUT message field count: 1 + 01: sqltype: 480 DOUBLE Nullable scale: 0 subtype: 0 len: 8 + : name: THE_FRAC alias: THE_FRAC + : table: schema: owner: + THE_FRAC -0.1415926535897931 + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -CREATE FUNCTION "PUBLIC"."THE_FRAC" failed + -Function "PUBLIC"."THE_FRAC" already exists Statement failed, SQLSTATE = 39000 Dynamic SQL Error -SQL error code = -804 -Function unknown - -THE_FRAC - + -"THE_FRAC" + INPUT message field count: 0 + OUTPUT message field count: 1 + 01: sqltype: 480 DOUBLE Nullable scale: 0 subtype: 0 len: 8 + : name: THE_FRAC alias: THE_FRAC + : table: schema: owner: + THE_FRAC -0.1415926535897931 Statement failed, SQLSTATE = 42000 unsuccessful metadata update - -CREATE FUNCTION THE_FRAC failed - -Function THE_FRAC already exists + -CREATE FUNCTION "PUBLIC"."THE_FRAC" failed + -Function "PUBLIC"."THE_FRAC" already exists """ @pytest.mark.version('>=4.0') -def test_2(act_2: Action): - act_2.expected_stdout = expected_stdout_2 - act_2.expected_stderr = expected_stderr_2 - act_2.execute() - assert (act_2.clean_stderr == act_2.clean_expected_stderr and - act_2.clean_stdout == act_2.clean_expected_stdout) - +def test_1(act_2: Action): + act_2.expected_stdout = expected_stdout_5x if act_2.is_version('<6') else expected_stdout_6x + act_2.execute(combine_output = True) + assert act_2.clean_stdout == act_2.clean_expected_stdout diff --git a/tests/bugs/core_3973_test.py b/tests/bugs/core_3973_test.py index 9934bb8c..06dff321 100644 --- a/tests/bugs/core_3973_test.py +++ b/tests/bugs/core_3973_test.py @@ -9,9 +9,13 @@ FBTEST: bugs.core_3973 NOTES: [11.12.2023] pzotov - Added 'SQLSTATE' in substitutions: runtime error must not be filtered out by '?!(...)' pattern - ("negative lookahead assertion", see https://docs.python.org/3/library/re.html#regular-expression-syntax). - Added 'combine_output = True' in order to see SQLSTATE if any error occurs. + Added 'SQLSTATE' in substitutions: runtime error must not be filtered out by '?!(...)' pattern + ("negative lookahead assertion", see https://docs.python.org/3/library/re.html#regular-expression-syntax). + Added 'combine_output = True' in order to see SQLSTATE if any error occurs. + [28.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -32,14 +36,20 @@ @pytest.mark.version('>=3.0') def test_1(act: Action): - expected_stdout = f""" + expected_stdout_5x = f""" : name: RDB$RELATION_ID alias: R_ID : table: RDB$DATABASE owner: {act.db.user.upper()} : name: RDB$CHARACTER_SET_NAME alias: RDB$CHARACTER_SET_NAME : table: RDB$DATABASE owner: {act.db.user.upper()} """ - act.expected_stdout = expected_stdout + expected_stdout_6x = f""" + : name: RDB$RELATION_ID alias: R_ID + : table: RDB$DATABASE schema: SYSTEM owner: {act.db.user.upper()} + : name: RDB$CHARACTER_SET_NAME alias: RDB$CHARACTER_SET_NAME + : table: RDB$DATABASE schema: SYSTEM owner: {act.db.user.upper()} + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_3977_test.py b/tests/bugs/core_3977_test.py index 058ae6be..6afe14e4 100644 --- a/tests/bugs/core_3977_test.py +++ b/tests/bugs/core_3977_test.py @@ -20,6 +20,13 @@ mon$sql_text contains some KNOWN phrase - see 'HEAVY_TAG'. See function check_mon_for_pid_appearance() Checked on 3.0.12.33725, 4.0.5.3040, 5.0.0.1294, 6.0.0.172 + + [18.01.2025] pzotov + Resultset of cursor that executes using instance of selectable PreparedStatement must be stored + in some variable in order to have ability close it EXPLICITLY (before PS will be freed). + Otherwise access violation raises during Python GC and pytest hangs at final point (does not return control to OS). + This occurs at least for: Python 3.11.2 / pytest: 7.4.4 / firebird.driver: 1.10.6 / Firebird.Qa: 0.19.3 + The reason of that was explained by Vlad, 26.10.24 17:42 ("oddities when use instances of selective statements"). """ import os @@ -64,32 +71,49 @@ def check_mon_for_pid_appearance(act: Action, p_async_launched: subprocess.Compl found_in_mon_tables = False with act.db.connect() as con_watcher: - custom_tpb = tpb(isolation = Isolation.SNAPSHOT, lock_timeout = -1) - tx_watcher = con_watcher.transaction_manager(custom_tpb) - cur_watcher = tx_watcher.cursor() - - ps = cur_watcher.prepare(chk_mon_sql) - - i = 0 - da = dt.now() - while True: - cur_watcher.execute(ps, (p_async_launched.pid, HEAVY_TAG,) ) - mon_result = -1 - for r in cur_watcher: - mon_result = r[0] - - tx_watcher.commit() - db = dt.now() - diff_ms = (db-da).seconds*1000 + (db-da).microseconds//1000 - if mon_result == 1: - found_in_mon_tables = True - break - elif diff_ms > MAX_WAIT_FOR_ISQL_PID_APPEARS_MS: - break + ps, rs = None, None + try: + custom_tpb = tpb(isolation = Isolation.SNAPSHOT, lock_timeout = -1) + tx_watcher = con_watcher.transaction_manager(custom_tpb) + cur_watcher = tx_watcher.cursor() + + ps = cur_watcher.prepare(chk_mon_sql) + + i = 0 + da = dt.now() + while True: + mon_result = -1 + + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + rs = cur_watcher.execute(ps, (p_async_launched.pid, HEAVY_TAG,) ) + for r in rs: + mon_result = r[0] + + tx_watcher.commit() + db = dt.now() + diff_ms = (db-da).seconds*1000 + (db-da).microseconds//1000 + if mon_result == 1: + found_in_mon_tables = True + break + elif diff_ms > MAX_WAIT_FOR_ISQL_PID_APPEARS_MS: + break + time.sleep(0.1) + + except DatabaseError as e: + print( e.__str__() ) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() - time.sleep(0.1) - ps.free() assert found_in_mon_tables, f'Could not find attachment in mon$ tables for {MAX_WAIT_FOR_ISQL_PID_APPEARS_MS} ms.' diff --git a/tests/bugs/core_3997_test.py b/tests/bugs/core_3997_test.py index 5ec91183..a961f93c 100644 --- a/tests/bugs/core_3997_test.py +++ b/tests/bugs/core_3997_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-3997 FBTEST: bugs.core_3997 +NOTES: + [28.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -43,13 +48,16 @@ act = isql_act('db', test_script) -expected_stdout = """ +expected_stdout_5x = """ PLAN JOIN (T_KEY NATURAL, T INDEX ()) """ +expected_stdout_6x = """ + PLAN JOIN ("PUBLIC"."T_KEY" NATURAL, "PUBLIC"."T" INDEX ()) +""" + @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4027_test.py b/tests/bugs/core_4027_test.py index 1c8f3b76..e8d8fc3d 100644 --- a/tests/bugs/core_4027_test.py +++ b/tests/bugs/core_4027_test.py @@ -7,6 +7,11 @@ DESCRIPTION: Broken output in ISQL command SHOW TABLE for computed-by field(s). JIRA: CORE-4027 FBTEST: bugs.core_4027 +NOTES: + [28.06.2025] pzotov + See also test for https://github.com/FirebirdSQL/firebird/issues/4402 + Replaced 'SHOW' command with query to RDB tables. Bug still can be seen in 2.5.9.27156. + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -14,7 +19,28 @@ db = db_factory() -test_script = """ +COMPUTED_EXPR_1 = """ + ( + select first 1 t.po_number + from turnovers t + where t.agent_id=test.agent_id + order by t.order_date + ) +""" + +COMPUTED_EXPR_2 = """ + ( + select agent_name + from contragents a + where a.agent_id = test.agent_id + ) +""" + +test_script = f""" + set list on; + set blob all; + set count on; + -- NB: fixed only in 3.0 (checked 30.03.2015) recreate table test (id int); commit; @@ -34,50 +60,53 @@ recreate table test ( agent_id integer not null, - first_po_number computed by ( - ( - select first 1 t.po_number - from turnovers t - where t.agent_id=test.agent_id - order by t.order_date - ) - ), - agent_name computed by ( - ( - select agent_name - from contragents a - where a.agent_id = test.agent_id - ) - ) + first_po_number computed by ( {COMPUTED_EXPR_1} ), + agent_name computed by ( {COMPUTED_EXPR_2} ) ); commit; - show table test; + + select + rf.rdb$field_name fld_name + ,f.rdb$field_type fld_type + ,f.rdb$field_length fld_length + ,f.rdb$field_scale fld_scale + ,f.rdb$computed_source as rdb_blob_id + from rdb$relation_fields rf + left join rdb$fields f on rf.rdb$field_source = f.rdb$field_name + where rf.rdb$relation_name = 'TEST'; """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' '), ('RDB_BLOB_ID.*', '')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ - AGENT_ID INTEGER Not Null - FIRST_PO_NUMBER Computed by: ( - ( - select first 1 t.po_number - from turnovers t - where t.agent_id=test.agent_id - order by t.order_date - ) - ) - AGENT_NAME Computed by: ( - ( - select agent_name - from contragents a - where a.agent_id = test.agent_id - ) - ) +expected_stdout = f""" + FLD_NAME AGENT_ID + FLD_TYPE 8 + FLD_LENGTH 4 + FLD_SCALE 0 + RDB_BLOB_ID + FLD_NAME FIRST_PO_NUMBER + FLD_TYPE 14 + FLD_LENGTH 8 + FLD_SCALE 0 + RDB_BLOB_ID 2:1e4 + ( + {COMPUTED_EXPR_1} + ) + FLD_NAME AGENT_NAME + FLD_TYPE 37 + FLD_LENGTH 25 + FLD_SCALE 0 + RDB_BLOB_ID 2:1e6 + ( + {COMPUTED_EXPR_2} + ) + Records affected: 3 """ @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4038_test.py b/tests/bugs/core_4038_test.py index 09d83b90..e84c7405 100644 --- a/tests/bugs/core_4038_test.py +++ b/tests/bugs/core_4038_test.py @@ -7,30 +7,40 @@ DESCRIPTION: JIRA: CORE-4038 FBTEST: bugs.core_4038 +NOTES: + [28.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """create table t (dbkey char(8) character set octets); -create index it on t (dbkey); -""" +db = db_factory() -db = db_factory(init=init_script) +test_script = """ + create table t (dbkey char(8) character set octets); + create index it on t (dbkey); -test_script = """SET PLANONLY; -select * from t as t1 - left join t as t2 on t2.dbkey = t1.rdb$db_key; + SET PLANONLY; + select * from t as t1 + left join t as t2 on t2.dbkey = t1.rdb$db_key; """ act = isql_act('db', test_script) -expected_stdout = """PLAN JOIN (T1 NATURAL, T2 INDEX (IT)) +expected_stdout_5x = """ + PLAN JOIN (T1 NATURAL, T2 INDEX (IT)) """ -@pytest.mark.version('>=3') +expected_stdout_6x = """ + PLAN JOIN ("T1" NATURAL, "T2" INDEX ("PUBLIC"."IT")) +""" + +@pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4054_test.py b/tests/bugs/core_4054_test.py index c50a7f0e..29beea45 100644 --- a/tests/bugs/core_4054_test.py +++ b/tests/bugs/core_4054_test.py @@ -117,6 +117,7 @@ WHAT_I_SEE 789654123 """ +@pytest.mark.es_eds @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_4067_test.py b/tests/bugs/core_4067_test.py index 86fd1f1a..d18202a0 100644 --- a/tests/bugs/core_4067_test.py +++ b/tests/bugs/core_4067_test.py @@ -14,11 +14,10 @@ from firebird.qa import * db = db_factory() - -act = python_act('db') +act = python_act('db', substitutions = [('[ \t]+', ' ')]) expected_stdout = """ - X 1 + SQL_DIALECT 1 """ temp_db = temp_file('tmp_4067_1.fdb') @@ -26,11 +25,14 @@ @pytest.mark.version('>=3') def test_1(act: Action, temp_db: Path): test_script = f""" - set sql dialect 1; - create database 'localhost:{str(temp_db)}' page_size 4096 default character set win1251 collation win1251; - set list on; - select mon$sql_dialect as x from mon$database; -""" + set bail on; + set list on; + set sql dialect 1; + create database 'localhost:{str(temp_db)}' page_size 4096 default character set win1251 collation win1251; + select mon$sql_dialect as sql_dialect from mon$database; + commit; + drop database; + """ act.expected_stdout = expected_stdout - act.isql(switches=[], input=test_script, connect_db=False) + act.isql(switches = ['-q'], input=test_script, connect_db=False, combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4070_test.py b/tests/bugs/core_4070_test.py index b905f9d9..40e0788a 100644 --- a/tests/bugs/core_4070_test.py +++ b/tests/bugs/core_4070_test.py @@ -3,10 +3,15 @@ """ ID: issue-4398 ISSUE: 4398 -TITLE: NOT-NULL-column can be used as primary key and filled with NULL-values +TITLE: NOT-NULL column can be used as primary key and filled with NULL-values DESCRIPTION: JIRA: CORE-4070 FBTEST: bugs.core_4070 +NOTES: + [28.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -21,16 +26,24 @@ act = isql_act('db', test_script) -expected_stderr = """ +act = isql_act('db', test_script) + +expected_stdout_5x = """ Statement failed, SQLSTATE = 42000 unsuccessful metadata update -ALTER TABLE TEST01 failed -Column: UID not defined as NOT NULL - cannot be used in PRIMARY KEY constraint definition """ +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -ALTER TABLE "PUBLIC"."TEST01" failed + -Column: "PUBLIC"."UID" not defined as NOT NULL - cannot be used in PRIMARY KEY constraint definition +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4074_test.py b/tests/bugs/core_4074_test.py index 9303b99a..6fedaf1a 100644 --- a/tests/bugs/core_4074_test.py +++ b/tests/bugs/core_4074_test.py @@ -7,40 +7,65 @@ DESCRIPTION: JIRA: CORE-4074 FBTEST: bugs.core_4074 +NOTES: + [28.06.2025] pzotov + Replaced 'SHOW' command with query to RDB tables. + See also test for https://github.com/FirebirdSQL/firebird/issues/4357 + Bug still can be seen in 2.5.9.27156 - rdb$computed_source has weird content: + ======= + ( 'fabio ' || position('x','schunig') ), + f02 numeric(8,2) default 0 + ) + ======= + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """ - recreate table test01 ( - f01 computed by ( 'fabio ' || position('x','schunig') ), +db = db_factory() + +COMPUTED_EXPR = "'fabio ' || position('x','schunig')" +test_script = f""" + set list on; + set blob all; + recreate table test ( + f01 computed by ( {COMPUTED_EXPR} ), f02 numeric(8,2) default 0 ); + commit; + select + rf.rdb$field_name fld_name + ,f.rdb$field_type fld_type + ,f.rdb$field_length fld_length + ,f.rdb$field_scale fld_scale + ,f.rdb$computed_source as rdb_blob_id + from rdb$relation_fields rf + left join rdb$fields f on rf.rdb$field_source = f.rdb$field_name + where rf.rdb$relation_name = upper('TEST'); """ -db = db_factory(init=init_script) - -test_script = """ - show table test01; - -- ::: NB ::: On WI-V2.5.4.26856, 26-mar-2015, output is: - -- F01 Computed by: ( 'fabio ' || position('x','schunig') ), - -- f02 numeric(8,2) default 0 - -- ) - -- F02 NUMERIC(8, 2) Nullable ) - -- (i.e. it DOES contain "strange" last line) -""" - -act = isql_act('db', test_script) +substitutions = [ ('[ \t]+', ' '), ('RDB_BLOB_ID.*', '') ] +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ - F01 Computed by: ( 'fabio ' || position('x','schunig') ) - F02 NUMERIC(8, 2) Nullable default 0 + FLD_NAME F01 + FLD_TYPE 37 + FLD_LENGTH 17 + FLD_SCALE 0 + RDB_BLOB_ID 2:1e4 + ( 'fabio ' || position('x','schunig') ) + + FLD_NAME F02 + FLD_TYPE 8 + FLD_LENGTH 4 + FLD_SCALE -2 + RDB_BLOB_ID """ @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4075_test.py b/tests/bugs/core_4075_test.py index 2f6ea156..51aa0beb 100644 --- a/tests/bugs/core_4075_test.py +++ b/tests/bugs/core_4075_test.py @@ -5,25 +5,27 @@ ISSUE: 4403 TITLE: Server bugchecks or crashes on exception in calculated index DESCRIPTION: -NOTES: -[18.10.2016] added test case from #4918 - NB: 2.5.x output contains TWO lines with error message, i.e.: - Statement failed, SQLSTATE = 22018 - conversion error from string "2014.02.33" - -conversion error from string "2014.02.33" - Decided to suppress second line because its unlikely to be fixed - (after get reply from dimitr, letter 18.10.2016 18:47). -[16.09.2017] added separate section for 4.0 because STDERR now - contains name of index that causes problem - this is so after core-5606 - was implemented ("Add expression index name to exception message ...") JIRA: CORE-4075 FBTEST: bugs.core_4075 +NOTES: + [18.10.2016] + Added test case from #4918 + [16.09.2017] + Added separate section for 4.0 because STDERR now contains name of index that causes problem (after core-5606 was implemented) + [27.06.2025] pzotov + Replaced subst: suppress any hyphen sign that occurs at starting position of every error message. + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """ +db = db_factory() + +test_script = """ recreate table TEST (BIT smallint); create index IDX_TEST_BIT on TEST computed by (bin_shl(1, TEST.BIT-1)); @@ -34,56 +36,51 @@ ); create index T_INDEX on T_TABLE computed by (cast(F_YEAR || '.' || F_MONTH_DAY as date)); commit; - - """ - -db = db_factory(init=init_script) - -test_script = """ + insert into test values (0); -- Trace: -- 335544606 : expression evaluation not supported -- 335544967 : Argument for BIN_SHL must be zero or positive - -- from core-4603: insert into T_TABLE (F_YEAR, F_MONTH_DAY) values ('2014', '02.33'); """ -act = isql_act('db', test_script, substitutions=[('-conversion error from string "2014.02.33"', '')]) - -# version: 3 +substitutions = [('^\\s*(-)?', '')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stderr_1 = """ +expected_stdout_3x = """ Statement failed, SQLSTATE = 42000 expression evaluation not supported - -Argument for BIN_SHL must be zero or positive + Argument for BIN_SHL must be zero or positive Statement failed, SQLSTATE = 22018 conversion error from string "2014.02.33" """ -@pytest.mark.version('>=3,<4.0') -def test_1(act: Action): - act.expected_stderr = expected_stderr_1 - act.execute() - assert act.clean_stderr == act.clean_expected_stderr - -# version: 4.0 - -expected_stderr_2 = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 42000 Expression evaluation error for index "IDX_TEST_BIT" on table "TEST" - -expression evaluation not supported - -Argument for BIN_SHL must be zero or positive + expression evaluation not supported + Argument for BIN_SHL must be zero or positive Statement failed, SQLSTATE = 22018 Expression evaluation error for index "T_INDEX" on table "T_TABLE" - -conversion error from string "2014.02.33" + conversion error from string "2014.02.33" """ -@pytest.mark.version('>=4.0') -def test_2(act: Action): - act.expected_stderr = expected_stderr_2 - act.execute() - assert act.clean_stderr == act.clean_expected_stderr +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + Expression evaluation error for index "PUBLIC"."IDX_TEST_BIT" on table "PUBLIC"."TEST" + expression evaluation not supported + Argument for BIN_SHL must be zero or positive + + Statement failed, SQLSTATE = 22018 + Expression evaluation error for index "PUBLIC"."T_INDEX" on table "PUBLIC"."T_TABLE" + conversion error from string "2014.02.33" +""" +@pytest.mark.version('>=3') +def test_1(act: Action): + act.expected_stdout = expected_stdout_3x if act.is_version('<4') else expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4084_test.py b/tests/bugs/core_4084_test.py index 2e73c203..d789f283 100644 --- a/tests/bugs/core_4084_test.py +++ b/tests/bugs/core_4084_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4084 FBTEST: bugs.core_4084 +NOTES: + [28.06.2025] pzotov + Data in STDOUT is irrelevant and may differ in among FB versions. + Only STDERR must be checked in this test. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -15,7 +21,6 @@ db = db_factory() test_script = """ - set planonly; select iif(d is null, 10, 0) + sys as sys, count(*) @@ -30,14 +35,12 @@ act = isql_act('db', test_script) -expected_stdout = """ - PLAN (D NATURAL) - PLAN SORT (R NATURAL) +expected_stderr = """ """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() - assert act.clean_stdout == act.clean_expected_stdout + act.expected_stderr = expected_stderr + act.execute(combine_output = False) # ::: NB ::: Only STDERR is checked in this test! + assert act.clean_stderr == act.clean_expected_stderr diff --git a/tests/bugs/core_4094_test.py b/tests/bugs/core_4094_test.py index 55f84143..9e9d014b 100644 --- a/tests/bugs/core_4094_test.py +++ b/tests/bugs/core_4094_test.py @@ -72,6 +72,7 @@ 'log_statement_start = true', ] +@pytest.mark.trace @pytest.mark.version('>=3') def test_1(act: Action, capsys): with act.trace(db_events=trace): diff --git a/tests/bugs/core_4102_test.py b/tests/bugs/core_4102_test.py index 16a1973f..907e5a0d 100644 --- a/tests/bugs/core_4102_test.py +++ b/tests/bugs/core_4102_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4102 FBTEST: bugs.core_4102 +NOTES: + [28.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -14,26 +20,33 @@ db = db_factory() -test_script = """SET PLANONLY; -select * from -( - select rdb$relation_id as id - from rdb$relations r - union all - select rdb$relation_id as id - from rdb$relations r -) x -where x.id = 0 or x.id = 1; +test_script = """ + SET PLANONLY; + select * from + ( + select rdb$relation_id as id + from rdb$relations r + union all + select rdb$relation_id as id + from rdb$relations r + ) x + where x.id = 0 or x.id = 1; """ act = isql_act('db', test_script) -expected_stdout = """PLAN (X R INDEX (RDB$INDEX_1, RDB$INDEX_1), X R INDEX (RDB$INDEX_1, RDB$INDEX_1)) +substitutions = [(r'RDB\$INDEX_\d+', 'RDB$INDEX_*')] +act = isql_act('db', test_script, substitutions = substitutions) + +expected_stdout_5x = """ + PLAN (X R INDEX (RDB$INDEX_*, RDB$INDEX_*), X R INDEX (RDB$INDEX_*, RDB$INDEX_*)) +""" +expected_stdout_6x = """ + PLAN ("X" "R" INDEX ("SYSTEM"."RDB$INDEX_*", "SYSTEM"."RDB$INDEX_*"), "X" "R" INDEX ("SYSTEM"."RDB$INDEX_*", "SYSTEM"."RDB$INDEX_*")) """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4117_test.py b/tests/bugs/core_4117_test.py index 4d073508..5fe18552 100644 --- a/tests/bugs/core_4117_test.py +++ b/tests/bugs/core_4117_test.py @@ -9,6 +9,12 @@ value if this field is not a part of excpression JIRA: CORE-4117 FBTEST: bugs.core_4117 +NOTES: + [28.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -79,24 +85,38 @@ update test2 set id = 2; """ -act = isql_act('db', test_script, substitutions=[('line: [0-9]+, col: [0-9]+', '')]) +act = isql_act('db', test_script, substitutions=[('line: \\d+.*', '')]) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = HY000 exception 1 -EX_BAD_COMPUTED_FIELD_VALUE -20 - -At trigger 'TEST1_BU' line: 3, col: 7 + -At trigger 'TEST1_BU' + Statement failed, SQLSTATE = HY000 exception 1 -EX_BAD_COMPUTED_FIELD_VALUE -20 - -At trigger 'TEST2_BU' line: 3, col: 7 + -At trigger 'TEST2_BU' """ -@pytest.mark.version('>=3') -def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr +expected_stdout_6x = """ + Statement failed, SQLSTATE = HY000 + exception 1 + -"PUBLIC"."EX_BAD_COMPUTED_FIELD_VALUE" + -20 + -At trigger "PUBLIC"."TEST1_BU" + Statement failed, SQLSTATE = HY000 + exception 1 + -"PUBLIC"."EX_BAD_COMPUTED_FIELD_VALUE" + -20 + -At trigger "PUBLIC"."TEST2_BU" +""" + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4118_test.py b/tests/bugs/core_4118_test.py index 7988fc6b..b5c13b39 100644 --- a/tests/bugs/core_4118_test.py +++ b/tests/bugs/core_4118_test.py @@ -7,33 +7,42 @@ DESCRIPTION: JIRA: CORE-4118 FBTEST: bugs.core_4118 +NOTES: + [28.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """create table t (id int, d timestamp); -create index itd on t computed (cast(d as date)); -COMMIT; -""" +db = db_factory() -db = db_factory(init=init_script) +test_script = """ + create table t (id int, d timestamp); + create index itd on t computed (cast(d as date)); + commit; -test_script = """SET PLAN ON; -select * from t where cast(d as date) = current_date; -select * from (select id, cast(d as date) as d from t) where d = current_date; + SET PLAN ON; + select * from t where cast(d as date) = current_date; + select * from (select id, cast(d as date) as d from t) where d = current_date; """ act = isql_act('db', test_script) -expected_stdout = """ -PLAN (T INDEX (ITD)) -PLAN (T INDEX (ITD)) +expected_stdout_5x = """ + PLAN (T INDEX (ITD)) + PLAN (T INDEX (ITD)) +""" +expected_stdout_6x = """ + PLAN ("PUBLIC"."T" INDEX ("PUBLIC"."ITD")) + PLAN ("PUBLIC"."T" INDEX ("PUBLIC"."ITD")) """ -@pytest.mark.version('>=3') +@pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4119_test.py b/tests/bugs/core_4119_test.py index 3a91c3c0..2ea6ed5f 100644 --- a/tests/bugs/core_4119_test.py +++ b/tests/bugs/core_4119_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4119 FBTEST: bugs.core_4119 +NOTES: + [28.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -18,28 +24,36 @@ act = python_act('db', substitutions=[('=.*', '')]) script_file = temp_file('test_script.sql') -expected_stdout = """ +expected_stdout_5x = """ + Procedure text: + begin + -- Моя процедура + end +""" + +expected_stdout_6x = """ + Procedure: PUBLIC.MYPROC Procedure text: - ============================================================================= begin -- Моя процедура end - ============================================================================= """ @pytest.mark.version('>=3.0') def test_1(act: Action, script_file: Path): - script_file.write_text(""" - set term ^; - create procedure myproc as - begin - -- Моя процедура - end^ - set term ;^ - show procedure myproc; - """, encoding='cp1251') - act.expected_stdout = expected_stdout - act.isql(switches=['-q'], input_file=script_file, charset='WIN1251') + sp_ddl = """ + set term ^; + create procedure myproc as + begin + -- Моя процедура + end^ + set term ;^ + show procedure myproc; + """ + + script_file.write_text(sp_ddl, encoding='cp1251') + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.isql(switches=['-q'], input_file = script_file, charset='win1251') assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4122_test.py b/tests/bugs/core_4122_test.py index 5ac8819a..f55eebfe 100644 --- a/tests/bugs/core_4122_test.py +++ b/tests/bugs/core_4122_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4122 FBTEST: bugs.core_4122 +NOTES: + [28.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -51,7 +57,7 @@ act = python_act('db', substitutions=[('/* CREATE DATABASE .*', '')]) -expected_stdout = """ +expected_stdout_5x = """ SET SQL DIALECT 3; /* CREATE DATABASE 'localhost/3330:test.fdb' PAGE_SIZE 4096 DEFAULT CHARACTER SET NONE; */ @@ -112,6 +118,7 @@ /* Package bodies */ /* Package body: PKG_TEST, Owner: SYSDBA */ + CREATE PACKAGE BODY PKG_TEST AS begin function F_TEST_INSIDE_PKG @@ -127,10 +134,75 @@ SET AUTODDL ON; """ +expected_stdout_6x = """ + SET SQL DIALECT 3; + /* + /* Schema definitions */ + /* Schema: PUBLIC, Owner: SYSDBA */ + CREATE OR ALTER SCHEMA PUBLIC; + COMMIT WORK; + COMMIT WORK; + COMMIT WORK; + SET AUTODDL OFF; + SET TERM ^ ; + /* Stored functions headers */ + CREATE OR ALTER FUNCTION PUBLIC.F_TEST_OUTSIDE_PKG RETURNS SMALLINT + AS + BEGIN END ^ + SET TERM ; ^ + COMMIT WORK; + SET AUTODDL ON; + COMMIT WORK; + SET AUTODDL OFF; + SET TERM ^ ; + /* Package headers */ + /* Package header: PUBLIC.PKG_TEST, Owner: SYSDBA */ + CREATE PACKAGE PUBLIC.PKG_TEST AS + begin + function F_TEST_INSIDE_PKG + returns smallint; + end^ + SET TERM ; ^ + COMMIT WORK; + SET AUTODDL ON; + COMMIT WORK; + SET AUTODDL OFF; + SET TERM ^ ; + /* Stored functions bodies */ + ALTER FUNCTION PUBLIC.F_TEST_OUTSIDE_PKG RETURNS SMALLINT + AS + begin + return -1; + end ^ + SET TERM ; ^ + COMMIT WORK; + SET AUTODDL ON; + COMMIT WORK; + SET AUTODDL OFF; + SET TERM ^ ; + /* Package bodies */ + /* Package body: PUBLIC.PKG_TEST, Owner: SYSDBA */ + CREATE PACKAGE BODY PUBLIC.PKG_TEST AS + begin + function F_TEST_INSIDE_PKG + returns smallint + as + begin + return 1; + end + end^ + SET TERM ; ^ + COMMIT WORK; + SET AUTODDL ON; + /* Grant permissions for this database */ + GRANT USAGE ON SCHEMA PUBLIC TO PUBLIC; +""" + + @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.isql(switches=['-x']) + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.isql(switches = ['-x'], combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4135_test.py b/tests/bugs/core_4135_test.py index 7511d760..0a0b4f4f 100644 --- a/tests/bugs/core_4135_test.py +++ b/tests/bugs/core_4135_test.py @@ -66,6 +66,7 @@ 'log_trigger_finish = true', ] +@pytest.mark.trace @pytest.mark.version('>=3.0') def test_1(act: Action, capsys): # How many rows must be inserted to the test table: diff --git a/tests/bugs/core_4137_test.py b/tests/bugs/core_4137_test.py index 52be3110..bba3d0e9 100644 --- a/tests/bugs/core_4137_test.py +++ b/tests/bugs/core_4137_test.py @@ -69,14 +69,18 @@ expected_stdout_6x = """ SET SQL DIALECT 3; + /* Schema definitions */ + /* Schema: PUBLIC, Owner: SYSDBA */ + CREATE OR ALTER SCHEMA PUBLIC; + COMMIT WORK; /* Character sets */ - ALTER CHARACTER SET ISO8859_1 SET DEFAULT COLLATION PT_BR; + ALTER CHARACTER SET SYSTEM.ISO8859_1 SET DEFAULT COLLATION SYSTEM.PT_BR; COMMIT WORK; COMMIT WORK; SET AUTODDL OFF; SET TERM ^ ; /* Stored procedures headers */ - CREATE OR ALTER PROCEDURE TEST (P01 CHAR(10)) + CREATE OR ALTER PROCEDURE PUBLIC.TEST (P01 CHAR(10)) RETURNS (O01 VARCHAR(30)) AS BEGIN EXIT; END ^ @@ -87,7 +91,7 @@ SET AUTODDL OFF; SET TERM ^ ; /* Stored procedures bodies */ - ALTER PROCEDURE TEST (P01 CHAR(10)) + ALTER PROCEDURE PUBLIC.TEST (P01 CHAR(10)) RETURNS (O01 VARCHAR(30)) AS begin @@ -96,6 +100,8 @@ SET TERM ; ^ COMMIT WORK; SET AUTODDL ON; + /* Grant permissions for this database */ + GRANT USAGE ON SCHEMA PUBLIC TO PUBLIC; """ @pytest.mark.version('>=3.0') diff --git a/tests/bugs/core_4149_test.py b/tests/bugs/core_4149_test.py index 982b0d18..c03e79d7 100644 --- a/tests/bugs/core_4149_test.py +++ b/tests/bugs/core_4149_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4149 FBTEST: bugs.core_4149 +NOTES: + [29.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -31,18 +37,29 @@ act = isql_act('db', test_script) -expected_stdout = """ -/* Grant permissions for this database */ -GRANT SELECT ON TEST TO PUBLIC +expected_stdout_5x = """ + /* Grant permissions for this database */ + GRANT SELECT ON TEST TO PUBLIC + + /* Grant permissions for this database */ + GRANT SELECT ON TEST TO PUBLIC + GRANT USAGE ON SEQUENCE G_TEST TO PUBLIC +""" + +expected_stdout_6x = """ + /* Grant permissions for this database */ + GRANT SELECT ON PUBLIC.TEST TO PUBLIC + GRANT USAGE ON SCHEMA PUBLIC TO PUBLIC -/* Grant permissions for this database */ -GRANT SELECT ON TEST TO PUBLIC -GRANT USAGE ON SEQUENCE G_TEST TO PUBLIC + /* Grant permissions for this database */ + GRANT SELECT ON PUBLIC.TEST TO PUBLIC + GRANT USAGE ON SEQUENCE PUBLIC.G_TEST TO PUBLIC + GRANT USAGE ON SCHEMA PUBLIC TO PUBLIC """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4156_test.py b/tests/bugs/core_4156_test.py index 7bdc5287..1218743d 100644 --- a/tests/bugs/core_4156_test.py +++ b/tests/bugs/core_4156_test.py @@ -12,6 +12,10 @@ Added 'SQLSTATE' in substitutions: runtime error must not be filtered out by '?!(...)' pattern ("negative lookahead assertion", see https://docs.python.org/3/library/re.html#regular-expression-syntax). Added 'combine_output = True' in order to see SQLSTATE if any error occurs. + + [29.06.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -32,15 +36,18 @@ act = isql_act('db', test_script, substitutions=[('^((?!(SQLSTATE|sqltype)).)*$', ''), ('[\t ]+', ' ')]) -expected_stdout = """ - 01: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 80 charset: 0 NONE - 02: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 80 charset: 0 NONE - 03: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 255 charset: 0 NONE - 01: sqltype: 496 LONG scale: 0 subtype: 0 len: 4 -""" @pytest.mark.version('>=3.0') def test_1(act: Action): + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'SYSTEM.' + expected_stdout = f""" + 01: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 80 charset: 0 {SQL_SCHEMA_PREFIX}NONE + 02: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 80 charset: 0 {SQL_SCHEMA_PREFIX}NONE + 03: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 255 charset: 0 {SQL_SCHEMA_PREFIX}NONE + 01: sqltype: 496 LONG scale: 0 subtype: 0 len: 4 + """ + act.expected_stdout = expected_stdout act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4160_test.py b/tests/bugs/core_4160_test.py index 4014b728..aa1f6d5b 100644 --- a/tests/bugs/core_4160_test.py +++ b/tests/bugs/core_4160_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4160 FBTEST: bugs.core_4160 +NOTES: + [29.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -49,43 +55,78 @@ execute procedure sp_alert('jp', -6); """ -act = isql_act('db', test_script, substitutions=[('-At procedure.*', '')]) +substitutions = [ (r'line(:)?\s+\d+.*', '') ] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = HY000 exception 1 -EX_NEGATIVE_REMAINDER - Czech: New Balance bude menší než nula (-1) - + -At procedure 'SP_ALERT' Statement failed, SQLSTATE = HY000 exception 1 -EX_NEGATIVE_REMAINDER - Portuguese: New saldo será menor do que zero (-2) - + -At procedure 'SP_ALERT' Statement failed, SQLSTATE = HY000 exception 1 -EX_NEGATIVE_REMAINDER - Danish: New Balance vil være mindre end nul (-3) - + -At procedure 'SP_ALERT' Statement failed, SQLSTATE = HY000 exception 1 -EX_NEGATIVE_REMAINDER - Greek: Νέα ισορροπία θα είναι κάτω από το μηδέν (-4) - + -At procedure 'SP_ALERT' Statement failed, SQLSTATE = HY000 exception 1 -EX_NEGATIVE_REMAINDER - French: Nouveau solde sera inférieur à zéro (-5) - + -At procedure 'SP_ALERT' Statement failed, SQLSTATE = HY000 exception 1 -EX_NEGATIVE_REMAINDER - Russian: Новый остаток будет меньше нуля (-6) + -At procedure 'SP_ALERT' """ +expected_stdout_6x = """ + Statement failed, SQLSTATE = HY000 + exception 1 + -"PUBLIC"."EX_NEGATIVE_REMAINDER" + - Czech: New Balance bude menší než nula (-1) + -At procedure "PUBLIC"."SP_ALERT" + Statement failed, SQLSTATE = HY000 + exception 1 + -"PUBLIC"."EX_NEGATIVE_REMAINDER" + - Portuguese: New saldo será menor do que zero (-2) + -At procedure "PUBLIC"."SP_ALERT" + Statement failed, SQLSTATE = HY000 + exception 1 + -"PUBLIC"."EX_NEGATIVE_REMAINDER" + - Danish: New Balance vil være mindre end nul (-3) + -At procedure "PUBLIC"."SP_ALERT" + Statement failed, SQLSTATE = HY000 + exception 1 + -"PUBLIC"."EX_NEGATIVE_REMAINDER" + - Greek: Νέα ισορροπία θα είναι κάτω από το μηδέν (-4) + -At procedure "PUBLIC"."SP_ALERT" + Statement failed, SQLSTATE = HY000 + exception 1 + -"PUBLIC"."EX_NEGATIVE_REMAINDER" + - French: Nouveau solde sera inférieur à zéro (-5) + -At procedure "PUBLIC"."SP_ALERT" + Statement failed, SQLSTATE = HY000 + exception 1 + -"PUBLIC"."EX_NEGATIVE_REMAINDER" + - Russian: Новый остаток будет меньше нуля (-6) + -At procedure "PUBLIC"."SP_ALERT" +""" + +@pytest.mark.intl @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4161_test.py b/tests/bugs/core_4161_test.py index fb3c8201..f5e4351a 100644 --- a/tests/bugs/core_4161_test.py +++ b/tests/bugs/core_4161_test.py @@ -142,6 +142,7 @@ MINE_F01 300 """ +@pytest.mark.es_eds @pytest.mark.version('>=3.0') def test_1(act: Action, tmp_user: User): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_4165_test.py b/tests/bugs/core_4165_test.py index d2422778..6e8a2ff3 100644 --- a/tests/bugs/core_4165_test.py +++ b/tests/bugs/core_4165_test.py @@ -7,8 +7,16 @@ DESCRIPTION: JIRA: CORE-4165 FBTEST: bugs.core_4165 +NOTES: + [29.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ +from firebird.driver import DatabaseError + import pytest from firebird.qa import * @@ -17,7 +25,7 @@ recreate table t2(id int); recreate table t3(id int); commit; - insert into t1 select rand()*100 from rdb$types,rdb$types; + insert into t1 select rand()*100 from rdb$types; commit; insert into t2 select * from t1; insert into t3 select * from t1; @@ -25,64 +33,131 @@ """ db = db_factory(init=init_script) +act = python_act('db', substitutions=[('record length.*', ''), ('key length.*', '')]) -test_script = """ - set planonly; - set explain on; - - select 0 i from t1 - union all - select 1 from t1 - union all - select 2 from t1 - ; - - - select 0 i from t2 - union - select 1 from t2 - union - select 2 from t2 - ; - - - select 0 i from t3 - union distinct - select 1 from t3 - union all - select 2 from t3 - ; - -- Note: values in 'record length' and 'key length' should be suppressed - -- because they contain not only size of field(s) but also db_key. -""" +#----------------------------------------------------------- -act = isql_act('db', test_script, substitutions=[('record length.*', ''), ('key length.*', '')]) - -expected_stdout = """ - Select Expression - -> Union - -> Table "T1" Full Scan - -> Table "T1" Full Scan - -> Table "T1" Full Scan - - Select Expression - -> Unique Sort (record length: 52, key length: 8) - -> Union - -> Table "T2" Full Scan - -> Table "T2" Full Scan - -> Table "T2" Full Scan - - Select Expression - -> Union - -> Unique Sort (record length: 44, key length: 8) - -> Union - -> Table "T3" Full Scan - -> Table "T3" Full Scan - -> Table "T3" Full Scan -""" +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- @pytest.mark.version('>=3.0') -def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() +def test_1(act: Action, capsys): + + qry_map = { + 1000 : + """ + select 0 i from t1 + union all + select 1 from t1 + union all + select 2 from t1 + """ + , + 2000 : + """ + select 0 i from t2 + union + select 1 from t2 + union + select 2 from t2 + """ + , + 3000 : + """ + select 0 i from t3 + union distinct + select 1 from t3 + union all + select 2 from t3 + """ + } + + with act.db.connect() as con: + cur = con.cursor() + + for k, v in qry_map.items(): + ps, rs = None, None + try: + ps = cur.prepare(v) + + print(v) + # Print explained plan with padding eash line by dots in order to see indentations: + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + print('') + + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + #rs = cur.execute(ps) + #for r in rs: + # print(r[0], r[1]) + except DatabaseError as e: + print(e.__str__()) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() + + expected_stdout_5x = f""" + {qry_map[1000]} + Select Expression + ....-> Union + ........-> Table "T1" Full Scan + ........-> Table "T1" Full Scan + ........-> Table "T1" Full Scan + + {qry_map[2000]} + Select Expression + ....-> Unique Sort ( + ........-> Union + ............-> Table "T2" Full Scan + ............-> Table "T2" Full Scan + ............-> Table "T2" Full Scan + + {qry_map[3000]} + Select Expression + ....-> Union + ........-> Unique Sort ( + ............-> Union + ................-> Table "T3" Full Scan + ................-> Table "T3" Full Scan + ........-> Table "T3" Full Scan + """ + + expected_stdout_6x = f""" + {qry_map[1000]} + Select Expression + ....-> Union + ........-> Table "PUBLIC"."T1" Full Scan + ........-> Table "PUBLIC"."T1" Full Scan + ........-> Table "PUBLIC"."T1" Full Scan + + {qry_map[2000]} + Select Expression + ....-> Unique Sort ( + ........-> Union + ............-> Table "PUBLIC"."T2" Full Scan + ............-> Table "PUBLIC"."T2" Full Scan + ............-> Table "PUBLIC"."T2" Full Scan + + {qry_map[3000]} + Select Expression + ....-> Union + ........-> Unique Sort ( + ............-> Union + ................-> Table "PUBLIC"."T3" Full Scan + ................-> Table "PUBLIC"."T3" Full Scan + ........-> Table "PUBLIC"."T3" Full Scan + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4203_test.py b/tests/bugs/core_4203_test.py index d306e668..2a73089d 100644 --- a/tests/bugs/core_4203_test.py +++ b/tests/bugs/core_4203_test.py @@ -7,6 +7,10 @@ DESCRIPTION: JIRA: CORE-4203 FBTEST: bugs.core_4203 +NOTES: + [29.06.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -16,14 +20,14 @@ test_script = """ set term ^; - create package test1 as + create package pkg_test as begin function f1(x char(3)) returns char(6) ; end ^ commit ^ - create package body test1 as + create package body pkg_test as begin function f1(x char(3)) returns char(6) as begin @@ -32,30 +36,32 @@ end ^ - show package test1 + show package pkg_test ^ """ act = isql_act('db', test_script) -expected_stdout = """ - TEST1 - Header source: - begin - function f1(x char(3)) returns char(6) ; - end +@pytest.mark.version('>=3.0') +def test_1(act: Action): - Body source: - begin - function f1(x char(3)) returns char(6) as - begin - return x; + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'PUBLIC.' + expected_stdout = f""" + {SQL_SCHEMA_PREFIX}PKG_TEST + Header source: + begin + function f1(x char(3)) returns char(6) ; end - end -""" -@pytest.mark.version('>=3.0') -def test_1(act: Action): + Body source: + begin + function f1(x char(3)) returns char(6) as + begin + return x; + end + end + """ + act.expected_stdout = expected_stdout act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4205_test.py b/tests/bugs/core_4205_test.py index 2cd39ecb..582b670a 100644 --- a/tests/bugs/core_4205_test.py +++ b/tests/bugs/core_4205_test.py @@ -7,6 +7,10 @@ DESCRIPTION: JIRA: CORE-4205 FBTEST: bugs.core_4205 +NOTES: + [29.06.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -23,15 +27,17 @@ act = python_act('db', substitutions=[('^((?!(SQLSTATE|CREATE GENERATOR)).)*$', '')]) -expected_stdout = """ - CREATE GENERATOR TMP_GEN_42051 START WITH 9223372036854775807 INCREMENT -2147483647; - CREATE GENERATOR TMP_GEN_42052 START WITH -9223372036854775808 INCREMENT 2147483647; - CREATE GENERATOR TMP_GEN_42053 START WITH 9223372036854775807 INCREMENT 2147483647; - CREATE GENERATOR TMP_GEN_42054 START WITH -9223372036854775808 INCREMENT -2147483647; -""" - @pytest.mark.version('>=3.0') def test_1(act: Action): + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'PUBLIC.' + expected_stdout = f""" + CREATE GENERATOR {SQL_SCHEMA_PREFIX}TMP_GEN_42051 START WITH 9223372036854775807 INCREMENT -2147483647; + CREATE GENERATOR {SQL_SCHEMA_PREFIX}TMP_GEN_42052 START WITH -9223372036854775808 INCREMENT 2147483647; + CREATE GENERATOR {SQL_SCHEMA_PREFIX}TMP_GEN_42053 START WITH 9223372036854775807 INCREMENT 2147483647; + CREATE GENERATOR {SQL_SCHEMA_PREFIX}TMP_GEN_42054 START WITH -9223372036854775808 INCREMENT -2147483647; + """ + act.expected_stdout = expected_stdout act.isql(switches=['-x'], combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4210_test.py b/tests/bugs/core_4210_test.py index 19b332ca..504248da 100644 --- a/tests/bugs/core_4210_test.py +++ b/tests/bugs/core_4210_test.py @@ -7,6 +7,10 @@ DESCRIPTION: JIRA: CORE-4210 FBTEST: bugs.core_4210 +NOTES: + [29.06.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -76,24 +80,26 @@ act = isql_act('db', test_script) -expected_stdout = """ - Before altering proc: +@pytest.mark.version('>=3.0') +def test_1(act: Action): - COMMENT ON PROCEDURE PARAMETER SP_TEST.A_ID1 IS input id1; - COMMENT ON PROCEDURE PARAMETER SP_TEST.A_DTS1 IS input timestamp1; - COMMENT ON PROCEDURE PARAMETER SP_TEST.O_ID1 IS output id1; - COMMENT ON PROCEDURE PARAMETER SP_TEST.O_DTS1 IS output timestamp1; + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'PUBLIC.' + expected_stdout = f""" + Before altering proc: - After altering proc: + COMMENT ON PROCEDURE PARAMETER {SQL_SCHEMA_PREFIX}SP_TEST.A_ID1 IS input id1; + COMMENT ON PROCEDURE PARAMETER {SQL_SCHEMA_PREFIX}SP_TEST.A_DTS1 IS input timestamp1; + COMMENT ON PROCEDURE PARAMETER {SQL_SCHEMA_PREFIX}SP_TEST.O_ID1 IS output id1; + COMMENT ON PROCEDURE PARAMETER {SQL_SCHEMA_PREFIX}SP_TEST.O_DTS1 IS output timestamp1; - COMMENT ON PROCEDURE PARAMETER SP_TEST.A_ID1 IS input id1; - COMMENT ON PROCEDURE PARAMETER SP_TEST.A_DTS2 IS input timestamp2; - COMMENT ON PROCEDURE PARAMETER SP_TEST.O_ID1 IS output id1; - COMMENT ON PROCEDURE PARAMETER SP_TEST.O_DTS2 IS output timestamp2; -""" + After altering proc: + + COMMENT ON PROCEDURE PARAMETER {SQL_SCHEMA_PREFIX}SP_TEST.A_ID1 IS input id1; + COMMENT ON PROCEDURE PARAMETER {SQL_SCHEMA_PREFIX}SP_TEST.A_DTS2 IS input timestamp2; + COMMENT ON PROCEDURE PARAMETER {SQL_SCHEMA_PREFIX}SP_TEST.O_ID1 IS output id1; + COMMENT ON PROCEDURE PARAMETER {SQL_SCHEMA_PREFIX}SP_TEST.O_DTS2 IS output timestamp2; + """ -@pytest.mark.version('>=3.0') -def test_1(act: Action): act.expected_stdout = expected_stdout act.execute() assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4214_test.py b/tests/bugs/core_4214_test.py index 86361b62..38405cbf 100644 --- a/tests/bugs/core_4214_test.py +++ b/tests/bugs/core_4214_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4214 FBTEST: bugs.core_4214 +NOTES: + [29.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -31,19 +37,32 @@ act = isql_act('db', test_script) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = HY000 unsuccessful metadata update -CREATE TABLE GTT_DETL failed -global temporary table "GTT_DETL" of type ON COMMIT DELETE ROWS cannot reference persistent table "FIX_MAIN" + Statement failed, SQLSTATE = HY000 unsuccessful metadata update -CREATE TABLE FIX_DETL failed -persistent table "FIX_DETL" cannot reference global temporary table "GTT_MAIN" of type ON COMMIT DELETE ROWS """ +expected_stdout_6x = """ + Statement failed, SQLSTATE = HY000 + unsuccessful metadata update + -CREATE TABLE "PUBLIC"."GTT_DETL" failed + -global temporary table "PUBLIC"."GTT_DETL" of type ON COMMIT DELETE ROWS cannot reference persistent table "PUBLIC"."FIX_MAIN" + + Statement failed, SQLSTATE = HY000 + unsuccessful metadata update + -CREATE TABLE "PUBLIC"."FIX_DETL" failed + -persistent table "PUBLIC"."FIX_DETL" cannot reference global temporary table "PUBLIC"."GTT_MAIN" of type ON COMMIT DELETE ROWS +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4218_test.py b/tests/bugs/core_4218_test.py index 4eb0b2b5..682801f2 100644 --- a/tests/bugs/core_4218_test.py +++ b/tests/bugs/core_4218_test.py @@ -6,46 +6,56 @@ TITLE: Add database owner to mon$database DESCRIPTION: JIRA: CORE-4218 -FBTEST: bugs.core_4218 +NOTES: + [13.01.2025] pzotov + Added (temporary ?) 'credentials = False' to prevent ISQL from using '-USER ... -PASS ...'. + This is needed since 6.0.0.570, otherwise we get (on attempting to create DB): + Statement failed, SQLSTATE = 28000 + Your user name and password are not defined. Ask your database administrator to set up a Firebird login. + -Different logins in connect and attach packets - client library error + (IMO, this is bug; see https://github.com/FirebirdSQL/firebird/issues/8385) """ - +import time +import locale import pytest from pathlib import Path from firebird.qa import * db = db_factory() -test_user: User = user_factory('db', name='TMP_U4218', password='123') - -act = python_act('db', substitutions=[('Commit current transaction \\(y/n\\)\\?', '')]) +tmp_user: User = user_factory('db', name='TMP_U4218', password='123') -expected_stdout = """ - WHO_AM_I TMP_U4218 - WHO_IS_OWNER TMP_U4218 - WHO_AM_I SYSDBA - WHO_IS_OWNER TMP_U4218 -""" +act = python_act('db', substitutions = [('[ \t]+', ' ')]) test_db = temp_file('owner-db.fdb') @pytest.mark.version('>=3.0') -def test_1(act: Action, test_user: User, test_db: Path): +def test_1(act: Action, tmp_user: User, test_db: Path): with act.db.connect() as con: c = con.cursor() - c.execute('grant create database to user TMP_U4218') + c.execute(f'grant create database to user {tmp_user.name}') con.commit() + test_script = f""" - create database 'localhost:{test_db}' user 'TMP_U4218' password '123'; - set list on; - set list on; -- Needed on Windows to really set list ON. - select current_user as who_am_i, mon$owner as who_is_owner from mon$database; - commit; - connect 'localhost:{test_db}'; - select current_user as who_am_i, mon$owner as who_is_owner from mon$database; - commit; - drop database; - quit; + -- set echo on; + create database 'localhost:{test_db}' user {tmp_user.name} password '{tmp_user.password}'; + set list on; + select current_user as who_am_i, mon$owner as who_is_owner from mon$database; + commit; + connect 'localhost:{test_db}' user {act.db.user} password '{act.db.password}'; + select current_user as who_am_i, mon$owner as who_is_owner from mon$database; + commit; + drop database; + quit; """ + + expected_stdout = f""" + WHO_AM_I {tmp_user.name.upper()} + WHO_IS_OWNER {tmp_user.name.upper()} + WHO_AM_I {act.db.user} + WHO_IS_OWNER {tmp_user.name.upper()} + """ + act.expected_stdout = expected_stdout - act.isql(switches=['-q'], input=test_script) + act.isql(switches=['-q'], input=test_script, connect_db=False, combine_output = True, credentials = False, io_enc = locale.getpreferredencoding()) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4244_test.py b/tests/bugs/core_4244_test.py index 4b7e6861..91cde4fe 100644 --- a/tests/bugs/core_4244_test.py +++ b/tests/bugs/core_4244_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-4244 FBTEST: bugs.core_4244 +NOTES: + [29.06.2025] pzotov + Confirmed bug on 2.1.7.18553. + Replaced 'SHOW' command with query to RDB tables. + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -14,9 +19,7 @@ db = db_factory() -test_script = """ - set term ^; - create or alter procedure sp_test as +PROC_DDL = """ declare char_one_byte char(1) character set dos864; declare str varchar(1000) character set dos864; begin @@ -24,31 +27,29 @@ str='B'; str=str||char_one_byte; end +""" +test_script = f""" + set list on; + set count on; + set term ^; + create or alter procedure sp_test as + {PROC_DDL} ^ set term ;^ commit; - -- Confirmed for 2.1.7: - -- Statement failed, SQLCODE = -802 - -- arithmetic exception, numeric overflow, or string truncation - -- -Cannot transliterate character between character sets - show proc sp_test; + select p.rdb$procedure_source as blob_id from rdb$procedures p where p.rdb$procedure_name = upper('sp_test'); """ -act = isql_act('db', test_script, substitutions=[('=.*', '')]) +substitutions = [('BLOB_ID.*', '')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ - Procedure text: - declare char_one_byte char(1) character set dos864; - declare str varchar(1000) character set dos864; - begin - char_one_byte='A'; - str='B'; - str=str||char_one_byte; - end +expected_stdout = f""" + {PROC_DDL} + Records affected: 1 """ @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4252_test.py b/tests/bugs/core_4252_test.py index 79392552..59a77cb3 100644 --- a/tests/bugs/core_4252_test.py +++ b/tests/bugs/core_4252_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4252 FBTEST: bugs.core_4252 +NOTES: + [28.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -34,32 +40,36 @@ db = db_factory(init=init_script) test_script = """ - show table t1; - show table "T2"; execute procedure sp_test(1); execute procedure sp_test(2); """ -act = isql_act('db', test_script, substitutions=[('line:.*', ''), ('col:.*', '')]) -expected_stdout = """ - X INTEGER Not Null - X INTEGER Not Null -""" +substitutions = [('line(:)?\\s+.*', '')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 23000 validation error for column "T1"."X", value "*** null ***" - -At procedure 'SP_TEST' line: 3, col: 26 + -At procedure 'SP_TEST' + Statement failed, SQLSTATE = 23000 validation error for column "T2"."X", value "*** null ***" - -At procedure 'SP_TEST' line: 4, col: 8 + -At procedure 'SP_TEST' +""" + +expected_stdout_6x = """ + Statement failed, SQLSTATE = 23000 + validation error for column "PUBLIC"."T1"."X", value "*** null ***" + -At procedure "PUBLIC"."SP_TEST" + + Statement failed, SQLSTATE = 23000 + validation error for column "PUBLIC"."T2"."X", value "*** null ***" + -At procedure "PUBLIC"."SP_TEST" """ -@pytest.mark.version('>=3') +@pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4262_test.py b/tests/bugs/core_4262_test.py index 308a58b8..d9db0bd2 100644 --- a/tests/bugs/core_4262_test.py +++ b/tests/bugs/core_4262_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4262 FBTEST: bugs.core_4262 +NOTES: + [29.06.2025] pzotov + Data in STDOUT has no matter. + Only STDERR must be checked in this test. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -14,7 +20,19 @@ db = db_factory() -test_script = """set planonly; +test_script = """ +set list on; +select + case + when col = 1 then 'y' + when col = 0 then 'n' + end + as text +from ( + select case when exists (select 1 from rdb$database ) then 1 else 0 end as col + from rdb$relations +); + select col as col1, col as col2 from ( select case when exists (select 1 from rdb$database ) then 1 else 0 end as col @@ -24,13 +42,11 @@ act = isql_act('db', test_script) -expected_stdout = """ -PLAN (RDB$DATABASE NATURAL) -PLAN (RDB$RELATIONS NATURAL) +expected_stderr = """ """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() - assert act.clean_stdout == act.clean_expected_stdout + act.expected_stderr = expected_stderr + act.execute(combine_output = False) # ::: NB ::: we have to check only EMPTY stderr here! + assert act.clean_stderr == act.clean_expected_stderr diff --git a/tests/bugs/core_4276_test.py b/tests/bugs/core_4276_test.py index 3a7386be..03270b2a 100644 --- a/tests/bugs/core_4276_test.py +++ b/tests/bugs/core_4276_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-4276 FBTEST: bugs.core_4276 +NOTES: + [29.06.2025] pzotov + Removed 'SHOW' commands as having no sense in this test (it is enough to query just created table and check its data). + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -15,56 +20,51 @@ db = db_factory() -act = python_act('db', substitutions=[('BLOB_CONTENT.*', '')]) +substitutions = [('[ \t]+', ' '), ('BLOB_ID.*', '')] +act = python_act('db', substitutions = substitutions) + +DATA_IN_VCHR = 'ÓßŌŃõÕµńĶķĻļņĒŅ' +DATA_IN_BLOB = """ + Green - viens no trim primārās krāsas, zaļā tiek uzskatīts diapazontsvetov spektrs ar viļņa + garumu aptuveni 500-565 nanometri. Sistēma CMYK druka zaļā iegūst, sajaucot dzelteno un + zilganzaļi (cyan).Dabā, Catalpa - zaļa augs. + Krāsu zaļie augi ir dabiski, ka cilvēks etalonomzeleni. + Zaļā koku varde. + Ir plaši izplatīti dabā. Lielākā daļa augu ir zaļā krāsā, jo tie satur pigmentu fotosintēzes - + hlorofilu (hlorofils absorbē lielu daļu no sarkano stariem saules spektra, atstājot uztveri + atstarotās un filtrē zaļā krāsā). Dzīvnieki ar zaļo krāsu tā izmantošanu maskēties fona augiem. +""" -test_script = """ +test_script = f""" recreate table "ĄČĘĢÆĖŠŚÖÜØ£"( "ąčęėįšųūž" varchar(50) character set dos775 ,"Õisu ja kariste järved" blob sub_type 1 character set dos775 ); commit; - show table; - show table "ĄČĘĢÆĖŠŚÖÜØ£"; + insert into "ĄČĘĢÆĖŠŚÖÜØ£"("ąčęėįšųūž", "Õisu ja kariste järved") values( - 'ÓßŌŃõÕµńĶķĻļņĒŅ', - 'Green - viens no trim primārās krāsas, zaļā tiek uzskatīts diapazontsvetov spektrs ar viļņa - garumu aptuveni 500-565 nanometri. Sistēma CMYK druka zaļā iegūst, sajaucot dzelteno un - zilganzaļi (cyan).Dabā, Catalpa - zaļa augs. - Krāsu zaļie augi ir dabiski, ka cilvēks etalonomzeleni. - Zaļā koku varde. - Ir plaši izplatīti dabā. Lielākā daļa augu ir zaļā krāsā, jo tie satur pigmentu fotosintēzes - - hlorofilu (hlorofils absorbē lielu daļu no sarkano stariem saules spektra, atstājot uztveri - atstarotās un filtrē zaļā krāsā). Dzīvnieki ar zaļo krāsu tā izmantošanu maskēties fona augiem.' + '{DATA_IN_VCHR}', + '{DATA_IN_BLOB}' ); set list on; set blob all; - select "ąčęėįšųūž", "Õisu ja kariste järved" as blob_content + select "ąčęėįšųūž", "Õisu ja kariste järved" as BLOB_ID from "ĄČĘĢÆĖŠŚÖÜØ£"; """ -expected_stdout = """ - ĄČĘĢÆĖŠŚÖÜØ£ - ąčęėįšųūž VARCHAR(50) CHARACTER SET DOS775 Nullable - Õisu ja kariste järved BLOB segment 80, subtype TEXT CHARACTER SET DOS775 Nullable - - ąčęėįšųūž ÓßŌŃõÕµńĶķĻļņĒŅ - BLOB_CONTENT 80:0 - Green - viens no trim primārās krāsas, zaļā tiek uzskatīts diapazontsvetov spektrs ar viļņa - garumu aptuveni 500-565 nanometri. Sistēma CMYK druka zaļā iegūst, sajaucot dzelteno un - zilganzaļi (cyan).Dabā, Catalpa - zaļa augs. - Krāsu zaļie augi ir dabiski, ka cilvēks etalonomzeleni. - Zaļā koku varde. - Ir plaši izplatīti dabā. Lielākā daļa augu ir zaļā krāsā, jo tie satur pigmentu fotosintēzes - - hlorofilu (hlorofils absorbē lielu daļu no sarkano stariem saules spektra, atstājot uztveri - atstarotās un filtrē zaļā krāsā). Dzīvnieki ar zaļo krāsu tā izmantošanu maskēties fona augiem. +expected_stdout = f""" + ąčęėįšųūž {DATA_IN_VCHR} + BLOB_ID 80:0 + {DATA_IN_BLOB} """ script_file = temp_file('test-script.sql') +@pytest.mark.intl @pytest.mark.version('>=3') def test_1(act: Action, script_file: Path): script_file.write_text(test_script, encoding='cp775') act.expected_stdout = expected_stdout - act.isql(switches=['-q', '-b'], input_file=script_file, charset='DOS775') + act.isql(switches=['-q', '-b'], combine_output = True, input_file=script_file, charset='DOS775') assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4280_test.py b/tests/bugs/core_4280_test.py index 6a998565..206b16b8 100644 --- a/tests/bugs/core_4280_test.py +++ b/tests/bugs/core_4280_test.py @@ -10,6 +10,10 @@ NOTES: [30.09.2023] pzotov Expected error message become differ in FB 6.x, added splitting. + + [29.06.2025] pzotov + Expected output on FB 6.x now has to include name of SQL schema. + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -40,10 +44,10 @@ expected_stdout_6x = """ Statement failed, SQLSTATE = 42000 - CREATE FUNCTION PSQL_FUNC_TEST failed + CREATE FUNCTION "PUBLIC"."PSQL_FUNC_TEST" failed -Dynamic SQL Error -SQL error code = -637 - -duplicate specification of A_X - not supported + -duplicate specification of "A_X" - not supported """ @pytest.mark.version('>=3.0') diff --git a/tests/bugs/core_4281_test.py b/tests/bugs/core_4281_test.py index c4d03b54..5bdc8c40 100644 --- a/tests/bugs/core_4281_test.py +++ b/tests/bugs/core_4281_test.py @@ -3,11 +3,14 @@ """ ID: issue-4604 ISSUE: 4604 -TITLE: FB 3: TYPE OF arguments of stored functions will hang firebird engine - if depending domain or column is changed +TITLE: FB 3: TYPE OF arguments of stored functions will hang firebird engine if depending domain or column is changed DESCRIPTION: JIRA: CORE-4281 FBTEST: bugs.core_4281 +NOTES: + [29.06.2025] pzotov + Removed 'SHOW' command. It is enought to check that function returns proper result after 'ALTE DOMAIN' statement. + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -16,28 +19,40 @@ db = db_factory() test_script = """ - create domain testdomain as integer; - commit; + set list on; - create function testfunction (arg1 type of testdomain) returns integer as + create domain dm_test as integer; + commit; + set term ^; + create function fn_test (a_x type of dm_test) returns integer as begin - end; - + return sign(a_x); + end ^ + set term ;^ commit; - alter domain testdomain type bigint; + select fn_test(-2147483648) as fn_neg from rdb$database; + select fn_test( 2147483648) as fb_pos from rdb$database; + + alter domain dm_test type bigint; commit; - show domain testdomain; + select fn_test( 2147483648) as fb_pos from rdb$database; """ -act = isql_act('db', test_script) +substitutions = [('[\t ]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ - TESTDOMAIN BIGINT Nullable + FN_NEG -1 + Statement failed, SQLSTATE = 22003 + arithmetic exception, numeric overflow, or string truncation + -numeric value is out of range + FB_POS 1 """ @pytest.mark.version('>=3.0') def test_1(act: Action): + act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4285_test.py b/tests/bugs/core_4285_test.py index b4316de9..9696e65a 100644 --- a/tests/bugs/core_4285_test.py +++ b/tests/bugs/core_4285_test.py @@ -7,6 +7,13 @@ DESCRIPTION: JIRA: CORE-4285 FBTEST: bugs.core_4285 +NOTES: + [29.06.2025] pzotov + Re-implemented: use f-notation and dictionary with queries which SQL will be substituted in the expected output. + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -39,139 +46,199 @@ """ db = db_factory(init=init_script) +act = python_act('db') -test_script = """ - set explain on; - set planonly; - set echo on; - - select 1 from test order by col1; - - ------ - select 1 from test where col1 = 0 order by col1; - - ------ - select 1 from test order by col1, col2; - - ------ - select 1 from test where col1 = 0 order by col1, col2; - - ------ - select 1 from test where col1 = 0 and col2 = 0 order by col1, col2; - - ------ - select 1 from test order by col1, col2, col3; - - ------ - select 1 from test where col1 = 0 order by col1, col2, col3; - - ------ - select 1 from test where col1 = 0 and col2 = 0 order by col1, col2, col3; - - ------ - select 1 from test where col1 = 0 and col3 = 0 order by col1; - - ------ - select 1 from test where col1 = 0 and col3 = 0 order by col1, col2, col3; - - ------ - select 1 from test where col1 = 0 and col3 = 0 order by col1, col3; - -""" - -act = isql_act('db', test_script) - -expected_stdout = """ - select 1 from test order by col1; - - Select Expression - -> Table "TEST" Access By ID - -> Index "TEST_COL1" Full Scan - - ------ - select 1 from test where col1 = 0 order by col1; - - Select Expression - -> Filter - -> Table "TEST" Access By ID - -> Index "TEST_COL1" Range Scan (full match) +#----------------------------------------------------------- - ------ - select 1 from test order by col1, col2; +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped - Select Expression - -> Table "TEST" Access By ID - -> Index "TEST_COL12" Full Scan - - ------ - select 1 from test where col1 = 0 order by col1, col2; - - Select Expression - -> Filter - -> Table "TEST" Access By ID - -> Index "TEST_COL12" Range Scan (partial match: 1/2) - - ------ - select 1 from test where col1 = 0 and col2 = 0 order by col1, col2; - - Select Expression - -> Filter - -> Table "TEST" Access By ID - -> Index "TEST_COL12" Range Scan (full match) - - ------ - select 1 from test order by col1, col2, col3; - - Select Expression - -> Table "TEST" Access By ID - -> Index "TEST_COL123" Full Scan - - ------ - select 1 from test where col1 = 0 order by col1, col2, col3; - - Select Expression - -> Filter - -> Table "TEST" Access By ID - -> Index "TEST_COL123" Range Scan (partial match: 1/3) - - ------ - select 1 from test where col1 = 0 and col2 = 0 order by col1, col2, col3; - - Select Expression - -> Filter - -> Table "TEST" Access By ID - -> Index "TEST_COL123" Range Scan (partial match: 2/3) - - ------ - select 1 from test where col1 = 0 and col3 = 0 order by col1; - - Select Expression - -> Filter - -> Table "TEST" Access By ID - -> Index "TEST_COL132" Range Scan (partial match: 2/3) - - ------ - select 1 from test where col1 = 0 and col3 = 0 order by col1, col2, col3; - - Select Expression - -> Sort (record length: 44, key length: 24) - -> Filter - -> Table "TEST" Access By ID - -> Bitmap - -> Index "TEST_COL132" Range Scan (partial match: 2/3) - - ------ - select 1 from test where col1 = 0 and col3 = 0 order by col1, col3; - - Select Expression - -> Filter - -> Table "TEST" Access By ID - -> Index "TEST_COL132" Range Scan (partial match: 2/3) - -""" +#----------------------------------------------------------- @pytest.mark.version('>=3.0') -def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() +def test_1(act: Action, capsys): + + qry_map = { + 1000 : 'select 1 from test order by col1' + ,1500 : 'select 1 from test where col1 = 0 order by col1' + ,2000 : 'select 1 from test order by col1, col2' + ,2500 : 'select 1 from test where col1 = 0 order by col1, col2' + ,3000 : 'select 1 from test where col1 = 0 and col2 = 0 order by col1, col2' + ,3500 : 'select 1 from test order by col1, col2, col3' + ,4000 : 'select 1 from test where col1 = 0 order by col1, col2, col3' + ,4500 : 'select 1 from test where col1 = 0 and col2 = 0 order by col1, col2, col3' + ,5000 : 'select 1 from test where col1 = 0 and col3 = 0 order by col1' + ,5500 : 'select 1 from test where col1 = 0 and col3 = 0 order by col1, col2, col3' + ,6000 : 'select 1 from test where col1 = 0 and col3 = 0 order by col1, col3' + } + + with act.db.connect() as con: + cur = con.cursor() + + for k, v in qry_map.items(): + ps, rs = None, None + try: + ps = cur.prepare(v) + + print(v) + # Print explained plan with padding eash line by dots in order to see indentations: + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + print('') + + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + #rs = cur.execute(ps) + #for r in rs: + # print(r[0], r[1]) + except DatabaseError as e: + print(e.__str__()) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() + + expected_stdout_5x = f""" + + {qry_map[1000]} + Select Expression + ....-> Table "TEST" Access By ID + ........-> Index "TEST_COL1" Full Scan + + {qry_map[1500]} + Select Expression + ....-> Filter + ........-> Table "TEST" Access By ID + ............-> Index "TEST_COL1" Range Scan (full match) + + {qry_map[2000]} + Select Expression + ....-> Table "TEST" Access By ID + ........-> Index "TEST_COL12" Full Scan + + {qry_map[2500]} + Select Expression + ....-> Filter + ........-> Table "TEST" Access By ID + ............-> Index "TEST_COL12" Range Scan (partial match: 1/2) + + {qry_map[3000]} + Select Expression + ....-> Filter + ........-> Table "TEST" Access By ID + ............-> Index "TEST_COL12" Range Scan (full match) + + {qry_map[3500]} + Select Expression + ....-> Table "TEST" Access By ID + ........-> Index "TEST_COL123" Full Scan + + {qry_map[4000]} + Select Expression + ....-> Filter + ........-> Table "TEST" Access By ID + ............-> Index "TEST_COL123" Range Scan (partial match: 1/3) + + {qry_map[4500]} + Select Expression + ....-> Filter + ........-> Table "TEST" Access By ID + ............-> Index "TEST_COL123" Range Scan (partial match: 2/3) + + {qry_map[5000]} + Select Expression + ....-> Filter + ........-> Table "TEST" Access By ID + ............-> Index "TEST_COL132" Range Scan (partial match: 2/3) + + {qry_map[5500]} + Select Expression + ....-> Sort (record length: 44, key length: 24) + ........-> Filter + ............-> Table "TEST" Access By ID + ................-> Bitmap + ....................-> Index "TEST_COL132" Range Scan (partial match: 2/3) + + {qry_map[6000]} + Select Expression + ....-> Filter + ........-> Table "TEST" Access By ID + ............-> Index "TEST_COL132" Range Scan (partial match: 2/3) + """ + + expected_stdout_6x = f""" + {qry_map[1000]} + Select Expression + ....-> Table "PUBLIC"."TEST" Access By ID + ........-> Index "PUBLIC"."TEST_COL1" Full Scan + + {qry_map[1500]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST" Access By ID + ............-> Index "PUBLIC"."TEST_COL1" Range Scan (full match) + + {qry_map[2000]} + Select Expression + ....-> Table "PUBLIC"."TEST" Access By ID + ........-> Index "PUBLIC"."TEST_COL12" Full Scan + + {qry_map[2500]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST" Access By ID + ............-> Index "PUBLIC"."TEST_COL12" Range Scan (partial match: 1/2) + + {qry_map[3000]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST" Access By ID + ............-> Index "PUBLIC"."TEST_COL12" Range Scan (full match) + + {qry_map[3500]} + Select Expression + ....-> Table "PUBLIC"."TEST" Access By ID + ........-> Index "PUBLIC"."TEST_COL123" Full Scan + + {qry_map[4000]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST" Access By ID + ............-> Index "PUBLIC"."TEST_COL123" Range Scan (partial match: 1/3) + + {qry_map[4500]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST" Access By ID + ............-> Index "PUBLIC"."TEST_COL123" Range Scan (partial match: 2/3) + + {qry_map[5000]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST" Access By ID + ............-> Index "PUBLIC"."TEST_COL132" Range Scan (partial match: 2/3) + + {qry_map[5500]} + Select Expression + ....-> Sort (record length: 44, key length: 24) + ........-> Filter + ............-> Table "PUBLIC"."TEST" Access By ID + ................-> Bitmap + ....................-> Index "PUBLIC"."TEST_COL132" Range Scan (partial match: 2/3) + + {qry_map[6000]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST" Access By ID + ............-> Index "PUBLIC"."TEST_COL132" Range Scan (partial match: 2/3) + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4301_test.py b/tests/bugs/core_4301_test.py index da3c1d07..aaff92c2 100644 --- a/tests/bugs/core_4301_test.py +++ b/tests/bugs/core_4301_test.py @@ -7,6 +7,9 @@ DESCRIPTION: JIRA: CORE-4301 FBTEST: bugs.core_4301 +NOTES: + [04.09.2024] pzotov + Added 'using plugin Srp' into 'CREATE USER' statements, in order to check 'COMMENT ON USER' with non-ascii text. """ import pytest @@ -20,31 +23,46 @@ user_b = user_factory('db', name='u30b', password='u30b', do_not_create=True) test_script = """ - -- Note: this test differs from ticket: instead of add COMMENTS to users - -- it only defines their `firstname` attribute, because sec$users.sec$description - -- can be displayed only when plugin UserManager = Srp. -- Field `firstname` is defined as: -- VARCHAR(32) CHARACTER SET UNICODE_FSS COLLATE UNICODE_FSS -- we can put in it max 16 non-ascii characters - create or alter user u30a password 'u30a' firstname 'Полиграф Шариков'; - create or alter user u30b password 'u30b' firstname 'Léopold Frédéric'; + create or alter user u30a password 'u30a' firstname 'Полиграф Шариков' using plugin Srp; + create or alter user u30b password 'u30b' firstname 'Léopold Frédéric' using plugin Srp; commit; + comment on user u30a is 'это кто-то из наших'; + comment on user u30b is 'é alguém do Brasil'; + commit; + /* + show domain rdb$user; + show domain SEC$NAME_PART; + show table sec$users; + */ set list on; - select u.sec$user_name, u.sec$first_name + select + -- 3.x: CHAR(31) CHARACTER SET UNICODE_FSS Nullable + -- 4.x, 5.x: (RDB$USER) CHAR(63) Nullable + -- FB 6.x: (RDB$USER) CHAR(63) CHARACTER SET UTF8 Nullable + u.sec$user_name + ,u.sec$first_name -- (SEC$NAME_PART) VARCHAR(32) Nullable + ,u.sec$description as descr_blob_id -- (RDB$DESCRIPTION) BLOB segment 80, subtype TEXT Nullable from sec$users u where upper(u.sec$user_name) in (upper('u30a'), upper('u30b')); commit; """ -act = isql_act('db', test_script) +act = isql_act('db', test_script, substitutions = [ ('DESCR_BLOB_ID.*',''),('[ \t]+',' ') ] ) expected_stdout = """ SEC$USER_NAME U30A SEC$FIRST_NAME Полиграф Шариков + это кто-то из наших + SEC$USER_NAME U30B SEC$FIRST_NAME Léopold Frédéric + é alguém do Brasil """ +@pytest.mark.intl @pytest.mark.version('>=3.0') def test_1(act: Action, user_a: User, user_b: User): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_4304_test.py b/tests/bugs/core_4304_test.py index 0edeb4cc..aeb628d7 100644 --- a/tests/bugs/core_4304_test.py +++ b/tests/bugs/core_4304_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4304 FBTEST: bugs.core_4304 +NOTES: + [29.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -15,36 +21,53 @@ db = db_factory() test_script = """ -recreate table t1(x int); -recreate table t1(x int, constraint t1_pk primary key(x), y int, constraint t1_fk foreign key(y) references t1(z)); -- NB: there is no field `z` in this table, this was misprit -recreate table t1(x int, constraint t1_pk primary key(x), y int, constraint t1_fk foreign key(y) references t1(x)); -commit; -show table t1; + set list on; + recreate table test(x int); + -- NB: there is no field `z` in this table: + recreate table test(x int, constraint test_pk primary key(x), y int, constraint test_fk foreign key(y) references test(z)); + recreate table test(x int, constraint test_pk primary key(x), y int, constraint test_fk foreign key(y) references test(x)); + commit; + insert into test(x, y) values(1, null); + insert into test(x, y) values(2, 1); + insert into test(x, y) values(3, 2); + update test set y = 3 where x = 1; + set count on; + select * from test order by x; """ -act = isql_act('db', test_script) +substitutions = [('[\t ]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ -X INTEGER Not Null -Y INTEGER Nullable -CONSTRAINT T1_FK: - Foreign key (Y) References T1 (X) -CONSTRAINT T1_PK: - Primary key (X) +expected_stdout_5x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -RECREATE TABLE TEST failed + -could not find UNIQUE or PRIMARY KEY constraint in table TEST with specified columns + X 1 + Y 3 + X 2 + Y 1 + X 3 + Y 2 + Records affected: 3 """ -expected_stderr = """ -Statement failed, SQLSTATE = 42000 -unsuccessful metadata update --RECREATE TABLE T1 failed --could not find UNIQUE or PRIMARY KEY constraint in table T1 with specified columns +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -RECREATE TABLE "PUBLIC"."TEST" failed + -could not find UNIQUE or PRIMARY KEY constraint in table "PUBLIC"."TEST" with specified columns + X 1 + Y 3 + X 2 + Y 1 + X 3 + Y 2 + Records affected: 3 """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4315_test.py b/tests/bugs/core_4315_test.py index be90895f..adbc3424 100644 --- a/tests/bugs/core_4315_test.py +++ b/tests/bugs/core_4315_test.py @@ -3,25 +3,27 @@ """ ID: issue-4638 ISSUE: 4638 -TITLE: Usage of field(s) alias in view WITH CHECK OPTION leads to incorrect compile - error or incorrect internal triggers +TITLE: Usage of field(s) alias in view WITH CHECK OPTION leads to incorrect compile error or incorrect internal triggers DESCRIPTION: JIRA: CORE-4315 FBTEST: bugs.core_4315 +NOTES: + [29.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """ +db = db_factory() + +test_script = """ recreate view v1 as select 1 id from rdb$database; commit; recreate table t1 (n1 integer, n2 integer); -""" - -db = db_factory(init=init_script) - -test_script = """ -- Compilation error in LI-T3.0.0.30830 (13-jan-2014): "Column unknown": recreate view v1 as select t1.n1 from t1 t1 where t1.n1 < t1.n2 with check option; recreate view v1 as select t1.n1 from t1 where t1.n1 < t1.n2 with check option; @@ -39,9 +41,10 @@ order by rdb$trigger_name; """ -act = isql_act('db', test_script, substitutions=[('RDB\\$TRIGGER_BLR.*', '')]) +substitutions = [('[\t ]+', ' '), ('RDB\\$TRIGGER_BLR.*', '')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ +expected_stdout_5x = """ RDB$TRIGGER_BLR c:2d2 blr_version5, blr_begin, @@ -82,9 +85,171 @@ blr_eoc """ +expected_stdout_6x = """ +RDB$TRIGGER_BLR c:1e0 + blr_version5, + blr_begin, + blr_for, + blr_rse, 1, + blr_relation2, 2, 'T','1', + 4, 34,'T','1',34, 2, + blr_boolean, + blr_and, + blr_lss, + blr_field, 2, 2, 'N','1', + blr_field, 2, 2, 'N','2', + blr_equiv, + blr_field, 0, 2, 'N','1', + blr_field, 2, 2, 'N','1', + blr_end, + blr_if, + blr_lss, + blr_field, 1, 2, 'N','1', + blr_field, 2, 2, 'N','2', + blr_begin, + blr_end, + blr_abort, blr_gds_code, 16, 'c','h','e','c','k','_','c','o','n','s','t','r','a','i','n','t', + blr_end, + blr_eoc + + +RDB$TRIGGER_BLR c:1e1 + blr_version5, + blr_begin, + blr_if, + blr_lss, + blr_field, 1, 2, 'N','1', + blr_null, + blr_begin, + blr_end, + blr_abort, blr_gds_code, 16, 'c','h','e','c','k','_','c','o','n','s','t','r','a','i','n','t', + blr_end, + blr_eoc + + +RDB$TRIGGER_BLR c:1e2 + blr_version5, + blr_begin, + blr_for, + blr_rse, 1, + blr_relation, 2, 'T','1', 2, + blr_boolean, + blr_and, + blr_lss, + blr_field, 2, 2, 'N','1', + blr_field, 2, 2, 'N','2', + blr_equiv, + blr_field, 0, 2, 'N','1', + blr_field, 2, 2, 'N','1', + blr_end, + blr_if, + blr_lss, + blr_field, 1, 2, 'N','1', + blr_field, 2, 2, 'N','2', + blr_begin, + blr_end, + blr_abort, blr_gds_code, 16, 'c','h','e','c','k','_','c','o','n','s','t','r','a','i','n','t', + blr_end, + blr_eoc + + +RDB$TRIGGER_BLR c:1e3 + blr_version5, + blr_begin, + blr_if, + blr_lss, + blr_field, 1, 2, 'N','1', + blr_null, + blr_begin, + blr_end, + blr_abort, blr_gds_code, 16, 'c','h','e','c','k','_','c','o','n','s','t','r','a','i','n','t', + blr_end, + blr_eoc + + +RDB$TRIGGER_BLR c:1e4 + blr_version5, + blr_begin, + blr_for, + blr_rse, 1, + blr_relation2, 2, 'T','1', + 3, 34,'X',34, 2, + blr_boolean, + blr_and, + blr_lss, + blr_field, 2, 2, 'N','1', + blr_field, 2, 2, 'N','2', + blr_equiv, + blr_field, 0, 2, 'N','1', + blr_field, 2, 2, 'N','1', + blr_end, + blr_if, + blr_lss, + blr_field, 1, 2, 'N','1', + blr_field, 2, 2, 'N','2', + blr_begin, + blr_end, + blr_abort, blr_gds_code, 16, 'c','h','e','c','k','_','c','o','n','s','t','r','a','i','n','t', + blr_end, + blr_eoc + + +RDB$TRIGGER_BLR c:1e5 + blr_version5, + blr_begin, + blr_if, + blr_lss, + blr_field, 1, 2, 'N','1', + blr_null, + blr_begin, + blr_end, + blr_abort, blr_gds_code, 16, 'c','h','e','c','k','_','c','o','n','s','t','r','a','i','n','t', + blr_end, + blr_eoc + + +RDB$TRIGGER_BLR c:1e6 + blr_version5, + blr_begin, + blr_for, + blr_rse, 1, + blr_relation, 2, 'T','1', 2, + blr_boolean, + blr_and, + blr_lss, + blr_field, 2, 2, 'N','1', + blr_field, 2, 2, 'N','2', + blr_equiv, + blr_field, 0, 1, 'A', + blr_field, 2, 2, 'N','1', + blr_end, + blr_if, + blr_lss, + blr_field, 1, 1, 'A', + blr_field, 2, 2, 'N','2', + blr_begin, + blr_end, + blr_abort, blr_gds_code, 16, 'c','h','e','c','k','_','c','o','n','s','t','r','a','i','n','t', + blr_end, + blr_eoc + + +RDB$TRIGGER_BLR c:1e7 + blr_version5, + blr_begin, + blr_if, + blr_lss, + blr_field, 1, 1, 'A', + blr_null, + blr_begin, + blr_end, + blr_abort, blr_gds_code, 16, 'c','h','e','c','k','_','c','o','n','s','t','r','a','i','n','t', + blr_end, + blr_eoc +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4318_test.py b/tests/bugs/core_4318_test.py index bac53dce..91686332 100644 --- a/tests/bugs/core_4318_test.py +++ b/tests/bugs/core_4318_test.py @@ -7,7 +7,23 @@ DESCRIPTION: JIRA: CORE-4318 FBTEST: bugs.core_4318 +NOTES: + [29.06.2025] pzotov + 1. In 3.0.0.30837 plan was: + Select Expression + -> Singularity Check + -> Filter + -> Aggregate + -> Table "T T2" Access By ID + -> Index "FK_T2_REF_T1" Scan + (i.e. there was NO "Filter" between "Aggregate" and "Table "T T2" Access By ID") + 2. Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. + """ +from firebird.driver import DatabaseError import pytest from firebird.qa import * @@ -55,64 +71,97 @@ db = db_factory(init=init_script) -test_script = """ - set explain on; - set planonly; +# line 5, column 15 ==> line N, column N +substitutions = [ ( r'line(:)?\s+\d+', 'line N' ), ( r'col(umn)?(:)?\s+\d+', 'column N' ) ] +act = python_act('db', substitutions = substitutions) - set term ^; - execute block returns (s integer) as - declare v integer = 1; - begin - with t as ( - select t1_id as t1_id, sum(id) as s -- FB 5.x: "Select Expression (line NNN, column MMM)" - from t2 - group by 1 - ) - select s - from t - where t1_id = :v - into :s; - - suspend; - end - ^ - set term ;^ - -- In 3.0.0.30837 plan was: - -- Select Expression - -- -> Singularity Check - -- -> Filter - -- -> Aggregate - -- -> Table "T T2" Access By ID - -- -> Index "FK_T2_REF_T1" Scan - -- (i.e. there was NO "Filter" between "Aggregate" and "Table "T T2" Access By ID") -""" +#----------------------------------------------------------- -act = isql_act('db', test_script, substitutions = [('line \\d+, col(umn)? \\d+', 'line, col')]) +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped -fb3x_expected_out = """ - Select Expression - -> Singularity Check - -> Filter - -> Aggregate - -> Filter - -> Table "T2" as "T T2" Access By ID - -> Index "FK_T2_REF_T1" Range Scan (full match) -""" - -fb5x_expected_out = """ - Select Expression (line 8, column 7) - -> Singularity Check - -> Filter - -> Aggregate - -> Filter - -> Table "T2" as "T T2" Access By ID - -> Index "FK_T2_REF_T1" Range Scan (full match) -""" +#----------------------------------------------------------- @pytest.mark.version('>=3.0') -def test_1(act: Action): - act.expected_stdout = fb3x_expected_out if act.is_version('<5') else fb5x_expected_out - act.execute() - #assert act.stdout == act.clean_expected_stdout +def test_1(act: Action, capsys): + + qry_map = { + 1000 : + """ + execute block returns (s integer) as + declare v integer = 1; + begin + with t as ( + select t1_id as t1_id, sum(id) as s -- FB 5.x: "Select Expression (line NNN, column MMM)" + from t2 + group by 1 + ) + select s + from t + where t1_id = :v + into :s; + + suspend; + end + + """ + } + + with act.db.connect() as con: + cur = con.cursor() + + for k, v in qry_map.items(): + ps, rs = None, None + try: + ps = cur.prepare(v) + + print(v) + # Print explained plan with padding eash line by dots in order to see indentations: + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + print('') + except DatabaseError as e: + print(e.__str__()) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() # <<< 29.06.2025 NEED EVEN IF CURSOR WAS NOT SELECT ANY DATA! OTHERWISE PYTEST CAN HANG ON EXIT! + + expected_stdout_4x = f""" + {qry_map[1000]} + Select Expression + ....-> Singularity Check + ........-> Filter + ............-> Aggregate + ................-> Filter + ....................-> Table "T2" as "T T2" Access By ID + ........................-> Index "FK_T2_REF_T1" Range Scan (full match) + """ + + expected_stdout_5x = f""" + {qry_map[1000]} + Select Expression (line N, column N) + ....-> Singularity Check + ........-> Filter + ............-> Aggregate + ................-> Filter + ....................-> Table "T2" as "T T2" Access By ID + ........................-> Index "FK_T2_REF_T1" Range Scan (full match) + """ + + expected_stdout_6x = f""" + {qry_map[1000]} + Select Expression (line N, column N) + ....-> Singularity Check + ........-> Filter + ............-> Aggregate + ................-> Filter + ....................-> Table "PUBLIC"."T2" as "T" "PUBLIC"."T2" Access By ID + ........................-> Index "PUBLIC"."FK_T2_REF_T1" Range Scan (full match) + """ + + act.expected_stdout = expected_stdout_4x if act.is_version('<5') else expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4319_test.py b/tests/bugs/core_4319_test.py index a29e4e5e..f8c228b9 100644 --- a/tests/bugs/core_4319_test.py +++ b/tests/bugs/core_4319_test.py @@ -52,6 +52,8 @@ """.split('\n') NO_SUCH_ALIAS = 'n0_$uch_f1le' + +@pytest.mark.trace @pytest.mark.version('>=3.0') def test_1(act: Action, capsys): diff --git a/tests/bugs/core_4321_test.py b/tests/bugs/core_4321_test.py index af43f41a..60e2c660 100644 --- a/tests/bugs/core_4321_test.py +++ b/tests/bugs/core_4321_test.py @@ -6,7 +6,12 @@ TITLE: Regression: ISQL does not destroy the SQL statement DESCRIPTION: JIRA: CORE-4321 -FBTEST: bugs.core_4321 +NOTES: + [31.12.2024] pzotov + Added forgotten semicolon after 'SET LIST ON'. + Parsing problem appeared on 6.0.0.0.570 after d6ad19aa07deeaac8107a25a9243c5699a3c4ea1 + ("Refactor ISQL creating FrontendParser class"). + It looks weird how it could work w/o 'token unknown / list' all this time in all major FB versions :-) """ import pytest @@ -16,54 +21,55 @@ test_script = """ -- NB: 2.1.7 FAILED, output contains '4' for select count(*) ... - set list on - select 1 x from rdb$database; - select 1 x from rdb$database; - select 1 x from rdb$database; - select 1 x from rdb$database; + set list on; + select /* tag_for_watch */ 1 point_a from rdb$database; + select /* tag_for_watch */ 1 point_a from rdb$database; + select /* tag_for_watch */ 1 point_a from rdb$database; + select /* tag_for_watch */ 1 point_a from rdb$database; - select count(*) c from mon$statements s - where s.mon$sql_text containing 'select 1 x' -- 08-may-2017: need for 4.0 Classic! Currently there is also query with RDB$AUTH_MAPPING data in mon$statements + select count(*) count_after_point_a from mon$statements s + where s.mon$sql_text containing '/* tag_for_watch */' ; commit; - select count(*) c from mon$statements s - where s.mon$sql_text containing 'select 1 x' + select count(*) count_after_commit_a from mon$statements s + where s.mon$sql_text containing '/* tag_for_watch */' ; - select 1 x from rdb$database; - select 1 x from rdb$database; - select 1 x from rdb$database; - select 1 x from rdb$database; + select /* tag_for_watch */ 1 point_b from rdb$database; + select /* tag_for_watch */ 1 point_b from rdb$database; + select /* tag_for_watch */ 1 point_b from rdb$database; + select /* tag_for_watch */ 1 point_b from rdb$database; - select count(*) c from mon$statements s - where s.mon$sql_text containing 'select 1 x' + select count(*) count_after_point_b from mon$statements s + where s.mon$sql_text containing '/* tag_for_watch */' ; commit; - select count(*) c from mon$statements s - where s.mon$sql_text containing 'select 1 x' + select count(*) count_after_commit_b from mon$statements s + where s.mon$sql_text containing '/* tag_for_watch */' ; """ -act = isql_act('db', test_script) +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')] ) expected_stdout = """ - X 1 - X 1 - X 1 - C 1 - C 1 - X 1 - X 1 - X 1 - X 1 - C 1 - C 1 + POINT_A 1 + POINT_A 1 + POINT_A 1 + POINT_A 1 + COUNT_AFTER_POINT_A 1 + COUNT_AFTER_COMMIT_A 1 + POINT_B 1 + POINT_B 1 + POINT_B 1 + POINT_B 1 + COUNT_AFTER_POINT_B 1 + COUNT_AFTER_COMMIT_B 1 """ @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4326_test.py b/tests/bugs/core_4326_test.py index c49125bc..e283ad7b 100644 --- a/tests/bugs/core_4326_test.py +++ b/tests/bugs/core_4326_test.py @@ -91,6 +91,7 @@ LNAME Zeppelin """ +@pytest.mark.es_eds @pytest.mark.version('>=3.0') def test_1(act: Action, tmp_user: User): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_4337_test.py b/tests/bugs/core_4337_test.py index f2e382bc..964308d7 100644 --- a/tests/bugs/core_4337_test.py +++ b/tests/bugs/core_4337_test.py @@ -133,6 +133,7 @@ 'log_connections = true', ] +@pytest.mark.trace @pytest.mark.version('>=3.0') def test_1(act: Action, sweep_log: Path, capsys): diff --git a/tests/bugs/core_4342_test.py b/tests/bugs/core_4342_test.py index 97096578..ad809449 100644 --- a/tests/bugs/core_4342_test.py +++ b/tests/bugs/core_4342_test.py @@ -7,101 +7,135 @@ DESCRIPTION: JIRA: CORE-4342 FBTEST: bugs.core_4342 +NOTES: + [29.06.2025] pzotov + Re-implemented: use f-notation to substitute fixture values in the expected output. + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * db = db_factory() -user_boss = user_factory('db', name='boss', password='123') -user_mngr = user_factory('db', name='mngr', password='456') - -test_script = """ - -- Add these DDL privileges in order to have some rows in - -- rdb$security_classes table for user BOSS: - grant create table to boss; - grant alter any table to boss; - grant drop any table to boss; - commit; - - set list on; - select current_user,count(*) acl_count from rdb$security_classes where rdb$acl containing 'boss'; - - select 1 from rdb$security_classes where rdb$acl containing 'boss' with lock; - update rdb$security_classes set rdb$security_class = rdb$security_class where rdb$acl containing 'boss'; - delete from rdb$security_classes where rdb$acl containing 'boss'; - commit; - - connect '$(DSN)' user 'MNGR' password '456'; - select current_user,count(*) acl_count from rdb$security_classes where rdb$acl containing 'boss'; - - select 1 from rdb$security_classes where rdb$acl containing 'boss' with lock; - update rdb$security_classes set rdb$security_class = rdb$security_class where rdb$acl containing 'boss'; - delete from rdb$security_classes where rdb$acl containing 'boss'; - commit; -""" +tmp_boss = user_factory('db', name='boss', password='123') +tmp_mngr = user_factory('db', name='mngr', password='456') -expected_stdout = """ - USER SYSDBA - ACL_COUNT 1 +substitutions = [('[\t ]+', ' ')] +act = isql_act('db', substitutions = substitutions) - USER MNGR - ACL_COUNT 1 -""" +@pytest.mark.version('>=3.0') +def test_1(act: Action, tmp_boss: User, tmp_mngr: User): -# version: 3.0 - -act = isql_act('db', test_script) - -expected_stderr_1 = """ - Statement failed, SQLSTATE = HY000 - Cannot select system table RDB$SECURITY_CLASSES for update WITH LOCK - Statement failed, SQLSTATE = 42000 - UPDATE operation is not allowed for system table RDB$SECURITY_CLASSES - Statement failed, SQLSTATE = 42000 - DELETE operation is not allowed for system table RDB$SECURITY_CLASSES - Statement failed, SQLSTATE = HY000 - Cannot select system table RDB$SECURITY_CLASSES for update WITH LOCK - Statement failed, SQLSTATE = 28000 - no permission for UPDATE access to TABLE RDB$SECURITY_CLASSES - Statement failed, SQLSTATE = 28000 - no permission for DELETE access to TABLE RDB$SECURITY_CLASSES -""" + test_script = f""" + -- Add these DDL privileges in order to have some rows in + -- rdb$security_classes table for user {tmp_boss.name}: + grant create table to {tmp_boss.name}; + grant alter any table to {tmp_boss.name}; + grant drop any table to {tmp_boss.name}; + commit; + set list on; + select current_user,count(*) acl_count from rdb$security_classes where rdb$acl containing '{tmp_boss.name}'; -@pytest.mark.version('>=3.0,<4.0') -def test_1(act: Action, user_boss: User, user_mngr: User): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr_1 - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - -# version: 4.0 - -expected_stderr_2 = """ - Statement failed, SQLSTATE = HY000 - Cannot select system table RDB$SECURITY_CLASSES for update WITH LOCK - Statement failed, SQLSTATE = 42000 - UPDATE operation is not allowed for system table RDB$SECURITY_CLASSES - Statement failed, SQLSTATE = 42000 - DELETE operation is not allowed for system table RDB$SECURITY_CLASSES - Statement failed, SQLSTATE = HY000 - Cannot select system table RDB$SECURITY_CLASSES for update WITH LOCK - Statement failed, SQLSTATE = 28000 - no permission for UPDATE access to TABLE RDB$SECURITY_CLASSES - -Effective user is MNGR - Statement failed, SQLSTATE = 28000 - no permission for DELETE access to TABLE RDB$SECURITY_CLASSES - -Effective user is MNGR -""" + select 1 from rdb$security_classes where rdb$acl containing '{tmp_boss.name}' with lock; + update rdb$security_classes set rdb$security_class = rdb$security_class where rdb$acl containing '{tmp_boss.name}'; + delete from rdb$security_classes where rdb$acl containing '{tmp_boss.name}'; + commit; + + connect '{act.db.dsn}' user '{tmp_mngr.name}' password '{tmp_mngr.password}'; + select current_user,count(*) acl_count from rdb$security_classes where rdb$acl containing '{tmp_boss.name}'; + + select 1 from rdb$security_classes where rdb$acl containing '{tmp_boss.name}' with lock; + update rdb$security_classes set rdb$security_class = rdb$security_class where rdb$acl containing '{tmp_boss.name}'; + delete from rdb$security_classes where rdb$acl containing '{tmp_boss.name}'; + commit; + """ + + expected_stdout_3x = f""" + USER {act.db.user.upper()} + ACL_COUNT 1 + + Statement failed, SQLSTATE = HY000 + Cannot select system table RDB$SECURITY_CLASSES for update WITH LOCK + + Statement failed, SQLSTATE = 42000 + UPDATE operation is not allowed for system table RDB$SECURITY_CLASSES + + Statement failed, SQLSTATE = 42000 + DELETE operation is not allowed for system table RDB$SECURITY_CLASSES + + USER {tmp_mngr.name.upper()} + ACL_COUNT 1 + + Statement failed, SQLSTATE = HY000 + Cannot select system table RDB$SECURITY_CLASSES for update WITH LOCK + + Statement failed, SQLSTATE = 28000 + no permission for UPDATE access to TABLE RDB$SECURITY_CLASSES + + Statement failed, SQLSTATE = 28000 + no permission for DELETE access to TABLE RDB$SECURITY_CLASSES + """ + + expected_stdout_5x = f""" + USER {act.db.user.upper()} + ACL_COUNT 1 + + Statement failed, SQLSTATE = HY000 + Cannot select system table RDB$SECURITY_CLASSES for update WITH LOCK + + Statement failed, SQLSTATE = 42000 + UPDATE operation is not allowed for system table RDB$SECURITY_CLASSES + + Statement failed, SQLSTATE = 42000 + DELETE operation is not allowed for system table RDB$SECURITY_CLASSES + + USER {tmp_mngr.name.upper()} + ACL_COUNT 1 + + Statement failed, SQLSTATE = HY000 + Cannot select system table RDB$SECURITY_CLASSES for update WITH LOCK + + Statement failed, SQLSTATE = 28000 + no permission for UPDATE access to TABLE RDB$SECURITY_CLASSES + -Effective user is {tmp_mngr.name.upper()} + + Statement failed, SQLSTATE = 28000 + no permission for DELETE access to TABLE RDB$SECURITY_CLASSES + -Effective user is {tmp_mngr.name.upper()} + """ + + + expected_stdout_6x = f""" + USER {act.db.user.upper()} + ACL_COUNT 1 + + Statement failed, SQLSTATE = HY000 + Cannot select system table "SYSTEM"."RDB$SECURITY_CLASSES" for update WITH LOCK + + Statement failed, SQLSTATE = 42000 + UPDATE operation is not allowed for system table "SYSTEM"."RDB$SECURITY_CLASSES" + Statement failed, SQLSTATE = 42000 + + DELETE operation is not allowed for system table "SYSTEM"."RDB$SECURITY_CLASSES" + USER {tmp_mngr.name.upper()} + ACL_COUNT 1 + + Statement failed, SQLSTATE = HY000 + Cannot select system table "SYSTEM"."RDB$SECURITY_CLASSES" for update WITH LOCK + Statement failed, SQLSTATE = 28000 + no permission for UPDATE access to TABLE "SYSTEM"."RDB$SECURITY_CLASSES" + -Effective user is {tmp_mngr.name.upper()} -@pytest.mark.version('>=4.0') -def test_2(act: Action, user_boss: User, user_mngr: User): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr_2 - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + Statement failed, SQLSTATE = 28000 + no permission for DELETE access to TABLE "SYSTEM"."RDB$SECURITY_CLASSES" + -Effective user is {tmp_mngr.name.upper()} + """ + act.expected_stdout = expected_stdout_3x if act.is_version('<4') else expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.isql(switches = ['-q'], input = test_script, combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4345_test.py b/tests/bugs/core_4345_test.py index e91a93ed..7dcd4620 100644 --- a/tests/bugs/core_4345_test.py +++ b/tests/bugs/core_4345_test.py @@ -74,6 +74,7 @@ 'log_function_finish = true' ] +@pytest.mark.trace @pytest.mark.version('>=3.0') def test_1(act: Action): output = [] @@ -100,7 +101,7 @@ def test_1(act: Action): """ # Case 1: Trace functions enabled with act.trace(db_events=trace): - act.isql(switches=['-n', '-q'], input=func_script % (123, 456)) + act.isql(switches=['-n', '-q'], input=func_script % (123, 456), combine_output = True) # for line in act.trace_log: if (func_start_ptn.search(line) @@ -111,7 +112,7 @@ def test_1(act: Action): # Case 2: Trace functions disabled act.trace_log.clear() with act.trace(db_events=trace[:-2]): - act.isql(switches=['-n', '-q'], input=func_script % (789, 987)) + act.isql(switches=['-n', '-q'], input=func_script % (789, 987), combine_output = True) # for line in act.trace_log: if (func_start_ptn.search(line) diff --git a/tests/bugs/core_4350_test.py b/tests/bugs/core_4350_test.py index 7fb76177..d341922d 100644 --- a/tests/bugs/core_4350_test.py +++ b/tests/bugs/core_4350_test.py @@ -5,17 +5,20 @@ ISSUE: 4672 TITLE: Support the SQL Standard ALTER SEQUENCE .. RESTART (without WITH clause) DESCRIPTION: -NOTES: -[18.08.2020] - FB 4.x has incompatible behaviour with all previous versions since build 4.0.0.2131 (06-aug-2020): - statement 'alter sequence restart with 0' changes rdb$generators.rdb$initial_value to -1 thus next call - gen_id(,1) will return 0 (ZERO!) rather than 1. - See also CORE-6084 and its fix: https://github.com/FirebirdSQL/firebird/commit/23dc0c6297825b2e9006f4d5a2c488702091033d - This is considered as *expected* and is noted in doc/README.incompatibilities.3to4.txt - - Because of this, it was decided to make separate section for check results of FB 4.x JIRA: CORE-4350 FBTEST: bugs.core_4350 +NOTES: + [18.08.2020] + FB 4.x has incompatible behaviour with all previous versions since build 4.0.0.2131 (06-aug-2020): + statement 'alter sequence restart with 0' changes rdb$generators.rdb$initial_value to -1 thus next call + gen_id(,1) will return 0 (ZERO!) rather than 1. + See also CORE-6084 and its fix: https://github.com/FirebirdSQL/firebird/commit/23dc0c6297825b2e9006f4d5a2c488702091033d + This is considered as *expected* and is noted in doc/README.incompatibilities.3to4.txt + Because of this, it was decided to make separate section for check results of FB 4.x + + [29.06.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -44,42 +47,35 @@ act = isql_act('db', test_script, substitutions=[('=.*', ''), ('[ \t]+', ' ')]) -# version: 3.0 - -expected_stdout_1 = """ - NEXT_VALUE 9223372034707292160 - NEXT_VALUE -9223372034707292161 - NEXT_VALUE -9223372034707292162 - NEXT_VALUE 9223372034707292161 - - Generator G1, current value: 9223372036854775807, initial value: 9223372036854775807, increment: -2147483647 - Generator G2, current value: -9223372036854775808, initial value: -9223372036854775808, increment: 2147483647 - Generator G3, current value: 9223372036854775807, initial value: 9223372036854775807, increment: 2147483647 - Generator G4, current value: -9223372036854775808, initial value: -9223372036854775808, increment: -2147483647 -""" - -@pytest.mark.version('>=3.0,<4.0') -def test_1(act: Action): - act.expected_stdout = expected_stdout_1 - act.execute() - assert act.clean_stdout == act.clean_expected_stdout - -# version: 4.0 - -expected_stdout_2 = """ - NEXT_VALUE 9223372036854775807 - NEXT_VALUE -9223372036854775808 - NEXT_VALUE 9223372036854775807 - NEXT_VALUE -9223372036854775808 - Generator G1, current value: -9223372034707292162, initial value: 9223372036854775807, increment: -2147483647 - Generator G2, current value: 9223372034707292161, initial value: -9223372036854775808, increment: 2147483647 - Generator G3, current value: 9223372034707292160, initial value: 9223372036854775807, increment: 2147483647 - Generator G4, current value: -9223372034707292161, initial value: -9223372036854775808, increment: -2147483647 -""" - -@pytest.mark.version('>=4.0') +@pytest.mark.version('>=3.0') def test_2(act: Action): - act.expected_stdout = expected_stdout_2 - act.execute() + + expected_stdout_3x = """ + NEXT_VALUE 9223372034707292160 + NEXT_VALUE -9223372034707292161 + NEXT_VALUE -9223372034707292162 + NEXT_VALUE 9223372034707292161 + + Generator G1, current value: 9223372036854775807, initial value: 9223372036854775807, increment: -2147483647 + Generator G2, current value: -9223372036854775808, initial value: -9223372036854775808, increment: 2147483647 + Generator G3, current value: 9223372036854775807, initial value: 9223372036854775807, increment: 2147483647 + Generator G4, current value: -9223372036854775808, initial value: -9223372036854775808, increment: -2147483647 + """ + + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'PUBLIC.' + expected_stdout_4x = f""" + NEXT_VALUE 9223372036854775807 + NEXT_VALUE -9223372036854775808 + NEXT_VALUE 9223372036854775807 + NEXT_VALUE -9223372036854775808 + Generator {SQL_SCHEMA_PREFIX}G1, current value: -9223372034707292162, initial value: 9223372036854775807, increment: -2147483647 + Generator {SQL_SCHEMA_PREFIX}G2, current value: 9223372034707292161, initial value: -9223372036854775808, increment: 2147483647 + Generator {SQL_SCHEMA_PREFIX}G3, current value: 9223372034707292160, initial value: 9223372036854775807, increment: 2147483647 + Generator {SQL_SCHEMA_PREFIX}G4, current value: -9223372034707292161, initial value: -9223372036854775808, increment: -2147483647 + """ + + act.expected_stdout = expected_stdout_3x if act.is_version('<4') else expected_stdout_4x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4359_test.py b/tests/bugs/core_4359_test.py index 55de67b7..d93037fd 100644 --- a/tests/bugs/core_4359_test.py +++ b/tests/bugs/core_4359_test.py @@ -7,105 +7,128 @@ DESCRIPTION: JIRA: CORE-4359 FBTEST: bugs.core_4359 +NOTES: + [29.06.2025] pzotov + Re-implemented: use f-notation to substitute fixture values in the expected output. + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * db = db_factory() -user_boss = user_factory('db', name='boss', password='123') +tmp_user = user_factory('db', name='boss', password='123') -test_script = """ - -- Test scenario attempts to modify (or lock record) from RDB$DATABASE - -- both for SYSDBA and non-privileged user. - set count on; +act = isql_act('db') - insert into rdb$database(rdb$security_class) values(''); - delete from rdb$database where rdb$security_class = ''; - update rdb$database set rdb$security_class = rdb$security_class where rdb$security_class = ''; - select current_user from rdb$database with lock; +@pytest.mark.version('>=3.0') +def test_1(act: Action, tmp_user: User): - commit; + test_script = f""" + -- Test scenario attempts to modify (or lock record) from RDB$DATABASE + -- both for SYSDBA and non-privileged user. + set count on; - connect '$(DSN)' user boss password '123'; + insert into rdb$database(rdb$security_class) values(''); + delete from rdb$database where rdb$security_class = ''; + update rdb$database set rdb$security_class = rdb$security_class where rdb$security_class = ''; + select current_user from rdb$database with lock; - insert into rdb$database(rdb$security_class) values(''); - delete from rdb$database where rdb$security_class = ''; - update rdb$database set rdb$security_class = rdb$security_class where rdb$security_class = ''; - select current_user from rdb$database with lock; + commit; - commit; -""" + connect '{act.db.dsn}' user {tmp_user.name} password '{tmp_user.password}'; -act = isql_act('db', test_script) + insert into rdb$database(rdb$security_class) values(''); + delete from rdb$database where rdb$security_class = ''; + update rdb$database set rdb$security_class = rdb$security_class where rdb$security_class = ''; + select current_user from rdb$database with lock; -expected_stdout = """ - Records affected: 0 - Records affected: 0 - Records affected: 0 -""" + commit; + """ -# version: 3.0 -expected_stderr_1 = """ - Statement failed, SQLSTATE = 42000 - INSERT operation is not allowed for system table RDB$DATABASE + expected_stdout_3x = """ + Statement failed, SQLSTATE = 42000 + INSERT operation is not allowed for system table RDB$DATABASE - Statement failed, SQLSTATE = HY000 - Cannot select system table RDB$DATABASE for update WITH LOCK + Records affected: 0 + Records affected: 0 + Records affected: 0 - Statement failed, SQLSTATE = 28000 - no permission for INSERT access to TABLE RDB$DATABASE + Statement failed, SQLSTATE = HY000 + Cannot select system table RDB$DATABASE for update WITH LOCK - Statement failed, SQLSTATE = 28000 - no permission for DELETE access to TABLE RDB$DATABASE + Statement failed, SQLSTATE = 28000 + no permission for INSERT access to TABLE RDB$DATABASE - Statement failed, SQLSTATE = 28000 - no permission for UPDATE access to TABLE RDB$DATABASE + Statement failed, SQLSTATE = 28000 + no permission for DELETE access to TABLE RDB$DATABASE - Statement failed, SQLSTATE = HY000 - Cannot select system table RDB$DATABASE for update WITH LOCK -""" + Statement failed, SQLSTATE = 28000 + no permission for UPDATE access to TABLE RDB$DATABASE -@pytest.mark.version('>=3.0,<4.0') -def test_1(act: Action, user_boss: User): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr_1 - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + Statement failed, SQLSTATE = HY000 + Cannot select system table RDB$DATABASE for update WITH LOCK + """ -# version: 4.0 + expected_stdout_4x = f""" + Statement failed, SQLSTATE = 42000 + INSERT operation is not allowed for system table RDB$DATABASE -expected_stderr_2 = """ - Statement failed, SQLSTATE = 42000 - INSERT operation is not allowed for system table RDB$DATABASE + Records affected: 0 + Records affected: 0 + Records affected: 0 - Statement failed, SQLSTATE = HY000 - Cannot select system table RDB$DATABASE for update WITH LOCK + Statement failed, SQLSTATE = HY000 + Cannot select system table RDB$DATABASE for update WITH LOCK - Statement failed, SQLSTATE = 28000 - no permission for INSERT access to TABLE RDB$DATABASE - -Effective user is BOSS + Statement failed, SQLSTATE = 28000 + no permission for INSERT access to TABLE RDB$DATABASE + -Effective user is {tmp_user.name.upper()} - Statement failed, SQLSTATE = 28000 - no permission for DELETE access to TABLE RDB$DATABASE - -Effective user is BOSS + Statement failed, SQLSTATE = 28000 + no permission for DELETE access to TABLE RDB$DATABASE + -Effective user is {tmp_user.name.upper()} - Statement failed, SQLSTATE = 28000 - no permission for UPDATE access to TABLE RDB$DATABASE - -Effective user is BOSS + Statement failed, SQLSTATE = 28000 + no permission for UPDATE access to TABLE RDB$DATABASE + -Effective user is {tmp_user.name.upper()} - Statement failed, SQLSTATE = HY000 - Cannot select system table RDB$DATABASE for update WITH LOCK -""" + Statement failed, SQLSTATE = HY000 + Cannot select system table RDB$DATABASE for update WITH LOCK + """ + + expected_stdout_6x = f""" + Statement failed, SQLSTATE = 42000 + INSERT operation is not allowed for system table "SYSTEM"."RDB$DATABASE" + + Records affected: 0 + Records affected: 0 + Records affected: 0 + + Statement failed, SQLSTATE = HY000 + Cannot select system table "SYSTEM"."RDB$DATABASE" for update WITH LOCK + + Statement failed, SQLSTATE = 28000 + no permission for INSERT access to TABLE "SYSTEM"."RDB$DATABASE" + -Effective user is {tmp_user.name.upper()} + + Statement failed, SQLSTATE = 28000 + no permission for DELETE access to TABLE "SYSTEM"."RDB$DATABASE" + + -Effective user is {tmp_user.name.upper()} + Statement failed, SQLSTATE = 28000 + no permission for UPDATE access to TABLE "SYSTEM"."RDB$DATABASE" + -Effective user is {tmp_user.name.upper()} + Statement failed, SQLSTATE = HY000 + Cannot select system table "SYSTEM"."RDB$DATABASE" for update WITH LOCK + """ -@pytest.mark.version('>=4.0') -def test_2(act: Action, user_boss: User): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr_2 - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + act.expected_stdout = expected_stdout_3x if act.is_version('<4') else expected_stdout_4x if act.is_version('<6') else expected_stdout_6x + act.isql(switches = ['-q'], input = test_script, combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4365_test.py b/tests/bugs/core_4365_test.py index aafa2ef9..51028e79 100644 --- a/tests/bugs/core_4365_test.py +++ b/tests/bugs/core_4365_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4365 FBTEST: bugs.core_4365 +NOTES: + [29.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -30,15 +36,18 @@ where id = 1; """ -act = isql_act('db', test_script, substitutions=[('RDB\\$INDEX_[0-9]+', 'RDB\\$INDEX')]) +act = isql_act('db', test_script, substitutions=[('RDB\\$INDEX_[0-9]+', 'RDB$INDEX_*')]) + +expected_stdout_5x = """ + PLAN JOIN (JOIN (X RF G1 INDEX (RDB$INDEX_*), X RF G2 INDEX (RDB$INDEX_*), X R INDEX (RDB$INDEX_*)), X P INDEX (RDB$INDEX_*)) +""" -expected_stdout = """ - PLAN JOIN (JOIN (X RF G1 INDEX (RDB$INDEX_46), X RF G2 INDEX (RDB$INDEX_46), X R INDEX (RDB$INDEX_1)), X P INDEX (RDB$INDEX_22)) +expected_stdout_6x = """ + PLAN JOIN (JOIN ("X" "RF" "G1" INDEX ("SYSTEM"."RDB$INDEX_*"), "X" "RF" "G2" INDEX ("SYSTEM"."RDB$INDEX_*"), "X" "R" INDEX ("SYSTEM"."RDB$INDEX_*")), "X" "P" INDEX ("SYSTEM"."RDB$INDEX_*")) """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4371_test.py b/tests/bugs/core_4371_test.py index f4d5beaa..616c6a9f 100644 --- a/tests/bugs/core_4371_test.py +++ b/tests/bugs/core_4371_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4371 FBTEST: bugs.core_4371 +NOTES: + [29.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -30,18 +36,25 @@ set term ;^ """ -act = isql_act('db', test_script) +substitutions = [('at offset \\d+', 'at offset')] +act = isql_act('db', test_script, substitutions = substitutions) + +expected_stdout_5x = """ + Statement failed, SQLSTATE = 2F000 + Error while parsing function FN_TEST's BLR + -invalid request BLR at offset + -exception EX_SOME_NON_EXISTENT_NAME not defined +""" -expected_stderr = """ -Statement failed, SQLSTATE = 2F000 -Error while parsing function FN_TEST's BLR --invalid request BLR at offset 55 --exception EX_SOME_NON_EXISTENT_NAME not defined +expected_stdout_6x = """ + Statement failed, SQLSTATE = 2F000 + Error while parsing function "PUBLIC"."FN_TEST"'s BLR + -invalid request BLR at offset + -exception "PUBLIC"."EX_SOME_NON_EXISTENT_NAME" not defined """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4373_test.py b/tests/bugs/core_4373_test.py index dfdb120a..6e96a02f 100644 --- a/tests/bugs/core_4373_test.py +++ b/tests/bugs/core_4373_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4373 FBTEST: bugs.core_4373 +NOTES: + [29.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -49,7 +55,7 @@ act = isql_act('db', test_script) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 42000 CREATE PACKAGE NEW_PACKAGE failed -Duplicate PROCEDURE EXTERNAL_PROC @@ -59,9 +65,18 @@ -Duplicate PROCEDURE INTERNAL_PROC """ +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + CREATE PACKAGE "PUBLIC"."NEW_PACKAGE" failed + -Duplicate PROCEDURE "EXTERNAL_PROC" + + Statement failed, SQLSTATE = 42000 + CREATE PACKAGE BODY "PUBLIC"."NEW_PACKAGE" failed + -Duplicate PROCEDURE "INTERNAL_PROC" +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4374_test.py b/tests/bugs/core_4374_test.py index 53966b73..e1a9a5ba 100644 --- a/tests/bugs/core_4374_test.py +++ b/tests/bugs/core_4374_test.py @@ -7,6 +7,18 @@ DESCRIPTION: JIRA: CORE-4374 FBTEST: bugs.core_4374 +NOTES: + [05.05.2015] pzotov + ::: NB ::: + Memory consumption of procedural objects under 64-bit environment is much bigger than on 32-bit one. + This test was retyped because it was encountered that previous limit for the size of BLR is too weak: + test failed at runtime with error "implementation limit exceeds". + New (more rigorous) limit was found by using 64-bit FB, build LI-T3.0.0.31822: BLR can not be larger + than ~2.35 Mb (previous: ~3.21 Mb) + + [15.05.2025] pzotov + Removed output of approximate BLR length because its change can be valuable: + 6.0.0.778 2025.05.07 d735e65a: 2097000 bytes instead of previous 2359000. """ import pytest @@ -15,12 +27,6 @@ db = db_factory() test_script = """ - -- ::: NB ::: - -- Memory consumption of procedural objects under 64-bit environment is much bigger than on 32-bit one. - -- This test was retyped because it was encountered that previous limit for the size of BLR is too weak: - -- test failed at runtime with error "implementation limit exceeds". - -- New (more rigorous) limit was found by using 64-bit FB, build LI-T3.0.0.31822: BLR can not be larger - -- than ~2.35 Mb (previous: ~3.21 Mb) set list on; set term ^; @@ -80,7 +86,7 @@ set term ^; - execute block returns(returned_rows int, proc_ddl_length int, proc_src_length int, approx_blr_length int) as + execute block returns(returned_rows int, proc_ddl_length int, proc_src_length int) as begin execute statement 'select count(*) cnt from test_proc' into returned_rows; @@ -91,32 +97,12 @@ from rdb$procedures where rdb$procedure_name = upper('test_proc') into proc_src_length; - select round(octet_length(rdb$procedure_blr), -3) - from rdb$procedures where rdb$procedure_name = upper('test_proc') - into approx_blr_length; - suspend; end ^ set term ;^ commit; - - /************************************** - - 32 bit, WI-T3.0.0.31824 - RETURNED_ROWS 119154 - PROC_DDL_LENGTH 1072455 - PROC_SRC_LENGTH 1072395 - APPROX_BLR_LENGTH 3217000 - - 64 bit, LI-T3.0.0.31822 - RETURNED_ROWS 87379 - PROC_DDL_LENGTH 786480 - PROC_SRC_LENGTH 786420 - APPROX_BLR_LENGTH 2359000 - - **************************************/ """ act = isql_act('db', test_script) @@ -125,7 +111,6 @@ RETURNED_ROWS 87379 PROC_DDL_LENGTH 786480 PROC_SRC_LENGTH 786420 - APPROX_BLR_LENGTH 2359000 """ @pytest.mark.version('>=3.0') diff --git a/tests/bugs/core_4376_test.py b/tests/bugs/core_4376_test.py index 22705f3a..c1252732 100644 --- a/tests/bugs/core_4376_test.py +++ b/tests/bugs/core_4376_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4376 FBTEST: bugs.core_4376 +NOTES: + [29.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -58,9 +64,10 @@ ^ """ -act = isql_act('db', test_script, substitutions=[('-At line[:]{0,1}[\\s]+[\\d]+,[\\s]+column[:]{0,1}[\\s]+[\\d]+', '')]) +substitutions = [(r'^\s*(-)?At line.*', '')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 42S02 unsuccessful metadata update -CREATE OR ALTER PROCEDURE SP_TEST failed @@ -68,7 +75,7 @@ -SQL error code = -204 -Table unknown -TEST - -At line 3, column 26 + Statement failed, SQLSTATE = 42S22 unsuccessful metadata update -RECREATE PACKAGE BODY PKG_TEST failed @@ -76,12 +83,28 @@ -SQL error code = -206 -Column unknown -NON_EXISTENT_FIELD - -At line 12, column 16 +""" + +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42S02 + unsuccessful metadata update + -CREATE OR ALTER PROCEDURE "PUBLIC"."SP_TEST" failed + -Dynamic SQL Error + -SQL error code = -204 + -Table unknown + -"TEST" + + Statement failed, SQLSTATE = 42S22 + unsuccessful metadata update + -RECREATE PACKAGE BODY "PUBLIC"."PKG_TEST" failed + -Dynamic SQL Error + -SQL error code = -206 + -Column unknown + -"NON_EXISTENT_FIELD" """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4379_test.py b/tests/bugs/core_4379_test.py index e0f29e74..9e08ac00 100644 --- a/tests/bugs/core_4379_test.py +++ b/tests/bugs/core_4379_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4379 FBTEST: bugs.core_4379 +NOTES: + [29.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -114,18 +120,16 @@ act = isql_act('db', test_script, substitutions = [('(--\\s+)?line \\d+, col(umn)? \\d+', '')]) # -- line 2, column 7 -expected_stdout = """ +expected_stdout_5x = """ PLAN (X ORDER T_PK_IDX) PLAN (A NATURAL) NATURAL_READS 20000 INDEXED_READS 39999 - PLAN (C_CUR X ORDER T_PK_IDX) PLAN (C_CUR A NATURAL) PLAN (C_CUR X ORDER T_PK_IDX) NATURAL_READS 20000 INDEXED_READS 39999 - PLAN (C_CUR X ORDER T_PK_IDX) PLAN (C_CUR A NATURAL) PLAN (C_CUR X ORDER T_PK_IDX) @@ -134,9 +138,26 @@ INDEXED_READS 59999 """ +expected_stdout_6x = """ + PLAN ("X" ORDER "PUBLIC"."T_PK_IDX") + PLAN ("A" NATURAL) + NATURAL_READS 20000 + INDEXED_READS 39999 + PLAN ("C_CUR" "X" ORDER "PUBLIC"."T_PK_IDX") + PLAN ("C_CUR" "A" NATURAL) + PLAN ("C_CUR" "X" ORDER "PUBLIC"."T_PK_IDX") + NATURAL_READS 20000 + INDEXED_READS 39999 + PLAN ("C_CUR" "X" ORDER "PUBLIC"."T_PK_IDX") + PLAN ("C_CUR" "A" NATURAL) + PLAN ("C_CUR" "X" ORDER "PUBLIC"."T_PK_IDX") + PLAN ("PUBLIC"."T" INDEX ()) + NATURAL_READS 20000 + INDEXED_READS 59999 +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4381_test.py b/tests/bugs/core_4381_test.py index cd5c19d1..0d4974a3 100644 --- a/tests/bugs/core_4381_test.py +++ b/tests/bugs/core_4381_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4381 FBTEST: bugs.core_4381 +NOTES: + [29.06.2025] pzotov + Added subst to suppress displaying name of stored procedure: on 6.x it is prefixed by SQL schema and enclosed in quotes. + For this test it is enough just to show proper numbers of line and column. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -32,17 +38,17 @@ ^ """ -act = isql_act('db', test_script) +substitutions = [('-At procedure \\S+', 'At procedure')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stderr = """ +expected_stdout = """ Statement failed, SQLSTATE = 22018 conversion error from string "a" - -At procedure 'P1' line: 3, col: 28 + At procedure line: 3, col: 28 """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr - + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4386_test.py b/tests/bugs/core_4386_test.py index ee5438df..2052e576 100644 --- a/tests/bugs/core_4386_test.py +++ b/tests/bugs/core_4386_test.py @@ -21,8 +21,22 @@ See 'tx2.commit()' in the code. If we replace it with 'con2.commit()' then Tx2 will be *silently* rolled back (!!despite that we issued con.commit() !!) and we will not get any error messages. I'm not sure whether this correct or no. - Checked on 3.0.8.33535 (SS/CS), 4.0.1.2692 (SS/CS), 5.0.0.730 + [22.08.2024] pzotov + * Changed DDL because of SubQueryConversion config parameter appearance. + We have to AVOID usage of queries which have plan that can be changed when firebird.conf has + SubQueryConversion = true. In that case some index can be excluded from plan and thus + it can be dropped on first iteration of 'for x_isol in tx_isol_lst' loop. This causes unexpected + error 'index not found' for subsequent checks. + * Added check for error message when we try to drop standalone function. + * Assert moved out to the point after loop in order to show whole result in case of some error + (rather than only one message block for some particular x_isol). + * Excluded check of FB 3.x (this version no more changed). + Checked on 6.0.0.442, 5.0.2.1479, 4.0.6.3142 + + [29.06.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -46,17 +60,24 @@ create index test1_id on test1(id); commit; - create descending index test2_id_x_desc on test2(id,x); + create descending index test2_x on test2(x); commit; create or alter view v_test as select id,x from test1 where id between 15 and 30; commit; set term ^; + create or alter function fn_worker(a_x int) returns int as + declare v_id int; + begin + execute statement ('select max(b.id) from test2 b where b.x >= ?') (:a_x) into v_id; + return v_id; + end + ^ create or alter procedure sp_worker(a_id int) returns(x int) as begin for - execute statement ('select v.x from v_test v where v.id = ? and exists(select * from test2 b where b.id = v.id)') (:a_id) + execute statement ('select v.x from v_test v where v.id = ? and v.id >= fn_worker(v.x)') (:a_id) into x do suspend; @@ -84,48 +105,18 @@ commit; """ -expected_stdout = """ - lock conflict on no wait transaction - -unsuccessful metadata update - -object PROCEDURE "SP_TEST" is in use - (335544345, 335544351, 335544453) - - lock conflict on no wait transaction - -unsuccessful metadata update - -object PROCEDURE "SP_WORKER" is in use - (335544345, 335544351, 335544453) - - lock conflict on no wait transaction - -unsuccessful metadata update - -object VIEW "V_TEST" is in use - (335544345, 335544351, 335544453) - - lock conflict on no wait transaction - -unsuccessful metadata update - -object TABLE "TEST2" is in use - (335544345, 335544351, 335544453) - - lock conflict on no wait transaction - -unsuccessful metadata update - -object INDEX "TEST1_ID" is in use - (335544345, 335544351, 335544453) - - lock conflict on no wait transaction - -unsuccessful metadata update - -object INDEX "TEST2_ID_X_DESC" is in use - (335544345, 335544351, 335544453) -""" - -@pytest.mark.version('>=3.0.6') +@pytest.mark.version('>=4.0') def test_1(act: Action, capsys): act.isql(switches=[], input=ddl_script) - drop_commands = ['drop procedure sp_test', - 'drop procedure sp_worker', - 'drop view v_test', - 'drop table test2', - 'drop index test1_id', - 'drop index test2_id_x_desc'] + drop_commands = [ 'drop procedure sp_test', + 'drop procedure sp_worker', + 'drop function fn_worker', + 'drop view v_test', + 'drop table test2', + 'drop index test1_id', + 'drop index test2_x' + ] tx_isol_lst = [ Isolation.READ_COMMITTED_NO_RECORD_VERSION, Isolation.READ_COMMITTED_RECORD_VERSION, @@ -147,6 +138,7 @@ def test_1(act: Action, capsys): for cmd in drop_commands: with act.db.connect() as con2: custom_tpb = tpb(isolation = x_isol, lock_timeout=0) + print(x_isol.name, cmd) tx2 = con2.transaction_manager(custom_tpb) tx2.begin() cur2 = tx2.cursor() @@ -164,7 +156,219 @@ def test_1(act: Action, capsys): print(e.__str__()) print(e.gds_codes) - act.expected_stdout = expected_stdout - act.stdout = capsys.readouterr().out - assert act.clean_stdout == act.clean_expected_stdout - act.reset() + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + act.expected_stdout = f""" + READ_COMMITTED_NO_RECORD_VERSION drop procedure sp_test + lock conflict on no wait transaction + -unsuccessful metadata update + -object PROCEDURE {SQL_SCHEMA_PREFIX}"SP_TEST" is in use + (335544345, 335544351, 335544453) + + READ_COMMITTED_NO_RECORD_VERSION drop procedure sp_worker + lock conflict on no wait transaction + -unsuccessful metadata update + -object PROCEDURE {SQL_SCHEMA_PREFIX}"SP_WORKER" is in use + (335544345, 335544351, 335544453) + + READ_COMMITTED_NO_RECORD_VERSION drop function fn_worker + lock conflict on no wait transaction + -unsuccessful metadata update + -object FUNCTION {SQL_SCHEMA_PREFIX}"FN_WORKER" is in use + (335544345, 335544351, 335544453) + + READ_COMMITTED_NO_RECORD_VERSION drop view v_test + lock conflict on no wait transaction + -unsuccessful metadata update + -object VIEW {SQL_SCHEMA_PREFIX}"V_TEST" is in use + (335544345, 335544351, 335544453) + + READ_COMMITTED_NO_RECORD_VERSION drop table test2 + lock conflict on no wait transaction + -unsuccessful metadata update + -object TABLE {SQL_SCHEMA_PREFIX}"TEST2" is in use + (335544345, 335544351, 335544453) + + READ_COMMITTED_NO_RECORD_VERSION drop index test1_id + lock conflict on no wait transaction + -unsuccessful metadata update + -object INDEX {SQL_SCHEMA_PREFIX}"TEST1_ID" is in use + (335544345, 335544351, 335544453) + + READ_COMMITTED_NO_RECORD_VERSION drop index test2_x + lock conflict on no wait transaction + -unsuccessful metadata update + -object INDEX {SQL_SCHEMA_PREFIX}"TEST2_X" is in use + (335544345, 335544351, 335544453) + + READ_COMMITTED_RECORD_VERSION drop procedure sp_test + lock conflict on no wait transaction + -unsuccessful metadata update + -object PROCEDURE {SQL_SCHEMA_PREFIX}"SP_TEST" is in use + (335544345, 335544351, 335544453) + + READ_COMMITTED_RECORD_VERSION drop procedure sp_worker + lock conflict on no wait transaction + -unsuccessful metadata update + -object PROCEDURE {SQL_SCHEMA_PREFIX}"SP_WORKER" is in use + (335544345, 335544351, 335544453) + + READ_COMMITTED_RECORD_VERSION drop function fn_worker + lock conflict on no wait transaction + -unsuccessful metadata update + -object FUNCTION {SQL_SCHEMA_PREFIX}"FN_WORKER" is in use + (335544345, 335544351, 335544453) + + READ_COMMITTED_RECORD_VERSION drop view v_test + lock conflict on no wait transaction + -unsuccessful metadata update + -object VIEW {SQL_SCHEMA_PREFIX}"V_TEST" is in use + (335544345, 335544351, 335544453) + + READ_COMMITTED_RECORD_VERSION drop table test2 + lock conflict on no wait transaction + -unsuccessful metadata update + -object TABLE {SQL_SCHEMA_PREFIX}"TEST2" is in use + (335544345, 335544351, 335544453) + + READ_COMMITTED_RECORD_VERSION drop index test1_id + lock conflict on no wait transaction + -unsuccessful metadata update + -object INDEX {SQL_SCHEMA_PREFIX}"TEST1_ID" is in use + (335544345, 335544351, 335544453) + + READ_COMMITTED_RECORD_VERSION drop index test2_x + lock conflict on no wait transaction + -unsuccessful metadata update + -object INDEX {SQL_SCHEMA_PREFIX}"TEST2_X" is in use + (335544345, 335544351, 335544453) + + SNAPSHOT drop procedure sp_test + lock conflict on no wait transaction + -unsuccessful metadata update + -object PROCEDURE {SQL_SCHEMA_PREFIX}"SP_TEST" is in use + (335544345, 335544351, 335544453) + + SNAPSHOT drop procedure sp_worker + lock conflict on no wait transaction + -unsuccessful metadata update + -object PROCEDURE {SQL_SCHEMA_PREFIX}"SP_WORKER" is in use + (335544345, 335544351, 335544453) + + SNAPSHOT drop function fn_worker + lock conflict on no wait transaction + -unsuccessful metadata update + -object FUNCTION {SQL_SCHEMA_PREFIX}"FN_WORKER" is in use + (335544345, 335544351, 335544453) + + SNAPSHOT drop view v_test + lock conflict on no wait transaction + -unsuccessful metadata update + -object VIEW {SQL_SCHEMA_PREFIX}"V_TEST" is in use + (335544345, 335544351, 335544453) + + SNAPSHOT drop table test2 + lock conflict on no wait transaction + -unsuccessful metadata update + -object TABLE {SQL_SCHEMA_PREFIX}"TEST2" is in use + (335544345, 335544351, 335544453) + + SNAPSHOT drop index test1_id + lock conflict on no wait transaction + -unsuccessful metadata update + -object INDEX {SQL_SCHEMA_PREFIX}"TEST1_ID" is in use + (335544345, 335544351, 335544453) + + SNAPSHOT drop index test2_x + lock conflict on no wait transaction + -unsuccessful metadata update + -object INDEX {SQL_SCHEMA_PREFIX}"TEST2_X" is in use + (335544345, 335544351, 335544453) + + SERIALIZABLE drop procedure sp_test + lock conflict on no wait transaction + -unsuccessful metadata update + -object PROCEDURE {SQL_SCHEMA_PREFIX}"SP_TEST" is in use + (335544345, 335544351, 335544453) + + SERIALIZABLE drop procedure sp_worker + lock conflict on no wait transaction + -unsuccessful metadata update + -object PROCEDURE {SQL_SCHEMA_PREFIX}"SP_WORKER" is in use + (335544345, 335544351, 335544453) + + SERIALIZABLE drop function fn_worker + lock conflict on no wait transaction + -unsuccessful metadata update + -object FUNCTION {SQL_SCHEMA_PREFIX}"FN_WORKER" is in use + (335544345, 335544351, 335544453) + + SERIALIZABLE drop view v_test + lock conflict on no wait transaction + -unsuccessful metadata update + -object VIEW {SQL_SCHEMA_PREFIX}"V_TEST" is in use + (335544345, 335544351, 335544453) + + SERIALIZABLE drop table test2 + lock conflict on no wait transaction + -unsuccessful metadata update + -object TABLE {SQL_SCHEMA_PREFIX}"TEST2" is in use + (335544345, 335544351, 335544453) + + SERIALIZABLE drop index test1_id + lock conflict on no wait transaction + -unsuccessful metadata update + -object INDEX {SQL_SCHEMA_PREFIX}"TEST1_ID" is in use + (335544345, 335544351, 335544453) + + SERIALIZABLE drop index test2_x + lock conflict on no wait transaction + -unsuccessful metadata update + -object INDEX {SQL_SCHEMA_PREFIX}"TEST2_X" is in use + (335544345, 335544351, 335544453) + + READ_COMMITTED_READ_CONSISTENCY drop procedure sp_test + lock conflict on no wait transaction + -unsuccessful metadata update + -object PROCEDURE {SQL_SCHEMA_PREFIX}"SP_TEST" is in use + (335544345, 335544351, 335544453) + + READ_COMMITTED_READ_CONSISTENCY drop procedure sp_worker + lock conflict on no wait transaction + -unsuccessful metadata update + -object PROCEDURE {SQL_SCHEMA_PREFIX}"SP_WORKER" is in use + (335544345, 335544351, 335544453) + + READ_COMMITTED_READ_CONSISTENCY drop function fn_worker + lock conflict on no wait transaction + -unsuccessful metadata update + -object FUNCTION {SQL_SCHEMA_PREFIX}"FN_WORKER" is in use + (335544345, 335544351, 335544453) + + READ_COMMITTED_READ_CONSISTENCY drop view v_test + lock conflict on no wait transaction + -unsuccessful metadata update + -object VIEW {SQL_SCHEMA_PREFIX}"V_TEST" is in use + (335544345, 335544351, 335544453) + + READ_COMMITTED_READ_CONSISTENCY drop table test2 + lock conflict on no wait transaction + -unsuccessful metadata update + -object TABLE {SQL_SCHEMA_PREFIX}"TEST2" is in use + (335544345, 335544351, 335544453) + + READ_COMMITTED_READ_CONSISTENCY drop index test1_id + lock conflict on no wait transaction + -unsuccessful metadata update + -object INDEX {SQL_SCHEMA_PREFIX}"TEST1_ID" is in use + (335544345, 335544351, 335544453) + + READ_COMMITTED_READ_CONSISTENCY drop index test2_x + lock conflict on no wait transaction + -unsuccessful metadata update + -object INDEX {SQL_SCHEMA_PREFIX}"TEST2_X" is in use + (335544345, 335544351, 335544453) + """ + + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() diff --git a/tests/bugs/core_4396_test.py b/tests/bugs/core_4396_test.py index a930dc1d..37683f9c 100644 --- a/tests/bugs/core_4396_test.py +++ b/tests/bugs/core_4396_test.py @@ -73,6 +73,7 @@ CNT 0 """ +@pytest.mark.es_eds @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_4403_test.py b/tests/bugs/core_4403_test.py index 300972ee..4981e868 100644 --- a/tests/bugs/core_4403_test.py +++ b/tests/bugs/core_4403_test.py @@ -7,12 +7,23 @@ DESCRIPTION: JIRA: CORE-4403 FBTEST: bugs.core_4403 +NOTES: + [29.06.2025] pzotov + Increased min_version to 4.0 because name of column is not shown in old 3.x. + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """ +db = db_factory() + +test_script = """ + set list on; + recreate table t1(id int primary key, x int, y int); recreate table t2(id int primary key, x int, y int); recreate table t3(id int primary key, x int, y int); @@ -32,12 +43,7 @@ ^ set term ;^ commit; -""" - -db = db_factory(init=init_script) -test_script = """ - set list on; set term ^; execute block returns( t1_id int, t1_x int, t1_y int @@ -80,10 +86,6 @@ end ^ - --/******************** - --### 29.05.2015. TODO ### UNCOMMENT LATER, AFTER FIX CORE-4819. CURRENTLY IT LEADS FB TO HANG / CRASH. - - -- Uncomment 06.08.2018: execute block returns(old_y int, new_y int) as begin for @@ -97,60 +99,48 @@ end end ^ - -- ********************/ - set term ;^ commit; set list off; """ -act = isql_act('db', test_script) - -expected_stdout = """ - T1_ID 1 - T1_X 10 - T1_Y 11 - T2_ID 2 - T2_X 10 - T2_Y 22 - T3_ID 3 - T3_X 10 - T3_Y 33 -""" - -# version: 3.0 - -expected_stderr_1 = """ +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) + +expected_stdout_5x = """ + T1_ID 1 + T1_X 10 + T1_Y 11 + T2_ID 2 + T2_X 10 + T2_Y 22 + T3_ID 3 + T3_X 10 + T3_Y 33 Statement failed, SQLSTATE = 42000 - attempted update of read-only column - + attempted update of read-only column CE.X Statement failed, SQLSTATE = 42000 - attempted update of read-only column + attempted update of read-only column CE.Y """ -@pytest.mark.version('>=3.0,<4.0') -def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr_1 - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - -# version: 4.0 - -expected_stderr_2 = """ +expected_stdout_6x = """ + T1_ID 1 + T1_X 10 + T1_Y 11 + T2_ID 2 + T2_X 10 + T2_Y 22 + T3_ID 3 + T3_X 10 + T3_Y 33 Statement failed, SQLSTATE = 42000 - attempted update of read-only column CE.X - + attempted update of read-only column "CE"."X" Statement failed, SQLSTATE = 42000 - attempted update of read-only column CE.Y + attempted update of read-only column "CE"."Y" """ @pytest.mark.version('>=4.0') -def test_2(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr_2 - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - +def test_1(act: Action): + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4415_test.py b/tests/bugs/core_4415_test.py index 47538d0c..55eb9390 100644 --- a/tests/bugs/core_4415_test.py +++ b/tests/bugs/core_4415_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4415 FBTEST: bugs.core_4415 +NOTES: + [29.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -21,17 +27,21 @@ -- TR, Sequence: 0, Type: BEFORE CREATE TABLE OR ALTER TABLE OR DROP TABLE OR ... OR , Active // length = 967 characters. """ -act = isql_act('db', test_script, substitutions=[('\\+.*', ''), ('\\=.*', ''), - ('Trigger text.*', '')]) +substitutions = [('\\+.*', ''), ('\\=.*', ''), ('Trigger text.*', '')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ +expected_stdout_5x = """ TR, Sequence: 0, Type: BEFORE ANY DDL STATEMENT, Active as begin end """ +expected_stdout_6x = """ + PUBLIC.TR, Sequence: 0, Type: BEFORE ANY DDL STATEMENT, Active + as begin end +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4447_test.py b/tests/bugs/core_4447_test.py index b5166f69..4b20eeff 100644 --- a/tests/bugs/core_4447_test.py +++ b/tests/bugs/core_4447_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4447 FBTEST: bugs.core_4447 +NOTES: + [29.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -45,14 +51,18 @@ act = isql_act('db', test_script, substitutions = [('(--\\s+)?line \\d+, col(umn)? \\d+', '')]) -expected_stdout = """ - PLAN (T INDEX (TT_PK_XY)) - PLAN (C TS NATURAL) +expected_stdout_5x = """ + PLAN (T INDEX (TT_PK_XY)) + PLAN (C TS NATURAL) +""" + +expected_stdout_6x = """ + PLAN ("T" INDEX ("PUBLIC"."TT_PK_XY")) + PLAN ("C" "PUBLIC"."TS" NATURAL) """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4451_test.py b/tests/bugs/core_4451_test.py index 852e63bc..3d7c976e 100644 --- a/tests/bugs/core_4451_test.py +++ b/tests/bugs/core_4451_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4451 FBTEST: bugs.core_4451 +NOTES: + [29.06.2025] pzotov + Suppressed name of table because on 6.x it is prefixed by SQL schema and is enclosed in quotes. + For this test it is enough just to show that explained form of plan presents in the trace. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -18,7 +24,7 @@ db = db_factory(init=init_script) -act = python_act('db', substitutions=[('[ \t]+', ' '), ('[ \t]+[\\d]+[ \t]+ms', '')]) +act = python_act('db', substitutions=[('[ \t]+', ' '), ('[ \t]+[\\d]+[ \t]+ms', ''), ('Table.*', 'Table')]) expected_stdout = """ Select Expression @@ -34,6 +40,7 @@ 'include_filter=%(from|join)[[:whitespace:]]test%', ] +@pytest.mark.trace @pytest.mark.version('>=3.0') def test_1(act: Action, capsys): with act.trace(db_events=trace): diff --git a/tests/bugs/core_4453_test.py b/tests/bugs/core_4453_test.py index 9dec560d..bb283e7f 100644 --- a/tests/bugs/core_4453_test.py +++ b/tests/bugs/core_4453_test.py @@ -5,19 +5,24 @@ ISSUE: 4773 TITLE: Regression: NOT NULL constraint, declared in domain, does not work DESCRIPTION: + Tests that manipulates with NULL fields/domains and check results: + CORE-1518 Adding a non-null restricted column to a populated table renders the table inconsistent + CORE-4453 (Regression: NOT NULL constraint, declared in domain, does not work) + CORE-4725 (Inconsistencies with ALTER DOMAIN and ALTER TABLE with DROP NOT NULL and PRIMARY KEYs) + CORE-4733 (Command "Alter table alter TYPE and makes incorrect assignments in to ZERO / JULIAN_DATE / ASCII(0) for types INT, TIMESTAMP and VARCHAR) JIRA: CORE-4453 FBTEST: bugs.core_4453 + [29.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * init_script = """ - -- Tests that manipulates with NULL fields/domains and check results: - -- CORE-1518 Adding a non-null restricted column to a populated table renders the table inconsistent - -- CORE-4453 (Regression: NOT NULL constraint, declared in domain, does not work) - -- CORE-4725 (Inconsistencies with ALTER DOMAIN and ALTER TABLE with DROP NOT NULL and PRIMARY KEYs) - -- CORE-4733 (Command "Alter table alter TYPE and makes incorrect assignments in to ZERO / JULIAN_DATE / ASCII(0) for types INT, TIMESTAMP and VARCHAR) create domain dm_01 varchar(20) not null; commit; create table t_01(s dm_01, x int); @@ -33,14 +38,18 @@ act = isql_act('db', test_script) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 23000 validation error for column "T_01"."S", value "*** null ***" """ +expected_stdout_6x = """ + Statement failed, SQLSTATE = 23000 + validation error for column "PUBLIC"."T_01"."S", value "*** null ***" +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4468_test.py b/tests/bugs/core_4468_test.py index 39471d2b..3dfaad17 100644 --- a/tests/bugs/core_4468_test.py +++ b/tests/bugs/core_4468_test.py @@ -6,290 +6,294 @@ TITLE: FB3: CREATE USER GRANT ADMIN ROLE does not work DESCRIPTION: JIRA: CORE-4468 -FBTEST: bugs.core_4468 +NOTES: + [08.03.2025] pzotov + 1. Commented out (and will be deleted later) code that expected error when user who was granted role + with admin option tries to revoke this role from himself. Seince fixed GH-8462 this is NOT so. + 2. Replaced hard-coded names/passwords with variables that are provided by fixtures (tmp_senior, tmp_junior). + Checked on 6.0.0.660; 5.0.3.1624; 4.0.6.3189; 3.0.13.33798 + + [15.05.2025] pzotov + Removed 'show grants' because its output very 'fragile' and can often change in master branch. + It is enough to use custom VIEW ('v_users') to check data. + + [29.06.2025] pzotov + Added variable 'PLG_VIEW_NAME' with value depending on major FB version (on 6.x it is prefixed with SQL schema name). + This variable is substituted in expected output via f-notation. + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ +import locale import pytest from firebird.qa import * substitutions = [('.*delete record.*', 'delete record'), - ('TABLE PLG\\$VIEW_USERS', 'TABLE PLG'), - ('TABLE PLG\\$SRP_VIEW', 'TABLE PLG'), - ('-OZZY_OSBOURNE is not grantor of (role|Role|ROLE) on RDB\\$ADMIN to OZZY_OSBOURNE.', - '-OZZY_OSBOURNE is not grantor of ROLE on RDB$ADMIN to OZZY_OSBOURNE.'), + #('TABLE PLG\\$VIEW_USERS', 'TABLE PLG'), + #('TABLE PLG\\$SRP_VIEW', 'TABLE PLG'), ('-Effective user is.*', '')] -init_script = """ - -- ::: NB ::: Name of table in STDERR depends on value of UserManager = { Srp | Legacy_UserManager }. - -- For 'Srp' it will be 'PLG$SRP_VIEW', for Legacy_UserManager -- PLG$VIEW_USERS. - -- Because of this, section 'substitution' has been added in order to ignore rest part of line - -- after words 'TABLE PLG'. - -- Also, text in message about deletion fault differs in case of UserManager setting: - -- 'find/delete record error' - for Legacy_UserManager - -- 'delete record error' = for Srp - -- This is minor bug in Legacy_UserManager but it will be remain 'as is', see letter from Alex 03-jun-2015 19:51. - recreate view v_users as - select current_user who_am_i, current_role whats_my_role, u.sec$user_name non_sysdba_user_name, u.sec$admin non_sysdba_has_admin_role - from rdb$database - left join sec$users u on u.sec$user_name in ( upper('ozzy_osbourne'), upper('bon_scott') ); -""" - -db = db_factory(init=init_script) - -# Well, we need to create/drop users in test script, but next user fioxtures are -# defined to make sure that no user will be left behind in case the test fails -user_ozzy = user_factory('db', name='ozzy_osbourne', password='123', admin=True, do_not_create=True) -user_scott = user_factory('db', name='bon_scott', password='456', do_not_create=True) - -test_script = """ - set wng off; - set list on; - set count on; - - select 'start' msg, v.* from v_users v; - commit; - - create or alter user ozzy_osbourne password '123' - grant admin role -- this is mandatory because it gives him admin role in Security DB - ; - revoke all on all from ozzy_osbourne; - grant rdb$admin to ozzy_osbourne; -- this is also mandatory: it gives him admin role in ($dsn) database - commit; - - select 'step-1' msg, v.* from v_users v; - commit; - - -- When 'ozzy_osbourne' connects to database ($dsn), there is no way for engine to recognize that this user - -- has been granted with admin role in 'CREATE USER ... GRANT ADMIN ROLE' statement. So, user has to specify - -- `role 'RDB$ADMIN'` in order to connect as ADMIN. - -- But with RDB$ADMIN only he can create objects in THAT database (tables etc), but not other USERS! - -- Thats why he should also be granted with admin role in 'CREATE USER ...' - see above. - connect '$(DSN)' user 'OZZY_OSBOURNE' password '123' role 'RDB$ADMIN'; - commit; - - -- Users are stored in Security DB, *not* in "this" database! - -- So, following statement will pass only if 'ozzy_osbourne' has been granted by 'admin role' - -- in his own 'create user' phase: - create or alter user bon_scott password '456' revoke admin role; - commit; - - select 'step-2' msg, v.* from v_users v; - - alter user bon_scott grant admin role; - commit; - show grants; - - select 'step-3' msg, v.* from v_users v; - - grant rdb$admin to bon_scott; - commit; - - show grants; - - alter user bon_scott revoke admin role; - commit; - - select 'step-4' msg, v.* from v_users v; - commit; - - revoke rdb$admin from bon_scott; - commit; - - show grants; - - drop user bon_scott; - commit; - - select 'step-5' msg, v.* from v_users v; - commit; - - -- User removes admin role from himself: - - -- 1. This will FAIL: - -- -REVOKE failed - -- -OZZY_OSBOURNE is not grantor of Role on RDB$ADMIN to OZZY_OSBOURNE. - revoke rdb$admin from ozzy_osbourne; - commit; - - -- 2 This will PASS, and it MUST be so (see letter from Alex, 03-jun-2015 19:46) - alter user ozzy_osbourne revoke admin role; - commit; - - show grants; - - select 'step-6' msg, v.* from v_users v; - commit; - - -- And after previous action he can not drop himself because now he is NOT member of admin role: - -- Statement failed, SQLSTATE = 28000 - -- find/delete record error - -- -no permission for DELETE access to TABLE PLG$VIEW_USERS - drop user ozzy_osbourne; - commit; - - select 'step-7' msg, v.* from v_users v; - commit; - - -- Trying reconnect with role RDB$ADMIN: - connect '$(DSN)' user 'OZZY_OSBOURNE' password '123' role 'RDB$ADMIN'; - commit; - - select 'step-8' msg, v.* from v_users v; - commit; - - show grants; - commit; - - connect '$(DSN)' user 'SYSDBA' password 'masterkey'; - drop user ozzy_osbourne; - commit; - - select 'final' msg, v.* from v_users v; - commit; -""" - -act = isql_act('db', test_script, substitutions=substitutions) - -expected_stdout = """ - MSG start - WHO_AM_I SYSDBA - WHATS_MY_ROLE NONE - NON_SYSDBA_USER_NAME - NON_SYSDBA_HAS_ADMIN_ROLE - Records affected: 1 - - MSG step-1 - WHO_AM_I SYSDBA - WHATS_MY_ROLE NONE - NON_SYSDBA_USER_NAME OZZY_OSBOURNE - NON_SYSDBA_HAS_ADMIN_ROLE - - Records affected: 1 +db = db_factory() +tmp_senior = user_factory('db', name='tmp_4468_senior', password='123', admin=True) +tmp_junior = user_factory('db', name='tmp_4468_junior', password='456') - MSG step-2 - WHO_AM_I OZZY_OSBOURNE - WHATS_MY_ROLE RDB$ADMIN - NON_SYSDBA_USER_NAME OZZY_OSBOURNE - NON_SYSDBA_HAS_ADMIN_ROLE - - MSG step-2 - WHO_AM_I OZZY_OSBOURNE - WHATS_MY_ROLE RDB$ADMIN - NON_SYSDBA_USER_NAME BON_SCOTT - NON_SYSDBA_HAS_ADMIN_ROLE - - - Records affected: 2 - - /* Grant permissions for this database */ - GRANT RDB$ADMIN TO OZZY_OSBOURNE - - MSG step-3 - WHO_AM_I OZZY_OSBOURNE - WHATS_MY_ROLE RDB$ADMIN - NON_SYSDBA_USER_NAME OZZY_OSBOURNE - NON_SYSDBA_HAS_ADMIN_ROLE - - MSG step-3 - WHO_AM_I OZZY_OSBOURNE - WHATS_MY_ROLE RDB$ADMIN - NON_SYSDBA_USER_NAME BON_SCOTT - NON_SYSDBA_HAS_ADMIN_ROLE - - - Records affected: 2 - - /* Grant permissions for this database */ - GRANT RDB$ADMIN TO BON_SCOTT GRANTED BY OZZY_OSBOURNE - GRANT RDB$ADMIN TO OZZY_OSBOURNE - - MSG step-4 - WHO_AM_I OZZY_OSBOURNE - WHATS_MY_ROLE RDB$ADMIN - NON_SYSDBA_USER_NAME OZZY_OSBOURNE - NON_SYSDBA_HAS_ADMIN_ROLE - - MSG step-4 - WHO_AM_I OZZY_OSBOURNE - WHATS_MY_ROLE RDB$ADMIN - NON_SYSDBA_USER_NAME BON_SCOTT - NON_SYSDBA_HAS_ADMIN_ROLE - - - Records affected: 2 - - /* Grant permissions for this database */ - GRANT RDB$ADMIN TO OZZY_OSBOURNE - - MSG step-5 - WHO_AM_I OZZY_OSBOURNE - WHATS_MY_ROLE RDB$ADMIN - NON_SYSDBA_USER_NAME OZZY_OSBOURNE - NON_SYSDBA_HAS_ADMIN_ROLE - - - Records affected: 1 - - /* Grant permissions for this database */ - GRANT RDB$ADMIN TO OZZY_OSBOURNE - - MSG step-6 - WHO_AM_I OZZY_OSBOURNE - WHATS_MY_ROLE RDB$ADMIN - NON_SYSDBA_USER_NAME OZZY_OSBOURNE - NON_SYSDBA_HAS_ADMIN_ROLE - - - Records affected: 1 - - MSG step-7 - WHO_AM_I OZZY_OSBOURNE - WHATS_MY_ROLE RDB$ADMIN - NON_SYSDBA_USER_NAME OZZY_OSBOURNE - NON_SYSDBA_HAS_ADMIN_ROLE - - - Records affected: 1 - - MSG step-8 - WHO_AM_I OZZY_OSBOURNE - WHATS_MY_ROLE RDB$ADMIN - NON_SYSDBA_USER_NAME OZZY_OSBOURNE - NON_SYSDBA_HAS_ADMIN_ROLE - - - Records affected: 1 - - /* Grant permissions for this database */ - GRANT RDB$ADMIN TO OZZY_OSBOURNE - - MSG final - WHO_AM_I SYSDBA - WHATS_MY_ROLE NONE - NON_SYSDBA_USER_NAME - NON_SYSDBA_HAS_ADMIN_ROLE - - - Records affected: 1 -""" - -expected_stderr = """ - Statement failed, SQLSTATE = 42000 - unsuccessful metadata update - -REVOKE failed - -OZZY_OSBOURNE is not grantor of Role on RDB$ADMIN to OZZY_OSBOURNE. - - Statement failed, SQLSTATE = 28000 - delete record error - -no permission for DELETE access to TABLE PLG$VIEW_USERS -""" +act = isql_act('db', substitutions=substitutions) @pytest.mark.version('>=3.0') -def test_1(act: Action, user_ozzy: User, user_scott: User): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - +def test_1(act: Action, tmp_senior: User, tmp_junior: User): + + test_sql = f""" + set wng off; + set list on; + set count on; + + -- ::: NB ::: Name of PLG-* depends on value of UserManager = Srp or Legacy_UserManager. + -- For 'Srp' it will be 'PLG$SRP_VIEW', for Legacy_UserManager -- PLG$VIEW_USERS. + -- Because of this, section 'substitution' has been added in order to ignore rest part of line + -- after words 'TABLE PLG'. + -- Also, text in message about deletion fault differs in case of UserManager setting: + -- 'find/delete record error' - for Legacy_UserManager + -- 'delete record error' = for Srp + -- This is minor bug in Legacy_UserManager but it will be remain 'as is', see letter from Alex 03-jun-2015 19:51. + recreate view v_users as + select + current_user who_am_i + ,current_role whats_my_role + ,u.sec$user_name user_name + ,u.sec$admin sec_admin + ,g.rdb$privilege is not null as rdb_admin + ,g.rdb$grant_option rdb_adm_grant_option + from rdb$database + left join sec$users u on u.sec$user_name in ( upper('{tmp_senior.name}'), upper('{tmp_junior.name}') ) + left join rdb$user_privileges g on u.sec$user_name = g.rdb$user and g.rdb$privilege = upper('m') and g.rdb$relation_name = upper('rdb$admin') + order by user_name + ; + commit; + + grant select on v_users to public; + commit; + + select 'start' msg, v.* from v_users v; + commit; + + revoke all on all from {tmp_senior.name}; + grant rdb$admin to {tmp_senior.name}; -- this is also mandatory: it gives him admin role in ($dsn) database + commit; + + select 'point-1' msg, v.* from v_users v; + commit; + + -- When '{tmp_senior.name}' connects to database ($dsn), there is no way for engine to recognize that this user + -- has been granted with admin role in 'CREATE USER ... GRANT ADMIN ROLE' statement. So, user has to specify + -- `role 'RDB$ADMIN'` in order to connect as ADMIN. + -- But with RDB$ADMIN only he can create objects in THAT database (tables etc), but not other USERS! + -- Thats why he should also be granted with admin role in 'CREATE USER ...' - see above. + connect '{act.db.dsn}' user '{tmp_senior.name}' password '{tmp_senior.password}' role 'RDB$ADMIN'; + commit; + + -- Users are stored in Security DB, *not* in "this" database! + -- So, following statement will pass only if '{tmp_senior.name}' has been granted by 'admin role' + -- in his own 'create user' phase: + create or alter user {tmp_junior.name} password '{tmp_junior.password}' revoke admin role; + commit; + + select 'point-2' msg, v.* from v_users v; + + alter user {tmp_junior.name} grant admin role; + commit; + + select 'point-3' msg, v.* from v_users v; + + grant rdb$admin to {tmp_junior.name}; + commit; + + select 'point-4' msg, v.* from v_users v; + + alter user {tmp_junior.name} revoke admin role; + commit; + + select 'point-5' msg, v.* from v_users v; + commit; + + revoke rdb$admin from {tmp_junior.name}; + commit; + + select 'point-6' msg, v.* from v_users v; + commit; + + -- User removes admin role from himself: + + /**************************************** + -- 1. This will FAIL: + -- -REVOKE failed + -- -{tmp_senior.name} is not grantor of Role on RDB$ADMIN to {tmp_senior.name}. + revoke rdb$admin from {tmp_senior.name}; + commit; + *******************************************/ + + -- 2 This will PASS, and it MUST be so (see letter from Alex, 03-jun-2015 19:46) + alter user {tmp_senior.name} revoke admin role; + commit; + + select 'point-7' msg, v.* from v_users v; + commit; + + -- And after previous action he can not drop himself because now he is NOT member of admin role: + -- Statement failed, SQLSTATE = 28000 + -- find/delete record error + -- -no permission for DELETE access to TABLE PLG$VIEW_USERS + drop user {tmp_senior.name}; + commit; + + select 'point-8' msg, v.* from v_users v; + commit; + + -- Trying reconnect with role RDB$ADMIN: + connect '{act.db.dsn}' user '{tmp_senior.name}' password '{tmp_senior.password}' role 'RDB$ADMIN'; + commit; + + select 'finish' msg, v.* from v_users v; + commit; + """ + + # 29.06.2025: name of view differs depending on major FB vefsion: + PLG_VIEW_NAME = 'PLG$SRP_VIEW' if act.is_version('<6') else '"PLG$SRP"."PLG$SRP_VIEW"' + + expected_out = f""" + MSG start + WHO_AM_I {act.db.user} + WHATS_MY_ROLE NONE + USER_NAME {tmp_junior.name} + SEC_ADMIN + RDB_ADMIN + RDB_ADM_GRANT_OPTION + MSG start + WHO_AM_I {act.db.user} + WHATS_MY_ROLE NONE + USER_NAME {tmp_senior.name} + SEC_ADMIN + RDB_ADMIN + RDB_ADM_GRANT_OPTION + Records affected: 2 + MSG point-1 + WHO_AM_I {act.db.user} + WHATS_MY_ROLE NONE + USER_NAME {tmp_junior.name} + SEC_ADMIN + RDB_ADMIN + RDB_ADM_GRANT_OPTION + MSG point-1 + WHO_AM_I {act.db.user} + WHATS_MY_ROLE NONE + USER_NAME {tmp_senior.name} + SEC_ADMIN + RDB_ADMIN + RDB_ADM_GRANT_OPTION 0 + Records affected: 2 + MSG point-2 + WHO_AM_I {tmp_senior.name} + WHATS_MY_ROLE RDB$ADMIN + USER_NAME {tmp_junior.name} + SEC_ADMIN + RDB_ADMIN + RDB_ADM_GRANT_OPTION + MSG point-2 + WHO_AM_I {tmp_senior.name} + WHATS_MY_ROLE RDB$ADMIN + USER_NAME {tmp_senior.name} + SEC_ADMIN + RDB_ADMIN + RDB_ADM_GRANT_OPTION 0 + Records affected: 2 + MSG point-3 + WHO_AM_I {tmp_senior.name} + WHATS_MY_ROLE RDB$ADMIN + USER_NAME {tmp_junior.name} + SEC_ADMIN + RDB_ADMIN + RDB_ADM_GRANT_OPTION + MSG point-3 + WHO_AM_I {tmp_senior.name} + WHATS_MY_ROLE RDB$ADMIN + USER_NAME {tmp_senior.name} + SEC_ADMIN + RDB_ADMIN + RDB_ADM_GRANT_OPTION 0 + Records affected: 2 + MSG point-4 + WHO_AM_I {tmp_senior.name} + WHATS_MY_ROLE RDB$ADMIN + USER_NAME {tmp_junior.name} + SEC_ADMIN + RDB_ADMIN + RDB_ADM_GRANT_OPTION 0 + MSG point-4 + WHO_AM_I {tmp_senior.name} + WHATS_MY_ROLE RDB$ADMIN + USER_NAME {tmp_senior.name} + SEC_ADMIN + RDB_ADMIN + RDB_ADM_GRANT_OPTION 0 + Records affected: 2 + MSG point-5 + WHO_AM_I {tmp_senior.name} + WHATS_MY_ROLE RDB$ADMIN + USER_NAME {tmp_junior.name} + SEC_ADMIN + RDB_ADMIN + RDB_ADM_GRANT_OPTION 0 + MSG point-5 + WHO_AM_I {tmp_senior.name} + WHATS_MY_ROLE RDB$ADMIN + USER_NAME {tmp_senior.name} + SEC_ADMIN + RDB_ADMIN + RDB_ADM_GRANT_OPTION 0 + Records affected: 2 + MSG point-6 + WHO_AM_I {tmp_senior.name} + WHATS_MY_ROLE RDB$ADMIN + USER_NAME {tmp_junior.name} + SEC_ADMIN + RDB_ADMIN + RDB_ADM_GRANT_OPTION + MSG point-6 + WHO_AM_I {tmp_senior.name} + WHATS_MY_ROLE RDB$ADMIN + USER_NAME {tmp_senior.name} + SEC_ADMIN + RDB_ADMIN + RDB_ADM_GRANT_OPTION 0 + Records affected: 2 + MSG point-7 + WHO_AM_I {tmp_senior.name} + WHATS_MY_ROLE RDB$ADMIN + USER_NAME {tmp_senior.name} + SEC_ADMIN + RDB_ADMIN + RDB_ADM_GRANT_OPTION 0 + Records affected: 1 + Statement failed, SQLSTATE = 28000 + delete record + -no permission for DELETE access to TABLE {PLG_VIEW_NAME} + MSG point-8 + WHO_AM_I {tmp_senior.name} + WHATS_MY_ROLE RDB$ADMIN + USER_NAME {tmp_senior.name} + SEC_ADMIN + RDB_ADMIN + RDB_ADM_GRANT_OPTION 0 + Records affected: 1 + MSG finish + WHO_AM_I {tmp_senior.name} + WHATS_MY_ROLE RDB$ADMIN + USER_NAME {tmp_senior.name} + SEC_ADMIN + RDB_ADMIN + RDB_ADM_GRANT_OPTION 0 + Records affected: 1 + """ + + act.expected_stdout = expected_out # expected_5x if act.is_version('<6') else expected_6x + act.isql(switches = ['-q'], input = test_sql, combine_output = True, io_enc = locale.getpreferredencoding()) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4470_test.py b/tests/bugs/core_4470_test.py index 04ba62a3..03baf9a9 100644 --- a/tests/bugs/core_4470_test.py +++ b/tests/bugs/core_4470_test.py @@ -10,6 +10,12 @@ Replaced PSQL function name 'localtime()' with 'fn_local_time()': first of them became keyword in FB 4.0 JIRA: CORE-4470 FBTEST: bugs.core_4470 +NOTES: + [29.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -254,11 +260,16 @@ act = python_act('db') -expected_stdout = """ +expected_stdout_5x = """ METEO WF """ +expected_stdout_6x = """ + PUBLIC.METEO + PUBLIC.WF +""" + fbk_file = temp_file('test.fbk') @pytest.mark.version('>=3.0') @@ -269,6 +280,13 @@ def test_1(act: Action, fbk_file: Path): srv.database.restore(backup=fbk_file, database=act.db.db_path, flags=SrvRestoreFlag.REPLACE) srv.wait() - act.expected_stdout = expected_stdout - act.isql(switches=['-q'], input='show view; show package;') + + test_script = """ + show view; + show package; + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.isql(switches=['-q'], input = test_script, combine_output = True) assert act.clean_stdout == act.clean_expected_stdout + diff --git a/tests/bugs/core_4484_test.py b/tests/bugs/core_4484_test.py index 0507c0b6..76462b1c 100644 --- a/tests/bugs/core_4484_test.py +++ b/tests/bugs/core_4484_test.py @@ -4,10 +4,14 @@ ID: issue-4804 ISSUE: 4804 TITLE: Description (COMMENT ON) for package procedures and functions, and its parameters -DESCRIPTION: - Test verifies ability to store comments and also to encode them in UTF8 +DESCRIPTION: Test verifies ability to store comments and also to encode them in UTF8 JIRA: CORE-4484 FBTEST: bugs.core_4484 +NOTES: + [30.06.2025] pzotov + Regression was found in FB 6.x. Fixed in: + https://github.com/FirebirdSQL/firebird/commit/f693bf4e72915534a0d45e9c4eec7a9f1959d2ee + Checked on 6.0.0.881; 5.0.3.1668 """ import pytest @@ -102,35 +106,29 @@ act = isql_act('db', test_script, substitutions=[('TEXT_BLOB.*', '')]) expected_stdout = """ - DESCR_FOR_WHAT package itself - OBJ_NAME PG_TEST - TEXT_BLOB 0:3 - MITÄ TÄMÄN - - DESCR_FOR_WHAT package proc - OBJ_NAME SP_TEST - TEXT_BLOB 0:6 - ÁÉÍÓÚÝ - - DESCR_FOR_WHAT package func - OBJ_NAME FN_TEST - TEXT_BLOB 0:9 - ÂÊÎÔÛ - - DESCR_FOR_WHAT package proc pars - OBJ_NAME I_X - TEXT_BLOB 0:c - ÃÑÕ ÄËÏÖÜŸ - - DESCR_FOR_WHAT package proc pars - OBJ_NAME O_Z - TEXT_BLOB 0:f - ÇŠ ΔΘΛΞΣΨΩ - - DESCR_FOR_WHAT package func args - OBJ_NAME I_X - TEXT_BLOB 0:12 - ĄĘŁŹŻ ЙЁ ЊЋЏ ĂŞŢ + DESCR_FOR_WHAT package itself + OBJ_NAME PG_TEST + MITÄ TÄMÄN + + DESCR_FOR_WHAT package proc + OBJ_NAME SP_TEST + ÁÉÍÓÚÝ + + DESCR_FOR_WHAT package func + OBJ_NAME FN_TEST + ÂÊÎÔÛ + + DESCR_FOR_WHAT package proc pars + OBJ_NAME I_X + ÃÑÕ ÄËÏÖÜŸ + + DESCR_FOR_WHAT package proc pars + OBJ_NAME O_Z + ÇŠ ΔΘΛΞΣΨΩ + + DESCR_FOR_WHAT package func args + OBJ_NAME I_X + ĄĘŁŹŻ ЙЁ ЊЋЏ ĂŞŢ """ @pytest.mark.version('>=3.0') @@ -138,4 +136,3 @@ def test_1(act: Action): act.expected_stdout = expected_stdout act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4492_test.py b/tests/bugs/core_4492_test.py index 1278d0e9..883256b4 100644 --- a/tests/bugs/core_4492_test.py +++ b/tests/bugs/core_4492_test.py @@ -5,21 +5,29 @@ ISSUE: 1566 TITLE: OR/IN predicates for RDB$DBKEY lead to NATURAL plan DESCRIPTION: -NOTES: -[25.11.2017] - Following query will not compile: - select 1 from rdb$relations a join rdb$relations b using ( rdb$db_key ); - Statement failed, SQLSTATE = 42000 / -Token unknown / -rdb$db_key ==> Why ? - Sent letter to dimitr, 25.11.2017 22:42. Waiting for reply. -[27.12.2017] seems that this note will remain unresolved for undef. time. JIRA: CORE-4492 FBTEST: bugs.core_4492 NOTES: + [25.11.2017] + Following query will not compile: + select 1 from rdb$relations a join rdb$relations b using ( rdb$db_key ); + Statement failed, SQLSTATE = 42000 / -Token unknown / -rdb$db_key ==> Why ? + Sent letter to dimitr, 25.11.2017 22:42. Waiting for reply. + + [27.12.2017] + Seems that this note will remain unresolved for undef. time. + [07.04.2022] pzotov FB 5.0.0.455 and later: data sources with equal cardinality now present in the HASH plan in order they are specified in the query. Reversed order was used before this build. Because of this, two cases of expected stdout must be taken in account, see variables 'fb3x_checked_stdout' and 'fb5x_checked_stdout'. + + [29.06.2025] pzotov + Added branch in defintion of expected output for FB-6x. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. + """ import pytest @@ -74,9 +82,17 @@ PLAN HASH (VU_A RDB$RELATIONS NATURAL, VU_A RDB$RELATIONS NATURAL, VU_B RDB$RELATIONS NATURAL, VU_B RDB$RELATIONS NATURAL) """ +fb6x_checked_stdout = """ + PLAN ("RR0" INDEX ()) + PLAN ("RR1" INDEX ()) + PLAN ("VU" "SYSTEM"."RDB$RELATIONS" INDEX (), "VU" "SYSTEM"."RDB$RELATIONS" INDEX ()) + PLAN JOIN ("RR_A" NATURAL, "RR_B" INDEX ()) + PLAN HASH ("VU_A" "SYSTEM"."RDB$RELATIONS" NATURAL, "VU_A" "SYSTEM"."RDB$RELATIONS" NATURAL, "VU_B" "SYSTEM"."RDB$RELATIONS" NATURAL, "VU_B" "SYSTEM"."RDB$RELATIONS" NATURAL) +""" + @pytest.mark.version('>=3.0.3') def test_1(act: Action): - act.expected_stdout = fb3x_checked_stdout if act.is_version('<5') else fb5x_checked_stdout - act.execute() + act.expected_stdout = fb3x_checked_stdout if act.is_version('<5') else fb5x_checked_stdout if act.is_version('<6') else fb6x_checked_stdout + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4498_test.py b/tests/bugs/core_4498_test.py index 73f80a42..25cc9ea8 100644 --- a/tests/bugs/core_4498_test.py +++ b/tests/bugs/core_4498_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4498 FBTEST: bugs.core_4498 +NOTES: + [29.06.2025] pzotov + Added subst to suppress displaying name of table: on 6.x it is prefixed by SQL schema and enclosed in quotes. + For this test it is enough just to show proper starting part of line with explained plan and check that no error occurs. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -19,7 +25,7 @@ select 1 from rdb$relations where rdb$db_key = cast('1234' as char(8) character set octets); """ -act = isql_act('db', test_script) +act = isql_act('db', test_script, substitutions = [('Table .*', 'Table')]) expected_stdout = """ Select Expression diff --git a/tests/bugs/core_4524_test.py b/tests/bugs/core_4524_test.py index 4fc4c318..6b014269 100644 --- a/tests/bugs/core_4524_test.py +++ b/tests/bugs/core_4524_test.py @@ -27,8 +27,11 @@ [21.09.2022] pzotov Test reads settings that are COMMON for all encryption-related tests and stored in act.files_dir/test_config.ini. QA-plugin prepares this by defining dictionary with name QA_GLOBALS which reads settings via ConfigParser mechanism. - Checked on Linux and Windows: 3.0.8.33535 (SS/CS), 4.0.1.2692 (SS/CS) + + [29.06.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import os import binascii @@ -268,15 +271,18 @@ def test_1(act_src: Action, act_res: Action, tmp_fbk:Path, capsys): # Final check: ensure that sequences have proper values: ############## - act_res.expected_stdout = """ - Generator GEN_7FFFFFFF, current value: 2147483646, initial value: 2147483647, increment: 1 - Generator GEN_BA0BAB, current value: 12192682, initial value: 12192683, increment: 1 - Generator GEN_BADF00D, current value: 195948556, initial value: 195948557, increment: 1 - Generator GEN_C0FFEE, current value: 12648429, initial value: 12648430, increment: 1 - Generator GEN_CACA0, current value: 830623, initial value: 830624, increment: 1 - Generator GEN_DEC0DE, current value: 14598365, initial value: 14598366, increment: 1 - Generator GEN_DECADE, current value: 14600925, initial value: 14600926, increment: 1 + SQL_SCHEMA_PREFIX = '' if act_res.is_version('<6') else 'PUBLIC.' + expected_stdout = f""" + Generator {SQL_SCHEMA_PREFIX}GEN_7FFFFFFF, current value: 2147483646, initial value: 2147483647, increment: 1 + Generator {SQL_SCHEMA_PREFIX}GEN_BA0BAB, current value: 12192682, initial value: 12192683, increment: 1 + Generator {SQL_SCHEMA_PREFIX}GEN_BADF00D, current value: 195948556, initial value: 195948557, increment: 1 + Generator {SQL_SCHEMA_PREFIX}GEN_C0FFEE, current value: 12648429, initial value: 12648430, increment: 1 + Generator {SQL_SCHEMA_PREFIX}GEN_CACA0, current value: 830623, initial value: 830624, increment: 1 + Generator {SQL_SCHEMA_PREFIX}GEN_DEC0DE, current value: 14598365, initial value: 14598366, increment: 1 + Generator {SQL_SCHEMA_PREFIX}GEN_DECADE, current value: 14600925, initial value: 14600926, increment: 1 """ + + act_res.expected_stdout = expected_stdout act_res.isql(switches = ['-q'], input = 'show sequ;', combine_output = True, io_enc = locale.getpreferredencoding()) assert act_res.clean_stdout == act_res.clean_expected_stdout act_res.reset() diff --git a/tests/bugs/core_4528_test.py b/tests/bugs/core_4528_test.py index b43c82f5..777f9c05 100644 --- a/tests/bugs/core_4528_test.py +++ b/tests/bugs/core_4528_test.py @@ -125,9 +125,19 @@ PLAN HASH (R TN NATURAL, S TN NATURAL) """ +fb6x_checked_stdout = """ + PLAN HASH ("R" "PUBLIC"."TN" NATURAL, "S" "PUBLIC"."TN" NATURAL) + PLAN HASH ("R" "PUBLIC"."TN" NATURAL, "S" "PUBLIC"."TN" NATURAL, "T" "PUBLIC"."TN" NATURAL) + PLAN HASH ("R" "PUBLIC"."TN" NATURAL, "S" "PUBLIC"."TN" NATURAL) + PLAN HASH ("R" "PUBLIC"."TN" NATURAL, "S" "PUBLIC"."TN" NATURAL, "T" "PUBLIC"."TN" NATURAL) + PLAN HASH ("R" "PUBLIC"."TN" NATURAL, "S" "PUBLIC"."TN" NATURAL, "T" "PUBLIC"."TN" NATURAL, "U" "PUBLIC"."TN" NATURAL) + PLAN HASH ("R" "PUBLIC"."TN" NATURAL, "S" "PUBLIC"."TN" NATURAL) + PLAN HASH ("R" "PUBLIC"."TN" NATURAL, "S" "PUBLIC"."TN" NATURAL) +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = fb3x_checked_stdout if act.is_version('<5') else fb5x_checked_stdout - act.execute() + act.expected_stdout = fb3x_checked_stdout if act.is_version('<5') else fb5x_checked_stdout if act.is_version('<6') else fb6x_checked_stdout + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4529_test.py b/tests/bugs/core_4529_test.py index d5f9ddc4..2426cf60 100644 --- a/tests/bugs/core_4529_test.py +++ b/tests/bugs/core_4529_test.py @@ -36,15 +36,20 @@ act = isql_act('db', test_script) -expected_stdout = """ +expected_stdout_5x = """ PLAN (T ORDER T_X_DESC) PLAN (T ORDER T_X_DESC) PLAN (T ORDER T_C_DESC) """ +expected_stdout_6x = """ + PLAN ("PUBLIC"."T" ORDER "PUBLIC"."T_X_DESC") + PLAN ("PUBLIC"."T" ORDER "PUBLIC"."T_X_DESC") + PLAN ("PUBLIC"."T" ORDER "PUBLIC"."T_C_DESC") +""" + @pytest.mark.version('>=4.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4530_test.py b/tests/bugs/core_4530_test.py index bfced314..b56dd28e 100644 --- a/tests/bugs/core_4530_test.py +++ b/tests/bugs/core_4530_test.py @@ -9,6 +9,12 @@ and thus performance JIRA: CORE-4530 FBTEST: bugs.core_4530 +NOTES: + [29.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -37,14 +43,18 @@ act = isql_act('db', test_script) -expected_stdout = """ - PLAN JOIN (X A ORDER T_PK_IDX, Z INDEX ()) - PLAN JOIN (X A ORDER T_PK_IDX, Z INDEX ()) +expected_stdout_5x = """ + PLAN JOIN (X A ORDER T_PK_IDX, Z INDEX ()) + PLAN JOIN (X A ORDER T_PK_IDX, Z INDEX ()) +""" + +expected_stdout_6x = """ + PLAN JOIN ("X" "A" ORDER "PUBLIC"."T_PK_IDX", "Z" INDEX ()) + PLAN JOIN ("X" "A" ORDER "PUBLIC"."T_PK_IDX", "Z" INDEX ()) """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4539_test.py b/tests/bugs/core_4539_test.py index b590c041..40aafc7b 100644 --- a/tests/bugs/core_4539_test.py +++ b/tests/bugs/core_4539_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4539 FBTEST: bugs.core_4539 +NOTES: + [29.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -41,13 +47,16 @@ act = isql_act('db', test_script) -expected_stdout = """ +expected_stdout_5x = """ PLAN JOIN (H NATURAL, C INDEX (COLOR_NAME)) """ +expected_stdout_6x = """ + PLAN JOIN ("H" NATURAL, "C" INDEX ("PUBLIC"."COLOR_NAME")) +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4555_test.py b/tests/bugs/core_4555_test.py index ee1531db..0289d49e 100644 --- a/tests/bugs/core_4555_test.py +++ b/tests/bugs/core_4555_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4555 FBTEST: bugs.core_4555 +NOTES: + [29.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -49,31 +55,38 @@ act = isql_act('db', test_script, substitutions=[('=.*', ''), ('line:\\s[0-9]+,', 'line: x'), ('col:\\s[0-9]+', 'col: y')]) -expected_stdout = """ +expected_stdout_5x = """ + Statement failed, SQLSTATE + unsuccessful metadata update + -CREATE EXCEPTION USER_EXCEPTION failed + -exception 1 + -DDL_EXCEPTION + -You have no right to create exceptions. Learn DDL triggers first! + -At trigger 'T_DDL' line: x col: y RDB$EXCEPTION_NAME DDL_EXCEPTION RDB$MESSAGE You have no right to create exceptions. Learn DDL triggers first! - RDB$EXCEPTION_NAME USER_EXCEPTION RDB$MESSAGE Invalid remainder found for case-2. - Records affected: 2 """ -expected_stderr = """ - Statement failed, SQLSTATE = HY000 +expected_stdout_6x = """ + Statement failed, SQLSTATE unsuccessful metadata update - -CREATE EXCEPTION USER_EXCEPTION failed + -CREATE EXCEPTION "PUBLIC"."USER_EXCEPTION" failed -exception 1 - -DDL_EXCEPTION + -"PUBLIC"."DDL_EXCEPTION" -You have no right to create exceptions. Learn DDL triggers first! - -At trigger 'T_DDL' line: 6, col: 9 + -At trigger "PUBLIC"."T_DDL" line: x col: y + RDB$EXCEPTION_NAME DDL_EXCEPTION + RDB$MESSAGE You have no right to create exceptions. Learn DDL triggers first! + RDB$EXCEPTION_NAME USER_EXCEPTION + RDB$MESSAGE Invalid remainder found for case-2. + Records affected: 2 """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4557_test.py b/tests/bugs/core_4557_test.py index c2452f52..92f4f678 100644 --- a/tests/bugs/core_4557_test.py +++ b/tests/bugs/core_4557_test.py @@ -3,56 +3,53 @@ """ ID: issue-1578 ISSUE: 1578 -TITLE: FB 3.0 crashes on EXIT (or QUIT) command if use UTF8-collation + create domain - based on it + issue SHOW DOMAIN +TITLE: FB 3.0 crashes on EXIT (or QUIT) command if use UTF8-collation + create domain based on it + issue SHOW DOMAIN DESCRIPTION: JIRA: CORE-4557 FBTEST: bugs.core_4557 +NOTES: + [29.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """ +db = db_factory() + +test_script = """ create collation name_coll for utf8 from unicode CASE INSENSITIVE; create collation nums_coll for utf8 from unicode CASE INSENSITIVE 'NUMERIC-SORT=1'; commit; create domain dm_name as varchar(80) character set utf8 collate name_coll; create domain dm_nums as varchar(20) character set utf8 collate nums_coll; commit; -""" - -db = db_factory(init=init_script) - -test_script = """ show domain; -- FB crashes if this will be uncommented exit; """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) -# version: 3.0 - -expected_stdout_1 = """ - DM_NAME DM_NUMS +expected_stdout_3x = """ + DM_NAME DM_NUMS """ -@pytest.mark.version('>=3.0,<4.0') -def test_1(act: Action): - act.expected_stdout = expected_stdout_1 - act.execute() - assert act.clean_stdout == act.clean_expected_stdout - -# version: 4.0 - -expected_stdout_2 = """ +expected_stdout_5x = """ DM_NAME DM_NUMS """ -@pytest.mark.version('>=4.0') +expected_stdout_6x = """ + PUBLIC.DM_NAME + PUBLIC.DM_NUMS +""" + +@pytest.mark.version('>=3.0') def test_2(act: Action): - act.expected_stdout = expected_stdout_2 - act.execute() + act.expected_stdout = expected_stdout_3x if act.is_version('<4') else expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4572_test.py b/tests/bugs/core_4572_test.py index b23c285b..19afb9ac 100644 --- a/tests/bugs/core_4572_test.py +++ b/tests/bugs/core_4572_test.py @@ -59,12 +59,14 @@ expected_stdout_6x = """ Statement failed, SQLSTATE = 07001 - Parameter mismatch for function FN_MULTIPLIER + Parameter mismatch for function "PUBLIC"."FN_MULTIPLIER" -Parameter A_TIMES has no default value and was not specified or was specified with DEFAULT + Statement failed, SQLSTATE = 07001 - Parameter mismatch for procedure SP_MULTIPLIER + Parameter mismatch for procedure "PUBLIC"."SP_MULTIPLIER" -Parameter A_TIMES has no default value and was not specified or was specified with DEFAULT """ + @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x diff --git a/tests/bugs/core_4585_test.py b/tests/bugs/core_4585_test.py index f07e014c..74e8eef7 100644 --- a/tests/bugs/core_4585_test.py +++ b/tests/bugs/core_4585_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4585 FBTEST: bugs.core_4585 +NOTES: + [29.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -25,23 +31,25 @@ select * from test; """ +substitutions = [('[ \t]+', ' ')] act = isql_act('db', test_script) -expected_stdout = """ +expected_stdout_5x = """ + Statement failed, SQLSTATE = 23000 + Operation violates CHECK constraint TEST_X_CHK on view or table TEST + -At trigger 'CHECK_1' X 1 """ -expected_stderr = """ +expected_stdout_6x = """ Statement failed, SQLSTATE = 23000 - Operation violates CHECK constraint TEST_X_CHK on view or table TEST - -At trigger 'CHECK_1' + Operation violates CHECK constraint "TEST_X_CHK" on view or table "PUBLIC"."TEST" + -At trigger "PUBLIC"."CHECK_1" + X 1 """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4599_test.py b/tests/bugs/core_4599_test.py index 71759594..377b7569 100644 --- a/tests/bugs/core_4599_test.py +++ b/tests/bugs/core_4599_test.py @@ -835,6 +835,7 @@ OVERLAY_UTF8_TO_ASCII_LO 1 """ +@pytest.mark.intl @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_4604_test.py b/tests/bugs/core_4604_test.py index 12e6d48f..78ff008f 100644 --- a/tests/bugs/core_4604_test.py +++ b/tests/bugs/core_4604_test.py @@ -91,6 +91,7 @@ STR_SIZE 36 """ +@pytest.mark.es_eds @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_4653_test.py b/tests/bugs/core_4653_test.py index 9c983b7e..de662f2f 100644 --- a/tests/bugs/core_4653_test.py +++ b/tests/bugs/core_4653_test.py @@ -7,6 +7,13 @@ DESCRIPTION: JIRA: CORE-4653 FBTEST: bugs.core_4653 +NOTES: + [30.06.2025] pzotov + Part of call stack ('At procedure line X col Y') must be supressed because its length is limited to 1024 characters + and number of lines (together with interrupting marker '...') depends on length of procedure name that is called recursively. + Difference of transactions before and after call to recursive SP must be checked to be sure that there was no crash. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -51,57 +58,30 @@ -- Old stderr: -- Statement failed, SQLSTATE = HY001 -- Stack overflow. The resource requirements of the runtime stack have exceeded the memory available to it. + set list on; + set term ^; + execute block as + begin + rdb$set_context('USER_TRANSACTION', 'INIT_TX', current_transaction); + end ^ + set term ;^ select * from p01(1); + select current_transaction - cast( rdb$get_context('USER_TRANSACTION', 'INIT_TX') as int) as tx_diff from rdb$database; """ -act = isql_act('db', test_script, substitutions=[('=.*', ''), ('line.*', ''), ('col.*', '')]) +substitutions = [ ('^((?!(SQLSTATE|Too many concurrent executions|TX_DIFF)).)*$', ''), ('[ \t]+', ' ') ] +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ - Z - ============ -""" - -expected_stderr = """ Statement failed, SQLSTATE = 54001 Too many concurrent executions of the same request - -At procedure 'P03' line: 3, col: 3 - At procedure 'P02' line: 3, col: 3 - At procedure 'P03' line: 3, col: 3 - At procedure 'P02' line: 3, col: 3 - At procedure 'P03' line: 3, col: 3 - At procedure 'P02' line: 3, col: 3 - At procedure 'P03' line: 3, col: 3 - At procedure 'P02' line: 3, col: 3 - At procedure 'P03' line: 3, col: 3 - At procedure 'P02' line: 3, col: 3 - At procedure 'P03' line: 3, col: 3 - At procedure 'P02' line: 3, col: 3 - At procedure 'P03' line: 3, col: 3 - At procedure 'P02' line: 3, col: 3 - At procedure 'P03' line: 3, col: 3 - At procedure 'P02' line: 3, col: 3 - At procedure 'P03' line: 3, col: 3 - At procedure 'P02' line: 3, col: 3 - At procedure 'P03' line: 3, col: 3 - At procedure 'P02' line: 3, col: 3 - At procedure 'P03' line: 3, col: 3 - At procedure 'P02' line: 3, col: 3 - At procedure 'P03' line: 3, col: 3 - At procedure 'P02' line: 3, col: 3 - At procedure 'P03' line: 3, col: 3 - At procedure 'P02' line: 3, col: 3 - At procedure 'P03' line: 3, col: 3 - At procedure 'P02' line: 3, col: 3 - At procedure 'P03' line: 3, col: 3 - At p... + TX_DIFF 0 """ @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4665_test.py b/tests/bugs/core_4665_test.py index 54d62973..49099f5d 100644 --- a/tests/bugs/core_4665_test.py +++ b/tests/bugs/core_4665_test.py @@ -3,11 +3,15 @@ """ ID: issue-1602 ISSUE: 1602 -TITLE: Wrong result when use "where STARTING WITH <:value> ORDER BY " - and field_C is leading part of compound index key: { field_C, field_N } +TITLE: Wrong result when use "where STARTING WITH <:value> ORDER BY " and field_C is leading part of compound index key: { field_C, field_N } DESCRIPTION: JIRA: CORE-4665 FBTEST: bugs.core_4665 +NOTES: + [30.06.2025] pzotov + Removed 'set plan on' because this test must check only result of query (data). + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -16,6 +20,7 @@ db = db_factory() test_script = """ + set list on; recreate table test (id int, unit varchar(10), y int, z int); commit; delete from test; @@ -34,18 +39,15 @@ create index test_unit_y_asc on test( unit, y ); commit; - set plan on; - - select id, t.unit, t.y, t.z + select 'point-1' msg, id, t.unit, t.y, t.z from test t where t.unit||'' starting with 'foo' order by t.y||''; - select id, t.unit, t.y, t.z + select 'point-2' msg, id, t.unit, t.y, t.z from test t where t.unit starting with 'foo' order by t.y; - set plan off; commit; drop index test_unit_y_asc; @@ -54,62 +56,141 @@ create descending index test_unit_y_desc on test( unit, y); commit; - set plan on; - select id, t.unit, t.y, t.z + select 'point-3' msg, id, t.unit, t.y, t.z from test t where t.unit starting with 'foo' order by t.y; - set plan off; """ -act = isql_act('db', test_script, substitutions=[('=.*', '')]) +substitutions = [ ('[ \t]+', ' ') ] +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ - PLAN SORT (T NATURAL) - - ID UNIT Y Z - ============ ========== ============ ============ - 8 fooo 1 22222 - 7 fooo 11 2222 - 6 fooo 111 222 - 5 fooo 1111 22 - 4 foo 3333 17682 - 3 foo 5555 21822 - 2 foo 8888 22520 - 1 foo 9999 23636 - - - PLAN SORT (T INDEX (TEST_UNIT_Y_ASC)) - - ID UNIT Y Z - ============ ========== ============ ============ - 8 fooo 1 22222 - 7 fooo 11 2222 - 6 fooo 111 222 - 5 fooo 1111 22 - 4 foo 3333 17682 - 3 foo 5555 21822 - 2 foo 8888 22520 - 1 foo 9999 23636 - - - PLAN SORT (T INDEX (TEST_UNIT_Y_DESC)) - - ID UNIT Y Z - ============ ========== ============ ============ - 8 fooo 1 22222 - 7 fooo 11 2222 - 6 fooo 111 222 - 5 fooo 1111 22 - 4 foo 3333 17682 - 3 foo 5555 21822 - 2 foo 8888 22520 - 1 foo 9999 23636 + MSG point-1 + ID 8 + UNIT fooo + Y 1 + Z 22222 + MSG point-1 + ID 7 + UNIT fooo + Y 11 + Z 2222 + MSG point-1 + ID 6 + UNIT fooo + Y 111 + Z 222 + MSG point-1 + ID 5 + UNIT fooo + Y 1111 + Z 22 + MSG point-1 + ID 4 + UNIT foo + Y 3333 + Z 17682 + MSG point-1 + ID 3 + UNIT foo + Y 5555 + Z 21822 + MSG point-1 + ID 2 + UNIT foo + Y 8888 + Z 22520 + MSG point-1 + ID 1 + UNIT foo + Y 9999 + Z 23636 + MSG point-2 + ID 8 + UNIT fooo + Y 1 + Z 22222 + MSG point-2 + ID 7 + UNIT fooo + Y 11 + Z 2222 + MSG point-2 + ID 6 + UNIT fooo + Y 111 + Z 222 + MSG point-2 + ID 5 + UNIT fooo + Y 1111 + Z 22 + MSG point-2 + ID 4 + UNIT foo + Y 3333 + Z 17682 + MSG point-2 + ID 3 + UNIT foo + Y 5555 + Z 21822 + MSG point-2 + ID 2 + UNIT foo + Y 8888 + Z 22520 + MSG point-2 + ID 1 + UNIT foo + Y 9999 + Z 23636 + MSG point-3 + ID 8 + UNIT fooo + Y 1 + Z 22222 + MSG point-3 + ID 7 + UNIT fooo + Y 11 + Z 2222 + MSG point-3 + ID 6 + UNIT fooo + Y 111 + Z 222 + MSG point-3 + ID 5 + UNIT fooo + Y 1111 + Z 22 + MSG point-3 + ID 4 + UNIT foo + Y 3333 + Z 17682 + MSG point-3 + ID 3 + UNIT foo + Y 5555 + Z 21822 + MSG point-3 + ID 2 + UNIT foo + Y 8888 + Z 22520 + MSG point-3 + ID 1 + UNIT foo + Y 9999 + Z 23636 """ @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4673_test.py b/tests/bugs/core_4673_test.py index f9a4a43e..a8b5d446 100644 --- a/tests/bugs/core_4673_test.py +++ b/tests/bugs/core_4673_test.py @@ -7,6 +7,10 @@ DESCRIPTION: JIRA: CORE-4673 FBTEST: bugs.core_4673 +NOTES: + [30.06.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -47,9 +51,10 @@ select count(*) from tc where '' || lpad('' || tc.z_expr, 10, 0) between '0000000302' and '0000000302'; """ -act = isql_act('db', test_script) +substitutions = [ ('[ \t]+', ' ') ] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ +expected_stdout_5x = """ PLAN (TC INDEX (TC_LPAD_Z_NOEX), TC NATURAL, TC INDEX (TC_LPAD_Z_EXPR), TC NATURAL) CHECK_CNT 1 CHECK_CNT 1 @@ -57,9 +62,16 @@ CHECK_CNT 1 """ +expected_stdout_6x = """ + PLAN ("PUBLIC"."TC" INDEX ("PUBLIC"."TC_LPAD_Z_NOEX"), "PUBLIC"."TC" NATURAL, "PUBLIC"."TC" INDEX ("PUBLIC"."TC_LPAD_Z_EXPR"), "PUBLIC"."TC" NATURAL) + CHECK_CNT 1 + CHECK_CNT 1 + CHECK_CNT 1 + CHECK_CNT 1 +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4694_test.py b/tests/bugs/core_4694_test.py index 5cb500f9..f59d77c3 100644 --- a/tests/bugs/core_4694_test.py +++ b/tests/bugs/core_4694_test.py @@ -3,8 +3,7 @@ """ ID: issue-5002 ISSUE: 5002 -TITLE: "Column unknown" error while preparing a recursive query if the recursive part - contains ALIASED datasource in the join with anchor table +TITLE: "Column unknown" error while preparing a recursive query if the recursive part contains ALIASED datasource in the join with anchor table DESCRIPTION: JIRA: CORE-4694 FBTEST: bugs.core_4694 @@ -22,8 +21,6 @@ ); commit; - set planonly; - with recursive r as ( select c.xcall_id @@ -46,12 +43,11 @@ act = isql_act('db', test_script) expected_stdout = """ - PLAN (R C NATURAL, R C NATURAL) """ @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4702_test.py b/tests/bugs/core_4702_test.py index bc5860eb..ef68ce36 100644 --- a/tests/bugs/core_4702_test.py +++ b/tests/bugs/core_4702_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4702 FBTEST: bugs.core_4702 +NOTES: + [30.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -90,13 +96,16 @@ act = isql_act('db', test_script) -expected_stdout = """ +expected_stdout_5x = """ PLAN JOIN (TRIAL INDEX (IDX_BYDATE), PRIZE INDEX (PK_PRIZE), TRIAL_LINE INDEX (FK_TRIAL_LINE_TRIAL)) """ +expected_stdout_6x = """ + PLAN JOIN ("PUBLIC"."TRIAL" INDEX ("PUBLIC"."IDX_BYDATE"), "PUBLIC"."PRIZE" INDEX ("PUBLIC"."PK_PRIZE"), "PUBLIC"."TRIAL_LINE" INDEX ("PUBLIC"."FK_TRIAL_LINE_TRIAL")) +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4706_test.py b/tests/bugs/core_4706_test.py index baaa4980..bdf23d27 100644 --- a/tests/bugs/core_4706_test.py +++ b/tests/bugs/core_4706_test.py @@ -7,32 +7,67 @@ DESCRIPTION: JIRA: CORE-4706 FBTEST: bugs.core_4706 +NOTES: + [04.07.2025] pzotov + Added check for column with maximal possible size = 63 characters. + Added case when column headers are in non-ascii form. + Blobs ID output is suppressed. + Increased min_version to 4.0 + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214 """ import pytest from firebird.qa import * -db = db_factory() +db = db_factory(charset = 'utf8') test_script = """ set blob all; - select cast('a' as blob) a, 1, cast('a' as blob) x2345678901234567890, 2 from rdb$database; + -- select cast('a' as blob) a, 1, cast('a' as blob) x2345678901234567890, 2 from rdb$database; + select + cast('a' as blob) a + ,1 + ,cast('b' as blob) x2345678901234567890 + ,2 + ,cast('c' as blob) x23456789012345678901234567890123456789012345678901234567890123 + ,3 + from rdb$database; + + select + cast('a' as blob) "α" + ,1 + ,cast('b' as blob) "έαισορροπίαθαείναικά" + ,2 + ,cast('c' as blob) "έαισορροπίαθαείναικάτωαπότομηδέαισορροπίαθαείναικάτωαπότομηδέα" + ,3 + from rdb$database; """ -act = isql_act('db', test_script, substitutions=[('=.*', '')]) +substitutions = [ ('\\d:\\d', 'x:x'), ('={3,}', '') ] +act = isql_act('db', substitutions = substitutions) expected_stdout = """ - A CONSTANT X2345678901234567890 CONSTANT - 0:2 1 0:1 2 -A: -a -X2345678901234567890: -a + A CONSTANT X2345678901234567890 CONSTANT X23456789012345678901234567890123456789012345678901234567890123 CONSTANT + x:x 1 x:x 2 x:x 3 + A: + a + X2345678901234567890: + b + X23456789012345678901234567890123456789012345678901234567890123: + c + α CONSTANT έαισορροπίαθαείναικά CONSTANT έαισορροπίαθαείναικάτωαπότομηδέαισορροπίαθαείναικάτωαπότομηδέα CONSTANT + x:x 1 x:x 2 x:x 3 + α: + a + έαισορροπίαθαείναικά: + b + έαισορροπίαθαείναικάτωαπότομηδέαισορροπίαθαείναικάτωαπότομηδέα: + c """ -@pytest.mark.version('>=3.0') +@pytest.mark.intl +@pytest.mark.version('>=4.0') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.isql(switches = ['-q'], charset = 'utf8', input = test_script, combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4707_test.py b/tests/bugs/core_4707_test.py index fbf3be60..d99cb1b0 100644 --- a/tests/bugs/core_4707_test.py +++ b/tests/bugs/core_4707_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4707 FBTEST: bugs.core_4707 +NOTES: + [30.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -43,32 +49,14 @@ db = db_factory(init=init_script) -act = python_act('db', substitutions=[('[\\d]{2}:[\\d]{2}:[\\d]{2}.[\\d]{2}', ''), - ('Relation [\\d]{3,4}', 'Relation')]) - -expected_stdout = """ - ISQL_MSG Starting EB with infinite pause. - 08:37:01.14 Validation started - 08:37:01.15 Relation 128 (TEST1) - 08:37:02.15 Acquire relation lock failed - 08:37:02.15 Relation 128 (TEST1) : 1 ERRORS found - 08:37:02.15 Relation 129 (TEST2) - 08:37:02.15 process pointer page 0 of 1 - 08:37:02.15 Index 1 (TEST2_PK) - 08:37:02.15 Index 2 (TEST2_S) - 08:37:02.15 Index 3 (TEST2_C) - 08:37:02.15 Index 4 (TEST2_T) - 08:37:02.17 Relation 129 (TEST2) is ok - 08:37:02.17 Relation 130 (TEST3) - 08:37:03.17 Acquire relation lock failed - 08:37:03.17 Relation 130 (TEST3) : 1 ERRORS found - 08:37:03.17 Validation finished -""" +substitutions=[ ('\\d{2}:\\d{2}:\\d{2}.\\d{2}', ''), ('Relation \\d{3,4}', 'Relation') ] +act = python_act('db', substitutions = substitutions) hang_script_file = temp_file('hang_script.sql') hang_output = temp_file('hang_script.out') -@pytest.mark.version('>=2.5.5') +@pytest.mark.es_eds +@pytest.mark.version('>=3.0') def test_1(act: Action, hang_script_file: Path, hang_output: Path, capsys, request): # Fializer for FB4 def drop_connections(): @@ -103,7 +91,7 @@ def drop_connections(): as user '{act.db.user}' password '{act.db.password}' role 'TMP$R4707' -- this will force to create new attachment, and its Tx will be paused on INFINITE time. ; - when any do begin end + -- COMMENTED 30.03.2025: we must know if some error occurred during infinite wait! --> when any do begin end end ^ set term ;^ select 'EB with pause finished.' as msg_2 from rdb$database; @@ -124,6 +112,46 @@ def drop_connections(): # print(hang_output.read_text()) print(act.stdout) - act.expected_stdout = expected_stdout + + + expected_stdout_5x = """ + ISQL_MSG Starting EB with infinite pause. + Validation started + Relation (TEST1) + Acquire relation lock failed + Relation (TEST1) : 1 ERRORS found + Relation (TEST2) + process pointer page 0 of 1 + Index 1 (TEST2_PK) + Index 2 (TEST2_S) + Index 3 (TEST2_C) + Index 4 (TEST2_T) + Relation (TEST2) is ok + Relation (TEST3) + Acquire relation lock failed + Relation (TEST3) : 1 ERRORS found + Validation finished + """ + + expected_stdout_6x = """ + ISQL_MSG Starting EB with infinite pause. + Validation started + Relation ("PUBLIC"."TEST1") + Acquire relation lock failed + Relation ("PUBLIC"."TEST1") : 1 ERRORS found + Relation ("PUBLIC"."TEST2") + process pointer page 0 of 1 + Index 1 ("PUBLIC"."TEST2_PK") + Index 2 ("PUBLIC"."TEST2_S") + Index 3 ("PUBLIC"."TEST2_C") + Index 4 ("PUBLIC"."TEST2_T") + Relation ("PUBLIC"."TEST2") is ok + Relation ("PUBLIC"."TEST3") + Acquire relation lock failed + Relation ("PUBLIC"."TEST3") : 1 ERRORS found + Validation finished + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4725_test.py b/tests/bugs/core_4725_test.py index 17dfe52a..6075b987 100644 --- a/tests/bugs/core_4725_test.py +++ b/tests/bugs/core_4725_test.py @@ -7,6 +7,18 @@ DESCRIPTION: JIRA: CORE-4725 FBTEST: bugs.core_4725 +NOTES: + [07.08.2024] pzotov + Splitted expected* text because system triggers now are created in C++/GDML code + See https://github.com/FirebirdSQL/firebird/pull/8202 + Commit (05-aug-2024 13:45): + https://github.com/FirebirdSQL/firebird/commit/0cc8de396a3c2bbe13b161ecbfffa8055e7b4929 + + [30.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -179,71 +191,105 @@ act = isql_act('db', test_script) -expected_stdout = """ +expected_stdout_5x = """ + Statement failed, SQLSTATE = 27000 + unsuccessful metadata update + -ALTER TABLE TEST00 failed + -action cancelled by trigger (2) to preserve data integrity + -Column used in a PRIMARY constraint must be NOT NULL. INFO_00 After try to drop NN on FIELD, NN was added by ALTER SET NOT NULL X INTEGER Not Null CONSTRAINT T_PK: - Primary key (X) - + Primary key (X) + Statement failed, SQLSTATE = 27000 + unsuccessful metadata update + -ALTER TABLE TEST01 failed + -action cancelled by trigger (2) to preserve data integrity + -Column used in a PRIMARY constraint must be NOT NULL. INFO_01 After try to drop NN on FIELD, NN was added directly in CREATE TABLE ( not null) X (DM_01) INTEGER Not Null CONSTRAINT TEST01_PK: - Primary key (X) - + Primary key (X) INFO_02 After try to drop NN on FIELD, NN was inherited from DOMAIN X (DM_02) INTEGER Not Null CONSTRAINT TEST02_PK: - Primary key (X) - + Primary key (X) + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -ALTER DOMAIN DM_03 failed + -Domain used in the PRIMARY KEY constraint of table TEST03 must be NOT NULL INFO_03 After try to drop NN on DOMAIN but dependent table exists DM_03 INTEGER Not Null X (DM_03) INTEGER Not Null CONSTRAINT TEST03_PK: - Primary key (X) - + Primary key (X) INFO_04 After try to drop NN on FIELD based on not-null domain, but NN was also specified in the field DDL X (DM_04) INTEGER Not Null CONSTRAINT TEST04_PK: - Primary key (X) - + Primary key (X) + Statement failed, SQLSTATE = 22006 + unsuccessful metadata update + -Cannot make field X of table TEST05 NOT NULL because there are NULLs present INFO_05 After try to set NN on DOMAIN when at least one table exists with NULL in its data DM_05 INTEGER Nullable - INFO_06 After try to set NN on DOMAIN when NO table exists with NULL in its data DM_06 INTEGER Not Null + Statement failed, SQLSTATE = 23000 + validation error for column "TEST06"."X", value "*** null ***" """ -expected_stderr = """ - Statement failed, SQLSTATE = 27000 +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 unsuccessful metadata update - -ALTER TABLE TEST00 failed - -action cancelled by trigger (2) to preserve data integrity + -ALTER TABLE "PUBLIC"."TEST00" failed -Column used in a PRIMARY constraint must be NOT NULL. - - Statement failed, SQLSTATE = 27000 + INFO_00 After try to drop NN on FIELD, NN was added by ALTER SET NOT NULL + Table: PUBLIC.TEST00 + X INTEGER Not Null + CONSTRAINT T_PK: + Primary key (X) + Statement failed, SQLSTATE = 42000 unsuccessful metadata update - -ALTER TABLE TEST01 failed - -action cancelled by trigger (2) to preserve data integrity + -ALTER TABLE "PUBLIC"."TEST01" failed -Column used in a PRIMARY constraint must be NOT NULL. - + INFO_01 After try to drop NN on FIELD, NN was added directly in CREATE TABLE ( not null) + Table: PUBLIC.TEST01 + X (PUBLIC.DM_01) INTEGER Not Null + CONSTRAINT TEST01_PK: + Primary key (X) + INFO_02 After try to drop NN on FIELD, NN was inherited from DOMAIN + Table: PUBLIC.TEST02 + X (PUBLIC.DM_02) INTEGER Not Null + CONSTRAINT TEST02_PK: + Primary key (X) Statement failed, SQLSTATE = 42000 unsuccessful metadata update - -ALTER DOMAIN DM_03 failed - -Domain used in the PRIMARY KEY constraint of table TEST03 must be NOT NULL - + -ALTER DOMAIN "PUBLIC"."DM_03" failed + -Domain used in the PRIMARY KEY constraint of table "PUBLIC"."TEST03" must be NOT NULL + INFO_03 After try to drop NN on DOMAIN but dependent table exists + PUBLIC.DM_03 INTEGER Not Null + Table: PUBLIC.TEST03 + X (PUBLIC.DM_03) INTEGER Not Null + CONSTRAINT TEST03_PK: + Primary key (X) + INFO_04 After try to drop NN on FIELD based on not-null domain, but NN was also specified in the field DDL + Table: PUBLIC.TEST04 + X (PUBLIC.DM_04) INTEGER Not Null + CONSTRAINT TEST04_PK: + Primary key (X) Statement failed, SQLSTATE = 22006 unsuccessful metadata update - -Cannot make field X of table TEST05 NOT NULL because there are NULLs present - + -Cannot make field "X" of table "PUBLIC"."TEST05" NOT NULL because there are NULLs present + INFO_05 After try to set NN on DOMAIN when at least one table exists with NULL in its data + PUBLIC.DM_05 INTEGER Nullable + INFO_06 After try to set NN on DOMAIN when NO table exists with NULL in its data + PUBLIC.DM_06 INTEGER Not Null Statement failed, SQLSTATE = 23000 - validation error for column "TEST06"."X", value "*** null ***" + validation error for column "PUBLIC"."TEST06"."X", value "*** null ***" """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4731_test.py b/tests/bugs/core_4731_test.py index c439c943..673b65fd 100644 --- a/tests/bugs/core_4731_test.py +++ b/tests/bugs/core_4731_test.py @@ -40,11 +40,15 @@ 8) select expressions that were PASSED without exceptions. NOTES: [18.02.2020] pzotov - REFACTORED: most of initial code was moved into $files_location/core_4731.sql; changed test_type to 'Python'. + REFACTORED: most of initial code was moved into $files_location/core_4731.sql; changed test_type to 'Python'. [04.03.2023] pzotov - Separated code for FB-4x because it allows now statement 'delete from RDB$BACKUP_HISTORY ...' - Checked on 5.0.0.970, 4.0.3.2904, 3.0.11.33665. - + Separated code for FB-4x because it allows now statement 'delete from RDB$BACKUP_HISTORY ...' + Checked on 5.0.0.970, 4.0.3.2904, 3.0.11.33665. + [08.08.2024] pzotov + Separated code for FB-6x because it became differ after implemented GH-8202 + https://github.com/FirebirdSQL/firebird/commit/0cc8de396a3c2bbe13b161ecbfffa8055e7b4929 + (05-aug-2024 13:45, "Regenerate system triggers improving formatting and constant names") + Checked on 6.0.0.419-3505a5e JIRA: CORE-4731 FBTEST: bugs.core_4731 """ @@ -129,6 +133,30 @@ -- gdscode list for blocked: 335544926 """ +fb6x_expected_out = """ + -- Executed with role: NONE. Expressions that passes WITHOUT errors: + -- count_of_passed: 0 + -- gdscode list for blocked: 335544926 + -- Executed with role: RDB$ADMIN. Expressions that passes WITHOUT errors: + -- count_of_passed: 14 + VULNERABLE_EXPR delete from RDB$BACKUP_HISTORY t rows 1 returning t.rdb$db_key; -- length of returned rdb$dbkey=8 + VULNERABLE_EXPR insert into RDB$BACKUP_HISTORY(RDB$BACKUP_ID , RDB$TIMESTAMP , RDB$BACKUP_LEVEL , RDB$GUID , RDB$SCN , RDB$FILE_NAME) values(null, null, null, null, null, null) returning rdb$db_key; -- length of returned rdb$dbkey=8 + VULNERABLE_EXPR delete from RDB$DB_CREATORS t rows 1 returning t.rdb$db_key; -- length of returned rdb$dbkey=8 + VULNERABLE_EXPR insert into RDB$DB_CREATORS(RDB$USER , RDB$USER_TYPE) values(null, null) returning rdb$db_key; -- length of returned rdb$dbkey=8 + VULNERABLE_EXPR update RDB$DB_CREATORS t set t.RDB$USER = 'C' rows 1 returning t.rdb$db_key; -- length of returned rdb$dbkey=8 + VULNERABLE_EXPR update RDB$DB_CREATORS t set t.RDB$USER = null rows 1 returning t.rdb$db_key; -- length of returned rdb$dbkey=8 + VULNERABLE_EXPR update RDB$DB_CREATORS t set t.RDB$USER_TYPE = 32767 rows 1 returning t.rdb$db_key; -- length of returned rdb$dbkey=8 + VULNERABLE_EXPR update RDB$DB_CREATORS t set t.RDB$USER_TYPE = null rows 1 returning t.rdb$db_key; -- length of returned rdb$dbkey=8 + VULNERABLE_EXPR update RDB$FUNCTIONS t set t.RDB$FUNCTION_SOURCE = null where coalesce(rdb$system_flag,0)=0 rows 1 returning t.rdb$db_key; -- length of returned rdb$dbkey=8 + VULNERABLE_EXPR update RDB$PACKAGES t set t.RDB$PACKAGE_BODY_SOURCE = null where coalesce(rdb$system_flag,0)=0 rows 1 returning t.rdb$db_key; -- length of returned rdb$dbkey=8 + VULNERABLE_EXPR update RDB$PACKAGES t set t.RDB$PACKAGE_HEADER_SOURCE = null where coalesce(rdb$system_flag,0)=0 rows 1 returning t.rdb$db_key; -- length of returned rdb$dbkey=8 + VULNERABLE_EXPR update RDB$PROCEDURES t set t.RDB$PROCEDURE_SOURCE = null where coalesce(rdb$system_flag,0)=0 rows 1 returning t.rdb$db_key; -- length of returned rdb$dbkey=8 + VULNERABLE_EXPR update RDB$RELATIONS t set t.RDB$VIEW_SOURCE = null where coalesce(rdb$system_flag,0)=0 rows 1 returning t.rdb$db_key; -- length of returned rdb$dbkey=8 + VULNERABLE_EXPR update RDB$TRIGGERS t set t.RDB$TRIGGER_SOURCE = null where coalesce(rdb$system_flag,0)=0 rows 1 returning t.rdb$db_key; -- length of returned rdb$dbkey=8 + -- gdscode list for blocked: 335544926 + +""" + @pytest.mark.version('>=3.0') def test_1(act: Action, dba_privileged_user: User, non_privileged_user: User, capsys): # Run prepare script @@ -225,6 +253,6 @@ def test_1(act: Action, dba_privileged_user: User, non_privileged_user: User, ca commit; """ - act.expected_stdout = fb3x_expected_out if act.is_version('<4') else fb4x_expected_out + act.expected_stdout = fb3x_expected_out if act.is_version('<4') else fb4x_expected_out if act.is_version('<6') else fb6x_expected_out act.isql(switches=['-q'], input=test_script, combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4733_test.py b/tests/bugs/core_4733_test.py index e9def6c2..d92eb548 100644 --- a/tests/bugs/core_4733_test.py +++ b/tests/bugs/core_4733_test.py @@ -9,6 +9,12 @@ DESCRIPTION: JIRA: CORE-4733 FBTEST: bugs.core_4733 +NOTES: + [30.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -64,40 +70,56 @@ act = isql_act('db', test_script) -expected_stdout = """ +expected_stdout_5x = """ + Statement failed, SQLSTATE = 22006 + unsuccessful metadata update + -Cannot make field NUM of table TEST NOT NULL because there are NULLs present + -Cannot make field DTS of table TEST NOT NULL because there are NULLs present + -Cannot make field STR of table TEST NOT NULL because there are NULLs present + -Cannot make field BOO of table TEST NOT NULL because there are NULLs present NUM INTEGER Nullable DTS TIMESTAMP Nullable STR VARCHAR(10) CHARACTER SET UTF8 Nullable BOO BOOLEAN Nullable - NUM DTS STR BOO - - + Statement failed, SQLSTATE = 23000 + validation error for column "TEST"."NUM", value "*** null ***" NUM (DM_NN_INT) INTEGER Not Null DTS (DM_NN_DTS) TIMESTAMP Not Null STR (DM_NN_UTF) VARCHAR(10) CHARACTER SET UTF8 Not Null BOO (DM_NN_BOO) BOOLEAN Not Null """ -expected_stderr = """ +expected_stdout_6x = """ Statement failed, SQLSTATE = 22006 unsuccessful metadata update - -Cannot make field NUM of table TEST NOT NULL because there are NULLs present - -Cannot make field DTS of table TEST NOT NULL because there are NULLs present - -Cannot make field STR of table TEST NOT NULL because there are NULLs present - -Cannot make field BOO of table TEST NOT NULL because there are NULLs present + -Cannot make field "NUM" of table "PUBLIC"."TEST" NOT NULL because there are NULLs present + -Cannot make field "DTS" of table "PUBLIC"."TEST" NOT NULL because there are NULLs present + -Cannot make field "STR" of table "PUBLIC"."TEST" NOT NULL because there are NULLs present + -Cannot make field "BOO" of table "PUBLIC"."TEST" NOT NULL because there are NULLs present + Table: PUBLIC.TEST + NUM INTEGER Nullable + DTS TIMESTAMP Nullable + STR VARCHAR(10) CHARACTER SET SYSTEM.UTF8 Nullable + BOO BOOLEAN Nullable + NUM + DTS + STR + BOO Statement failed, SQLSTATE = 23000 - validation error for column "TEST"."NUM", value "*** null ***" + validation error for column "PUBLIC"."TEST"."NUM", value "*** null ***" + Table: PUBLIC.TEST + NUM (PUBLIC.DM_NN_INT) INTEGER Not Null + DTS (PUBLIC.DM_NN_DTS) TIMESTAMP Not Null + STR (PUBLIC.DM_NN_UTF) VARCHAR(10) CHARACTER SET SYSTEM.UTF8 Not Null + BOO (PUBLIC.DM_NN_BOO) BOOLEAN Not Null """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4735_test.py b/tests/bugs/core_4735_test.py index e4bbc1c4..8d224744 100644 --- a/tests/bugs/core_4735_test.py +++ b/tests/bugs/core_4735_test.py @@ -3,15 +3,20 @@ """ ID: issue-5041 ISSUE: 5041 -TITLE: Expression 'where bool_field IS true | false' should also use index as - 'where bool_field = true | false' (if such index exists) +TITLE: Expression 'where bool_field IS true | false' should also use index as 'where bool_field = true | false' (if such index exists) DESCRIPTION: -NOTES: -[28.01.2019] - Changed expected PLAN of execution after dimitr's letter 28.01.2019 17:28: - 'is NOT ' and 'is distinct from ' should use PLAN NATURAL. JIRA: CORE-4735 FBTEST: bugs.core_4735 +NOTES: + [28.01.2019] pzotov + Changed expected PLAN of execution after dimitr's letter 28.01.2019 17:28: + 'is NOT ' and 'is distinct from ' should use PLAN NATURAL. + + [30.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -36,7 +41,7 @@ act = isql_act('db', test_script) -expected_stdout = """ +expected_stdout_5x = """ PLAN (TEST INDEX (TEST_X)) PLAN (TEST INDEX (TEST_X)) PLAN (TEST INDEX (TEST_X)) @@ -47,9 +52,19 @@ PLAN (TEST NATURAL) """ +expected_stdout_6x = """ + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_X")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_X")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_X")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_X")) + PLAN ("PUBLIC"."TEST" NATURAL) + PLAN ("PUBLIC"."TEST" NATURAL) + PLAN ("PUBLIC"."TEST" NATURAL) + PLAN ("PUBLIC"."TEST" NATURAL) +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4738_test.py b/tests/bugs/core_4738_test.py index fa49c324..5b30cda7 100644 --- a/tests/bugs/core_4738_test.py +++ b/tests/bugs/core_4738_test.py @@ -3,11 +3,18 @@ """ ID: issue-5043 ISSUE: 5043 -TITLE: Command "Alter table alter type " does not work: - "BLR syntax error: expected valid BLR code at offset 15, encountered 255" +TITLE: Command "Alter table alter type " does not work: "BLR syntax error: expected valid BLR code ..." DESCRIPTION: JIRA: CORE-4738 FBTEST: bugs.core_4738 +NOTES: + [30.06.2025] pzotov + This issue caused by regression when core-4733 ( https://github.com/FirebirdSQL/firebird/issues/5039 ) was fixed. + Commit for core-4733 (to check regression): http://sourceforge.net/p/firebird/code/61241 2015-04-05 02:24:40 +0000 + Confirmed bug on 3.0.0.31771. + + Replaced 'SHOW TABLE' command with qery to RDB tables that displays field_type and field_length instead. + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -16,27 +23,56 @@ db = db_factory() test_script = """ - create domain dm_id int; + set list on; + create domain dm_id bigint; + commit; + + create table test(num smallint); commit; - create table test(num int); + create view v_info as + select + rf.rdb$field_name fld_name + ,f.rdb$field_type fld_type + ,f.rdb$field_length fld_length + ,f.rdb$field_scale fld_scale + from rdb$relation_fields rf + left join rdb$fields f on rf.rdb$field_source = f.rdb$field_name + where rf.rdb$relation_name = 'TEST'; commit; + select 'point-1' msg, v.* from v_info v; + alter table test alter num type dm_id; commit; - show table test; + select 'point-2' msg, v.* from v_info v; + + insert into test(num) values(-2147483648); + select * from test; """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ - NUM (DM_ID) INTEGER Nullable + MSG point-1 + FLD_NAME NUM + FLD_TYPE 7 + FLD_LENGTH 2 + FLD_SCALE 0 + + MSG point-2 + FLD_NAME NUM + FLD_TYPE 16 + FLD_LENGTH 8 + FLD_SCALE 0 + + NUM -2147483648 """ @pytest.mark.version('>=3') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4739_test.py b/tests/bugs/core_4739_test.py index 90a330f0..98238d77 100644 --- a/tests/bugs/core_4739_test.py +++ b/tests/bugs/core_4739_test.py @@ -63,6 +63,7 @@ Records affected: 0 """ +@pytest.mark.intl @pytest.mark.version('>=4.0') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_4743_test.py b/tests/bugs/core_4743_test.py index 6c28ed6d..995a5d69 100644 --- a/tests/bugs/core_4743_test.py +++ b/tests/bugs/core_4743_test.py @@ -167,6 +167,7 @@ privilege:exec : YES """ +@pytest.mark.intl @pytest.mark.version('>=4.0') def test_1(act: Action, non_acii_user: User, test_role: Role, capsys): act.isql(switches=['-b', '-q'], input=ddl_script) diff --git a/tests/bugs/core_4744_test.py b/tests/bugs/core_4744_test.py index 0b40461d..7c3cce69 100644 --- a/tests/bugs/core_4744_test.py +++ b/tests/bugs/core_4744_test.py @@ -3,11 +3,16 @@ """ ID: issue-5049 ISSUE: 5049 -TITLE: ALTER DATABASE SET DEFAULT CHARACTER SET: 1) take effect only for once for - current attachment; 2) does not check that new char set exists untill it will be used +TITLE: ALTER DATABASE SET DEFAULT CHARACTER SET: 1) take effect only for once for current attachment; 2) does not check that new char set exists untill it will be used DESCRIPTION: JIRA: CORE-4744 FBTEST: bugs.core_4744 +NOTES: + [30.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -18,7 +23,7 @@ test_script = """ set list on; - select rdb$character_set_name from rdb$database; + select rdb$character_set_name as db_char_set from rdb$database; commit; @@ -75,8 +80,9 @@ act = isql_act('db', test_script) -expected_stdout = """ - RDB$CHARACTER_SET_NAME UTF8 + +expected_stdout_5x = """ + DB_CHAR_SET UTF8 TAB_NAME TAB_850 FLD_NAME TXT_850 @@ -93,32 +99,63 @@ FLD_CSET WIN1252 DB_DEFAULT_CSET WIN1252 + Statement failed, SQLSTATE = 2C000 + unsuccessful metadata update + -ALTER DATABASE failed + -CHARACTER SET FOO_BAR_8859_4 is not defined + + Statement failed, SQLSTATE = HY004 + unsuccessful metadata update + -CREATE TABLE TAB_BAR failed + -Dynamic SQL Error + -SQL error code = -204 + -Data type unknown + -CHARACTER SET FOO_BAR_8859_4 is not defined + TAB_NAME TAB_FOO FLD_NAME TXT_8859_4 FLD_CSET WIN1252 DB_DEFAULT_CSET WIN1252 """ -expected_stderr = """ +expected_stdout_6x = """ + DB_CHAR_SET UTF8 + + TAB_NAME TAB_850 + FLD_NAME TXT_850 + FLD_CSET DOS850 + DB_DEFAULT_CSET DOS850 + + TAB_NAME TAB_866 + FLD_NAME TXT_866 + FLD_CSET DOS866 + DB_DEFAULT_CSET DOS866 + + TAB_NAME TAB_1252 + FLD_NAME TXT_1252 + FLD_CSET WIN1252 + DB_DEFAULT_CSET WIN1252 + Statement failed, SQLSTATE = 2C000 unsuccessful metadata update -ALTER DATABASE failed - -CHARACTER SET FOO_BAR_8859_4 is not defined - + -CHARACTER SET "PUBLIC"."FOO_BAR_8859_4" is not defined Statement failed, SQLSTATE = HY004 unsuccessful metadata update - -CREATE TABLE TAB_BAR failed + -CREATE TABLE "PUBLIC"."TAB_BAR" failed -Dynamic SQL Error -SQL error code = -204 -Data type unknown - -CHARACTER SET FOO_BAR_8859_4 is not defined + -CHARACTER SET "PUBLIC"."FOO_BAR_8859_4" is not defined + + TAB_NAME TAB_FOO + FLD_NAME TXT_8859_4 + FLD_CSET WIN1252 + DB_DEFAULT_CSET WIN1252 """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4747_test.py b/tests/bugs/core_4747_test.py index 08ebe429..a7b781b1 100644 --- a/tests/bugs/core_4747_test.py +++ b/tests/bugs/core_4747_test.py @@ -35,6 +35,7 @@ act = python_act('db', substitutions = substitutions) +@pytest.mark.es_eds @pytest.mark.version('>=3') def test_1(act: Action): diff --git a/tests/bugs/core_4754_test.py b/tests/bugs/core_4754_test.py index 001f4716..16b69722 100644 --- a/tests/bugs/core_4754_test.py +++ b/tests/bugs/core_4754_test.py @@ -2,12 +2,17 @@ """ ID: issue-5058 -ISSUE: 5058 -TITLE: Bugcheck 167 (invalid SEND request) while working with GTT from several - attachments (using EXECUTE STATEMENT ... ON EXTERNAL and different roles) +ISSUE: https://github.com/FirebirdSQL/firebird/issues/5058 +TITLE: Bugcheck 167 (invalid SEND request) while working with GTT from several attachments (using EXECUTE STATEMENT ... ON EXTERNAL and different roles) DESCRIPTION: JIRA: CORE-4754 FBTEST: bugs.core_4754 +NOTES: + [30.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -24,11 +29,18 @@ act = python_act('db') -expected_stdout = """ -Error-1: -lock conflict on no wait transaction --unsuccessful metadata update --object TABLE "GTT_SESSION" is in use +expected_stdout_5x = """ + Error-1: + lock conflict on no wait transaction + -unsuccessful metadata update + -object TABLE "GTT_SESSION" is in use +""" + +expected_stdout_6x = """ + Error-1: + lock conflict on no wait transaction + -unsuccessful metadata update + -object TABLE "PUBLIC"."GTT_SESSION" is in use """ @pytest.mark.version('>=3') @@ -61,6 +73,6 @@ def test_1(act: Action, capsys): print('Error-2:') print(e.args[0]) # - act.expected_stdout = expected_stdout + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4755_test.py b/tests/bugs/core_4755_test.py index 962009da..d093a467 100644 --- a/tests/bugs/core_4755_test.py +++ b/tests/bugs/core_4755_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4755 FBTEST: bugs.core_4755 +NOTES: + [30.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -80,19 +86,28 @@ act = isql_act('db', test_script, substitutions=[('-At block line: [\\d]+, col: [\\d]+', '-At block line')]) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = HY000 exception 1 -EX_SOMETHING_WRONG -Arguments for raising exeption: 10001 10002 10003 10004 10005 10006 10007 10008 10009 - -At block line: 12, col: 5 + -At block line + Statement failed, SQLSTATE = 07002 + Number of arguments (10) exceeds the maximum (9) number of EXCEPTION USING arguments +""" + +expected_stdout_6x = """ + Statement failed, SQLSTATE = HY000 + exception 1 + -"PUBLIC"."EX_SOMETHING_WRONG" + -Arguments for raising exeption: 10001 10002 10003 10004 10005 10006 10007 10008 10009 + -At block line Statement failed, SQLSTATE = 07002 Number of arguments (10) exceeds the maximum (9) number of EXCEPTION USING arguments """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4760_test.py b/tests/bugs/core_4760_test.py index e4d2b18f..ee633b84 100644 --- a/tests/bugs/core_4760_test.py +++ b/tests/bugs/core_4760_test.py @@ -39,6 +39,7 @@ WHOAMI : {NON_ASCII_NAME} """ +@pytest.mark.intl @pytest.mark.version('>=4.0') def test_1(act: Action, non_ascii_user: User, capsys): with act.db.connect(user=non_ascii_user.name, password=non_ascii_user.password) as con: diff --git a/tests/bugs/core_4766_test.py b/tests/bugs/core_4766_test.py index 6e444029..c21e726c 100644 --- a/tests/bugs/core_4766_test.py +++ b/tests/bugs/core_4766_test.py @@ -3,19 +3,22 @@ """ ID: issue-5066 ISSUE: 5066 -TITLE: AV when trying to manage users list using EXECUTE STATEMENT on behalf of - non-sysdba user which has RDB$ADMIN role +TITLE: AV when trying to manage users list using EXECUTE STATEMENT on behalf of non-sysdba user which has RDB$ADMIN role DESCRIPTION: -NOTES: -[24.11.2021] pcisar - On FB v4.0.0.2496 this test fails as provided script file 'core_4766.sql' raises error in - execute block->execute statement->create/drop user: - Statement failed, SQLSTATE = 28000 - Your user name and password are not defined. Ask your database administrator to set up a Firebird login. - -At block line: 3, col: 9 - Variant for FB 3 works fine. JIRA: CORE-4766 FBTEST: bugs.core_4766 +NOTES: + [24.11.2021] pcisar + On FB v4.0.0.2496 this test fails as provided script file 'core_4766.sql' raises error in + execute block->execute statement->create/drop user: + Statement failed, SQLSTATE = 28000 / Your user name and password are not defined... + Variant for FB 3 works fine. + + [30.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -26,82 +29,56 @@ act = python_act('db', substitutions=[('TCPv.*', 'TCP'), ('.*After line \\d+.*', ''), ('find/delete', 'delete'), ('TABLE PLG\\$.*', 'TABLE PLG')]) -# version: 3.0 - -expected_stdout_1 = """ - Srp: BOSS_SEC_NAME TMP_4766_BOSS - Srp: BOSS_SEC_PLUGIN Srp - Srp: BOSS_SEC_IS_ADMIN - Srp: Statement failed, SQLSTATE = 28000 - Srp: add record error - Srp: -no permission for INSERT access to TABLE PLG$SRP_VIEW - - Srp: MNGR_SEC_NAME - Srp: MNGR_SEC_PLUGIN - Srp: MNGR_SEC_IS_ADMIN - Srp: Statement failed, SQLSTATE = 28000 - Srp: delete record error - Srp: -no permission for DELETE access to TABLE PLG$SRP_VIEW - - +expected_stdout_3x = """ Leg: BOSS_SEC_NAME TMP_4766_BOSS Leg: BOSS_SEC_PLUGIN Legacy_UserManager Leg: BOSS_SEC_IS_ADMIN Leg: Statement failed, SQLSTATE = 28000 Leg: add record error - Leg: -no permission for INSERT access to TABLE PLG$VIEW_USERS - + Leg: -no permission for INSERT access to TABLE PLG Leg: MNGR_SEC_NAME Leg: MNGR_SEC_PLUGIN Leg: MNGR_SEC_IS_ADMIN - Leg: Statement failed, SQLSTATE = 28000 - Leg: find/delete record error - Leg: -no permission for DELETE access to TABLE PLG$VIEW_USERS - + Leg: delete record error + Leg: -no permission for DELETE access to TABLE PLG """ -@pytest.mark.version('>=3.0,<4') -def test_1(act: Action, capsys): - sql_text = (act.files_dir / 'core_4766.sql').read_text() - subs = {'dsn': act.db.dsn, 'user_name': act.db.user, 'user_password': act.db.password, - 'current_auth_plugin': None,} - for current_auth_plugin in ['Srp', 'Legacy_UserManager']: - subs['current_auth_plugin'] = current_auth_plugin - act.reset() - act.isql(switches=['-q'], input=sql_text % subs, combine_output=True) - for line in act.clean_stdout.splitlines(): - if line.strip(): - print(current_auth_plugin[:3] + ': ' + line) - # - act.reset() - act.expected_stdout = expected_stdout_1 - act.stdout = capsys.readouterr().out - assert act.clean_stdout == act.clean_expected_stdout - -# version: 4.0 - -expected_stdout_2 = """ +expected_stdout_5x = """ Leg: BOSS_SEC_NAME TMP_4766_BOSS Leg: BOSS_SEC_PLUGIN Legacy_UserManager Leg: BOSS_SEC_IS_ADMIN - Leg: Statement failed, SQLSTATE = 28000 Leg: add record error - Leg: -no permission for INSERT access to TABLE PLG$VIEW_USERS + Leg: -no permission for INSERT access to TABLE PLG Leg: -Effective user is TMP_4766_BOSS - Leg: MNGR_SEC_NAME Leg: MNGR_SEC_PLUGIN Leg: MNGR_SEC_IS_ADMIN + Leg: Statement failed, SQLSTATE = 28000 + Leg: delete record error + Leg: -no permission for DELETE access to TABLE PLG + Leg: -Effective user is TMP_4766_BOSS +""" +expected_stdout_6x = """ + Leg: BOSS_SEC_NAME TMP_4766_BOSS + Leg: BOSS_SEC_PLUGIN Legacy_UserManager + Leg: BOSS_SEC_IS_ADMIN + Leg: Statement failed, SQLSTATE = 28000 + Leg: add record error + Leg: -no permission for INSERT access to TABLE "PLG$LEGACY_SEC"."PLG$VIEW_USERS" + Leg: -Effective user is TMP_4766_BOSS + Leg: MNGR_SEC_NAME + Leg: MNGR_SEC_PLUGIN + Leg: MNGR_SEC_IS_ADMIN Leg: Statement failed, SQLSTATE = 28000 - Leg: find/delete record error - Leg: -no permission for DELETE access to TABLE PLG$VIEW_USERS + Leg: delete record error + Leg: -no permission for DELETE access to TABLE "PLG$LEGACY_SEC"."PLG$VIEW_USERS" Leg: -Effective user is TMP_4766_BOSS """ -@pytest.mark.version('>=4.0') +@pytest.mark.version('>=3.0') def test_2(act: Action, capsys): sql_text = (act.files_dir / 'core_4766.sql').read_text() # ::: NB ::: @@ -119,6 +96,7 @@ def test_2(act: Action, capsys): print(current_auth_plugin[:3] + ': ' + line) # act.reset() - act.expected_stdout = expected_stdout_2 + + act.expected_stdout = expected_stdout_3x if act.is_version('<4') else expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4774_test.py b/tests/bugs/core_4774_test.py index 0623e970..9e862177 100644 --- a/tests/bugs/core_4774_test.py +++ b/tests/bugs/core_4774_test.py @@ -5,13 +5,19 @@ ISSUE: 5073 TITLE: Table aliasing is unnecessary required when doing UPDATE ... RETURNING RDB$ pseudo-columns DESCRIPTION: -NOTES: - After fix #6815 execution plan contains 'Local_Table' (FB 5.0+) for DML with RETURNING clauses: - "When such a statement is executed, Firebird should execute the statement to completion - and collect all requested data in a type of temporary table, once execution is complete, - fetches are done against this temporary table" JIRA: CORE-4774 FBTEST: bugs.core_4774 +NOTES: + After fix #6815 execution plan contains 'Local_Table' (FB 5.0+) for DML with RETURNING clauses: + "When such a statement is executed, Firebird should execute the statement to completion + and collect all requested data in a type of temporary table, once execution is complete, + fetches are done against this temporary table" + + [30.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -33,36 +39,32 @@ act = isql_act('db', test_script) -# version: 3.0 - -expected_stdout_1 = """ +expected_stdout_4x = """ PLAN (T NATURAL) PLAN (T NATURAL) PLAN (T NATURAL) """ -@pytest.mark.version('>=3.0,<5.0') -def test_1(act: Action): - act.expected_stdout = expected_stdout_1 - act.execute() - assert act.clean_stdout == act.clean_expected_stdout - -# version: 5.0 - -expected_stdout_2 = """ +expected_stdout_5x = """ PLAN (T NATURAL) PLAN (Local_Table NATURAL) - PLAN (T NATURAL) PLAN (Local_Table NATURAL) - PLAN (T NATURAL) PLAN (Local_Table NATURAL) """ -@pytest.mark.version('>=5.0') -def test_2(act: Action): - act.expected_stdout = expected_stdout_2 - act.execute() - assert act.clean_stdout == act.clean_expected_stdout +expected_stdout_6x = """ + PLAN ("PUBLIC"."T" NATURAL) + PLAN (Local_Table NATURAL) + PLAN ("PUBLIC"."T" NATURAL) + PLAN (Local_Table NATURAL) + PLAN ("PUBLIC"."T" NATURAL) + PLAN (Local_Table NATURAL) +""" +@pytest.mark.version('>=3.0') +def test(act: Action): + act.expected_stdout = expected_stdout_4x if act.is_version('<5') else expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4782_test.py b/tests/bugs/core_4782_test.py index b201a6a6..0eff0136 100644 --- a/tests/bugs/core_4782_test.py +++ b/tests/bugs/core_4782_test.py @@ -2,11 +2,22 @@ """ ID: issue-5081 -ISSUE: 5081 -TITLE: Command `SHOW TABLE` fails when the table contains field with unicode collationin its DDL +ISSUE: https://github.com/FirebirdSQL/firebird/issues/5081 +TITLE: Command `SHOW TABLE` fails when the table contains field with unicode collation in its DDL DESCRIPTION: JIRA: CORE-4782 FBTEST: bugs.core_4782 +NOTES: + [30.06.2025] pzotov + 1. Regression was fixed in http://sourceforge.net/p/firebird/code/61521 2015-05-11 15:48:35 +0000 + Confirmed bug on 3.0.0.31828, got: + Statement failed, SQLSTATE = 22001 + arithmetic exception, numeric overflow, or string truncation + -string right truncation + -expected length 7, actual 9 + 2. Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -14,25 +25,31 @@ db = db_factory(charset='UTF8') -test_script = """ +TABLE_DDL = 'create table "ĄČĘĢÆĖŠŚÖÜØ£"("ąčęėįšųūž" varchar(100) character set utf8 collate test_coll)' +test_script = f""" -- NB: it was connection charset = UTF8 that causes error, title of ticket should be changed. - create view v_test as select d.rdb$relation_id from rdb$database d; + create collation test_coll for utf8 from unicode; commit; - show view v_test; + {TABLE_DDL}; + commit; + show table "ĄČĘĢÆĖŠŚÖÜØ£"; """ act = isql_act('db', test_script, substitutions=[('=.*', '')]) -expected_stdout = """ - RDB$RELATION_ID (RDB$RELATION_ID) SMALLINT Nullable - View Source: - ==== ====== - select d.rdb$relation_id from rdb$database d +expected_stdout_5x = """ + ąčęėįšųūž VARCHAR(100) CHARACTER SET UTF8 Nullable + COLLATE TEST_COLL +""" + +expected_stdout_6x = """ + Table: PUBLIC."ĄČĘĢÆĖŠŚÖÜØ£" + "ąčęėįšųūž" VARCHAR(100) CHARACTER SET SYSTEM.UTF8 COLLATE PUBLIC.TEST_COLL Nullable """ +@pytest.mark.intl @pytest.mark.version('>=3.0') -def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() +def test(act: Action): + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4786_test.py b/tests/bugs/core_4786_test.py index 6bb48c9d..3667a90e 100644 --- a/tests/bugs/core_4786_test.py +++ b/tests/bugs/core_4786_test.py @@ -3,11 +3,14 @@ """ ID: issue-5085 ISSUE: 5085 -TITLE: Problematic key value (when attempt to insert duplicate in PK/UK) is not shown - where length of key >= 127 characters +TITLE: Problematic key value (when attempt to insert duplicate in PK/UK) is not shown where length of key >= 127 characters DESCRIPTION: JIRA: CORE-4786 FBTEST: bugs.core_4786 +NOTES: + [30.06.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -68,71 +71,74 @@ act = isql_act('db', test_script) -expected_stderr = """ - Statement failed, SQLSTATE = 23000 - violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_NONE_UNQ" on table "TEST_NONE" - -Problematic key value is ("S" = '01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234ABCD...) - - Statement failed, SQLSTATE = 23000 - violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table "TEST_UTF8" - -Problematic key value is ("S" = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABCDEF') - - Statement failed, SQLSTATE = 23000 - violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table "TEST_UTF8" - -Problematic key value is ("S" = 'ÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁ...) - - Statement failed, SQLSTATE = 23000 - violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table "TEST_UTF8" - -Problematic key value is ("S" = 'ЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊ...) - - Statement failed, SQLSTATE = 23000 - violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table "TEST_UTF8" - -Problematic key value is ("S" = '€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€...) - - Statement failed, SQLSTATE = 23000 - violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table "TEST_UTF8" - -Problematic key value is ("S" = '∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑...) - - Statement failed, SQLSTATE = 23000 - violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table "TEST_UTF8" - -Problematic key value is ("S" = 'Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á...) - - Statement failed, SQLSTATE = 23000 - violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table "TEST_UTF8" - -Problematic key value is ("S" = 'ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ...) - - Statement failed, SQLSTATE = 23000 - violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table "TEST_UTF8" - -Problematic key value is ("S" = 'ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ...) - - Statement failed, SQLSTATE = 23000 - violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table "TEST_UTF8" - -Problematic key value is ("S" = 'ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑...) - - Statement failed, SQLSTATE = 23000 - violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table "TEST_UTF8" - -Problematic key value is ("S" = 'ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁ...) - - Statement failed, SQLSTATE = 23000 - violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table "TEST_UTF8" - -Problematic key value is ("S" = 'Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑...) - - Statement failed, SQLSTATE = 23000 - violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table "TEST_UTF8" - -Problematic key value is ("S" = 'Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑...) - - Statement failed, SQLSTATE = 23000 - violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table "TEST_UTF8" - -Problematic key value is ("S" = 'ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑Á...) - - Statement failed, SQLSTATE = 23000 - violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table "TEST_UTF8" - -Problematic key value is ("S" = 'Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑...) -""" - +@pytest.mark.intl @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute(charset='utf8') - assert act.clean_stderr == act.clean_expected_stderr - + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + + expected_stdout = f""" + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_NONE_UNQ" on table {SQL_SCHEMA_PREFIX}"TEST_NONE" + -Problematic key value is ("S" = '01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234ABCD...) + + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table {SQL_SCHEMA_PREFIX}"TEST_UTF8" + -Problematic key value is ("S" = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABCDEF') + + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table {SQL_SCHEMA_PREFIX}"TEST_UTF8" + -Problematic key value is ("S" = 'ÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁÁ...) + + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table {SQL_SCHEMA_PREFIX}"TEST_UTF8" + -Problematic key value is ("S" = 'ЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊЊ...) + + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table {SQL_SCHEMA_PREFIX}"TEST_UTF8" + -Problematic key value is ("S" = '€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€...) + + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table {SQL_SCHEMA_PREFIX}"TEST_UTF8" + -Problematic key value is ("S" = '∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑∑...) + + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table {SQL_SCHEMA_PREFIX}"TEST_UTF8" + -Problematic key value is ("S" = 'Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á∑Á...) + + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table {SQL_SCHEMA_PREFIX}"TEST_UTF8" + -Problematic key value is ("S" = 'ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ∑ÁÁ...) + + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table {SQL_SCHEMA_PREFIX}"TEST_UTF8" + -Problematic key value is ("S" = 'ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ∑ÁÁÁ...) + + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table {SQL_SCHEMA_PREFIX}"TEST_UTF8" + -Problematic key value is ("S" = 'ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑∑ÁÁÁ∑...) + + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table {SQL_SCHEMA_PREFIX}"TEST_UTF8" + -Problematic key value is ("S" = 'ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁÁ∑ÁÁÁ...) + + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table {SQL_SCHEMA_PREFIX}"TEST_UTF8" + -Problematic key value is ("S" = 'Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑Á∑∑...) + + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table {SQL_SCHEMA_PREFIX}"TEST_UTF8" + -Problematic key value is ("S" = 'Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑∑∑Á∑...) + + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table {SQL_SCHEMA_PREFIX}"TEST_UTF8" + -Problematic key value is ("S" = 'ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑ÁÁ∑∑∑Á...) + + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST_CSET_UTF8_UNQ" on table {SQL_SCHEMA_PREFIX}"TEST_UTF8" + -Problematic key value is ("S" = 'Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑∑Á∑∑∑...) + """ + + act.expected_stdout = expected_stdout + act.execute(charset='utf8', combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4802_test.py b/tests/bugs/core_4802_test.py index d615b730..3002474c 100644 --- a/tests/bugs/core_4802_test.py +++ b/tests/bugs/core_4802_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-4802 FBTEST: bugs.core_4802 +NOTES: + [03.07.2025] pzotov + Reimplemented: removed usage of hard-coded values for user and role name. + SQL schema and double quotes must be taken in acount when specifying data in expected output. + Checked on 6.0.0.892; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -14,117 +19,105 @@ db = db_factory() -user_a = user_factory('db', name='BIG_BROTHER', password='123') -user_b = user_factory('db', name='SENIOR_MNGR', password='456') -user_c = user_factory('db', name='JUNIOR_MNGR', password='789') -role_a = role_factory('db', name='FLD_FOR_SENIORS_UPDATER') -role_b = role_factory('db', name='FLD_FOR_JUNIORS_UPDATER') +u_big_bro = user_factory('db', name='BIG_BROTHER', password='123') +u_senior = user_factory('db', name='SENIOR_MNGR', password='456') +u_junior = user_factory('db', name='JUNIOR_MNGR', password='789') +r_senior = role_factory('db', name='FLD_FOR_SENIORS_UPDATER') +r_junior = role_factory('db', name='FLD_FOR_JUNIORS_UPDATER') -test_script = """ - set wng off; - - recreate table test(fld_for_seniors varchar(70), fld_for_juniors varchar(70)); - commit; - - grant select on test to PUBLIC; - - grant update(fld_for_seniors) on test to BIG_BROTHER; - commit; - - grant update(fld_for_seniors) on test to FLD_FOR_SENIORS_UPDATER; - grant update(fld_for_juniors) on test to FLD_FOR_JUNIORS_UPDATER; - - grant FLD_FOR_SENIORS_UPDATER to SENIOR_MNGR; - grant FLD_FOR_JUNIORS_UPDATER to JUNIOR_MNGR; - commit; - - show grants; - - insert into test values( 'created by '||upper(current_user), 'created by '||lower(current_user) ); - commit; - set list on; - - --set echo on; - - connect '$(DSN)' user 'BIG_BROTHER' password '123'; - select current_user, current_role from rdb$database; - update test set fld_for_seniors = 'updated by '||upper(current_user)||', role: '||upper(current_role); - select * from test; - - update test set fld_for_juniors = 'updated by '||lower(current_user)||', role: '||lower(current_role); - select * from test; - commit; - --------------------------------------------------------------- - - connect '$(DSN)' user 'SENIOR_MNGR' password '456' role 'FLD_FOR_SENIORS_UPDATER'; - select current_user, current_role from rdb$database; - update test set fld_for_seniors = 'updated by '||upper(current_user)||', role: '||upper(current_role); - select * from test; - - update test set fld_for_juniors ='updated by '||lower(current_user)||', role: '||lower(current_role); - select * from test; - commit; - --------------------------------------------------------------- - - connect '$(DSN)' user 'JUNIOR_MNGR' password '789' role 'FLD_FOR_JUNIORS_UPDATER'; - select current_user, current_role from rdb$database; - update test set fld_for_seniors = 'updated by '||upper(current_user)||', role: '||upper(current_role); - select * from test; - - update test set fld_for_juniors ='updated by '||lower(current_user)||', role: '||lower(current_role); - select * from test; - commit; -""" - -act = isql_act('db', test_script, substitutions=[('GRANT.*TMP.*', ''), ('-Effective user is.*', '')]) - -expected_stdout = """ - /* Grant permissions for this database */ - GRANT UPDATE (FLD_FOR_SENIORS) ON TEST TO USER BIG_BROTHER - GRANT UPDATE (FLD_FOR_JUNIORS) ON TEST TO ROLE FLD_FOR_JUNIORS_UPDATER - GRANT UPDATE (FLD_FOR_SENIORS) ON TEST TO ROLE FLD_FOR_SENIORS_UPDATER - GRANT SELECT ON TEST TO PUBLIC - GRANT FLD_FOR_JUNIORS_UPDATER TO JUNIOR_MNGR - GRANT FLD_FOR_SENIORS_UPDATER TO SENIOR_MNGR - - USER BIG_BROTHER - ROLE NONE - FLD_FOR_SENIORS updated by BIG_BROTHER, role: NONE - FLD_FOR_JUNIORS created by sysdba - FLD_FOR_SENIORS updated by BIG_BROTHER, role: NONE - FLD_FOR_JUNIORS created by sysdba - - USER SENIOR_MNGR - ROLE FLD_FOR_SENIORS_UPDATER - FLD_FOR_SENIORS updated by SENIOR_MNGR, role: FLD_FOR_SENIORS_UPDATER - FLD_FOR_JUNIORS created by sysdba - FLD_FOR_SENIORS updated by SENIOR_MNGR, role: FLD_FOR_SENIORS_UPDATER - FLD_FOR_JUNIORS created by sysdba - - USER JUNIOR_MNGR - ROLE FLD_FOR_JUNIORS_UPDATER - FLD_FOR_SENIORS updated by SENIOR_MNGR, role: FLD_FOR_SENIORS_UPDATER - FLD_FOR_JUNIORS created by sysdba - FLD_FOR_SENIORS updated by SENIOR_MNGR, role: FLD_FOR_SENIORS_UPDATER - FLD_FOR_JUNIORS updated by junior_mngr, role: fld_for_juniors_updater -""" - -expected_stderr = """ - Statement failed, SQLSTATE = 28000 - no permission for UPDATE access to COLUMN TEST.FLD_FOR_JUNIORS - - Statement failed, SQLSTATE = 28000 - no permission for UPDATE access to COLUMN TEST.FLD_FOR_JUNIORS - - Statement failed, SQLSTATE = 28000 - no permission for UPDATE access to COLUMN TEST.FLD_FOR_SENIORS -""" +substitutions = [ ('[ \t]+', ' '), ('GRANT.*TMP.*', ''), ('-Effective user is.*', '') ] +act = isql_act('db', substitutions = substitutions) @pytest.mark.version('>=3.0') -def test_1(act: Action, user_a: User, user_b: User, user_c: User, role_a: Role, role_b: Role): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) +def test_1(act: Action, u_big_bro: User, u_senior: User, u_junior: User, r_senior: Role, r_junior: Role): + + test_script = f""" + set wng off; + + recreate table test(fld_for_seniors varchar(70), fld_for_juniors varchar(70)); + commit; + + grant select on test to PUBLIC; + + grant update(fld_for_seniors) on test to {u_big_bro.name}; + commit; + + grant update(fld_for_seniors) on test to {r_senior.name}; + grant update(fld_for_juniors) on test to {r_junior.name}; + + grant {r_senior.name} to {u_senior.name}; + grant {r_junior.name} to {u_junior.name}; + commit; + + insert into test values( 'created by ' || upper(current_user), 'created by ' || lower(current_user) ); + commit; + set list on; + + connect '{act.db.dsn}' user '{u_big_bro.name}' password '{u_big_bro.password}'; + select current_user, current_role from rdb$database; + update test set fld_for_seniors = 'updated by '||upper(current_user)||', role: '||upper(current_role); + select * from test; + + update test set fld_for_juniors = 'updated by '||lower(current_user)||', role: '||lower(current_role); + select * from test; + commit; + --------------------------------------------------------------- + + connect '{act.db.dsn}' user '{u_senior.name}' password '{u_senior.password}' role '{r_senior.name}'; + select current_user, current_role from rdb$database; + update test set fld_for_seniors = 'updated by '||upper(current_user)||', role: '||upper(current_role); + select * from test; + + update test set fld_for_juniors ='updated by '||lower(current_user)||', role: '||lower(current_role); + select * from test; + commit; + --------------------------------------------------------------- + + connect '{act.db.dsn}' user '{u_junior.name}' password '{u_junior.password}' role '{r_junior.name}'; + select current_user, current_role from rdb$database; + update test set fld_for_seniors = 'updated by '||upper(current_user)||', role: '||upper(current_role); + select * from test; + + update test set fld_for_juniors ='updated by '||lower(current_user)||', role: '||lower(current_role); + select * from test; + commit; + """ + + FLD_JUNIORS_NAME = 'TEST.FLD_FOR_JUNIORS' if act.is_version('<6') else '"PUBLIC"."TEST"."FLD_FOR_JUNIORS"' + FLD_SENIORS_NAME = 'TEST.FLD_FOR_SENIORS' if act.is_version('<6') else '"PUBLIC"."TEST"."FLD_FOR_SENIORS"' + expected_stdout = f""" + USER {u_big_bro.name.upper()} + ROLE NONE + FLD_FOR_SENIORS updated by {u_big_bro.name.upper()}, role: NONE + FLD_FOR_JUNIORS created by {act.db.user.lower()} + + Statement failed, SQLSTATE = 28000 + no permission for UPDATE access to COLUMN {FLD_JUNIORS_NAME} + + FLD_FOR_SENIORS updated by {u_big_bro.name.upper()}, role: NONE + FLD_FOR_JUNIORS created by {act.db.user.lower()} + USER {u_senior.name.upper()} + ROLE {r_senior.name.upper()} + FLD_FOR_SENIORS updated by {u_senior.name.upper()}, role: {r_senior.name.upper()} + FLD_FOR_JUNIORS created by {act.db.user.lower()} + + Statement failed, SQLSTATE = 28000 + no permission for UPDATE access to COLUMN {FLD_JUNIORS_NAME} + + FLD_FOR_SENIORS updated by {u_senior.name.upper()}, role: {r_senior.name.upper()} + FLD_FOR_JUNIORS created by {act.db.user.lower()} + USER {u_junior.name.upper()} + ROLE {r_junior.name.upper()} + + Statement failed, SQLSTATE = 28000 + no permission for UPDATE access to COLUMN {FLD_SENIORS_NAME} + + FLD_FOR_SENIORS updated by {u_senior.name.upper()}, role: {r_senior.name.upper()} + FLD_FOR_JUNIORS created by {act.db.user.lower()} + FLD_FOR_SENIORS updated by {u_senior.name.upper()}, role: {r_senior.name.upper()} + FLD_FOR_JUNIORS updated by junior_mngr, role: fld_for_juniors_updater + """ + act.expected_stdout = expected_stdout + act.isql(switches = ['-q'], input = test_script, combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4806_test.py b/tests/bugs/core_4806_test.py index 4ed8ea6b..21dca760 100644 --- a/tests/bugs/core_4806_test.py +++ b/tests/bugs/core_4806_test.py @@ -16,17 +16,26 @@ Also, we do additional check for second user: try to connect WITHOUT specifying role and see/change sequence. Error must be in this case (SQLSTATE = 28000). Third user must NOT see neither value of generator nor to change it (SQLSTATE = 28000). -NOTES: -[18.08.2020] - FB 4.x has incompatible behaviour with all previous versions since build 4.0.0.2131 (06-aug-2020): - statement 'CREATE SEQUENCE ' will create generator with current value LESS FOR 1 then it was before. - Thus, 'create sequence g;' followed by 'show sequence;' will output "current value: -1" (!!) rather than 0. - See also CORE-6084 and its fix: https://github.com/FirebirdSQL/firebird/commit/23dc0c6297825b2e9006f4d5a2c488702091033d - - This is considered as *expected* and is noted in doc/README.incompatibilities.3to4.txt - Because of this, it was decided to filter out concrete values that are produced in 'SHOW SEQUENCE' command. JIRA: CORE-4806 FBTEST: bugs.core_4806 +NOTES: + [18.08.2020] pzotov + FB 4.x has incompatible behaviour with all previous versions since build 4.0.0.2131 (06-aug-2020): + statement 'CREATE SEQUENCE ' will create generator with current value LESS FOR 1 then it was before. + Thus, 'create sequence g;' followed by 'show sequence;' will output "current value: -1" (!!) rather than 0. + See also CORE-6084 and its fix: https://github.com/FirebirdSQL/firebird/commit/23dc0c6297825b2e9006f4d5a2c488702091033d + + This is considered as *expected* and is noted in doc/README.incompatibilities.3to4.txt + Because of this, it was decided to filter out concrete values that are produced in 'SHOW SEQUENCE' command. + + [15.05.2025] pzotov + Removed 'show grants' because its output very 'fragile' and can often change in fresh FB versions. + + [30.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -48,7 +57,6 @@ grant usage on sequence g to role stockmgr; grant stockmgr to Bill_Junior; commit; - show grants; set list on; @@ -101,53 +109,104 @@ commit; """ -act = isql_act('db', test_script, substitutions=[('-Effective user is.*', ''), - ('current value.*', 'current value')]) - -expected_stdout = """ - /* Grant permissions for this database */ - GRANT STOCKMGR TO BILL_JUNIOR - GRANT USAGE ON SEQUENCE G TO USER BIG_BROTHER - GRANT USAGE ON SEQUENCE G TO ROLE STOCKMGR - - USER BIG_BROTHER - ROLE NONE - Generator G, current value: 0, initial value: 0, increment: 1 - NEW_GEN -111 - - USER BILL_JUNIOR - ROLE STOCKMGR - Generator G, current value: -111, initial value: 0, increment: 1 - NEW_GEN -333 - - USER BILL_JUNIOR - ROLE NONE - - USER MAVERICK - ROLE NONE -""" - -expected_stderr = """ - Statement failed, SQLSTATE = 28000 - no permission for USAGE access to GENERATOR G - There is no generator G in this database - - Statement failed, SQLSTATE = 28000 - no permission for USAGE access to GENERATOR G +substitutions = [ # ('[ \t]+', ' '), + # ('(-)?Effective user is.*', ''), + ('current value.*', 'current value') + ] - Statement failed, SQLSTATE = 28000 - no permission for USAGE access to GENERATOR G - There is no generator G in this database - - Statement failed, SQLSTATE = 28000 - no permission for USAGE access to GENERATOR G -""" +act = isql_act('db', test_script, substitutions = substitutions) @pytest.mark.version('>=3.0') def test_1(act: Action, user_a: User, user_b: User, user_c: User, role_a: Role): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + # user_a = user_factory('db', name='Maverick', password='123') + # user_b = user_factory('db', name='Big_Brother', password='456') + # user_c = user_factory('db', name='Bill_Junior', password='789') + # role_a = role_factory('db', name='stockmgr') + + expected_stdout_3x = f""" + USER {user_b.name.upper()} + ROLE NONE + Generator G, current value + NEW_GEN -111 + USER {user_c.name.upper()} + ROLE {role_a.name.upper()} + Generator G, current value + NEW_GEN -333 + USER {user_c.name.upper()} + ROLE NONE + Statement failed, SQLSTATE = 28000 + no permission for USAGE access to GENERATOR G + There is no generator G in this database + Statement failed, SQLSTATE = 28000 + no permission for USAGE access to GENERATOR G + USER {user_a.name.upper()} + ROLE NONE + Statement failed, SQLSTATE = 28000 + no permission for USAGE access to GENERATOR G + There is no generator G in this database + Statement failed, SQLSTATE = 28000 + no permission for USAGE access to GENERATOR G + """ + + expected_stdout_5x = f""" + USER {user_b.name.upper()} + ROLE NONE + Generator G, current value: 0, initial value: 1, increment: 1 + NEW_GEN -111 + USER {user_c.name.upper()} + ROLE {role_a.name.upper()} + Generator G, current value: -111, initial value: 1, increment: 1 + NEW_GEN -333 + USER {user_c.name.upper()} + ROLE NONE + Statement failed, SQLSTATE = 28000 + no permission for USAGE access to GENERATOR G + -Effective user is {user_c.name.upper()} + There is no generator G in this database + Statement failed, SQLSTATE = 28000 + no permission for USAGE access to GENERATOR G + -Effective user is {user_c.name.upper()} + USER {user_a.name.upper()} + ROLE NONE + Statement failed, SQLSTATE = 28000 + no permission for USAGE access to GENERATOR G + -Effective user is {user_a.name.upper()} + There is no generator G in this database + Statement failed, SQLSTATE = 28000 + no permission for USAGE access to GENERATOR G + -Effective user is {user_a.name.upper()} + """ + + expected_stdout_6x = f""" + USER {user_b.name.upper()} + ROLE NONE + Generator PUBLIC.G, current value + NEW_GEN -111 + USER {user_c.name.upper()} + ROLE {role_a.name.upper()} + Generator PUBLIC.G, current value + NEW_GEN -333 + USER {user_c.name.upper()} + ROLE NONE + Statement failed, SQLSTATE = 28000 + no permission for USAGE access to GENERATOR "PUBLIC"."G" + -Effective user is {user_c.name.upper()} + There is no generator G in this database + Statement failed, SQLSTATE = 28000 + no permission for USAGE access to GENERATOR "PUBLIC"."G" + -Effective user is {user_c.name.upper()} + USER {user_a.name.upper()} + ROLE NONE + Statement failed, SQLSTATE = 28000 + no permission for USAGE access to GENERATOR "PUBLIC"."G" + -Effective user is {user_a.name.upper()} + There is no generator G in this database + Statement failed, SQLSTATE = 28000 + no permission for USAGE access to GENERATOR "PUBLIC"."G" + -Effective user is {user_a.name.upper()} + """ + + act.expected_stdout = expected_stdout_3x if act.is_version('<4') else expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4807_test.py b/tests/bugs/core_4807_test.py index 08df0577..465d1b07 100644 --- a/tests/bugs/core_4807_test.py +++ b/tests/bugs/core_4807_test.py @@ -5,10 +5,14 @@ ISSUE: 5105 TITLE: Regression: List of aggregation is not checked properly DESCRIPTION: - Field inside subquery not present in GROUP BY clause and therefore can't be used in - SELECT list as is (only as argument of some aggregation function). + Field inside subquery not present in GROUP BY clause and therefore can't be used in + SELECT list as is (only as argument of some aggregation function). JIRA: CORE-4807 FBTEST: bugs.core_4807 +NOTES: + [30.06.2025] pzotov + Removed check of STDOUT: no sense in this test. Only STDERR must be checked. + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -29,11 +33,6 @@ act = isql_act('db', test_script, substitutions=[('SORT \\(\\(T NATURAL\\)\\)', 'SORT (T NATURAL)')]) -expected_stdout = """ - PLAN (RDB$DATABASE NATURAL) - PLAN SORT ((T NATURAL)) -""" - expected_stderr = """ Statement failed, SQLSTATE = 42000 Dynamic SQL Error @@ -41,11 +40,8 @@ -Invalid expression in the select list (not contained in either an aggregate function or the GROUP BY clause) """ -@pytest.mark.version('>=3') +@pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.execute(combine_output = False) + assert act.clean_stderr == act.clean_expected_stderr diff --git a/tests/bugs/core_4809_test.py b/tests/bugs/core_4809_test.py index 425e6c5e..94d96322 100644 --- a/tests/bugs/core_4809_test.py +++ b/tests/bugs/core_4809_test.py @@ -18,6 +18,12 @@ result of HASH(A, B) is considered now as having greater cardinality than HASH(C). This causes optimizer to put HASH(A,B) as first source. Checked on 5.0.0.1149. + + [30.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -227,9 +233,30 @@ PLAN HASH (HASH (HASH (DATASOURCE_A TF NATURAL, DATASOURCE_B TF NATURAL), DATASOURCE_C TF NATURAL), DATASOURCE_D TF NATURAL) """ +fb6x_checked_stdout = """ + PLAN HASH ("DATASOURCE_A" "PUBLIC"."TK" NATURAL, "DATASOURCE_B" "PUBLIC"."TK" NATURAL) + PLAN HASH ("DATASOURCE_A" "PUBLIC"."TK" NATURAL, "DATASOURCE_B" "PUBLIC"."TK" NATURAL, "DATASOURCE_C" "PUBLIC"."TK" NATURAL) + PLAN HASH ("DATASOURCE_A" "PUBLIC"."TK" NATURAL, "DATASOURCE_B" "PUBLIC"."TK" NATURAL, "DATASOURCE_C" "PUBLIC"."TK" NATURAL, "DATASOURCE_D" "PUBLIC"."TK" NATURAL) + PLAN HASH ("DATASOURCE_A" "PUBLIC"."TK" NATURAL, "DATASOURCE_B" "PUBLIC"."TK" NATURAL) + PLAN HASH (HASH ("DATASOURCE_A" "PUBLIC"."TK" NATURAL, "DATASOURCE_B" "PUBLIC"."TK" NATURAL), "DATASOURCE_C" "PUBLIC"."TK" NATURAL) + PLAN HASH (HASH (HASH ("DATASOURCE_A" "PUBLIC"."TK" NATURAL, "DATASOURCE_B" "PUBLIC"."TK" NATURAL), "DATASOURCE_C" "PUBLIC"."TK" NATURAL), "DATASOURCE_D" "PUBLIC"."TK" NATURAL) + PLAN HASH ("DATASOURCE_A" "PUBLIC"."TK" NATURAL, "DATASOURCE_B" "PUBLIC"."TK" NATURAL) + PLAN HASH (HASH ("DATASOURCE_A" "PUBLIC"."TK" NATURAL, "DATASOURCE_B" "PUBLIC"."TK" NATURAL), "DATASOURCE_C" "PUBLIC"."TK" NATURAL) + PLAN HASH (HASH (HASH ("DATASOURCE_A" "PUBLIC"."TK" NATURAL, "DATASOURCE_B" "PUBLIC"."TK" NATURAL), "DATASOURCE_C" "PUBLIC"."TK" NATURAL), "DATASOURCE_D" "PUBLIC"."TK" NATURAL) + PLAN HASH ("DATASOURCE_A" "PUBLIC"."TF" NATURAL, "DATASOURCE_B" "PUBLIC"."TF" NATURAL) + PLAN HASH ("DATASOURCE_A" "PUBLIC"."TF" NATURAL, "DATASOURCE_B" "PUBLIC"."TF" NATURAL, "DATASOURCE_C" "PUBLIC"."TF" NATURAL) + PLAN HASH ("DATASOURCE_A" "PUBLIC"."TF" NATURAL, "DATASOURCE_B" "PUBLIC"."TF" NATURAL, "DATASOURCE_C" "PUBLIC"."TF" NATURAL, "DATASOURCE_D" "PUBLIC"."TF" NATURAL) + PLAN HASH ("DATASOURCE_A" "PUBLIC"."TF" NATURAL, "DATASOURCE_B" "PUBLIC"."TF" NATURAL) + PLAN HASH (HASH ("DATASOURCE_A" "PUBLIC"."TF" NATURAL, "DATASOURCE_B" "PUBLIC"."TF" NATURAL), "DATASOURCE_C" "PUBLIC"."TF" NATURAL) + PLAN HASH (HASH (HASH ("DATASOURCE_A" "PUBLIC"."TF" NATURAL, "DATASOURCE_B" "PUBLIC"."TF" NATURAL), "DATASOURCE_C" "PUBLIC"."TF" NATURAL), "DATASOURCE_D" "PUBLIC"."TF" NATURAL) + PLAN HASH ("DATASOURCE_A" "PUBLIC"."TF" NATURAL, "DATASOURCE_B" "PUBLIC"."TF" NATURAL) + PLAN HASH (HASH ("DATASOURCE_A" "PUBLIC"."TF" NATURAL, "DATASOURCE_B" "PUBLIC"."TF" NATURAL), "DATASOURCE_C" "PUBLIC"."TF" NATURAL) + PLAN HASH (HASH (HASH ("DATASOURCE_A" "PUBLIC"."TF" NATURAL, "DATASOURCE_B" "PUBLIC"."TF" NATURAL), "DATASOURCE_C" "PUBLIC"."TF" NATURAL), "DATASOURCE_D" "PUBLIC"."TF" NATURAL) +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = fb3x_checked_stdout if act.is_version('<4') else fb4x_checked_stdout if act.is_version('<5') else fb5x_checked_stdout + act.expected_stdout = fb3x_checked_stdout if act.is_version('<4') else fb4x_checked_stdout if act.is_version('<5') else fb5x_checked_stdout if act.is_version('<6') else fb6x_checked_stdout act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4811_test.py b/tests/bugs/core_4811_test.py index 2bbaf2d7..3c79cdb6 100644 --- a/tests/bugs/core_4811_test.py +++ b/tests/bugs/core_4811_test.py @@ -7,182 +7,177 @@ DESCRIPTION: JIRA: CORE-4811 FBTEST: bugs.core_4811 +NOTES: + [29.06.2025] pzotov + Re-implemented: use f-notation and variables to be substituted in the expected output. + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -substitutions = [('set echo.*', ''), ('Use CONNECT or CREATE DATABASE.*', ''), - ('Your user name and password.*', ''), ('line: [0-9]+, col: [0-9]+', ''), - ('exception [0-9]+', 'exception')] - db = db_factory() tmp_user = user_factory('db', name='tmp$c4811', password='1') tmp_role = role_factory('db', name='Boss') -test_script = """ - set wng off; - set list on; - create or alter procedure sp_check_actual_role as begin end; - commit; - - recreate exception ex_have_no_role 'You''ve specified role: >@1< -- but your actual role is NONE.'; - - set term ^; - create or alter procedure sp_check_actual_role( - a_probe_role varchar(31) - ) returns( - checking varchar(80), - result varchar(31) - ) as - begin - if ( upper(current_role) = 'NONE' ) - then - exception ex_have_no_role using ( a_probe_role ); - - checking = 'role: >' || a_probe_role || '< - ' - || trim( - iif( a_probe_role containing '''', 'in apostrophes', - iif( a_probe_role containing '"', 'in double quotes', 'without delimiters' ) - ) - ) - || ', ' || iif( upper(a_probe_role) = a_probe_role, 'UPPER case', 'CaMeL case' ) - ; - result = current_role; - suspend; - end - ^ - set term ;^ - commit; - - set bail on; - set echo on; - grant Boss to Tmp$c4811; - grant usage on exception ex_have_no_role to Tmp$c4811; - grant execute on procedure sp_check_actual_role to Tmp$c4811; - set echo off; - set bail off; - -- show grants; - commit; - - -- set echo on; - - -- checking for USER name: - - connect '$(DSN)' user 'Tmp$c4811' password '1'; - -- PASSES since http://sourceforge.net/p/firebird/code/62016 (2015-07-16 14:26), this was build = 31981 - select 'user: >''Tmp$c4811''< - in apostrophes, CaMeL case' checking, current_user as result from rdb$database; - commit; - - connect '$(DSN)' user 'TMP$C4811' password '1'; -- should PASS, checked on builds 31948, 31981 - select 'user: >''TMP$C4811''< - in apostrophes, UPPER case' checking, current_user as result from rdb$database; - commit; - - connect '$(DSN)' user Tmp$c4811 password '1'; -- should PASS, checked on builds 31948, 31981 - select 'user: >Tmp$c4811< - without delimiters, CaMeL case' checking, current_user as result from rdb$database; - commit; - - connect '$(DSN)' user TMP$C4811 password '1'; -- should PASS, checked on builds 31948, 31981 - select 'user: >TMP$C4811< - without delimiters, UPPER case' checking, current_user as result from rdb$database; - commit; - - connect '$(DSN)' user "Tmp$c4811" password '1'; -- should *** FAIL *** - select 'user: >"Tmp$c4811"< - in double quotes, CaMeL case' checking, current_user as result from rdb$database; - commit; - - connect '$(DSN)' user "TMP$C4811" password '1'; -- should PASS, checked on builds 31948, 31981 - select 'user: >"TMP$C4811" - in double quotes, UPPER case' checking, current_user as result from rdb$database; - commit; - - -- checking for ROLE (actual role in all following cases will be: [BOSS], checked on builds 31948, 31981) - - -- Statement that created role (see above): - -- create role Boss; - - -- Enclosing role in apostrophes and specifying it exactly like it was in its creation sttm: - connect '$(DSN)' user 'TMP$C4811' password '1' role 'Boss'; - select * from sp_check_actual_role( '''Boss''' ); --------------- should return: BOSS - commit; - - -- Enclosing role in apostrophes and specifying it in UPPERCASE (i.e. differ than in its CREATE ROLE statement): - connect '$(DSN)' user 'TMP$C4811' password '1' role 'BOSS'; - select * from sp_check_actual_role( '''BOSS''' ); --------------- should return: BOSS - commit; - - -- do NOT enclosing role in any delimiters and change CaSe of its characters (i.e. differ than in its CREATE ROLE statement): - connect '$(DSN)' user 'TMP$C4811' password '1' role BosS; - select * from sp_check_actual_role( 'BosS' ); --------------- should return: BOSS - commit; - - -- do NOT enclosing role in any delimiters and specifying it in UPPERCASE (i.e. differ than in its CREATE ROLE statement): - connect '$(DSN)' user 'TMP$C4811' password '1' role BOSS; - select * from sp_check_actual_role( 'BOSS' ); --------------- should return: BOSS - commit; - - -- Enclosing role in double quotes and change CaSe of its characters (i.e. differ than in its CREATE ROLE statement): - connect '$(DSN)' user 'TMP$C4811' password '1' role "BoSs"; - select * from sp_check_actual_role( '"BoSs"' ); --------------- should raise EX_HAVE_NO_ROLE, actual role will be 'NONE' - commit; - - -- Enclosing role in double quotes and specifying it in UPPERCASE (i.e. differ than in its CREATE ROLE statement): - connect '$(DSN)' user 'TMP$C4811' password '1' role "BOSS"; - select * from sp_check_actual_role( '"BOSS"' ); --------------- should return: BOSS - commit; -""" - -act = isql_act('db', test_script, substitutions=substitutions) - -expected_stdout = """ - grant Boss to Tmp$c4811; - grant usage on exception ex_have_no_role to Tmp$c4811; - grant execute on procedure sp_check_actual_role to Tmp$c4811; - - CHECKING user: >'Tmp$c4811'< - in apostrophes, CaMeL case - RESULT TMP$C4811 - - CHECKING user: >'TMP$C4811'< - in apostrophes, UPPER case - RESULT TMP$C4811 - - CHECKING user: >Tmp$c4811< - without delimiters, CaMeL case - RESULT TMP$C4811 - - CHECKING user: >TMP$C4811< - without delimiters, UPPER case - RESULT TMP$C4811 - - CHECKING user: >"TMP$C4811" - in double quotes, UPPER case - RESULT TMP$C4811 +substitutions = [('set echo.*', ''), ('[ \t]+', ' '), ('exception \\d+', 'exception'), ('line(:)?\\s+\\d+.*', '')] +act = isql_act('db', substitutions = substitutions) - CHECKING role: >'Boss'< - in apostrophes, CaMeL case - RESULT BOSS - - CHECKING role: >'BOSS'< - in apostrophes, UPPER case - RESULT BOSS - - CHECKING role: >BosS< - without delimiters, CaMeL case - RESULT BOSS - - CHECKING role: >BOSS< - without delimiters, UPPER case - RESULT BOSS - - CHECKING role: >"BOSS"< - in double quotes, UPPER case - RESULT BOSS -""" - -expected_stderr = """ - Statement failed, SQLSTATE = 28000 - Statement failed, SQLSTATE = HY000 - exception 3 - -EX_HAVE_NO_ROLE - -You've specified role: >"BoSs"< -- but your actual role is NONE. - -At procedure 'SP_CHECK_ACTUAL_ROLE' -""" @pytest.mark.version('>=3.0') def test_1(act: Action, tmp_user: User, tmp_role: Role): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + test_script = f""" + set wng off; + set list on; + create or alter procedure sp_check_actual_role as begin end; + commit; + + recreate exception ex_have_no_role 'You''ve specified role: >@1< -- but your actual role is NONE.'; + + set term ^; + create or alter procedure sp_check_actual_role( + a_probe_role varchar(31) + ) returns( + checking varchar(80), + result varchar(31) + ) as + begin + if ( upper(current_role) = 'NONE' ) + then + exception ex_have_no_role using ( a_probe_role ); + + checking = 'role: >' || a_probe_role || '< - ' + || trim( + iif( a_probe_role containing '''', 'in apostrophes', + iif( a_probe_role containing '"', 'in double quotes', 'without delimiters' ) + ) + ) + || ', ' || iif( upper(a_probe_role) = a_probe_role, 'UPPER case', 'CaMeL case' ) + ; + result = current_role; + suspend; + end + ^ + set term ;^ + commit; + + set bail on; + set echo on; + grant Boss to Tmp$c4811; + grant usage on exception ex_have_no_role to Tmp$c4811; + grant execute on procedure sp_check_actual_role to Tmp$c4811; + set echo off; + set bail off; + -- show grants; + commit; + + -- set echo on; + + -- checking for USER name: + + connect '{act.db.dsn}' user 'Tmp$c4811' password '1'; + -- PASSES since http://sourceforge.net/p/firebird/code/62016 (2015-07-16 14:26), this was build = 31981 + select 'user: >''Tmp$c4811''< - in apostrophes, CaMeL case' checking, current_user as result from rdb$database; + commit; + + connect '{act.db.dsn}' user 'TMP$C4811' password '1'; -- should PASS, checked on builds 31948, 31981 + select 'user: >''TMP$C4811''< - in apostrophes, UPPER case' checking, current_user as result from rdb$database; + commit; + + connect '{act.db.dsn}' user Tmp$c4811 password '1'; -- should PASS, checked on builds 31948, 31981 + select 'user: >Tmp$c4811< - without delimiters, CaMeL case' checking, current_user as result from rdb$database; + commit; + + connect '{act.db.dsn}' user TMP$C4811 password '1'; -- should PASS, checked on builds 31948, 31981 + select 'user: >TMP$C4811< - without delimiters, UPPER case' checking, current_user as result from rdb$database; + commit; + + connect '{act.db.dsn}' user "Tmp$c4811" password '1'; -- should *** FAIL *** + select 'user: >"Tmp$c4811"< - in double quotes, CaMeL case' checking, current_user as result from rdb$database; + commit; + + connect '{act.db.dsn}' user "TMP$C4811" password '1'; -- should PASS, checked on builds 31948, 31981 + select 'user: >"TMP$C4811" - in double quotes, UPPER case' checking, current_user as result from rdb$database; + commit; + + -- checking for ROLE (actual role in all following cases will be: [BOSS], checked on builds 31948, 31981) + + -- Statement that created role (see above): + -- create role Boss; + + -- Enclosing role in apostrophes and specifying it exactly like it was in its creation sttm: + connect '{act.db.dsn}' user 'TMP$C4811' password '1' role 'Boss'; + select * from sp_check_actual_role( '''Boss''' ); --------------- should return: BOSS + commit; + + -- Enclosing role in apostrophes and specifying it in UPPERCASE (i.e. differ than in its CREATE ROLE statement): + connect '{act.db.dsn}' user 'TMP$C4811' password '1' role 'BOSS'; + select * from sp_check_actual_role( '''BOSS''' ); --------------- should return: BOSS + commit; + + -- do NOT enclosing role in any delimiters and change CaSe of its characters (i.e. differ than in its CREATE ROLE statement): + connect '{act.db.dsn}' user 'TMP$C4811' password '1' role BosS; + select * from sp_check_actual_role( 'BosS' ); --------------- should return: BOSS + commit; + + -- do NOT enclosing role in any delimiters and specifying it in UPPERCASE (i.e. differ than in its CREATE ROLE statement): + connect '{act.db.dsn}' user 'TMP$C4811' password '1' role BOSS; + select * from sp_check_actual_role( 'BOSS' ); --------------- should return: BOSS + commit; + + -- Enclosing role in double quotes and change CaSe of its characters (i.e. differ than in its CREATE ROLE statement): + connect '{act.db.dsn}' user 'TMP$C4811' password '1' role "BoSs"; + select * from sp_check_actual_role( '"BoSs"' ); --------------- should raise EX_HAVE_NO_ROLE, actual role will be 'NONE' + commit; + + -- Enclosing role in double quotes and specifying it in UPPERCASE (i.e. differ than in its CREATE ROLE statement): + connect '{act.db.dsn}' user 'TMP$C4811' password '1' role "BOSS"; + select * from sp_check_actual_role( '"BOSS"' ); --------------- should return: BOSS + commit; + """ + + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + EXCEPTION_NAME = "EX_HAVE_NO_ROLE" if act.is_version('<6') else '"EX_HAVE_NO_ROLE"' + STORED_PROC_NAME = "'SP_CHECK_ACTUAL_ROLE'" if act.is_version('<6') else '"SP_CHECK_ACTUAL_ROLE"' + expected_stdout = f""" + grant Boss to Tmp$c4811; + grant usage on exception ex_have_no_role to Tmp$c4811; + grant execute on procedure sp_check_actual_role to Tmp$c4811; + CHECKING user: >'Tmp$c4811'< - in apostrophes, CaMeL case + RESULT {tmp_user.name.upper()} + CHECKING user: >'TMP$C4811'< - in apostrophes, UPPER case + RESULT {tmp_user.name.upper()} + CHECKING user: >Tmp$c4811< - without delimiters, CaMeL case + RESULT {tmp_user.name.upper()} + CHECKING user: >TMP$C4811< - without delimiters, UPPER case + RESULT {tmp_user.name.upper()} + Statement failed, SQLSTATE = 28000 + Your user name and password are not defined. Ask your database administrator to set up a Firebird login. + CHECKING user: >"TMP$C4811" - in double quotes, UPPER case + RESULT {tmp_user.name.upper()} + CHECKING role: >'Boss'< - in apostrophes, CaMeL case + RESULT {tmp_role.name.upper()} + CHECKING role: >'BOSS'< - in apostrophes, UPPER case + RESULT {tmp_role.name.upper()} + CHECKING role: >BosS< - without delimiters, CaMeL case + RESULT {tmp_role.name.upper()} + CHECKING role: >BOSS< - without delimiters, UPPER case + RESULT {tmp_role.name.upper()} + Statement failed, SQLSTATE = HY000 + exception + -{SQL_SCHEMA_PREFIX}{EXCEPTION_NAME} + -You've specified role: >"BoSs"< -- but your actual role is NONE. + -At procedure {SQL_SCHEMA_PREFIX}{STORED_PROC_NAME} + CHECKING role: >"BOSS"< - in double quotes, UPPER case + RESULT {tmp_role.name.upper()} + """ + + act.expected_stdout = expected_stdout + act.isql(switches = ['-q'], input = test_script, combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4819_test.py b/tests/bugs/core_4819_test.py index 4f94d3a5..6412961f 100644 --- a/tests/bugs/core_4819_test.py +++ b/tests/bugs/core_4819_test.py @@ -3,11 +3,16 @@ """ ID: issue-5116 ISSUE: 5116 -TITLE: EXECUTE PROCEDURE's RETURNING_VALUES and EXECUTE STATEMENT's INTO does not - check validity of assignments targets leading to bugcheck +TITLE: EXECUTE PROCEDURE's RETURNING_VALUES and EXECUTE STATEMENT's INTO does not check validity of assignments targets leading to bugcheck DESCRIPTION: JIRA: CORE-4819 FBTEST: bugs.core_4819 +NOTES: + [30.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -108,7 +113,7 @@ new.y as new_y; select - 'after update' msg, + 'after update' msg, cast(rdb$get_context('USER_SESSION', 'X_TRG_AIUD1') as int) as X_TRG_AIUD1, cast(rdb$get_context('USER_SESSION', 'Y_TRG_AIUD1') as int) as Y_TRG_AIUD1, cast(rdb$get_context('USER_SESSION', 'X_TRG_AIUD2') as int) as X_TRG_AIUD2, @@ -185,21 +190,28 @@ rollback; """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ +fb3x_checked_stdout = """ MSG initial state ID 1 X 3 Y 7 - + Statement failed, SQLSTATE = 42000 + attempted update of read-only column + Statement failed, SQLSTATE = 42000 + attempted update of read-only column + Statement failed, SQLSTATE = 42000 + attempted update of read-only column MSG after delete X_TRG_AIUD1 Y_TRG_AIUD1 X_TRG_AIUD2 Y_TRG_AIUD2 - - MSG after update + Statement failed, SQLSTATE = 42000 + attempted update of read-only column + MSG after update X_TRG_AIUD1 Y_TRG_AIUD1 X_TRG_AIUD2 @@ -207,71 +219,85 @@ ID 1 X 3 Y 7 -""" - -# version: 3.0 - -expected_stderr_1 = """ - Statement failed, SQLSTATE = 42000 - attempted update of read-only column - - Statement failed, SQLSTATE = 42000 - attempted update of read-only column - Statement failed, SQLSTATE = 42000 attempted update of read-only column - - Statement failed, SQLSTATE = 42000 - attempted update of read-only column - - Statement failed, SQLSTATE = 42000 - attempted update of read-only column - Statement failed, SQLSTATE = 42000 attempted update of read-only column - Statement failed, SQLSTATE = 42000 attempted update of read-only column """ -@pytest.mark.version('>=3.0,<4.0') -def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr_1 - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - -# version: 4.0 - -expected_stderr_2 = """ +fb5x_checked_stdout = """ + MSG initial state + ID 1 + X 3 + Y 7 Statement failed, SQLSTATE = 42000 attempted update of read-only column TEST.X - Statement failed, SQLSTATE = 42000 attempted update of read-only column TEST.X - Statement failed, SQLSTATE = 42000 attempted update of read-only column TEST.X - + MSG after delete + X_TRG_AIUD1 + Y_TRG_AIUD1 + X_TRG_AIUD2 + Y_TRG_AIUD2 Statement failed, SQLSTATE = 42000 attempted update of read-only column TEST.X - + MSG after update + X_TRG_AIUD1 + Y_TRG_AIUD1 + X_TRG_AIUD2 + Y_TRG_AIUD2 + ID 1 + X 3 + Y 7 Statement failed, SQLSTATE = 42000 attempted update of read-only column TEST.X - Statement failed, SQLSTATE = 42000 attempted update of read-only column CE.Y - Statement failed, SQLSTATE = 42000 attempted update of read-only column CE.Y """ -@pytest.mark.version('>=4.0') -def test_2(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr_2 - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) +fb6x_checked_stdout = """ + MSG initial state + ID 1 + X 3 + Y 7 + Statement failed, SQLSTATE = 42000 + attempted update of read-only column "PUBLIC"."TEST"."X" + Statement failed, SQLSTATE = 42000 + attempted update of read-only column "PUBLIC"."TEST"."X" + Statement failed, SQLSTATE = 42000 + attempted update of read-only column "PUBLIC"."TEST"."X" + MSG after delete + X_TRG_AIUD1 + Y_TRG_AIUD1 + X_TRG_AIUD2 + Y_TRG_AIUD2 + Statement failed, SQLSTATE = 42000 + attempted update of read-only column "PUBLIC"."TEST"."X" + MSG after update + X_TRG_AIUD1 + Y_TRG_AIUD1 + X_TRG_AIUD2 + Y_TRG_AIUD2 + ID 1 + X 3 + Y 7 + Statement failed, SQLSTATE = 42000 + attempted update of read-only column "PUBLIC"."TEST"."X" + Statement failed, SQLSTATE = 42000 + attempted update of read-only column "CE"."Y" + Statement failed, SQLSTATE = 42000 + attempted update of read-only column "CE"."Y" +""" + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + act.expected_stdout = fb3x_checked_stdout if act.is_version('<4') else fb5x_checked_stdout if act.is_version('<6') else fb6x_checked_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4826_test.py b/tests/bugs/core_4826_test.py index 9a7a340b..47b88256 100644 --- a/tests/bugs/core_4826_test.py +++ b/tests/bugs/core_4826_test.py @@ -12,6 +12,10 @@ Added 'SQLSTATE' in substitutions: runtime error must not be filtered out by '?!(...)' pattern ("negative lookahead assertion", see https://docs.python.org/3/library/re.html#regular-expression-syntax). Added 'combine_output = True' in order to see SQLSTATE if any error occurs. + + [30.06.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -49,27 +53,29 @@ act = isql_act('db', test_script, substitutions = [ ('^((?!SQLSTATE|sqltype).)*$', ''), ('[ \t]+', ' ') ] ) -expected_stdout = """ - 01: sqltype: 500 SHORT Nullable scale: 0 subtype: 0 len: 2 - 02: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 - 03: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 - 04: sqltype: 580 INT64 Nullable scale: -4 subtype: 1 len: 8 - 05: sqltype: 480 DOUBLE Nullable scale: 0 subtype: 0 len: 8 - 06: sqltype: 482 FLOAT Nullable scale: 0 subtype: 0 len: 4 - 07: sqltype: 570 SQL DATE Nullable scale: 0 subtype: 0 len: 4 - 08: sqltype: 560 TIME Nullable scale: 0 subtype: 0 len: 4 - 09: sqltype: 510 TIMESTAMP Nullable scale: 0 subtype: 0 len: 8 - 10: sqltype: 32764 BOOLEAN Nullable scale: 0 subtype: 0 len: 1 - 11: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 4 charset: 4 UTF8 - 12: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 10 charset: 21 ISO8859_1 - 13: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 1 charset: 21 ISO8859_1 - 14: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 32760 charset: 21 ISO8859_1 - 15: sqltype: 520 BLOB Nullable scale: 0 subtype: 1 len: 8 charset: 52 WIN1251 - 16: sqltype: 520 BLOB Nullable scale: 0 subtype: 0 len: 8 -""" - @pytest.mark.version('>=3.0') def test_1(act: Action): + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'SYSTEM.' + expected_stdout = f""" + 01: sqltype: 500 SHORT Nullable scale: 0 subtype: 0 len: 2 + 02: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + 03: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + 04: sqltype: 580 INT64 Nullable scale: -4 subtype: 1 len: 8 + 05: sqltype: 480 DOUBLE Nullable scale: 0 subtype: 0 len: 8 + 06: sqltype: 482 FLOAT Nullable scale: 0 subtype: 0 len: 4 + 07: sqltype: 570 SQL DATE Nullable scale: 0 subtype: 0 len: 4 + 08: sqltype: 560 TIME Nullable scale: 0 subtype: 0 len: 4 + 09: sqltype: 510 TIMESTAMP Nullable scale: 0 subtype: 0 len: 8 + 10: sqltype: 32764 BOOLEAN Nullable scale: 0 subtype: 0 len: 1 + 11: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 4 charset: 4 {SQL_SCHEMA_PREFIX}UTF8 + 12: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 10 charset: 21 {SQL_SCHEMA_PREFIX}ISO8859_1 + 13: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 1 charset: 21 {SQL_SCHEMA_PREFIX}ISO8859_1 + 14: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 32760 charset: 21 {SQL_SCHEMA_PREFIX}ISO8859_1 + 15: sqltype: 520 BLOB Nullable scale: 0 subtype: 1 len: 8 charset: 52 {SQL_SCHEMA_PREFIX}WIN1251 + 16: sqltype: 520 BLOB Nullable scale: 0 subtype: 0 len: 8 + """ + act.expected_stdout = expected_stdout act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4831_test.py b/tests/bugs/core_4831_test.py index bef272a0..ce66cfb9 100644 --- a/tests/bugs/core_4831_test.py +++ b/tests/bugs/core_4831_test.py @@ -14,29 +14,39 @@ db = db_factory() -role = role_factory('db', name='r_20150608_20h03m') - -test_script = """ - set wng off; - -- create role r_20150608_20h03m; - -- commit; - revoke all on all from role r_20150608_20h03m; -- this was failed, possibly due to: http://sourceforge.net/p/firebird/code/61729 - commit; - show grants; - -- commit; - -- drop role r_20150608_20h03m; - --commit; -""" - -act = isql_act('db', test_script) - -expected_stderr = """ -There is no privilege granted in this database -""" +tmp_role = role_factory('db', name='r_20150608_20h03m') +act = isql_act('db') @pytest.mark.version('>=3.0') -def test_1(act: Action, role: Role): - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stdout == act.clean_expected_stdout and - act.clean_stderr == act.clean_expected_stderr) +def test_1(act: Action, tmp_role: Role): + + test_sql = f""" + set list on; + set count on; + set wng off; + + -- this failed, possibly by: http://sourceforge.net/p/firebird/code/61729 + -- alexpeshkoff 2015-06-04 + -- Postfix for CORE-4821: fixed segfault in REVOKE ALL ON ALL + revoke all on all from role {tmp_role.name}; + + commit; + select + g.rdb$privilege + ,g.rdb$grant_option + ,g.rdb$relation_name + ,g.rdb$user + ,g.rdb$object_type + from rdb$user_privileges g + join sec$users u on g.rdb$user = u.sec$user_name + where g.rdb$user = '{tmp_role.name.upper()}' + ; + """ + + act.expected_stdout = """ + Records affected: 0 + """ + + act.isql(input = test_sql, combine_output = True) + + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4836_test.py b/tests/bugs/core_4836_test.py index cc0e6bf4..5f964d9d 100644 --- a/tests/bugs/core_4836_test.py +++ b/tests/bugs/core_4836_test.py @@ -12,6 +12,12 @@ Added 'SQLSTATE' in substitutions: runtime error must not be filtered out by '?!(...)' pattern ("negative lookahead assertion", see https://docs.python.org/3/library/re.html#regular-expression-syntax). Added 'combine_output = True' in order to see SQLSTATE if any error occurs. + + [30.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -49,15 +55,22 @@ act = isql_act('db', test_script, substitutions=[('^((?!SQLSTATE|C4836|R4836).)*$', '')]) -expected_stdout = """ - GRANT UPDATE (TEXT) ON TEST TO USER TMP$C4836 WITH GRANT OPTION - GRANT UPDATE (TEXT) ON TEST TO ROLE TMP$R4836 GRANTED BY TMP$C4836 - GRANT UPDATE (TEXT) ON TEST TO USER TMP$C4836 WITH GRANT OPTION -""" - +@pytest.mark.intl @pytest.mark.version('>=3.0') def test_1(act: Action, tmp_user: User, tmp_role: Role): - act.expected_stdout = expected_stdout + + expected_stdout_5x = f""" + GRANT UPDATE (TEXT) ON TEST TO USER {tmp_user.name.upper()} WITH GRANT OPTION + GRANT UPDATE (TEXT) ON TEST TO ROLE {tmp_role.name.upper()} GRANTED BY {tmp_user.name.upper()} + GRANT UPDATE (TEXT) ON TEST TO USER {tmp_user.name.upper()} WITH GRANT OPTION + """ + + expected_stdout_6x = f""" + GRANT UPDATE (TEXT) ON PUBLIC.TEST TO USER {tmp_user.name.upper()} WITH GRANT OPTION + GRANT UPDATE (TEXT) ON PUBLIC.TEST TO ROLE {tmp_role.name.upper()} GRANTED BY {tmp_user.name.upper()} + GRANT UPDATE (TEXT) ON PUBLIC.TEST TO USER {tmp_user.name.upper()} WITH GRANT OPTION + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4839_test.py b/tests/bugs/core_4839_test.py index 43a0299c..d37631f3 100644 --- a/tests/bugs/core_4839_test.py +++ b/tests/bugs/core_4839_test.py @@ -7,6 +7,16 @@ DESCRIPTION: JIRA: CORE-4839 FBTEST: bugs.core_4839 +NOTES: + [15.05.2025] pzotov + Added substitutions in order to suppress excessive lines produced by 'SHOW GRANTS': + they may remain after some failed test teardown phases. + + [30.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -14,7 +24,7 @@ db = db_factory() -test_user = user_factory('db', name='tmp$c4839', password='123') +tmp_user = user_factory('db', name='tmp$c4839', password='123') test_script = """ recreate exception exc_foo 'Houston we have a problem: next sequence value is @1'; @@ -28,17 +38,22 @@ commit; """ -act = isql_act('db', test_script) +substitutions = [('^((?!USAGE ON (SEQUENCE|EXCEPTION)).)*$', '')] -expected_stdout = """ - /* Grant permissions for this database */ - GRANT USAGE ON SEQUENCE GEN_BAR TO USER TMP$C4839 - GRANT USAGE ON EXCEPTION EXC_FOO TO USER TMP$C4839 -""" +act = isql_act('db', test_script, substitutions = substitutions) @pytest.mark.version('>=3.0') -def test_1(act: Action, test_user: User): - act.expected_stdout = expected_stdout - act.execute() +def test_1(act: Action, tmp_user: User): + expected_stdout_5x = f""" + GRANT USAGE ON SEQUENCE GEN_BAR TO USER {tmp_user.name.upper()} + GRANT USAGE ON EXCEPTION EXC_FOO TO USER {tmp_user.name.upper()} + """ + + expected_stdout_6x = f""" + GRANT USAGE ON SEQUENCE PUBLIC.GEN_BAR TO USER {tmp_user.name.upper()} + GRANT USAGE ON EXCEPTION PUBLIC.EXC_FOO TO USER {tmp_user.name.upper()} + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4846_test.py b/tests/bugs/core_4846_test.py index 800e17a2..a8c10c0d 100644 --- a/tests/bugs/core_4846_test.py +++ b/tests/bugs/core_4846_test.py @@ -7,6 +7,10 @@ DESCRIPTION: JIRA: CORE-4846 FBTEST: bugs.core_4846 +NOTES: + [30.06.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -14,17 +18,33 @@ db = db_factory() -test_script = """ - set term ^; - execute block as + +TRG_BODY_01 = """ begin - execute statement 'drop trigger muddy_trg'; - when any do begin end + new.id2 = coalesce( new.id2, gen_id(g, 1) ); end - ^ - set term ;^ - commit; +""" + +TRG_BODY_02 = """ + begin + new.id1 = coalesce( new.id1, gen_id(g, 1) ); + end +""" + +TRG_BODY_03 = """ + begin + insert into test1(id1, x1) values(old.id2, old.x2); + end +""" + +TRG_BODY_04 = """ + begin + if (not inserting) then insert into test2(id2, x2) values(old.id1, old.x1); + else insert into test2(id2, x2) values(new.id1, new.x1); + end +""" +test_script = f""" recreate table test1( id1 int primary key, x1 int); recreate table test2( id2 int primary key, x2 int); recreate sequence g; @@ -32,9 +52,7 @@ set term ^; create or alter trigger muddy_trg for test2 active before insert position 23184 as - begin - new.id2 = coalesce( new.id2, gen_id(g, 1) ); - end + {TRG_BODY_01} ^ set term ;^ commit; @@ -49,9 +67,7 @@ set term ^; create or alter trigger muddy_trg for test1 inactive before insert position 17895 as - begin - new.id1 = coalesce( new.id1, gen_id(g, 1) ); - end + {TRG_BODY_02} ^ set term ;^ commit; @@ -59,9 +75,7 @@ set term ^; recreate trigger muddy_trg for test2 inactive after delete position 11133 as - begin - insert into test1(id1, x1) values(old.id2, old.x2); - end + {TRG_BODY_03} ^ set term ;^ commit; @@ -70,10 +84,7 @@ set term ^; recreate trigger muddy_trg for test1 active before delete or update or insert position 24187 as - begin - if (not inserting) then insert into test2(id2, x2) values(old.id1, old.x1); - else insert into test2(id2, x2) values(new.id1, new.x1); - end + {TRG_BODY_04} ^ set term ;^ commit; @@ -83,41 +94,34 @@ act = isql_act('db', test_script, substitutions=[('\\+.*', ''), ('\\=.*', ''), ('Trigger text.*', '')]) -expected_stdout = """ - Triggers on Table TEST2: - MUDDY_TRG, Sequence: 23184, Type: BEFORE INSERT, Active - as - begin - new.id2 = coalesce( new.id2, gen_id(g, 1) ); - end +@pytest.mark.version('>=3.0') +def test_1(act: Action): + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'PUBLIC.' + expected_stdout = f""" + Triggers on Table {SQL_SCHEMA_PREFIX}TEST2: + {SQL_SCHEMA_PREFIX}MUDDY_TRG, Sequence: 23184, Type: BEFORE INSERT, Active + as + {TRG_BODY_01} - Triggers on Table TEST1: - MUDDY_TRG, Sequence: 17895, Type: BEFORE INSERT, Inactive - as - begin - new.id1 = coalesce( new.id1, gen_id(g, 1) ); - end - Triggers on Table TEST2: - MUDDY_TRG, Sequence: 11133, Type: AFTER DELETE, Inactive - as - begin - insert into test1(id1, x1) values(old.id2, old.x2); - end + Triggers on Table {SQL_SCHEMA_PREFIX}TEST1: + {SQL_SCHEMA_PREFIX}MUDDY_TRG, Sequence: 17895, Type: BEFORE INSERT, Inactive + as + {TRG_BODY_02} - Triggers on Table TEST1: - MUDDY_TRG, Sequence: 24187, Type: BEFORE DELETE OR UPDATE OR INSERT, Active - as - begin - if (not inserting) then insert into test2(id2, x2) values(old.id1, old.x1); - else insert into test2(id2, x2) values(new.id1, new.x1); - end -""" + Triggers on Table {SQL_SCHEMA_PREFIX}TEST2: + {SQL_SCHEMA_PREFIX}MUDDY_TRG, Sequence: 11133, Type: AFTER DELETE, Inactive + as + {TRG_BODY_03} + + Triggers on Table {SQL_SCHEMA_PREFIX}TEST1: + {SQL_SCHEMA_PREFIX}MUDDY_TRG, Sequence: 24187, Type: BEFORE DELETE OR UPDATE OR INSERT, Active + as + {TRG_BODY_04} + """ -@pytest.mark.version('>=3.0') -def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4855_test.py b/tests/bugs/core_4855_test.py index 10addeef..6efbb513 100644 --- a/tests/bugs/core_4855_test.py +++ b/tests/bugs/core_4855_test.py @@ -3,11 +3,14 @@ """ ID: issue-5151 ISSUE: 5151 -TITLE: Online validation during DML activity in other connection leads to message - "Error while trying to read from file" and "page in use during flush (210), file: cch.cpp line: 2672" +TITLE: Online validation during DML activity in other connection leads to message "Error while trying to read from file" and "page in use during flush (210), file: cch.cpp line: 2672" DESCRIPTION: JIRA: CORE-4855 FBTEST: bugs.core_4855 +NOTES: + [30.06.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -25,48 +28,26 @@ act = python_act('db', substitutions=substitutions) -expected_stdout = """ -Iteration #1: -21:16:28.31 Validation started -21:16:28.31 Relation 128 (TEST) -21:16:29.31 Acquire relation lock failed -21:16:29.31 Relation 128 (TEST) : 1 ERRORS found -21:16:30.04 Relation 129 (STOP) -21:16:30.04 process pointer page 0 of 1 -21:16:30.04 Relation 129 (STOP) is ok -21:16:30.04 Validation finished -Iteration #2: -21:16:32.46 Validation started -21:16:32.46 Relation 128 (TEST) -21:16:33.46 Acquire relation lock failed -21:16:33.46 Relation 128 (TEST) : 1 ERRORS found -21:16:35.09 Relation 129 (STOP) -21:16:35.09 process pointer page 0 of 1 -21:16:35.09 Relation 129 (STOP) is ok -21:16:35.09 Validation finished -INSERTED_ROWS OK, LOT OF. -""" - heavy_script = """ -recreate sequence g; -recreate table test(id int, s varchar( 36 ) unique using index test_s_unq); -recreate table stop(id int); -commit; -set list on; -set transaction read committed; -set term ^; -execute block returns( inserted_rows varchar(20) ) as -begin - while ( not exists(select * from stop) ) do - begin - insert into test(id, s) values( gen_id(g,1), rpad('', 36, uuid_to_char(gen_uuid())) ); - end - inserted_rows = iif(gen_id(g,0) > 0, 'OK, LOT OF.', 'FAIL: ZERO!'); - suspend; -end -^ -set term ;^ -commit; + recreate sequence g; + recreate table test(id int, s varchar( 36 ) unique using index test_s_unq); + recreate table stop(id int); + commit; + set list on; + set transaction read committed; + set term ^; + execute block returns( inserted_rows varchar(20) ) as + begin + while ( not exists(select * from stop) ) do + begin + insert into test(id, s) values( gen_id(g,1), rpad('', 36, uuid_to_char(gen_uuid())) ); + end + inserted_rows = iif(gen_id(g,0) > 0, 'OK, LOT OF.', 'FAIL: ZERO!'); + suspend; + end + ^ + set term ;^ + commit; """ heavy_script_file = temp_file('heavy_script.sql') @@ -77,12 +58,15 @@ def test_1(act: Action, heavy_script_file: Path, heavy_output: Path, capsys): # Preparing script for ISQL that will do 'heavy DML' heavy_script_file.write_text(heavy_script) with open(heavy_output, mode='w') as heavy_out: + ############################################# + ### a s y n c l a u n c h i s q l ### + ############################################# p_heavy_sql = subprocess.Popen([act.vars['isql'], '-i', str(heavy_script_file), '-user', act.db.user, '-password', act.db.password, act.db.dsn], stdout=heavy_out, stderr=subprocess.STDOUT) try: - time.sleep(4) + time.sleep(4) # todo: reimplement this via query to mon$ tables every 0.1 second # Run validation twice with act.connect_server() as srv: print('Iteration #1:') @@ -99,6 +83,33 @@ def test_1(act: Action, heavy_script_file: Path, heavy_output: Path, capsys): print(heavy_output.read_text()) # Check act.reset() + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + TABLE_TEST_NAME = 'TEST' if act.is_version('<6') else f'{SQL_SCHEMA_PREFIX}"TEST"' + TABLE_STOP_NAME = 'STOP' if act.is_version('<6') else f'{SQL_SCHEMA_PREFIX}"STOP"' + + expected_stdout = f""" + Iteration #1: + 21:16:28.31 Validation started + 21:16:28.31 Relation 128 ({TABLE_TEST_NAME}) + 21:16:29.31 Acquire relation lock failed + 21:16:29.31 Relation 128 ({TABLE_TEST_NAME}) : 1 ERRORS found + 21:16:30.04 Relation 129 ({TABLE_STOP_NAME}) + 21:16:30.04 process pointer page 0 of 1 + 21:16:30.04 Relation 129 ({TABLE_STOP_NAME}) is ok + 21:16:30.04 Validation finished + Iteration #2: + 21:16:32.46 Validation started + 21:16:32.46 Relation 128 ({TABLE_TEST_NAME}) + 21:16:33.46 Acquire relation lock failed + 21:16:33.46 Relation 128 ({TABLE_TEST_NAME}) : 1 ERRORS found + 21:16:35.09 Relation 129 ({TABLE_STOP_NAME}) + 21:16:35.09 process pointer page 0 of 1 + 21:16:35.09 Relation 129 ({TABLE_STOP_NAME}) is ok + 21:16:35.09 Validation finished + INSERTED_ROWS OK, LOT OF. + """ + act.expected_stdout = expected_stdout act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4881_test.py b/tests/bugs/core_4881_test.py index 5d8d4c1c..e9c78ece 100644 --- a/tests/bugs/core_4881_test.py +++ b/tests/bugs/core_4881_test.py @@ -34,6 +34,7 @@ C_LEN_UTF8_MIXED 16383 """ +@pytest.mark.intl @pytest.mark.version('>=3.0') def test_1(act: Action): script_file = Path(act.files_dir / 'core_4881.zip', at='core_4881_script.sql') diff --git a/tests/bugs/core_4884_test.py b/tests/bugs/core_4884_test.py index 69c56adf..ba3c1967 100644 --- a/tests/bugs/core_4884_test.py +++ b/tests/bugs/core_4884_test.py @@ -9,6 +9,10 @@ Batch file that generates .sql with arbitrary level of begin..end statements can be seen in the traker. JIRA: CORE-4884 FBTEST: bugs.core_4884 +NOTES: + [30.06.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -1568,16 +1572,19 @@ substitutions=[('exception [0-9]+', 'exception'), ('time=.*', ''), ('-At block line: [\\d]+, col: [\\d]+', '-At block line')]) -expected_stderr = """ - Statement failed, SQLSTATE = HY000 - exception 4 - -EX_TEST - -Hi from Mariana Trench, depth=511, time=2015-08-24 13:47:25.1330 - -At block line: 1026, col: 5 -""" - @pytest.mark.version('>=3.0') def test_1(act: Action): + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + EXCEPTION_NAME = 'EX_TEST' if act.is_version('<6') else f'{SQL_SCHEMA_PREFIX}"EX_TEST"' + expected_stderr = f""" + Statement failed, SQLSTATE = HY000 + exception 4 + -{EXCEPTION_NAME} + -Hi from Mariana Trench, depth=511, time=2015-08-24 13:47:25.1330 + -At block line: 1026, col: 5 + """ + act.expected_stderr = expected_stderr act.execute() assert act.clean_stderr == act.clean_expected_stderr diff --git a/tests/bugs/core_4889_test.py b/tests/bugs/core_4889_test.py index 8cc7dfd0..c1d0d5bf 100644 --- a/tests/bugs/core_4889_test.py +++ b/tests/bugs/core_4889_test.py @@ -54,6 +54,7 @@ 'log_errors = true', ] +@pytest.mark.trace @pytest.mark.version('>=3.0') def test_1(act: Action, capsys): with act.trace(db_events=trace): diff --git a/tests/bugs/core_4904_test.py b/tests/bugs/core_4904_test.py index 18d7bafa..b873e20e 100644 --- a/tests/bugs/core_4904_test.py +++ b/tests/bugs/core_4904_test.py @@ -14,6 +14,10 @@ 4. Start validation of database: index should NOT be corrupted in its report. JIRA: CORE-4904 FBTEST: bugs.core_4904 +NOTES: + [30.06.2025] pzotov + Test can not be executed on FB 6.x for suitable time because minimal page_size in this version is 8192. + Number of rows that is needed to be inserted in index with key length = ~2K is too large. """ import pytest @@ -61,7 +65,7 @@ Validation finished """ -@pytest.mark.version('>=3.0') +@pytest.mark.version('>=3.0,<6') def test_1(act: Action, capsys): # Move database to FW = OFF in order to increase speed of insertions and output its header info: with act.connect_server() as srv: diff --git a/tests/bugs/core_4905_test.py b/tests/bugs/core_4905_test.py index bfe6ec73..f117c490 100644 --- a/tests/bugs/core_4905_test.py +++ b/tests/bugs/core_4905_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4905 FBTEST: bugs.core_4905 +NOTES: + [30.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -29,15 +35,17 @@ where p.rdb$procedure_name = upper('SP_TEST'); """ -act = isql_act('db', test_script, substitutions=[('SP_BLR_BLOB.*', '')]) +act = isql_act('db', test_script, substitutions = [ ('[ \t]+', ' '), ('SP_BLR_BLOB.*', '')] ) -expected_stdout = """ +expected_stdout_5x = """ blr_version5,blr_begin, blr_message, 0, 2,0, blr_column_name, 0, 9, 'R','D','B','$','T','Y','P','E','S', 14, 'R','D','B','$','F','I','E','L','D','_','N','A','M','E', blr_short, 0, blr_message, 1, 1,0, blr_short, 0, blr_receive, 0, blr_begin, blr_stall, blr_label, 0, blr_begin, blr_end, blr_end, blr_send, 1, blr_begin, blr_assignment, blr_literal, blr_short, 0, 0,0, blr_parameter, 1, 0,0, blr_end, blr_end, blr_eoc """ +expected_stdout_6x = """ + blr_version5,blr_begin, blr_message, 0, 2,0, blr_column_name3, 0, 6, 'S','Y','S','T','E','M', 9, 'R','D','B','$','T','Y','P','E','S', 14, 'R','D','B','$','F','I','E','L','D','_','N','A','M','E',0, blr_short, 0, blr_message, 1, 1,0, blr_short, 0, blr_receive, 0, blr_begin, blr_stall, blr_label, 0, blr_begin, blr_end, blr_end, blr_send, 1, blr_begin, blr_assignment, blr_literal, blr_short, 0, 0,0, blr_parameter, 1, 0,0, blr_end, blr_end, blr_eoc +""" @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4917_test.py b/tests/bugs/core_4917_test.py index 9a520da9..ccb4c204 100644 --- a/tests/bugs/core_4917_test.py +++ b/tests/bugs/core_4917_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-4917 FBTEST: bugs.core_4917 +NOTES: + [30.06.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' and variables to store domain names - to be substituted in expected_* on FB 6.x + Removed 'SHOW DOMAIN' command as its output has no matter for this test. + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -45,47 +50,41 @@ -- this also should FAIL becase new domain name is written in UPPER case (despite quotes): alter domain "rdb$2" to "RDB$3"; - show domain; - """ act = isql_act('db', test_script) -expected_stdout = """ - rdb$2 -""" - -expected_stderr = """ - Statement failed, SQLSTATE = 42000 - unsuccessful metadata update - -CREATE DOMAIN RDB$1 failed - -SQL error code = -637 - -Implicit domain name RDB$1 not allowed in user created domain - - Statement failed, SQLSTATE = 42000 - unsuccessful metadata update - -CREATE DOMAIN RDB$2 failed - -SQL error code = -637 - -Implicit domain name RDB$2 not allowed in user created domain - - Statement failed, SQLSTATE = 42000 - unsuccessful metadata update - -ALTER DOMAIN rdb$2 failed - -SQL error code = -637 - -Implicit domain name RDB$3 not allowed in user created domain - - Statement failed, SQLSTATE = 42000 - unsuccessful metadata update - -ALTER DOMAIN rdb$2 failed - -SQL error code = -637 - -Implicit domain name RDB$3 not allowed in user created domain -""" - @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + DOMAIN_1_UPPER = 'RDB$1' if act.is_version('<6') else '"RDB$1"' + DOMAIN_2_UPPER = 'RDB$2' if act.is_version('<6') else '"RDB$2"' + DOMAIN_2_LOWER = 'rdb$2' if act.is_version('<6') else '"rdb$2"' + + expected_stdout = f""" + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -CREATE DOMAIN {SQL_SCHEMA_PREFIX}{DOMAIN_1_UPPER} failed + -SQL error code = -637 + -Implicit domain name {SQL_SCHEMA_PREFIX}{DOMAIN_1_UPPER} not allowed in user created domain + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -CREATE DOMAIN {SQL_SCHEMA_PREFIX}{DOMAIN_2_UPPER} failed + -SQL error code = -637 + -Implicit domain name {SQL_SCHEMA_PREFIX}{DOMAIN_2_UPPER} not allowed in user created domain + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -ALTER DOMAIN {SQL_SCHEMA_PREFIX}{DOMAIN_2_LOWER} failed + -SQL error code = -637 + -Implicit domain name RDB$3 not allowed in user created domain + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -ALTER DOMAIN {SQL_SCHEMA_PREFIX}{DOMAIN_2_LOWER} failed + -SQL error code = -637 + -Implicit domain name RDB$3 not allowed in user created domain + """ + + act.expected_stdout = expected_stdout # _5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4921_test.py b/tests/bugs/core_4921_test.py index 16e593b2..f8fd2113 100644 --- a/tests/bugs/core_4921_test.py +++ b/tests/bugs/core_4921_test.py @@ -5,12 +5,20 @@ ISSUE: 1956 TITLE: Predicate IS [NOT] DISTINCT FROM is not pushed into unions/aggregates thus causing sub-optimal plans DESCRIPTION: - Implementation for 3.0 does NOT use 'set explain on' (decision after discuss with Dmitry, letter 02-sep-2015 15:42). - Test only checks that: - 1) in case when NATURAL scan occured currently index T*_SINGLE_X is used; - 2) in case when it was only PARTIAL matching index Y*_COMPOUND_X is in use. + Implementation for 3.0 does NOT use 'set explain on' (decision after discuss with Dmitry, letter 02-sep-2015 15:42). + Test only checks that: + 1) in case when NATURAL scan occured currently index T*_SINGLE_X is used; + 2) in case when it was only PARTIAL matching index Y*_COMPOUND_X is in use. JIRA: CORE-4921 FBTEST: bugs.core_4921 +NOTES: + [30.06.2025] pzotov + Re-implemented. Explained form is used for all checked FB versions, including 3.x + + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -42,25 +50,137 @@ commit; """ -db = db_factory(init=init_script) +db = db_factory(init = init_script) -test_script = """ - set planonly; - select * from v_test where x is not distinct from 1; - select * from v_test where x = 1 and y is not distinct from 1; - set planonly; -""" +act = python_act('db', substitutions=[('record length.*', ''), ('key length.*', '')]) -act = isql_act('db', test_script) +#----------------------------------------------------------- -expected_stdout = """ - PLAN (V_TEST T1 INDEX (T1_SINGLE_X), V_TEST T2 INDEX (T2_SINGLE_X), V_TEST T3 INDEX (T3_SINGLE_X)) - PLAN (V_TEST T1 INDEX (T1_COMPOUND_X_Y), V_TEST T2 INDEX (T2_COMPOUND_X_Y), V_TEST T3 INDEX (T3_COMPOUND_X_Y)) -""" +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- @pytest.mark.version('>=3.0') -def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() - assert act.clean_stdout == act.clean_expected_stdout +def test_1(act: Action, capsys): + qry_map = { + 1000 : + """ + select * from v_test where x is not distinct from 1 + """ + , + 2000 : + """ + select * from v_test where x = 1 and y is not distinct from 1 + """ + } + + with act.db.connect() as con: + cur = con.cursor() + + for k, v in qry_map.items(): + ps, rs = None, None + try: + ps = cur.prepare(v) + print(v) + # Print explained plan with padding eash line by dots in order to see indentations: + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + print('') + + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + #rs = cur.execute(ps) + #for r in rs: + # print(r[0], r[1]) + except DatabaseError as e: + print(e.__str__()) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() + + + expected_stdout_5x = f""" + {qry_map[1000]} + Select Expression + ....-> Filter + ........-> Union + ............-> Filter + ................-> Table "T1" as "V_TEST T1" Access By ID + ....................-> Bitmap + ........................-> Index "T1_SINGLE_X" Range Scan (full match) + ............-> Filter + ................-> Table "T2" as "V_TEST T2" Access By ID + ....................-> Bitmap + ........................-> Index "T2_SINGLE_X" Range Scan (full match) + ............-> Filter + ................-> Table "T3" as "V_TEST T3" Access By ID + ....................-> Bitmap + ........................-> Index "T3_SINGLE_X" Range Scan (full match) + + + {qry_map[2000]} + Select Expression + ....-> Filter + ........-> Union + ............-> Filter + ................-> Table "T1" as "V_TEST T1" Access By ID + ....................-> Bitmap + ........................-> Index "T1_COMPOUND_X_Y" Range Scan (full match) + ............-> Filter + ................-> Table "T2" as "V_TEST T2" Access By ID + ....................-> Bitmap + ........................-> Index "T2_COMPOUND_X_Y" Range Scan (full match) + ............-> Filter + ................-> Table "T3" as "V_TEST T3" Access By ID + ....................-> Bitmap + ........................-> Index "T3_COMPOUND_X_Y" Range Scan (full match) + """ + + expected_stdout_6x = f""" + {qry_map[1000]} + Select Expression + ....-> Filter + ........-> Union + ............-> Filter + ................-> Table "PUBLIC"."T1" as "PUBLIC"."V_TEST" "PUBLIC"."T1" Access By ID + ....................-> Bitmap + ........................-> Index "PUBLIC"."T1_SINGLE_X" Range Scan (full match) + ............-> Filter + ................-> Table "PUBLIC"."T2" as "PUBLIC"."V_TEST" "PUBLIC"."T2" Access By ID + ....................-> Bitmap + ........................-> Index "PUBLIC"."T2_SINGLE_X" Range Scan (full match) + ............-> Filter + ................-> Table "PUBLIC"."T3" as "PUBLIC"."V_TEST" "PUBLIC"."T3" Access By ID + ....................-> Bitmap + ........................-> Index "PUBLIC"."T3_SINGLE_X" Range Scan (full match) + + {qry_map[2000]} + Select Expression + ....-> Filter + ........-> Union + ............-> Filter + ................-> Table "PUBLIC"."T1" as "PUBLIC"."V_TEST" "PUBLIC"."T1" Access By ID + ....................-> Bitmap + ........................-> Index "PUBLIC"."T1_COMPOUND_X_Y" Range Scan (full match) + ............-> Filter + ................-> Table "PUBLIC"."T2" as "PUBLIC"."V_TEST" "PUBLIC"."T2" Access By ID + ....................-> Bitmap + ........................-> Index "PUBLIC"."T2_COMPOUND_X_Y" Range Scan (full match) + ............-> Filter + ................-> Table "PUBLIC"."T3" as "PUBLIC"."V_TEST" "PUBLIC"."T3" Access By ID + ....................-> Bitmap + ........................-> Index "PUBLIC"."T3_COMPOUND_X_Y" Range Scan (full match) + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4927_test.py b/tests/bugs/core_4927_test.py index 58a60437..3d0c1534 100644 --- a/tests/bugs/core_4927_test.py +++ b/tests/bugs/core_4927_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-4927 FBTEST: bugs.core_4927 +NOTES: + [30.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -84,14 +90,14 @@ db = db_factory(init=init_script) -act = python_act('db', substitutions=[('^((?!HEADER_|DETAIL_).)*$', ''), - ('HEADER_2100.*', 'HEADER_2100'), - ('DETAIL_2100.*', 'DETAIL_2100')]) +substitutions = [ + ('^((?!HEADER_|DETAIL_).)*$', ''), + ('(")?HEADER_2100(")?.*', 'HEADER_2100'), + ('(")?DETAIL_2100(")?.*', 'DETAIL_2100'), + ('"PUBLIC"', 'PUBLIC') +] -expected_stdout = """ - HEADER_2100 - DETAIL_2100 -""" +act = python_act('db', substitutions = substitutions) trace = ['time_threshold = 0', 'log_initfini = false', @@ -99,11 +105,22 @@ 'print_perf = true', ] +@pytest.mark.trace @pytest.mark.version('>=3.0') def test_1(act: Action, capsys): with act.trace(db_events=trace): act.isql(switches=[], input='set list on; select result from sp_test;') - # Check - act.expected_stdout = expected_stdout + + expected_stdout_5x = """ + HEADER_2100 + DETAIL_2100 + """ + + expected_stdout_6x = """ + PUBLIC.HEADER_2100 + PUBLIC.DETAIL_2100 + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.trace_to_stdout() assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4933_test.py b/tests/bugs/core_4933_test.py index d52a74b3..20259aca 100644 --- a/tests/bugs/core_4933_test.py +++ b/tests/bugs/core_4933_test.py @@ -129,90 +129,94 @@ @pytest.mark.version('>=3.0.6') def test_1(act: Action, addi_script: Path, main_script: Path, tmp_db: Path): - addi_script.write_text(f""" - create database 'localhost:{tmp_db}' user {act.db.user} password '{act.db.password}'; - - recreate view v_check as - select - decode(t.mon$isolation_mode, 0,'consistency', 1,'snapshot', 2,'rc rec_vers', 3,'rc no_recv', 4,'rc read_cons', 'UNKNOWN') as tx_til_mon_trans, - rdb$get_context('SYSTEM', 'ISOLATION_LEVEL') as tx_til_rdb_get_context, - decode(t.mon$lock_timeout, -1, 'wait', 0, 'no_wait', 'timeout ' || t.mon$lock_timeout) as tx_lock_timeout_mon_trans, - rdb$get_context('SYSTEM', 'LOCK_TIMEOUT') as tx_lock_timeout_rdb_get_context, - iif(t.mon$read_only=1,'read_only','read_write') as tx_read_only_mon_trans, - rdb$get_context('SYSTEM', 'READ_ONLY') as tx_read_only_rdb_get_context, - t.mon$auto_undo as tx_autoundo_mon_trans - -- only in FB 4.x+: ,t.mon$auto_commit as tx_autocommit_mon_trans - from mon$transactions t - where t.mon$transaction_id = current_transaction; - commit; - - select 'addi_script: create_new_db' as msg, v.* from v_check v; - rollback; - - connect 'localhost:{tmp_db}' user {act.db.user} password '{act.db.password}'; - select 'addi_script: reconnect' as msg, v.* from v_check v; - rollback; - - drop database; - """) - main_script.write_text(f""" - set list on; - connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; - recreate view v_check as - select - decode(t.mon$isolation_mode, 0,'consistency', 1,'snapshot', 2,'rc rec_vers', 3,'rc no_recv', 4,'rc read_cons', 'UNKNOWN') as tx_til_mon_trans, - rdb$get_context('SYSTEM', 'ISOLATION_LEVEL') as tx_til_rdb_get_context, - decode(t.mon$lock_timeout, -1, 'wait', 0, 'no_wait', 'timeout ' || t.mon$lock_timeout) as tx_lock_timeout_mon_trans, - rdb$get_context('SYSTEM', 'LOCK_TIMEOUT') as tx_lock_timeout_rdb_get_context, - iif(t.mon$read_only=1,'read_only','read_write') as tx_read_only_mon_trans, - rdb$get_context('SYSTEM', 'READ_ONLY') as tx_read_only_rdb_get_context, - t.mon$auto_undo as tx_autoundo_mon_trans - -- only 4.x: ,t.mon$auto_commit as tx_autocommit_mon_trans - from mon$transactions t - where t.mon$transaction_id = current_transaction; - commit; - - select 'main_script: initial' as msg, v.* from v_check v; - commit; - - set keep_tran on; - commit; - - set transaction read only read committed record_version lock timeout 5 no auto undo; -- only in 4.x: auto commit; - - select 'main_script: started Tx' as msg, v.* from v_check v; - - commit; -------------------------------------------------------------------------------------- [ 1 ] - - select 'main_script: after_commit' as msg, v.* from v_check v; - - rollback; ------------------------------------------------------------------------------------ [ 2 ] - - select 'main_script: after_rollback' as msg, v.* from v_check v; - - rollback; - - connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; --------------------------- [ 3 ] - - select 'main_script: after_reconnect' as msg, v.* from v_check v; - rollback; - - --################### - in {addi_script}; - --################### - - connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; --------------------------- [ 5 ] - - select 'main_script: resume' as msg, v.* from v_check v; - rollback; - - set keep_tran off; - commit; - - select 'keep_tran: turned_off' as msg, v.* from v_check v; - commit; - """) + addi_script.write_text( + f""" + create database 'localhost:{tmp_db}' user {act.db.user} password '{act.db.password}'; + + recreate view v_check as + select + decode(t.mon$isolation_mode, 0,'consistency', 1,'snapshot', 2,'rc rec_vers', 3,'rc no_recv', 4,'rc read_cons', 'UNKNOWN') as tx_til_mon_trans, + rdb$get_context('SYSTEM', 'ISOLATION_LEVEL') as tx_til_rdb_get_context, + decode(t.mon$lock_timeout, -1, 'wait', 0, 'no_wait', 'timeout ' || t.mon$lock_timeout) as tx_lock_timeout_mon_trans, + rdb$get_context('SYSTEM', 'LOCK_TIMEOUT') as tx_lock_timeout_rdb_get_context, + iif(t.mon$read_only=1,'read_only','read_write') as tx_read_only_mon_trans, + rdb$get_context('SYSTEM', 'READ_ONLY') as tx_read_only_rdb_get_context, + t.mon$auto_undo as tx_autoundo_mon_trans + -- only in FB 4.x+: ,t.mon$auto_commit as tx_autocommit_mon_trans + from mon$transactions t + where t.mon$transaction_id = current_transaction; + commit; + + select 'addi_script: create_new_db' as msg, v.* from v_check v; + rollback; + + connect 'localhost:{tmp_db}' user {act.db.user} password '{act.db.password}'; + select 'addi_script: reconnect' as msg, v.* from v_check v; + rollback; + + drop database; + """ + ) + main_script.write_text( + f""" + set list on; + connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; + recreate view v_check as + select + decode(t.mon$isolation_mode, 0,'consistency', 1,'snapshot', 2,'rc rec_vers', 3,'rc no_recv', 4,'rc read_cons', 'UNKNOWN') as tx_til_mon_trans, + rdb$get_context('SYSTEM', 'ISOLATION_LEVEL') as tx_til_rdb_get_context, + decode(t.mon$lock_timeout, -1, 'wait', 0, 'no_wait', 'timeout ' || t.mon$lock_timeout) as tx_lock_timeout_mon_trans, + rdb$get_context('SYSTEM', 'LOCK_TIMEOUT') as tx_lock_timeout_rdb_get_context, + iif(t.mon$read_only=1,'read_only','read_write') as tx_read_only_mon_trans, + rdb$get_context('SYSTEM', 'READ_ONLY') as tx_read_only_rdb_get_context, + t.mon$auto_undo as tx_autoundo_mon_trans + -- only 4.x: ,t.mon$auto_commit as tx_autocommit_mon_trans + from mon$transactions t + where t.mon$transaction_id = current_transaction; + commit; + + select 'main_script: initial' as msg, v.* from v_check v; + commit; + + set keep_tran on; + commit; + + set transaction read only read committed record_version lock timeout 5 no auto undo; -- only in 4.x: auto commit; + + select 'main_script: started Tx' as msg, v.* from v_check v; + + commit; -------------------------------------------------------------------------------------- [ 1 ] + + select 'main_script: after_commit' as msg, v.* from v_check v; + + rollback; ------------------------------------------------------------------------------------ [ 2 ] + + select 'main_script: after_rollback' as msg, v.* from v_check v; + + rollback; + + connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; --------------------------- [ 3 ] + + select 'main_script: after_reconnect' as msg, v.* from v_check v; + rollback; + + --################### + in {addi_script}; + --################### + + connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; --------------------------- [ 5 ] + + select 'main_script: resume' as msg, v.* from v_check v; + rollback; + + set keep_tran off; + commit; + + select 'keep_tran: turned_off' as msg, v.* from v_check v; + commit; + """ + ) # Check act.expected_stdout = expected_stdout - act.isql(switches=['-q'], input_file=main_script) + act.isql(switches = ['-q'], input_file = main_script, connect_db = False, combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4940_test.py b/tests/bugs/core_4940_test.py index add53583..2a2f81ef 100644 --- a/tests/bugs/core_4940_test.py +++ b/tests/bugs/core_4940_test.py @@ -19,44 +19,78 @@ db = db_factory() -test_script = """ - -- 1. Specify 'deterministic' flag - it should be reflected in SHOW command: - set term ^; - create or alter function fn_infinity returns bigint deterministic as +FUNC_DDL_1 = """ begin return 9223372036854775807; end +""" + +FUNC_DDL_2 = """ + begin + return rand() * 9223372036854775807; + end +""" + +test_script = f""" + -- 1. Specify 'deterministic' flag - it should be reflected in SHOW command: + set term ^; + create or alter function fn_test returns bigint deterministic as + {FUNC_DDL_1} ^ set term ;^ commit; - show function fn_infinity; + show function fn_test; -- 2. Remove 'deterministic' flag - it also should be reflected in SHOW command: set term ^; - alter function fn_infinity returns bigint as - begin - return 9223372036854775807; - end + alter function fn_test returns bigint as + {FUNC_DDL_2} ^ set term ;^ commit; - show function fn_infinity; + show function fn_test; """ -act = isql_act('db', test_script, substitutions=[('^((?!SQLSTATE|[Ff]unction|[Dd]eterministic).)*$', '')]) - -expected_stdout = """ - Deterministic function - Function text: - - Function text: -""" +substitutions = [('===.*', '')] +act = isql_act('db', test_script, substitutions = substitutions) @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout + + expected_stdout_5x = f""" + Deterministic function + Function text: + {FUNC_DDL_1} + + Parameters: + OUTPUT BIGINT + + Function text: + {FUNC_DDL_2} + + Parameters: + OUTPUT BIGINT + """ + + expected_stdout_6x = f""" + Function: PUBLIC.FN_TEST + Deterministic function + Function text: + {FUNC_DDL_1} + + Parameters: + OUTPUT BIGINT + + Function: PUBLIC.FN_TEST + Function text: + {FUNC_DDL_2} + + Parameters: + OUTPUT BIGINT + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_4964_test.py b/tests/bugs/core_4964_test.py index 69ea1246..efb77963 100644 --- a/tests/bugs/core_4964_test.py +++ b/tests/bugs/core_4964_test.py @@ -1,140 +1,205 @@ -#coding:utf-8 - -""" -ID: issue-5255 -ISSUE: 5255 -JIRA: CORE-4964 -FBTEST: bugs.core_4964 -TITLE: Real errors during connect to security database are hidden by Srp user manager. Errors should be logged no matter what AuthServer is used -DESCRIPTION: - Test does following: - 1) creates temporary user using plugin Srp (in order to avoid occasional connect as SYSDBA using Legacy plugin); - 2) makes copy of test DB to file which is specified n databases.conf as database for alias defined by variable with name REQUIRED_ALIAS - (its value: 'tmp_4964_alias'; test will try to connect to this file via ALIAS from pre-created databases.conf); - 3) uses pre-created databases.conf which has alias and SecurityDatabase parameter in its details. - This parameter that points to existing file that for sure can NOT be a Firebird database - (file $(dir_conf)/firebird.msg is used for this purpose). - - Then we: - 1) obtain content of server firebird.log; - 2) try to make connect to alias and (as expected) get error; - 3) obtain again content of server firebird.log and compare to origin one. - -NOTES: - [02.08.2022] pzotov - 1. One need to be sure that firebird.conf does NOT contain DatabaseAccess = None. - 2. Value of REQUIRED_ALIAS must be EXACTLY the same as alias specified in the pre-created databases.conf - (for LINUX this equality is case-sensitive, even when aliases are compared!) - 3. Make sure that firebird was launched by user who is currently runs this test. - Otherwise shutil.copy2() failes with "[Errno 13] Permission denied". - 4. Content of databases.conf must be taken from $QA_ROOT/files/qa-databases.conf (one need to replace it before every test session). - Discussed with pcisar, letters since 30-may-2022 13:48, subject: - "new qa, core_4964_test.py: strange outcome when use... shutil.copy() // comparing to shutil.copy2()" - - Checked on 5.0.0.591, 4.0.1.2692, 3.0.8.33535 - both on Windows and Linux. -""" - -import re -import time -from pathlib import Path -from difflib import unified_diff - -import pytest -from firebird.qa import * - -substitutions = [('[ \t]+', ' '), ('file .* is not a valid database', 'file is not a valid database'), ] - -REQUIRED_ALIAS = 'tmp_core_4964_alias' - -db = db_factory() -act = python_act('db', substitutions=substitutions) -tmp_user = user_factory('db', name='tmp$c4964', password='123', plugin = 'Srp') - -expected_stdout_isql = """ - Statement failed, SQLSTATE = 08006 - Error occurred during login, please check server firebird.log for details -""" - -expected_stdout_log_diff = """ - Authentication error - file is not a valid database -""" - -@pytest.mark.version('>=3.0') -def test_1(act: Action, tmp_user: User, capsys): - - fblog_1 = act.get_firebird_log() - - # Scan line-by-line through databases.conf, find line starting with REQUIRED_ALIAS and extract name of file that - # must be created in the $(dir_sampleDb)/qa/ folder. This name will be used further as target database (tmp_fdb). - # NOTE: we have to SKIP lines which are commented out, i.e. if they starts with '#': - p_required_alias_ptn = re.compile( '^(?!#)((^|\\s+)' + REQUIRED_ALIAS + ')\\s*=\\s*\\$\\(dir_sampleDb\\)/qa/', re.IGNORECASE ) - fname_in_dbconf = None - - with open(act.home_dir/'databases.conf', 'r') as f: - for line in f: - if p_required_alias_ptn.search(line): - # If databases.conf contains line like this: - # tmp_4964_alias = $(dir_sampleDb)/qa/tmp_qa_4964.fdb - # - then we extract filename: 'tmp_qa_4964.fdb' (see below): - fname_in_dbconf = Path(line.split('=')[1].strip()).name - break - - # if 'fname_in_dbconf' remains undefined here then propably REQUIRED_ALIAS not equals to specified in the databases.conf! - # - assert fname_in_dbconf - - # Full path + filename of database to which we will try to connect: - # - tmp_fdb = Path( act.vars['sample_dir'], 'qa', fname_in_dbconf ) - - # PermissionError: [Errno 13] Permission denied --> probably because - # Firebird was started by root rather than current (non-privileged) user. - # - tmp_fdb.write_bytes(act.db.db_path.read_bytes()) - - check_sql = f''' - set bail on; - set list on; - connect '{act.host+":" if act.host else ""}{tmp_fdb}' user {tmp_user.name} password {tmp_user.password}; - -- This can occus only if we databases.conf contains {REQUIRED_ALIAS} - -- but without reference to invalid security DB (e.g., alias without curly braces at all): - select mon$database_name as "UNEXPECTED CONNECTION:" from mon$database; - quit; - ''' - - ############################################################################################################### - # POINT-1: check that ISQL raises: - # "SQLSTATE = 08006 / Error occurred during login, please check server firebird.log ..." - # - act.expected_stdout = expected_stdout_isql - try: - act.isql(switches = ['-q'], input = check_sql, connect_db=False, credentials = False, combine_output = True) - finally: - tmp_fdb.unlink() - - assert act.clean_stdout == act.clean_expected_stdout - act.reset() - - time.sleep(1) # Allow content of firebird log be fully flushed on disk. - fblog_2 = act.get_firebird_log() - - diff_patterns = [ - "\\+\\s+Authentication error", - "\\+\\s+file .* is not a valid database", - ] - diff_patterns = [re.compile(s) for s in diff_patterns] - - for line in unified_diff(fblog_1, fblog_2): - if line.startswith('+'): - if act.match_any(line, diff_patterns): - print(line.split('+')[-1]) - - ############################################################################################################### - # POINT-2: check that diff between firebird.log initial and current content has phrases - # 'Authentication error' and '... file is not a valid database': - # - act.expected_stdout = expected_stdout_log_diff - act.stdout = capsys.readouterr().out - assert act.clean_stdout == act.clean_expected_stdout - act.reset() +#coding:utf-8 + +""" +ID: issue-5255 +ISSUE: 5255 +JIRA: CORE-4964 +FBTEST: bugs.core_4964 +TITLE: Real errors during connect to security database are hidden by Srp user manager. Errors should be logged no matter what AuthServer is used +DESCRIPTION: + Test does following: + 1) creates temporary user using plugin Srp (in order to avoid occasional connect as SYSDBA using Legacy plugin); + 2) makes copy of test DB to file which is specified n databases.conf as database for alias defined by variable with name REQUIRED_ALIAS + (its value: 'tmp_4964_alias'; test will try to connect to this file via ALIAS from pre-created databases.conf); + 3) uses pre-created databases.conf which has alias and SecurityDatabase parameter in its details. + This parameter that points to existing file that for sure can NOT be a Firebird database + (file $(dir_conf)/firebird.msg is used for this purpose). + + Then we: + 1) obtain content of server firebird.log; + 2) try to make connect to alias and (as expected) get error; + 3) obtain again content of server firebird.log and compare to origin one. + +NOTES: + [02.08.2022] pzotov + 1. One need to be sure that firebird.conf does NOT contain DatabaseAccess = None. + 2. Value of REQUIRED_ALIAS must be EXACTLY the same as alias specified in the pre-created databases.conf + (for LINUX this equality is case-sensitive, even when aliases are compared!) + 3. Make sure that firebird was launched by user who is currently runs this test. + Otherwise shutil.copy2() failes with "[Errno 13] Permission denied". + 4. Content of databases.conf must be taken from $QA_ROOT/files/qa-databases.conf (one need to replace it before every test session). + Discussed with pcisar, letters since 30-may-2022 13:48, subject: + "new qa, core_4964_test.py: strange outcome when use... shutil.copy() // comparing to shutil.copy2()" + + [31.07.2024] pzotov + Replaced assert for ISQL output: added diff_patterns that must filter STDERR because we have to suppress message with text + "file ... is not a valid database" as it can be seen only in dev-builds. + Expected ISQL output must be compared with filtered capsys.readouterr().out rather than with act.stdout + Noted by Dimitry Sibiryakov, https://github.com/FirebirdSQL/firebird-qa/issues/27 + + Checked on 5.0.0.591, 4.0.1.2692, 3.0.8.33535 - both on Windows and Linux. + + [26.02.2025] pzotov + Old alias renamed to 'tmp_core_4964_alias_5x' - it will be used only for FB 3.x ... 5.x. + For 6.x+: added two aliases (for Windows and Linux) in the preliminary created databases.conf: 'tmp_core_4964_alias_win' and 'tmp_core_4964_alias_nix'. + Both contain SecurityDatabase that points to library fbSampleDbCrypt.dll (Linux: libfbSampleDbCrypt.so) which is binary file and its signature DIFFERS + from firebird.msg which was in use before. + The problem appeared since 6.0.0.647 (21.02.2025) when ODS was changed. Among other changes, FB does not do additional check for header, i.e.: + *** WAS: *** + if (header->hdr_header.pag_type != pag_header || header->hdr_sequence) + ERR_post; + *** NOW: *** + if (header->hdr_header.pag_type != pag_header) + ERR_post; + (field 'header->hdr_sequence' no more exists). + But firebird.msg starting byte is 0x01 - and this is equal to pag_header, thus firebird.msg is no more suitable for this test in 6.x. + Explained by dimitr, letter 25.02.2025 22:45 +""" +import os +import re +import time +from pathlib import Path +from difflib import unified_diff + +import pytest +from firebird.qa import * + +substitutions = [ + ('[ \t]+', ' ') + ,('(-)?file .* is not a valid database', 'file is not a valid database') + ] + +db = db_factory() +act = python_act('db', substitutions = substitutions) +tmp_user = user_factory('db', name='tmp$c4964', password='123', plugin = 'Srp') + +expected_stdout_isql = """ + Statement failed, SQLSTATE = 08006 + Error occurred during login, please check server firebird.log for details +""" + +expected_stdout_log_diff = """ + Authentication error + file is not a valid database +""" + +@pytest.mark.version('>=3.0') +def test_1(act: Action, tmp_user: User, capsys): + + if act.is_version('<6'): + REQUIRED_ALIAS = 'tmp_core_4964_alias_5x' + else: + REQUIRED_ALIAS = 'tmp_core_4964_alias_win' if os.name == 'nt' else 'tmp_core_4964_alias_nix' + + fblog_1 = act.get_firebird_log() + + # Scan line-by-line through databases.conf, find line starting with REQUIRED_ALIAS and extract name of file that + # must be created in the $(dir_sampleDb)/qa/ folder. This name will be used further as target database (tmp_fdb). + # NOTE: we have to SKIP lines which are commented out, i.e. if they starts with '#': + p_required_alias_ptn = re.compile( '^(?!#)((^|\\s+)' + REQUIRED_ALIAS + ')\\s*=\\s*\\$\\(dir_sampleDb\\)/qa/', re.IGNORECASE ) + fname_in_dbconf = None + + with open(act.home_dir/'databases.conf', 'r') as f: + for line in f: + if p_required_alias_ptn.search(line): + # If databases.conf contains line like this: + # tmp_4964_alias = $(dir_sampleDb)/qa/tmp_qa_4964.fdb + # - then we extract filename: 'tmp_qa_4964.fdb' (see below): + fname_in_dbconf = Path(line.split('=')[1].strip()).name + break + + # if 'fname_in_dbconf' remains undefined here then propably REQUIRED_ALIAS not equals to specified in the databases.conf! + # + assert fname_in_dbconf + + # Full path + filename of database to which we will try to connect: + # + tmp_fdb = Path( act.vars['sample_dir'], 'qa', fname_in_dbconf ) + + # Permiss. Error: [Errno 13] Permiss. denied --> probably because + # Firebird was started by root rather than current (non-privileged) user. + # + tmp_fdb.write_bytes(act.db.db_path.read_bytes()) + + check_sql = f''' + set bail on; + set list on; + connect '{act.host+":" if act.host else ""}{tmp_fdb}' user {tmp_user.name} password {tmp_user.password}; + -- This can occus only if we databases.conf contains {REQUIRED_ALIAS} + -- but without reference to invalid security DB (e.g., alias without curly braces at all): + select mon$database_name as "UNEXPECTED CONNECTION:" from mon$database; + quit; + ''' + + ############################################################################################################### + # POINT-1: check that ISQL raises: + # "SQLSTATE = 08006 / Error occurred during login, please check server firebird.log ..." + # + + # release build: + # ================================== + # Statement failed, SQLSTATE = 08006 + # Error occurred during login, please check server firebird.log for details + # ================================== + + # dev-build: + # ================================== + # Statement failed, SQLSTATE = 08006 + # Error occurred during login, please check server firebird.log for details + # -file ... is not a valid database + # ================================== + # Last line ("file ... is not a valid database") will be suppressed by substitutions set: + # + + isql_err_diff_patterns = [ + "Statement failed, SQLSTATE = 08006" + ,"Error occurred during login, please check server firebird.log for details" + ] + isql_err_diff_patterns = [re.compile(s) for s in isql_err_diff_patterns] + + act.expected_stdout = expected_stdout_isql + try: + act.isql(switches = ['-q'], input = check_sql, connect_db=False, credentials = False, combine_output = True) + finally: + tmp_fdb.unlink() + + # ::: NB ::: + # Expected ISQL output must be compared with filtered capsys.readouterr().out rather than with act.stdout + for line in act.stdout.splitlines(): + if act.match_any(line, isql_err_diff_patterns): + print(line) + + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() + + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + time.sleep(1) # Allow content of firebird log be fully flushed on disk. + fblog_2 = act.get_firebird_log() + + fb_log_diff_patterns = [ + "\\+\\s+Authentication error" + ,"\\+\\s+file .* is not a valid database" + ] + fb_log_diff_patterns = [re.compile(s) for s in fb_log_diff_patterns] + + # BOTH release and dev build will print in firebird.log: + # + # Authentication error + # file .* is not a valid database + # + for line in unified_diff(fblog_1, fblog_2): + if line.startswith('+'): + if act.match_any(line, fb_log_diff_patterns): + print(line.split('+')[-1]) + + ############################################################################################################### + # POINT-2: check that diff between firebird.log initial and current content has phrases + # 'Authentication error' and '... file is not a valid database': + # + act.expected_stdout = expected_stdout_log_diff + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() + diff --git a/tests/bugs/core_4977_test.py b/tests/bugs/core_4977_test.py index 45875790..b0737be3 100644 --- a/tests/bugs/core_4977_test.py +++ b/tests/bugs/core_4977_test.py @@ -5,13 +5,13 @@ ISSUE: 5268 TITLE: Detach using Linux client takes much longer than from Windows DESCRIPTION: - We measure APPROXIMATE time that is required for detaching from database by evaluating number of seconds that passed - from UNIX standard epoch time inside ISQL and writing it to log. After returning control from ISQL we evaluate again - that number by calling Python 'time.time()' - and it will return value upto current UTC time, i.e. it WILL take in - account local timezone from OS settings (this is so at least on Windows). Thus we have to add/substract time shift - between UTC and local time - this is done by 'time.timezone' command. - On PC-host with CPU 3.0 GHz and 2Gb RAM) in almost all cases difference was less than 1000 ms, so it was decided - to set MAX_DETACH_TIME_THRESHOLD = 1200 ms. + We measure APPROXIMATE time that is required for detaching from database by evaluating number of seconds that passed + from UNIX standard epoch time inside ISQL and writing it to log. After returning control from ISQL we evaluate again + that number by calling Python 'time.time()' - and it will return value upto current UTC time, i.e. it WILL take in + account local timezone from OS settings (this is so at least on Windows). Thus we have to add/substract time shift + between UTC and local time - this is done by 'time.timezone' command. + On PC-host with CPU 3.0 GHz and 2Gb RAM in almost all cases difference was less than 1000 ms, so it was decided + to set MAX_DETACH_TIME_THRESHOLD = 1200 ms. JIRA: CORE-4977 FBTEST: bugs.core_4977 """ @@ -26,11 +26,20 @@ @pytest.mark.version('>=3.0') def test_1(act: Action): - MAX_DETACH_TIME_THRESHOLD=1200 + + ################################ + MAX_DETACH_TIME_THRESHOLD = 1200 + ################################ + act.script = """ - set list on; - select datediff(second from timestamp '01.01.1970 00:00:00.000' to current_timestamp) as " " - from rdb$types rows 1; + set heading off; + set term ^; + execute block returns(dd bigint) as + begin + dd = datediff(second from timestamp '01.01.1970 00:00:00.000' to cast('now' as timestamp)); + suspend; + end + ^ """ act.execute() ms_before_detach = 0 @@ -41,5 +50,5 @@ def test_1(act: Action): splitted = line.split() if splitted and splitted[0].isdigit(): ms_before_detach = int(splitted[0]) - detach_during_ms = int((time.time() - ms_before_detach - time.timezone) * 1000) - assert detach_during_ms < MAX_DETACH_TIME_THRESHOLD + time_for_detach_ms = int((time.time() - ms_before_detach - time.timezone) * 1000) + assert time_for_detach_ms <= MAX_DETACH_TIME_THRESHOLD, f'{time_for_detach_ms=} - greater than {MAX_DETACH_TIME_THRESHOLD=}' diff --git a/tests/bugs/core_4980_test.py b/tests/bugs/core_4980_test.py index 67d9e156..4e76fb58 100644 --- a/tests/bugs/core_4980_test.py +++ b/tests/bugs/core_4980_test.py @@ -10,73 +10,68 @@ Query to table TEST should be denied, but queries to RDB-tables should run OK and display their data. JIRA: CORE-4980 FBTEST: bugs.core_4980 +NOTES: + [30.06.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' and variables - to be substituted in expected_* on FB 6.x + Separated expected output for FB major versions prior/since 6.x. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ - +import locale import pytest from firebird.qa import * db = db_factory() - tmp_user = user_factory('db', name='tmp_c4980', password='123') -test_script = """ - set wng off; - - recreate table test(id int); - commit; - insert into test values(1); - commit; - - connect '$(DSN)' user tmp_c4980 password '123'; - - -- All subsequent statements (being issued by TMP_C4980) failed on 3.0.0.32134 and runs OK on build 32136: - set list on; +act = isql_act('db') - select current_user as who_am_i from rdb$database; - select current_user as who_am_i, r.rdb$character_set_name from rdb$database r; - select current_user as who_am_i, r.rdb$relation_name from rdb$relations r order by rdb$relation_id rows 1; - select current_user as who_am_i, t.id from test t; -- this should ALWAYS fail because this is non-system table. - commit; -""" - -act = isql_act('db', test_script) - -expected_stdout = """ - WHO_AM_I TMP_C4980 - WHO_AM_I TMP_C4980 - RDB$CHARACTER_SET_NAME NONE - WHO_AM_I TMP_C4980 - RDB$RELATION_NAME RDB$PAGES -""" - -# version: 3.0 - -expected_stderr_1 = """ - Statement failed, SQLSTATE = 28000 - no permission for SELECT access to TABLE TEST -""" - -@pytest.mark.version('>=3.0,<4.0') +@pytest.mark.version('>=3.0') def test_1(act: Action, tmp_user: User): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr_1 - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - -# version: 4.0 - -expected_stderr_2 = """ - Statement failed, SQLSTATE = 28000 - no permission for SELECT access to TABLE TEST - -Effective user is TMP_C4980 -""" - -@pytest.mark.version('>=4.0') -def test_2(act: Action, tmp_user: User): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr_2 - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + test_script = f""" + set wng off; + + recreate table test(id int); + commit; + insert into test values(1); + commit; + + connect '{act.db.dsn}' user {tmp_user.name.upper()} password '{tmp_user.password}'; + + -- All subsequent statements (being issued by TMP_C4980) failed on 3.0.0.32134 and runs OK on build 32136: + set list on; + + select current_user as who_am_i from rdb$database; + select current_user as who_am_i, r.rdb$character_set_name from rdb$database r; + select current_user as who_am_i, r.rdb$relation_name from rdb$relations r order by rdb$relation_id rows 1; + select current_user as who_am_i, t.id from test t; -- this should ALWAYS fail because this is non-system table. + commit; + """ + + expected_stdout_3x = f""" + WHO_AM_I {tmp_user.name.upper()} + WHO_AM_I {tmp_user.name.upper()} + RDB$CHARACTER_SET_NAME NONE + WHO_AM_I {tmp_user.name.upper()} + RDB$RELATION_NAME RDB$PAGES + Statement failed, SQLSTATE = 28000 + no permission for SELECT access to TABLE TEST + """ + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + TABLE_NAME = 'TEST' if act.is_version('<6') else f'{SQL_SCHEMA_PREFIX}"TEST"' + expected_stdout_5x = f""" + WHO_AM_I {tmp_user.name.upper()} + WHO_AM_I {tmp_user.name.upper()} + RDB$CHARACTER_SET_NAME NONE + WHO_AM_I {tmp_user.name.upper()} + RDB$RELATION_NAME RDB$PAGES + Statement failed, SQLSTATE = 28000 + no permission for SELECT access to TABLE {TABLE_NAME} + -Effective user is {tmp_user.name.upper()} + """ + + act.expected_stdout = expected_stdout_3x if act.is_version('<4') else expected_stdout_5x + act.isql(switches = ['-q'], input = test_script, combine_output = True, io_enc = locale.getpreferredencoding()) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_4985_test.py b/tests/bugs/core_4985_test.py index 91f71db3..945dac8b 100644 --- a/tests/bugs/core_4985_test.py +++ b/tests/bugs/core_4985_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-4985 FBTEST: bugs.core_4985 +NOTES: + [30.06.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' and variables - to be substituted in expected_* on FB 6.x + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -38,29 +43,29 @@ commit; """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ - Records affected: 7 - WHO_AM_I TMP$C4985 - Records affected: 1 -""" +@pytest.mark.version('>=4.0') +def test_1(act: Action, tmp_user: User): -expected_stderr = """ - Statement failed, SQLSTATE = 28000 - no permission for SELECT access to TABLE TEST - -Effective user is TMP$C4985 + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + TABLE_NAME = 'TEST' if act.is_version('<6') else f'{SQL_SCHEMA_PREFIX}"TEST"' + expected_stdout = f""" + Records affected: 7 - Statement failed, SQLSTATE = 28000 - no permission for SELECT access to TABLE TEST - -Effective user is TMP$C4985 -""" + WHO_AM_I {tmp_user.name.upper()} + Records affected: 1 -@pytest.mark.version('>=4.0') -def test_1(act: Action, tmp_user: User): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + Statement failed, SQLSTATE = 28000 + no permission for SELECT access to TABLE {TABLE_NAME} + -Effective user is {tmp_user.name.upper()} + Statement failed, SQLSTATE = 28000 + no permission for SELECT access to TABLE {TABLE_NAME} + -Effective user is {tmp_user.name.upper()} + """ + + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5020_test.py b/tests/bugs/core_5020_test.py index 154befeb..c86d30be 100644 --- a/tests/bugs/core_5020_test.py +++ b/tests/bugs/core_5020_test.py @@ -5,16 +5,25 @@ ISSUE: 5308 TITLE: Regression: ORDER BY clause on compound index may disable usage of other indices DESCRIPTION: + Plan in 3.0.0.32179 (before fix): PLAN (ZF ORDER IXA_FK__ID__KONT_ID) + Fixed in 3.0 since: http://sourceforge.net/p/firebird/code/62570 + Checked on 2.5.5.26952 - plans are the same now. JIRA: CORE-5020 FBTEST: bugs.core_5020 +NOTES: + [17.11.2024] pzotov + Re-implemented after https://github.com/FirebirdSQL/firebird/commit/26e64e9c08f635d55ac7a111469498b3f0c7fe81 + ( Cost-based decision between ORDER and SORT plans (#8316) ). + Execution plan was replaced with explained. Plans are splitted for versions up to 5.x and 6.x+. + Discussed with dimitr, letters 16.11.2024. + + Checked on 6.0.0.532; 5.0.2.1567; 4.0.6.3168; 3.0.13.33794. """ import pytest from firebird.qa import * -db = db_factory() - -test_script = """ +init_sql = """ recreate table zf( id integer not null primary key, kont_id integer not null @@ -76,28 +85,64 @@ create index ixa_fk__id__kont_id on zf(id, kont_id); commit; +""" - set planonly; - select zf.* - from zf - where zf.kont_id=5 - order by zf.id, kont_id; - - -- Plan in 3.0.0.32179 (before fix): PLAN (ZF ORDER IXA_FK__ID__KONT_ID) - -- Fixed in 3.0 since: http://sourceforge.net/p/firebird/code/62570 - -- Checked on 2.5.5.26952 - plans are the same now. +db = db_factory(init = init_sql) +act = python_act('db', substitutions = [(r'record length: \d+, key length: \d+', 'record length: NN, key length: MM')]) -""" +#----------------------------------------------------------- -act = isql_act('db', test_script) +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped -expected_stdout = """ - PLAN (ZF ORDER IXA_FK__ID__KONT_ID INDEX (FK_ZF__K)) -""" +#----------------------------------------------------------- @pytest.mark.version('>=3') -def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() +def test_1(act: Action, capsys): + test_sql = """ + select zf.* + from zf + where zf.kont_id=5 + order by zf.id, kont_id + """ + + with act.db.connect() as con: + cur = con.cursor() + ps = None + try: + ps = cur.prepare(test_sql) + + # Print explained plan with padding eash line by dots in order to see indentations: + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + print('') + except DatabaseError as e: + print(e.__str__()) + print(e.gds_codes) + finally: + if ps: + ps.free() + + + expected_stdout_5x = """ + Select Expression + ....-> Filter + ........-> Table "ZF" Access By ID + ............-> Index "IXA_FK__ID__KONT_ID" Full Scan + ................-> Bitmap + ....................-> Index "FK_ZF__K" Range Scan (full match) + """ + + expected_stdout_6x = """ + Select Expression + ....-> Sort (record length: NN, key length: MM) + ........-> Filter + ............-> Table "PUBLIC"."ZF" Access By ID + ................-> Bitmap + ....................-> Index "PUBLIC"."FK_ZF__K" Range Scan (full match) + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + + act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_5049_test.py b/tests/bugs/core_5049_test.py index eac58c55..8d08aba1 100644 --- a/tests/bugs/core_5049_test.py +++ b/tests/bugs/core_5049_test.py @@ -40,14 +40,16 @@ act = isql_act('db', test_script, substitutions=[('^((?!SQLSTATE|sqltype).)*$', ''), ('[ \t]+', ' ')]) -expected_stdout = """ - 01: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 8000 charset: 4 UTF8 - 02: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 8000 charset: 4 UTF8 - 03: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 8000 charset: 4 UTF8 -""" - @pytest.mark.version('>=3.0') def test_1(act: Action): + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'SYSTEM.' + expected_stdout = f""" + 01: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 8000 charset: 4 {SQL_SCHEMA_PREFIX}UTF8 + 02: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 8000 charset: 4 {SQL_SCHEMA_PREFIX}UTF8 + 03: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 8000 charset: 4 {SQL_SCHEMA_PREFIX}UTF8 + """ + act.expected_stdout = expected_stdout act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5062_test.py b/tests/bugs/core_5062_test.py index e91ab20a..06e1de73 100644 --- a/tests/bugs/core_5062_test.py +++ b/tests/bugs/core_5062_test.py @@ -3,26 +3,31 @@ """ ID: issue-5349 ISSUE: 5349 -TITLE: CHAR_TO_UUID on column with index throws expression evaluation not supported - Human readable UUID argument for CHAR_TO_UUID must be of exact length 36 +TITLE: CHAR_TO_UUID on column with index throws expression evaluation not supported. Human readable UUID argument for CHAR_TO_UUID must be of exact length 36 DESCRIPTION: JIRA: CORE-5062 -FBTEST: bugs.core_5062 +NOTES: + [18.01.2025] pzotov + Resultset of cursor that executes using instance of selectable PreparedStatement must be stored + in some variable in order to have ability close it EXPLICITLY (before PS will be freed). + Otherwise access violation raises during Python GC and pytest hangs at final point (does not return control to OS). + This occurs at least for: Python 3.11.2 / pytest: 7.4.4 / firebird.driver: 1.10.6 / Firebird.Qa: 0.19.3 + The reason of that was explained by Vlad, 26.10.24 17:42 ("oddities when use instances of selective statements"). """ import pytest from firebird.qa import * +from firebird.driver import DatabaseError init_script = """ -recreate table test_uuid( - datavalue int, - uuid char(16) character set octets, - constraint test_uuid_unq unique(uuid) -); -commit; -insert into test_uuid(datavalue, uuid) values( 1, char_to_uuid('57F2B8C7-E1D8-4B61-9086-C66D1794F2D9') ); ---insert into test_uuid(datavalue, uuid) values( 2, char_to_uuid('37F2B8C3-E1D8-4B31-9083-C33D1794F2D3') ); -commit; + recreate table test_uuid( + datavalue int, + uuid char(16) character set octets, + constraint test_uuid_unq unique(uuid) + ); + commit; + insert into test_uuid(datavalue, uuid) values( 1, char_to_uuid('57F2B8C7-E1D8-4B61-9086-C66D1794F2D9') ); + commit; """ db = db_factory(init=init_script) @@ -30,11 +35,41 @@ act = python_act('db') @pytest.mark.version('>=3.0') -def test_1(act: Action): +def test_1(act: Action, capsys): with act.db.connect() as con: - c = con.cursor() - stmt = c.prepare("select datavalue from test_uuid where uuid = char_to_uuid(?)") - assert stmt.plan == 'PLAN (TEST_UUID INDEX (TEST_UUID_UNQ))' - result = c.execute(stmt, ['57F2B8C7-E1D8-4B61-9086-C66D1794F2D9']).fetchall() - assert result == [(1, )] + ps, rs = None, None + cur = con.cursor() + try: + ps = cur.prepare("select datavalue from test_uuid where uuid = char_to_uuid(?)") + print(ps.plan) + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + rs = cur.execute(ps, ['57F2B8C7-E1D8-4B61-9086-C66D1794F2D9']) + for r in rs: + print(r[0]) + except DatabaseError as e: + print( e.__str__() ) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + TABLE_NAME = 'TEST_UUID' if act.is_version('<6') else '"TEST_UUID"' + INDEX_NAME = 'TEST_UUID_UNQ' if act.is_version('<6') else '"TEST_UUID_UNQ"' + expected_stdout = f""" + PLAN ({SQL_SCHEMA_PREFIX}{TABLE_NAME} INDEX ({SQL_SCHEMA_PREFIX}{INDEX_NAME})) + 1 + """ + + act.expected_stdout = expected_stdout + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() diff --git a/tests/bugs/core_5064_test.py b/tests/bugs/core_5064_test.py index 06ca172c..03ed0e75 100644 --- a/tests/bugs/core_5064_test.py +++ b/tests/bugs/core_5064_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-5064 FBTEST: bugs.core_5064 +NOTES: + [01.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' and variables - to be substituted in expected_* on FB 6.x + Separated expected output for FB major versions prior/since 6.x. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -35,39 +41,65 @@ """ -act = isql_act('db', test_script) - -expected_stdout = """ - INPUT message field count: 0 - - OUTPUT message field count: 3 - 01: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 8 charset: 1 OCTETS - : name: DB_KEY alias: DB_KEY - : table: TEST owner: SYSDBA - 02: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 8 charset: 1 OCTETS - : name: X alias: X - : table: TEST owner: SYSDBA - 03: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 8 charset: 1 OCTETS - : name: Y alias: Y - : table: TEST owner: SYSDBA - - INPUT message field count: 0 - - OUTPUT message field count: 2 - 01: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 8 charset: 1 OCTETS - : name: X alias: X - : table: owner: - 02: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 8 charset: 1 OCTETS - : name: Y alias: Y - : table: owner: - - X - Y -""" +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) @pytest.mark.version('>=4.0') def test_1(act: Action): - act.expected_stdout = expected_stdout + + expected_stdout_5x = """ + INPUT message field count: 0 + + OUTPUT message field count: 3 + 01: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 8 charset: 1 OCTETS + : name: DB_KEY alias: DB_KEY + : table: TEST owner: SYSDBA + 02: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 8 charset: 1 OCTETS + : name: X alias: X + : table: TEST owner: SYSDBA + 03: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 8 charset: 1 OCTETS + : name: Y alias: Y + : table: TEST owner: SYSDBA + + INPUT message field count: 0 + + OUTPUT message field count: 2 + 01: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 8 charset: 1 OCTETS + : name: X alias: X + : table: owner: + 02: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 8 charset: 1 OCTETS + : name: Y alias: Y + : table: owner: + + X + Y + """ + + expected_stdout_6x = """ + INPUT message field count: 0 + OUTPUT message field count: 3 + 01: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 8 charset: 1 SYSTEM.OCTETS + : name: DB_KEY alias: DB_KEY + : table: TEST schema: PUBLIC owner: SYSDBA + 02: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 8 charset: 1 SYSTEM.OCTETS + : name: X alias: X + : table: TEST schema: PUBLIC owner: SYSDBA + 03: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 8 charset: 1 SYSTEM.OCTETS + : name: Y alias: Y + : table: TEST schema: PUBLIC owner: SYSDBA + INPUT message field count: 0 + OUTPUT message field count: 2 + 01: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 8 charset: 1 SYSTEM.OCTETS + : name: X alias: X + : table: schema: owner: + 02: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 8 charset: 1 SYSTEM.OCTETS + : name: Y alias: Y + : table: schema: owner: + X + Y + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.execute() assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5070_test.py b/tests/bugs/core_5070_test.py index 08023278..383f0f25 100644 --- a/tests/bugs/core_5070_test.py +++ b/tests/bugs/core_5070_test.py @@ -7,14 +7,18 @@ DESCRIPTION: JIRA: CORE-5070 FBTEST: bugs.core_5070 +NOTES: + [01.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -db = db_factory() - -test_script = """ +init_script = """ recreate table test1 ( ia integer not null, id integer not null, @@ -22,44 +26,93 @@ dt date not null, constraint test1_pk_ia_id primary key (ia,id) ); +""" - set plan on; - set explain on; +db = db_factory(init = init_script) +substitutions = [] # [('record length.*', ''), ('key length.*', '')] +act = python_act('db', substitutions = substitutions) - select * - from test1 - where - ia=1 and dt='01/01/2015' and it=1 - order by id - ; +#----------------------------------------------------------- +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped - select id - from test1 - where - ia=1 and dt='01/01/2015' and it=1 - group by id - ; +#----------------------------------------------------------- -""" +@pytest.mark.version('>=3.0') +def test_1(act: Action, capsys): + qry_map = { + 1000 : + """ + select * + from test1 + where + ia=1 and dt='01/01/2015' and it=1 + order by id + """ + , + 2000 : + """ + select id + from test1 + where + ia=1 and dt='01/01/2015' and it=1 + group by id + """ + } -act = isql_act('db', test_script) + with act.db.connect() as con: + cur = con.cursor() -expected_stdout = """ - Select Expression - -> Filter - -> Table "TEST1" Access By ID - -> Index "TEST1_PK_IA_ID" Range Scan (partial match: 1/2) + for k, v in qry_map.items(): + ps, rs = None, None + try: + ps = cur.prepare(v) - Select Expression - -> Aggregate - -> Filter - -> Table "TEST1" Access By ID - -> Index "TEST1_PK_IA_ID" Range Scan (partial match: 1/2) -""" + print(v) + # Print explained plan with padding eash line by dots in order to see indentations: + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + print('') + except DatabaseError as e: + print(e.__str__()) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() + + expected_stdout_5x = f""" + {qry_map[1000]} + Select Expression + ....-> Filter + ........-> Table "TEST1" Access By ID + ............-> Index "TEST1_PK_IA_ID" Range Scan (partial match: 1/2) -@pytest.mark.version('>=3.0') -def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + {qry_map[2000]} + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Table "TEST1" Access By ID + ................-> Index "TEST1_PK_IA_ID" Range Scan (partial match: 1/2) + """ + + expected_stdout_6x = f""" + {qry_map[1000]} + Select Expression + ....-> Filter + ........-> Table "PUBLIC"."TEST1" Access By ID + ............-> Index "PUBLIC"."TEST1_PK_IA_ID" Range Scan (partial match: 1/2) + + {qry_map[2000]} + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Table "PUBLIC"."TEST1" Access By ID + ................-> Index "PUBLIC"."TEST1_PK_IA_ID" Range Scan (partial match: 1/2) + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5074_test.py b/tests/bugs/core_5074_test.py index 65b40d6e..f7472a1e 100644 --- a/tests/bugs/core_5074_test.py +++ b/tests/bugs/core_5074_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-5074 FBTEST: bugs.core_5074 +NOTES: + [01.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -16,26 +22,33 @@ test_script = """ recreate table test( - a char(10)[0:3] character set octets + array_col char(10)[0:3] character set octets ); set sqlda_display on; - select a[0] from test; + select array_col[0] from test; """ act = isql_act('db', test_script) -expected_stdout = """ - INPUT message field count: 0 - - OUTPUT message field count: 1 - 01: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 10 charset: 1 OCTETS - : name: A alias: A - : table: TEST owner: SYSDBA -""" - @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + expected_stdout_5x = """ + INPUT message field count: 0 + OUTPUT message field count: 1 + 01: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 10 charset: 1 OCTETS + : name: ARRAY_COL alias: ARRAY_COL + : table: TEST owner: SYSDBA + """ + + expected_stdout_6x = """ + INPUT message field count: 0 + OUTPUT message field count: 1 + 01: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 10 charset: 1 SYSTEM.OCTETS + : name: ARRAY_COL alias: ARRAY_COL + : table: TEST schema: PUBLIC owner: SYSDBA + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output =True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5093_test.py b/tests/bugs/core_5093_test.py index 5f50a901..af981b98 100644 --- a/tests/bugs/core_5093_test.py +++ b/tests/bugs/core_5093_test.py @@ -20,6 +20,12 @@ Currently gh-7924 fixed only for FB 6.x, thus charsets for FB 3.x ... 5.x will not be changed. Because of that, expected_output depends on major FB version, see its definition in 'blob_new_cset'. Checked on 6.0.0.223, 5.0.1.1322 + + [01.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -90,47 +96,83 @@ select * from t1; """ -expected_stdout_a = """ - 01: SQLTYPE: 496 LONG NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 - : NAME: N0 ALIAS: N0 - 02: SQLTYPE: 500 SHORT NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 2 - : NAME: SI ALIAS: SI - 03: SQLTYPE: 580 INT64 NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 8 - : NAME: BI ALIAS: BI - 04: SQLTYPE: 500 SHORT NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 2 - : NAME: S2 ALIAS: S2 - 05: SQLTYPE: 480 DOUBLE NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 8 - : NAME: DX ALIAS: DX - 06: SQLTYPE: 482 FLOAT NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 - : NAME: FX ALIAS: FX - 07: SQLTYPE: 500 SHORT NULLABLE SCALE: -1 SUBTYPE: 1 LEN: 2 - : NAME: NF ALIAS: NF - 08: SQLTYPE: 570 SQL DATE NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 - : NAME: DT ALIAS: DT - 09: SQLTYPE: 560 TIME NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 - : NAME: TM ALIAS: TM - 10: SQLTYPE: 452 TEXT NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 CHARSET: 4 UTF8 - : NAME: C_CHANGE_CB_VALUE ALIAS: C_CHANGE_CB_VALUE - 11: SQLTYPE: 452 TEXT NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 CHARSET: 4 UTF8 - : NAME: C_CHANGE_CHARSET ALIAS: C_CHANGE_CHARSET - 12: SQLTYPE: 452 TEXT NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 CHARSET: 4 UTF8 - : NAME: C_CHANGE_LENGTH ALIAS: C_CHANGE_LENGTH - 13: SQLTYPE: 520 BLOB NULLABLE SCALE: 0 SUBTYPE: 1 LEN: 8 CHARSET: 4 UTF8 - : NAME: B_CHANGE_CB_VALUE ALIAS: B_CHANGE_CB_VALUE - 14: SQLTYPE: 520 BLOB NULLABLE SCALE: 0 SUBTYPE: 1 LEN: 8 CHARSET: 4 UTF8 - : NAME: B_CHANGE_CHARSET ALIAS: B_CHANGE_CHARSET - 15: SQLTYPE: 520 BLOB NULLABLE SCALE: 0 SUBTYPE: 1 LEN: 8 CHARSET: 4 UTF8 - : NAME: B_REMOVE_CHARSET ALIAS: B_REMOVE_CHARSET - 16: SQLTYPE: 520 BLOB NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 8 - : NAME: B_ADDED_CHARSET ALIAS: B_ADDED_CHARSET -""" - BLOB_NEW_CSET_5X = 'CHARSET: 0 NONE' BLOB_NEW_CSET_6X = 'CHARSET: 4 UTF8' @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout_a + + expected_out_5x_a = """ + 01: SQLTYPE: 496 LONG NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 + : NAME: N0 ALIAS: N0 + 02: SQLTYPE: 500 SHORT NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 2 + : NAME: SI ALIAS: SI + 03: SQLTYPE: 580 INT64 NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 8 + : NAME: BI ALIAS: BI + 04: SQLTYPE: 500 SHORT NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 2 + : NAME: S2 ALIAS: S2 + 05: SQLTYPE: 480 DOUBLE NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 8 + : NAME: DX ALIAS: DX + 06: SQLTYPE: 482 FLOAT NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 + : NAME: FX ALIAS: FX + 07: SQLTYPE: 500 SHORT NULLABLE SCALE: -1 SUBTYPE: 1 LEN: 2 + : NAME: NF ALIAS: NF + 08: SQLTYPE: 570 SQL DATE NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 + : NAME: DT ALIAS: DT + 09: SQLTYPE: 560 TIME NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 + : NAME: TM ALIAS: TM + 10: SQLTYPE: 452 TEXT NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 CHARSET: 4 UTF8 + : NAME: C_CHANGE_CB_VALUE ALIAS: C_CHANGE_CB_VALUE + 11: SQLTYPE: 452 TEXT NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 CHARSET: 4 UTF8 + : NAME: C_CHANGE_CHARSET ALIAS: C_CHANGE_CHARSET + 12: SQLTYPE: 452 TEXT NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 CHARSET: 4 UTF8 + : NAME: C_CHANGE_LENGTH ALIAS: C_CHANGE_LENGTH + 13: SQLTYPE: 520 BLOB NULLABLE SCALE: 0 SUBTYPE: 1 LEN: 8 CHARSET: 4 UTF8 + : NAME: B_CHANGE_CB_VALUE ALIAS: B_CHANGE_CB_VALUE + 14: SQLTYPE: 520 BLOB NULLABLE SCALE: 0 SUBTYPE: 1 LEN: 8 CHARSET: 4 UTF8 + : NAME: B_CHANGE_CHARSET ALIAS: B_CHANGE_CHARSET + 15: SQLTYPE: 520 BLOB NULLABLE SCALE: 0 SUBTYPE: 1 LEN: 8 CHARSET: 4 UTF8 + : NAME: B_REMOVE_CHARSET ALIAS: B_REMOVE_CHARSET + 16: SQLTYPE: 520 BLOB NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 8 + : NAME: B_ADDED_CHARSET ALIAS: B_ADDED_CHARSET + """ + + expected_out_6x_a = """ + 01: SQLTYPE: 496 LONG NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 + : NAME: N0 ALIAS: N0 + 02: SQLTYPE: 500 SHORT NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 2 + : NAME: SI ALIAS: SI + 03: SQLTYPE: 580 INT64 NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 8 + : NAME: BI ALIAS: BI + 04: SQLTYPE: 500 SHORT NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 2 + : NAME: S2 ALIAS: S2 + 05: SQLTYPE: 480 DOUBLE NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 8 + : NAME: DX ALIAS: DX + 06: SQLTYPE: 482 FLOAT NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 + : NAME: FX ALIAS: FX + 07: SQLTYPE: 500 SHORT NULLABLE SCALE: -1 SUBTYPE: 1 LEN: 2 + : NAME: NF ALIAS: NF + 08: SQLTYPE: 570 SQL DATE NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 + : NAME: DT ALIAS: DT + 09: SQLTYPE: 560 TIME NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 + : NAME: TM ALIAS: TM + 10: SQLTYPE: 452 TEXT NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 CHARSET: 4 SYSTEM.UTF8 + : NAME: C_CHANGE_CB_VALUE ALIAS: C_CHANGE_CB_VALUE + 11: SQLTYPE: 452 TEXT NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 CHARSET: 4 SYSTEM.UTF8 + : NAME: C_CHANGE_CHARSET ALIAS: C_CHANGE_CHARSET + 12: SQLTYPE: 452 TEXT NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 CHARSET: 4 SYSTEM.UTF8 + : NAME: C_CHANGE_LENGTH ALIAS: C_CHANGE_LENGTH + 13: SQLTYPE: 520 BLOB NULLABLE SCALE: 0 SUBTYPE: 1 LEN: 8 CHARSET: 4 SYSTEM.UTF8 + : NAME: B_CHANGE_CB_VALUE ALIAS: B_CHANGE_CB_VALUE + 14: SQLTYPE: 520 BLOB NULLABLE SCALE: 0 SUBTYPE: 1 LEN: 8 CHARSET: 4 SYSTEM.UTF8 + : NAME: B_CHANGE_CHARSET ALIAS: B_CHANGE_CHARSET + 15: SQLTYPE: 520 BLOB NULLABLE SCALE: 0 SUBTYPE: 1 LEN: 8 CHARSET: 4 SYSTEM.UTF8 + : NAME: B_REMOVE_CHARSET ALIAS: B_REMOVE_CHARSET + 16: SQLTYPE: 520 BLOB NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 8 + : NAME: B_ADDED_CHARSET ALIAS: B_ADDED_CHARSET + """ + + act.expected_stdout = expected_out_5x_a if act.is_version('<6') else expected_out_6x_a act.isql(switches=['-q', '-m'], input='set sqlda_display on; select * from t1;') act.stdout = act.stdout.upper() assert act.clean_stdout == act.clean_expected_stdout @@ -142,7 +184,8 @@ def test_1(act: Action): # blob_new_cset = BLOB_NEW_CSET_5X if act.is_version('<6') else BLOB_NEW_CSET_6X #################################################### - expected_stdout_b = f""" + + expected_out_5x_b = f""" 01: SQLTYPE: 496 LONG NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 : NAME: N0 ALIAS: N0 02: SQLTYPE: 496 LONG NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 @@ -177,7 +220,42 @@ def test_1(act: Action): : NAME: B_ADDED_CHARSET ALIAS: B_ADDED_CHARSET """ - act.expected_stdout = expected_stdout_b + expected_out_6x_b = f""" + 01: SQLTYPE: 496 LONG NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 + : NAME: N0 ALIAS: N0 + 02: SQLTYPE: 496 LONG NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 + : NAME: SI ALIAS: SI + 03: SQLTYPE: 496 LONG NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 + : NAME: BI ALIAS: BI + 04: SQLTYPE: 500 SHORT NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 2 + : NAME: S2 ALIAS: S2 + 05: SQLTYPE: 482 FLOAT NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 + : NAME: DX ALIAS: DX + 06: SQLTYPE: 482 FLOAT NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 + : NAME: FX ALIAS: FX + 07: SQLTYPE: 580 INT64 NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 8 + : NAME: NF ALIAS: NF + 08: SQLTYPE: 570 SQL DATE NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 + : NAME: DT ALIAS: DT + 09: SQLTYPE: 510 TIMESTAMP NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 8 + : NAME: TM ALIAS: TM + 10: SQLTYPE: 452 TEXT NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 CHARSET: 4 SYSTEM.UTF8 + : NAME: C_CHANGE_CB_VALUE ALIAS: C_CHANGE_CB_VALUE + 11: SQLTYPE: 452 TEXT NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 4 CHARSET: 4 SYSTEM.UTF8 + : NAME: C_CHANGE_CHARSET ALIAS: C_CHANGE_CHARSET + 12: SQLTYPE: 452 TEXT NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 8 CHARSET: 4 SYSTEM.UTF8 + : NAME: C_CHANGE_LENGTH ALIAS: C_CHANGE_LENGTH + 13: SQLTYPE: 520 BLOB NULLABLE SCALE: 0 SUBTYPE: 1 LEN: 8 CHARSET: 4 SYSTEM.UTF8 + : NAME: B_CHANGE_CB_VALUE ALIAS: B_CHANGE_CB_VALUE + 14: SQLTYPE: 520 BLOB NULLABLE SCALE: 0 SUBTYPE: 1 LEN: 8 CHARSET: 4 SYSTEM.UTF8 + : NAME: B_CHANGE_CHARSET ALIAS: B_CHANGE_CHARSET + 15: SQLTYPE: 520 BLOB NULLABLE SCALE: 0 SUBTYPE: 0 LEN: 8 + : NAME: B_REMOVE_CHARSET ALIAS: B_REMOVE_CHARSET + 16: SQLTYPE: 520 BLOB NULLABLE SCALE: 0 SUBTYPE: 1 LEN: 8 CHARSET: 4 SYSTEM.UTF8 + : NAME: B_ADDED_CHARSET ALIAS: B_ADDED_CHARSET + """ + + act.expected_stdout = expected_out_5x_b if act.is_version('<6') else expected_out_6x_b act.isql(switches=['-q', '-m'], input=sql_script) act.stdout = act.stdout.upper() assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5118_test.py b/tests/bugs/core_5118_test.py index baff0af6..92ce523c 100644 --- a/tests/bugs/core_5118_test.py +++ b/tests/bugs/core_5118_test.py @@ -7,6 +7,21 @@ DESCRIPTION: JIRA: CORE-5118 FBTEST: bugs.core_5118 +NOTES: + [12.09.2024] pzotov + Replaced test query so that it does not use index navigation ('plan order') but still checks indexed access. + Three separate queries with 'PLAN ... INDEX' are used instead of one with 'where IN '. + This is because of optimizer changed in 5.x and issues plan with only *one* occurrence of 'INDEX' for such cases. + See: https://github.com/FirebirdSQL/firebird/pull/7707 - "Better processing and optimization if IN ". + Commit: https://github.com/FirebirdSQL/firebird/commit/0493422c9f729e27be0112ab60f77e753fabcb5b, 04-sep-2023. + Requested by dimitr, letters with subj 'core_5118_test', since 11.09.2024 17:26. + Checked on 6.0.0.452, 5.0.2.1493, 4.0.5.3136, 3.0.13.33789. + + [01.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.884; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -32,40 +47,53 @@ commit; """ -db = db_factory(init=init_script) - -act = python_act('db') - -expected_stdout = """ - PLAN (TEST ORDER TEST_CONCAT_TEXT) - - ID 1 - X nom1 - Y prenom1 - CONCAT_TEXT nom1 prenom1 - - ID 2 - X nom2 - Y prenom2 - CONCAT_TEXT nom2 prenom2 +db = db_factory(init = init_script) - ID 3 - X nom3 - Y prenom3 - CONCAT_TEXT nom3 prenom3 +act = python_act('db', substitutions = [ ('[ \t]+',' ') ]) - Records affected: 3 +test_sql = """ + set list on; + set plan on; + set count on; + select concat_text from test where concat_text = 'nom1 prenom1'; + select concat_text from test where concat_text = 'nom2 prenom2'; + select concat_text from test where concat_text = 'nom3 prenom3'; """ @pytest.mark.version('>=3.0') def test_1(act: Action): with act.connect_server() as srv: backup = BytesIO() - srv.database.local_backup(database=act.db.db_path, backup_stream=backup) + srv.database.local_backup(database = act.db.db_path, backup_stream = backup) backup.seek(0) - srv.database.local_restore(database=act.db.db_path, backup_stream=backup, - flags=SrvRestoreFlag.REPLACE) - act.expected_stdout = expected_stdout - act.isql(switches=['-q'], - input='set list on; set plan on; set count on; select * from test order by concat_text;') + srv.database.local_restore(database = act.db.db_path, backup_stream=backup, flags = SrvRestoreFlag.REPLACE) + + expected_stdout_5x = """ + PLAN (TEST INDEX (TEST_CONCAT_TEXT)) + CONCAT_TEXT nom1 prenom1 + Records affected: 1 + + PLAN (TEST INDEX (TEST_CONCAT_TEXT)) + CONCAT_TEXT nom2 prenom2 + Records affected: 1 + + PLAN (TEST INDEX (TEST_CONCAT_TEXT)) + CONCAT_TEXT nom3 prenom3 + Records affected: 1 + """ + + expected_stdout_6x = """ + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_CONCAT_TEXT")) + CONCAT_TEXT nom1 prenom1 + Records affected: 1 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_CONCAT_TEXT")) + CONCAT_TEXT nom2 prenom2 + Records affected: 1 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_CONCAT_TEXT")) + CONCAT_TEXT nom3 prenom3 + Records affected: 1 + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.isql(switches=['-q'], input = test_sql, combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5122_test.py b/tests/bugs/core_5122_test.py index 2a7caf07..fc7b23f2 100644 --- a/tests/bugs/core_5122_test.py +++ b/tests/bugs/core_5122_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-5122 FBTEST: bugs.core_5122 +NOTES: + [01.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.884; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -16,8 +22,9 @@ act = python_act('db') -expected_stdout = """ - PLAN (TEST INDEX (TEST_CALC_S)) +test_sql= """ + set planonly; + select * from test where 'zxc' || s starting with 'qwe'; """ @pytest.mark.version('>=3') @@ -27,8 +34,15 @@ def test_1(act: Action): cur1.execute("recreate table test(s varchar(10))") cur1.execute("create index test_calc_s on test computed by ('zxc' || s)") con1.commit() - # - act.expected_stdout = expected_stdout - act.isql(switches=['-n'], - input="set planonly; select * from test where 'zxc' || s starting with 'qwe';") + + expected_stdout_5x = """ + PLAN (TEST INDEX (TEST_CALC_S)) + """ + expected_stdout_6x = """ + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_CALC_S")) + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + + act.isql(switches=['-q', '-n'], input = test_sql) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5130_test.py b/tests/bugs/core_5130_test.py index 5c1f6be2..e6fb0c7e 100644 --- a/tests/bugs/core_5130_test.py +++ b/tests/bugs/core_5130_test.py @@ -8,6 +8,10 @@ DESCRIPTION: JIRA: CORE-5130 FBTEST: bugs.core_5130 +NOTES: + [01.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.884; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -31,17 +35,19 @@ act = isql_act('db', test_script) -expected_stderr = """ - Statement failed, SQLSTATE = 42000 - unsuccessful metadata update - -ALTER VIEW V1 failed - -Dynamic SQL Error - -SQL error code = -607 - -No subqueries permitted for VIEW WITH CHECK OPTION -""" - @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + VIEW_NAME = 'V1' if act.is_version('<6') else '"V1"' + expected_stdout = f""" + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -ALTER VIEW {SQL_SCHEMA_PREFIX}{VIEW_NAME} failed + -Dynamic SQL Error + -SQL error code = -607 + -No subqueries permitted for VIEW WITH CHECK OPTION + """ + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5146_test.py b/tests/bugs/core_5146_test.py index 56e807ff..ad71012c 100644 --- a/tests/bugs/core_5146_test.py +++ b/tests/bugs/core_5146_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-5146 FBTEST: bugs.core_5146 +NOTES: + [01.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.884; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -36,13 +42,17 @@ act = isql_act('db', test_script) -expected_stdout = """ - PLAN JOIN (HI INDEX (HI_PROJID), HE INDEX (HE_ITEMID)) -""" - @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + + expected_stdout_5x = """ + PLAN JOIN (HI INDEX (HI_PROJID), HE INDEX (HE_ITEMID)) + """ + expected_stdout_6x = """ + PLAN JOIN ("HI" INDEX ("PUBLIC"."HI_PROJID"), "HE" INDEX ("PUBLIC"."HE_ITEMID")) + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5161_test.py b/tests/bugs/core_5161_test.py index 7477eadb..a8c9e50c 100644 --- a/tests/bugs/core_5161_test.py +++ b/tests/bugs/core_5161_test.py @@ -5,8 +5,16 @@ ISSUE: 5444 TITLE: Unique index could be created on non-unique data DESCRIPTION: + Confirmed on: WI-V3.0.0.32378, WI-V2.5.6.26980: + one might to create unique index when number of inserted rows was >= 3276. JIRA: CORE-5161 FBTEST: bugs.core_5161 +NOTES: + [01.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.884; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -15,9 +23,8 @@ db = db_factory() test_script = """ - -- Confirmed on: WI-V3.0.0.32378, WI-V2.5.6.26980: - -- one might to create unique index when number of inserted rows was >= 3276. + set list on; recreate table t (id int, x int); set term ^; execute block as @@ -32,13 +39,8 @@ end ^ set term ;^ - - set list on; - select sign(count(*)) as cnt_non_zero from t; - set echo on; - insert into t values(1, -999999999); commit; @@ -48,15 +50,14 @@ select id, x from t where id = 1; """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ +expected_stdout_5x = """ CNT_NON_ZERO 1 - insert into t values(1, -999999999); - commit; - create unique index t_id_unique on t(id); - set plan on; - select id, x from t where id = 1; + Statement failed, SQLSTATE = 23000 + attempt to store duplicate value (visible to active transactions) in unique index "T_ID_UNIQUE" + -Problematic key value is ("ID" = 1) PLAN (T NATURAL) ID 1 X -888888888 @@ -64,17 +65,20 @@ X -999999999 """ -expected_stderr = """ +expected_stdout_6x = """ + CNT_NON_ZERO 1 Statement failed, SQLSTATE = 23000 - attempt to store duplicate value (visible to active transactions) in unique index "T_ID_UNIQUE" + attempt to store duplicate value (visible to active transactions) in unique index "PUBLIC"."T_ID_UNIQUE" -Problematic key value is ("ID" = 1) + PLAN ("PUBLIC"."T" NATURAL) + ID 1 + X -888888888 + ID 1 + X -999999999 """ @pytest.mark.version('>=2.5.6') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5166_test.py b/tests/bugs/core_5166_test.py index 8d749239..247da651 100644 --- a/tests/bugs/core_5166_test.py +++ b/tests/bugs/core_5166_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-5166 FBTEST: bugs.core_5166 +NOTES: + [01.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.884; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -51,40 +57,56 @@ update test2 set u=true, v=null, w=true where coalesce(u,v,w) is null rows 1; """ -act = isql_act('db', test_script) - -expected_stdout = """ - X - Records affected: 0 - Records affected: 1 - Records affected: 1 - Records affected: 1 - Records affected: 1 - Records affected: 1 - Records affected: 1 - Records affected: 0 - Records affected: 0 -""" - -expected_stderr = """ - Statement failed, SQLSTATE = 23000 - violation of PRIMARY or UNIQUE KEY constraint "TEST1_X_UNQ" on table "TEST1" - -Problematic key value is ("X" = TRUE) - - Statement failed, SQLSTATE = 23000 - violation of PRIMARY or UNIQUE KEY constraint "TEST2_UVW_UNQ" on table "TEST2" - -Problematic key value is ("U" = TRUE, "V" = TRUE, "W" = TRUE) - - Statement failed, SQLSTATE = 23000 - violation of PRIMARY or UNIQUE KEY constraint "TEST2_UVW_UNQ" on table "TEST2" - -Problematic key value is ("U" = TRUE, "V" = NULL, "W" = TRUE) -""" +substitutions = [] # [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + expected_stdout_5x = """ + X + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST1_X_UNQ" on table "TEST1" + -Problematic key value is ("X" = TRUE) + Records affected: 0 + Records affected: 1 + Records affected: 1 + Records affected: 1 + Records affected: 1 + Records affected: 1 + Records affected: 1 + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST2_UVW_UNQ" on table "TEST2" + -Problematic key value is ("U" = TRUE, "V" = TRUE, "W" = TRUE) + Records affected: 0 + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST2_UVW_UNQ" on table "TEST2" + -Problematic key value is ("U" = TRUE, "V" = NULL, "W" = TRUE) + Records affected: 0 + """ + + expected_stdout_6x = """ + X + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST1_X_UNQ" on table "PUBLIC"."TEST1" + -Problematic key value is ("X" = TRUE) + Records affected: 0 + Records affected: 1 + Records affected: 1 + Records affected: 1 + Records affected: 1 + Records affected: 1 + Records affected: 1 + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST2_UVW_UNQ" on table "PUBLIC"."TEST2" + -Problematic key value is ("U" = TRUE, "V" = TRUE, "W" = TRUE) + Records affected: 0 + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST2_UVW_UNQ" on table "PUBLIC"."TEST2" + -Problematic key value is ("U" = TRUE, "V" = NULL, "W" = TRUE) + Records affected: 0 + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5194_test.py b/tests/bugs/core_5194_test.py index a933ed23..79abc5a0 100644 --- a/tests/bugs/core_5194_test.py +++ b/tests/bugs/core_5194_test.py @@ -5,10 +5,10 @@ ISSUE: 5475 TITLE: Invalid computed by definition generated by isql -x DESCRIPTION: - We create table and then run ISQL with '-x' key and saving its output to file. - This operation should NOT produce any error (see var. 'f_xmeta_err'). - Then we drop table and run ISQL again but for APPLYING extracted metadata. - If "ISQL -x" will produce script with invalid syntax, compiler will raise error. + We create table and then run ISQL with '-x' key and saving its output to file. + This operation should NOT produce any error (see var. 'f_xmeta_err'). + Then we drop table and run ISQL again but for APPLYING extracted metadata. + If "ISQL -x" will produce script with invalid syntax, compiler will raise error. JIRA: CORE-5194 FBTEST: bugs.core_5194 """ @@ -26,6 +26,7 @@ test_script = """ set list on; + -- instead of 'show tab': select rf.rdb$field_name ,ff.rdb$field_length @@ -38,19 +39,6 @@ order by rdb$field_name; """ -expected_stdout = """ - RDB$FIELD_NAME A - RDB$FIELD_LENGTH 8 - RDB$FIELD_SCALE 0 - RDB$FIELD_TYPE 35 - RDB$COMPUTED_SOURCE - RDB$FIELD_NAME B - RDB$FIELD_LENGTH 8 - RDB$FIELD_SCALE -9 - RDB$FIELD_TYPE 16 - RDB$COMPUTED_SOURCE (current_timestamp - a) -""" - @pytest.mark.version('>=3.0') def test_1(act: Action): act.isql(switches=['-x']) @@ -62,10 +50,24 @@ def test_1(act: Action): att1.commit() # act.reset() - act.isql(switches=[], input=init_meta) + act.isql(switches = ['-q'], input = init_meta) assert act.clean_stdout == '' - # This should issue DDL of table TEST which was just created by extracted metadata: act.reset() + + expected_stdout = """ + RDB$FIELD_NAME A + RDB$FIELD_LENGTH 8 + RDB$FIELD_SCALE 0 + RDB$FIELD_TYPE 35 + RDB$COMPUTED_SOURCE + RDB$FIELD_NAME B + RDB$FIELD_LENGTH 8 + RDB$FIELD_SCALE -9 + RDB$FIELD_TYPE 16 + RDB$COMPUTED_SOURCE (current_timestamp - a) + """ + + # This should issue DDL of table TEST which was just created by extracted metadata: act.expected_stdout = expected_stdout - act.isql(switches=['-q'], input= test_script) + act.isql(switches=['-q'], input= test_script, combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5201_test.py b/tests/bugs/core_5201_test.py index dbf8af94..daee085e 100644 --- a/tests/bugs/core_5201_test.py +++ b/tests/bugs/core_5201_test.py @@ -23,6 +23,12 @@ gbak: ERROR:Database is not online due to failure to activate one or more indices. gbak: ERROR: Run gfix -online to bring database online without active indices. (actual since 5.0.0.932; will be soon also for FB 3.x and 4.x - see letter from Alex, 07.02.2023 11:53). + + [01.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.884; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -47,23 +53,33 @@ act = python_act('db') -gbak_expected_stdout = """ - gbak: ERROR:attempt to store duplicate value (visible to active transactions) in unique index "TEST_1_UNQ" - gbak: ERROR: Problematic key value is ( = 1) - gbak: ERROR:Database is not online due to failure to activate one or more indices. - gbak: ERROR: Run gfix -online to bring database online without active indices. -""" - fbk_file = temp_file('core_5201.fbk') tmp_db_file = temp_file('tmp_core_5201.fdb') @pytest.mark.version('>=3.0') def test_1(act: Action, fbk_file: Path, tmp_db_file: Path): + with act.connect_server() as srv: srv.database.backup(database=act.db.db_path, backup=fbk_file) assert srv.readlines() == [] # - act.expected_stdout = gbak_expected_stdout + + gbak_expected_out_5x = """ + gbak: ERROR:attempt to store duplicate value (visible to active transactions) in unique index "TEST_1_UNQ" + gbak: ERROR: Problematic key value is ( = 1) + gbak: ERROR:Database is not online due to failure to activate one or more indices. + gbak: ERROR: Run gfix -online to bring database online without active indices. + """ + + gbak_expected_out_6x = """ + gbak: ERROR:attempt to store duplicate value (visible to active transactions) in unique index "PUBLIC"."TEST_1_UNQ" + gbak: ERROR: Problematic key value is ( = 1) + gbak: ERROR:Database is not online due to failure to activate one or more indices. + gbak: ERROR: Run gfix -online to bring database online without active indices. + """ + + act.expected_stdout = gbak_expected_out_5x if act.is_version('<6') else gbak_expected_out_6x + act.gbak(switches=['-rep', '-v', str(fbk_file), str(tmp_db_file)], combine_output = True) p_gbak_err = re.compile('^gbak:\s?ERROR:', re.IGNORECASE) diff --git a/tests/bugs/core_5207_test.py b/tests/bugs/core_5207_test.py index a612a026..afbc1ad3 100644 --- a/tests/bugs/core_5207_test.py +++ b/tests/bugs/core_5207_test.py @@ -11,6 +11,10 @@ Afterall, we try to apply extracted metadata to temp database (that was created auto by fbtest). JIRA: CORE-5207 FBTEST: bugs.core_5207 +NOTES: + [01.07.2025] pzotov + Added check to return_code for proper check outcome of extracting and applying metadata. + Checked on 6.0.0.884 (SS/CS); 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -32,13 +36,18 @@ def test_1(act: Action, fbk_file: Path, fdb_file: Path, capsys): with act.connect_server() as srv: srv.database.restore(database=fdb_file, backup=fbk_file) srv.wait() - act.isql(switches=['-x', str(fdb_file)], connect_db=False) + # ------------------------------------------- + act.isql(switches=['-x', str(fdb_file)], connect_db = False) + assert act.return_code == 0, f'Attempt to extract metadata failed:\n{act.clean_stdout}' metadata = act.stdout + act.reset() + # ------------------------------------------- # Check metadata for line in metadata.splitlines(): if 'GRANT USAGE ON DOMAIN' in line: pytest.fail(f'WRONG GRANT: {line}') + # ------------------------------------------- # Apply metadata to main test database - act.reset() - act.isql(switches=[], input=metadata) - assert act.clean_stdout == act.clean_expected_stdout + act.xpected_stdout = '' + act.isql(switches = ['-q'], input = metadata, combine_output = True) + assert act.return_code == 0, f'Attempt to apply metadata failed:\n{act.clean_stdout}' diff --git a/tests/bugs/core_5216_test.py b/tests/bugs/core_5216_test.py index 79b3f384..3af2d721 100644 --- a/tests/bugs/core_5216_test.py +++ b/tests/bugs/core_5216_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-5216 FBTEST: bugs.core_5216 +NOTES: + [01.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.884; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -19,8 +25,8 @@ db = db_factory() test_script = """ - recreate exception rio 'Exception w/o parameter test. Invalid value detected'; - recreate exception foo 'Exception with parameter test. Invalid value of BAR = @1'; + recreate exception exc_no_param 'Exception w/o parameter test. Invalid value detected'; + recreate exception exc_with_param 'Exception with parameter test. Invalid value: @1'; recreate table test(id int constraint test_pk primary key using index test_pk, x int not null, y int not null); commit; insert into test(id, x, y) values(1, 100, 200); @@ -165,7 +171,7 @@ x = x * 100; when any do begin - exception rio; + exception exc_no_param; end end '''' into x; end @@ -177,7 +183,7 @@ ^ -- -FOO - -- -Exception with parameter test. Invalid value of BAR = *** null *** + -- -Exception with parameter test. Invalid value: *** null *** execute block as begin execute statement @@ -198,7 +204,7 @@ x = 99999; when any do begin - exception foo using(:x); + exception exc_with_param using(:x); end end '''' @@ -215,68 +221,113 @@ act = isql_act('db', test_script, substitutions=substitutions) -expected_stderr = """ - Statement failed, SQLSTATE = 42S22 - Dynamic SQL Error - -SQL error code = -206 - -Column unknown - -NON_EXISTING_COLUMN - -At line 1, column 8 - -At block line: 3, col: 9 - - Statement failed, SQLSTATE = 22012 - arithmetic exception, numeric overflow, or string truncation - -Integer divide by zero. The code attempted to divide an integer value by an integer divisor of zero. - -At block line: 1, col: 37 - -At block line: 3, col: 9 - - Statement failed, SQLSTATE = 42000 - validation error for variable R, value "*** null ***" - -At block line: 5, col: 17 - -At block line: 3, col: 9 - - Statement failed, SQLSTATE = 23000 - validation error for column "TEST"."X", value "*** null ***" - -At block line: 4, col: 25 - -At block line: 5, col: 17 - -At block line: 3, col: 9 - - Statement failed, SQLSTATE = 23000 - validation error for column "TEST"."Y", value "*** null ***" - -At block line: 5, col: 25 - -At block line: 5, col: 17 - -At block line: 3, col: 9 - - Statement failed, SQLSTATE = 23000 - violation of PRIMARY or UNIQUE KEY constraint "TEST_PK" on table "TEST" - -Problematic key value is ("ID" = 1) - -At block line: 4, col: 33 - -At block line: 5, col: 25 - -At block line: 5, col: 17 - -At block line: 3, col: 9 - - Statement failed, SQLSTATE = HY000 - exception 55 - -RIO - -Exception w/o parameter test. Invalid value detected - -At block line: 8, col: 33 - -At block line: 4, col: 27 - -At block line: 5, col: 17 - -At block line: 3, col: 9 - - Statement failed, SQLSTATE = HY000 - exception 56 - -FOO - -Exception with parameter test. Invalid value of BAR = *** null *** - -At block line: 8, col: 41 - -At block line: 5, col: 29 - -At block line: 5, col: 17 - -At block line: 3, col: 9 -""" - @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + expected_stdout_5x = f""" + Statement failed, SQLSTATE = 42S22 + Dynamic SQL Error + -SQL error code = -206 + -Column unknown + -NON_EXISTING_COLUMN + -At line N, column M + At block line: N, col: M + Statement failed, SQLSTATE = 22012 + arithmetic exception, numeric overflow, or string truncation + -Integer divide by zero. The code attempted to divide an integer value by an integer divisor of zero. + At block line: N, col: M + At block line: N, col: M + Statement failed, SQLSTATE = 42000 + validation error for variable R, value "*** null ***" + At block line: N, col: M + At block line: N, col: M + Statement failed, SQLSTATE = 23000 + validation error for column "TEST"."X", value "*** null ***" + At block line: N, col: M + At block line: N, col: M + At block line: N, col: M + Statement failed, SQLSTATE = 23000 + validation error for column "TEST"."Y", value "*** null ***" + At block line: N, col: M + At block line: N, col: M + At block line: N, col: M + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST_PK" on table "TEST" + -Problematic key value is ("ID" = 1) + At block line: N, col: M + At block line: N, col: M + At block line: N, col: M + At block line: N, col: M + Statement failed, SQLSTATE = HY000 + exception K + -EXC_NO_PARAM + -Exception w/o parameter test. Invalid value detected + At block line: N, col: M + At block line: N, col: M + At block line: N, col: M + At block line: N, col: M + Statement failed, SQLSTATE = HY000 + exception K + -EXC_WITH_PARAM + -Exception with parameter test. Invalid value: *** null *** + At block line: N, col: M + At block line: N, col: M + At block line: N, col: M + At block line: N, col: M + """ + + expected_stdout_6x = f""" + Statement failed, SQLSTATE = 42S22 + Dynamic SQL Error + -SQL error code = -206 + -Column unknown + -"NON_EXISTING_COLUMN" + -At line N, column M + At block line: N, col: M + Statement failed, SQLSTATE = 22012 + arithmetic exception, numeric overflow, or string truncation + -Integer divide by zero. The code attempted to divide an integer value by an integer divisor of zero. + At block line: N, col: M + At block line: N, col: M + Statement failed, SQLSTATE = 42000 + validation error for variable "R", value "*** null ***" + At block line: N, col: M + At block line: N, col: M + Statement failed, SQLSTATE = 23000 + validation error for column "PUBLIC"."TEST"."X", value "*** null ***" + At block line: N, col: M + At block line: N, col: M + At block line: N, col: M + Statement failed, SQLSTATE = 23000 + validation error for column "PUBLIC"."TEST"."Y", value "*** null ***" + At block line: N, col: M + At block line: N, col: M + At block line: N, col: M + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST_PK" on table "PUBLIC"."TEST" + -Problematic key value is ("ID" = 1) + At block line: N, col: M + At block line: N, col: M + At block line: N, col: M + At block line: N, col: M + Statement failed, SQLSTATE = HY000 + exception K + -"PUBLIC"."EXC_NO_PARAM" + -Exception w/o parameter test. Invalid value detected + At block line: N, col: M + At block line: N, col: M + At block line: N, col: M + At block line: N, col: M + Statement failed, SQLSTATE = HY000 + exception K + -"PUBLIC"."EXC_WITH_PARAM" + -Exception with parameter test. Invalid value: *** null *** + At block line: N, col: M + At block line: N, col: M + At block line: N, col: M + At block line: N, col: M + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5218_test.py b/tests/bugs/core_5218_test.py index 52e04115..7dcac782 100644 --- a/tests/bugs/core_5218_test.py +++ b/tests/bugs/core_5218_test.py @@ -7,6 +7,10 @@ DESCRIPTION: JIRA: CORE-5218 FBTEST: bugs.core_5218 +NOTES: + [01.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.884; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -31,21 +35,26 @@ act = python_act('db') -expected_stdout = """ - CREATE TABLE TEST (F01 INTEGER CONSTRAINT F01_NN NOT NULL, - F02 INTEGER CONSTRAINT F02_NN NOT NULL, - F03 INTEGER CONSTRAINT F03_NN NOT NULL, - CONSTRAINT F01_PK PRIMARY KEY (F01), - CONSTRAINT F02_UK UNIQUE (F02)); - ALTER TABLE TEST ADD CONSTRAINT F03_FK FOREIGN KEY (F03) REFERENCES TEST (F01); -""" @pytest.mark.version('>=2.5.6') def test_1(act: Action): - act.expected_stdout = expected_stdout act.isql(switches=['-x']) - # filter stdout + assert act.return_code == 0, f'Attempt to extract metadata failed:\n{act.clean_stdout}' act.stdout = '\n'.join([line for line in act.stdout.splitlines() if 'CONSTRAINT' in line]) + + # ----------------------------------- + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'PUBLIC.' + expected_stdout = f""" + CREATE TABLE {SQL_SCHEMA_PREFIX}TEST (F01 INTEGER CONSTRAINT F01_NN NOT NULL, + F02 INTEGER CONSTRAINT F02_NN NOT NULL, + F03 INTEGER CONSTRAINT F03_NN NOT NULL, + CONSTRAINT F01_PK PRIMARY KEY (F01), + CONSTRAINT F02_UK UNIQUE (F02)); + ALTER TABLE {SQL_SCHEMA_PREFIX}TEST ADD CONSTRAINT F03_FK FOREIGN KEY (F03) REFERENCES {SQL_SCHEMA_PREFIX}TEST (F01); + """ + + act.expected_stdout = expected_stdout + # filter stdout assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5220_test.py b/tests/bugs/core_5220_test.py index 48374cd9..e7dc216f 100644 --- a/tests/bugs/core_5220_test.py +++ b/tests/bugs/core_5220_test.py @@ -62,6 +62,7 @@ r.rdb$system_flag is distinct from 1; """ +@pytest.mark.intl @pytest.mark.version('>=3.0') def test_1(act: Action): # diff --git a/tests/bugs/core_5225_test.py b/tests/bugs/core_5225_test.py index 9644e111..80c7debb 100644 --- a/tests/bugs/core_5225_test.py +++ b/tests/bugs/core_5225_test.py @@ -73,6 +73,7 @@ WHOAMI_SRP TMP$C5225 """ +@pytest.mark.es_eds @pytest.mark.version('>=3.0.1') def test_1(act: Action, user_srp: User, user_leg: User): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_5229_test.py b/tests/bugs/core_5229_test.py index 030a8522..4a3e48ce 100644 --- a/tests/bugs/core_5229_test.py +++ b/tests/bugs/core_5229_test.py @@ -2,18 +2,40 @@ """ ID: issue-5508 -ISSUE: 5508 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/5508 TITLE: Allow to enforce IPv4 or IPv6 in URL-like connection strings DESCRIPTION: -NOTES: -[04.02.2022] pcisar - Test may fail with IPv6. - For example it fails on my Linux OpenSuSE Tumbleweed with regular setup (IPv6 should not be disabled). - Test should IMHO check IPv4/IPv6 availability on test host before runs inet6:// check. JIRA: CORE-5229 FBTEST: bugs.core_5229 -""" +NOTES: + [04.02.2022] pcisar + Test may fail with IPv6. + For example it fails on my Linux OpenSuSE Tumbleweed with regular setup (IPv6 should not be disabled). + Test should IMHO check IPv4/IPv6 availability on test host before runs inet6:// check. + [13.06.2024] pzotov + 1. Added check for ability to use IPv6. + 2. Attempt to specify explicitly IPv6 address "[::1]" in ES/EDS caused error: + ======== + Statement failed, SQLSTATE = 42000 + External Data Source provider 'inet6://[' not found + ======== + It was fixed in gh-8156. + 3. On Windows there is no way to make IPv6 'fully disabled': address '::1' remains active. + According to https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/configure-ipv6-in-windows + "You cannot completely disable IPv6 as IPv6 is used internally on the system for many TCPIP tasks. + For example, you will still be able to run ping ::1 after configuring this setting" + We can turn off listening of '::1' by FB server if do following: + * run PowerShell and type there: Enable-NetAdapterBinding -Name "*" -ComponentID ms_tcpip6 + * chcp 65001, then: ipconfig /all | findstr /i /r /c:" IPv6.*(preferred)" + * save somewhere IPv6 address from previous command (e.g. 'fe80::f53c:9ecf:aad:4761%14') + * change in firebird.conf: RemoteBindAddress = fe80::f53c:9ecf:aad:4761 + But this requires RESTART of FB server thus cannot be used in QA. + Discussed with Vlad 13-jun-2024. + [14.06.2024] pzotov + Checked "on external 'inet6://[::1]/{act.db.db_path}'" after fixed GH-8156, builds: + 3.0.12.33757, 4.0.5.3112, 5.0.1.1416, 6.0.0.374 +""" import pytest from firebird.qa import * @@ -21,24 +43,64 @@ act = python_act('db') -expected_stdout = """ - PROCOTOL_WHEN_CONNECT_FROM_OS TCPv4 - PROCOTOL_WHEN_CONNECT_FROM_ISQL TCPv4 - PROTOCOL_WHEN_CONNECT_BY_ES_EDS TCPv4 - PROCOTOL_WHEN_CONNECT_FROM_ISQL TCPv6 - PROTOCOL_WHEN_CONNECT_BY_ES_EDS TCPv6 -""" +#------------------------------------------ +# https://stackoverflow.com/questions/66246308/detect-if-ipv6-is-supported-os-agnostic-no-external-program/66249915#66249915 +# https://stackoverflow.com/a/66249915 + +def check_ipv6_avail(): + import socket + import errno + + # On Windows, the E* constants will use the WSAE* values + # So no need to hardcode an opaque integer in the sets. + _ADDR_NOT_AVAIL = {errno.EADDRNOTAVAIL, errno.EAFNOSUPPORT} + _ADDR_IN_USE = {errno.EADDRINUSE} + + res = -1 + if not socket.has_ipv6: + # If the socket library has no support for IPv6, then the + # question is moot as we can't use IPv6 anyways. + return res -@pytest.mark.skip("FIXME: see notes") + sock = None + try: + #with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as sock: + # sock.bind(("::1", 0)) + sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + sock.bind(("::1", 0)) + #sock.shutdown(socket.SHUT_RDWR) # [Errno 107] Transport endpoint is not connected + sock.close() + res = 0 + except socket.error as x: + # sysctl net.ipv6.conf.all.disable_ipv6=1 + # sysctl net.ipv6.conf.default.disable_ipv6=1 + # sock.bind(("::1", 0)) --> socket.error: [Errno 99] Cannot assign requested address + #print(x) + res = -2 + except OSError as e: + if e.errno in _ADDR_NOT_AVAIL: + res = -3 + elif e.errno in _ADDR_IN_USE: + # This point shouldn't ever be reached. But just in case... + res = -4 + else: + # Other errors should be inspected + res = -5 + + return res +#------------------------------------------ + +@pytest.mark.es_eds @pytest.mark.version('>=3.0.1') def test_1(act: Action): + + if (res := check_ipv6_avail()) < 0: + pytest.skip(f"IPv6 not avail, retcode: {res}") + sql_chk = f""" set list on; - select mon$remote_protocol as procotol_when_connect_from_os - from mon$attachments where mon$attachment_id = current_connection; - commit; - connect 'inet4://{act.db.db_path}'; + connect 'inet4://127.0.0.1/{act.db.db_path}'; select mon$remote_protocol as procotol_when_connect_from_isql from mon$attachments where mon$attachment_id = current_connection; @@ -49,7 +111,7 @@ def test_1(act: Action): begin for execute statement (stt) - on external 'inet4://{act.db.db_path}' + on external 'inet4://127.0.0.1/{act.db.db_path}' as user '{act.db.user}' password '{act.db.password}' into protocol_when_connect_by_es_eds do @@ -59,8 +121,8 @@ def test_1(act: Action): set term ;^ commit; - -- since 27.10.2019: - connect 'inet6://{act.db.db_path}'; + -- since 27.10.2019; checked again 13.06.2024 + connect 'inet6://[::1]/{act.db.db_path}'; select mon$remote_protocol as procotol_when_connect_from_isql from mon$attachments where mon$attachment_id = current_connection; @@ -71,7 +133,8 @@ def test_1(act: Action): begin for execute statement (stt) - on external 'inet6://{act.db.db_path}' + -- Failed before fix #8156 ("Can not specify concrete IPv6 address in ES/EDS connection string"): + on external 'inet6://[::1]/{act.db.db_path}' as user '{act.db.user}' password '{act.db.password}' into protocol_when_connect_by_es_eds do @@ -80,23 +143,16 @@ def test_1(act: Action): ^ set term ;^ commit; + """ + + expected_stdout = """ + PROCOTOL_WHEN_CONNECT_FROM_ISQL TCPv4 + PROTOCOL_WHEN_CONNECT_BY_ES_EDS TCPv4 + PROCOTOL_WHEN_CONNECT_FROM_ISQL TCPv6 + PROTOCOL_WHEN_CONNECT_BY_ES_EDS TCPv6 + """ - -- |||||||||||||||||||||||||||| - -- ###################################||| FB 4.0+, SS and SC |||############################## - -- |||||||||||||||||||||||||||| - -- If we check SS or SC and ExtConnPoolLifeTime > 0 (config parameter FB 4.0+) then current - -- DB (bugs.core_NNNN.fdb) will be 'captured' by firebird.exe process and fbt_run utility - -- will not able to drop this database at the final point of test. - -- Moreover, DB file will be hold until all activity in firebird.exe completed and AFTER this - -- we have to wait for seconds after it (discussion and small test see - -- in the letter to hvlad and dimitr 13.10.2019 11:10). - -- This means that one need to kill all connections to prevent from exception on cleanup phase: - -- SQLCODE: -901 / lock time-out on wait transaction / object is in use - -- ############################################################################################# - delete from mon$attachments where mon$attachment_id != current_connection; - commit; - """ act.expected_stdout = expected_stdout - act.isql(switches=['-q', f'inet4://{act.db.db_path}'], input=sql_chk, connect_db=False) + act.isql(switches=['-q', f'inet://{act.db.db_path}'], input=sql_chk, connect_db=False) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5231_test.py b/tests/bugs/core_5231_test.py index 4519b083..326ddeef 100644 --- a/tests/bugs/core_5231_test.py +++ b/tests/bugs/core_5231_test.py @@ -2,15 +2,15 @@ """ ID: issue-5510 -ISSUE: 5510 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/5510 TITLE: EXECUTE STATEMENT: BLR error if more than 256 output parameters exist DESCRIPTION: - We define here number of output args for which one need to made test - see var 'sp_args_count'. - Then we open .sql file and GENERATE it content based on value of 'sp_args_count' (procedure will - have header and body with appropriate number of arguments and statement to be executed). - Finally, we run ISQL subprocess with giving to it for execution just generated .sql script. - ISQL should _not_ issue any error and all lines of its STDOUT should start from the names of - output arguments (letter 'O': O1, O2, ... O5000). + We define here number of output args for which one need to made test - see var 'sp_args_count'. + Then we open .sql file and GENERATE it content based on value of 'sp_args_count' (procedure will + have header and body with appropriate number of arguments and statement to be executed). + Finally, we run ISQL subprocess with giving to it for execution just generated .sql script. + ISQL should _not_ issue any error and all lines of its STDOUT should start from the names of + output arguments (letter 'O': O1, O2, ... O5000). JIRA: CORE-5231 FBTEST: bugs.core_5231 """ @@ -71,5 +71,5 @@ def build_script(ddl_script: Path): @pytest.mark.version('>=3.0') def test_1(act: Action, ddl_script: Path): build_script(ddl_script) - act.isql(switches=[], input_file=ddl_script, charset='NONE') + act.isql(switches=[], input_file=ddl_script, charset='NONE', combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5236_test.py b/tests/bugs/core_5236_test.py index 1a8dd9b2..3b1b451e 100644 --- a/tests/bugs/core_5236_test.py +++ b/tests/bugs/core_5236_test.py @@ -12,10 +12,7 @@ -> Nested Loop Join (inner) <<<<<<< no filter of "DP_REGISTRO" table -> Table "DP_REGISTRO" Full Scan <<<<<<< after it was scanned -> Filter - -> Table "DP_RECIBO" Access By ID - -> Bitmap - -> Index "UNQ1_DP_RECIBO" Range Scan (partial match: 1/2) - + ... Plan AFTER fix (confirmed on 4.0.0.313): ... Select Expression @@ -23,11 +20,19 @@ -> Filter <<<<<<<<<<<<<<<<<<<<<<<<<<< EARLY FILTERING MUST BE HERE <<<<< -> Table "DP_REGISTRO" Full Scan -> Filter - -> Table "DP_RECIBO" Access By ID - -> Bitmap - -> Index "UNQ1_DP_RECIBO" Range Scan (partial match: 1/2) + ... JIRA: CORE-5236 FBTEST: bugs.core_5236 +NOTES: + [24.06.2025] pzotov + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + Also, for this test 'schema:' in SQLDA output is suppressed because as not relevant to check. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Adjusted explained plan in 6.x to actual. + + Checked on 6.0.0.858; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -64,11 +69,22 @@ db = db_factory(init=init_script) -act = python_act('db') +substitutions = [] + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + +act = python_act('db', substitutions=substitutions) #----------------------------------------------------------- -def replace_leading(source, char="#"): +def replace_leading(source, char="."): stripped = source.lstrip() return char * (len(source) - len(stripped)) + stripped @@ -93,35 +109,51 @@ def test_1(act: Action, capsys): fb4x_expected_out = """ Select Expression - ####-> Filter - ########-> Filter - ############-> Table "DP_REGISTRO_OEST" Access By ID - ################-> Bitmap - ####################-> Index "UNQ1_DP_REGISTRO_OEST" Unique Scan + ....-> Filter + ........-> Filter + ............-> Table "DP_REGISTRO_OEST" Access By ID + ................-> Bitmap + ....................-> Index "UNQ1_DP_REGISTRO_OEST" Unique Scan Select Expression - ####-> Nested Loop Join (inner) - ########-> Filter - ############-> Table "DP_REGISTRO" Full Scan - ########-> Filter - ############-> Table "DP_RECIBO" Access By ID - ################-> Bitmap - ####################-> Index "UNQ1_DP_RECIBO" Range Scan (partial match: 1/2) + ....-> Nested Loop Join (inner) + ........-> Filter + ............-> Table "DP_REGISTRO" Full Scan + ........-> Filter + ............-> Table "DP_RECIBO" Access By ID + ................-> Bitmap + ....................-> Index "UNQ1_DP_RECIBO" Range Scan (partial match: 1/2) """ fb5x_expected_out = """ Sub-query - ####-> Filter - ########-> Filter - ############-> Table "DP_REGISTRO_OEST" Access By ID - ################-> Bitmap - ####################-> Index "UNQ1_DP_REGISTRO_OEST" Unique Scan + ....-> Filter + ........-> Filter + ............-> Table "DP_REGISTRO_OEST" Access By ID + ................-> Bitmap + ....................-> Index "UNQ1_DP_REGISTRO_OEST" Unique Scan + Select Expression + ....-> Nested Loop Join (inner) + ........-> Filter + ............-> Table "DP_REGISTRO" Full Scan + ........-> Filter + ............-> Table "DP_RECIBO" Access By ID + ................-> Bitmap + ....................-> Index "UNQ1_DP_RECIBO" Range Scan (partial match: 1/2) + """ + + fb6x_expected_out = """ Select Expression - ####-> Filter - ########-> Hash Join (inner) - ############-> Table "DP_RECIBO" Full Scan - ############-> Record Buffer (record length: 25) - ################-> Filter - ####################-> Table "DP_REGISTRO" Full Scan + ....-> Nested Loop Join (semi) + ........-> Nested Loop Join (inner) + ............-> Table "DP_REGISTRO" Full Scan + ............-> Filter + ................-> Table "DP_RECIBO" Access By ID + ....................-> Bitmap + ........................-> Index "UNQ1_DP_RECIBO" Range Scan (partial match: 1/2) + ........-> Filter + ............-> Table "DP_REGISTRO_OEST" Access By ID + ................-> Bitmap + ....................-> Index "UNQ1_DP_REGISTRO_OEST" Unique Scan """ with act.db.connect() as con: @@ -129,7 +161,7 @@ def test_1(act: Action, capsys): ps = cur.prepare(test_sql) print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) - act.expected_stdout = fb4x_expected_out if act.is_version('<5') else fb5x_expected_out + act.expected_stdout = fb4x_expected_out if act.is_version('<5') else fb5x_expected_out if act.is_version('<6') else fb6x_expected_out act.execute(combine_output = True) act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5248_test.py b/tests/bugs/core_5248_test.py index 1246ad79..b2bb7e51 100644 --- a/tests/bugs/core_5248_test.py +++ b/tests/bugs/core_5248_test.py @@ -5,396 +5,337 @@ ISSUE: 5527 TITLE: Improve consistency in GRANT syntax between roles and privileges according to SQL standard DESCRIPTION: -JIRA: CORE-5248 -FBTEST: bugs.core_5248 +NOTES: + [08.03.2025] pzotov + 1. Removed old test that changed only FB 3.x (no more sense in ti because 3.x is almost at EOL). + 2. Commented out (and will be deleted later) code that expected error when user who was granted role + with admin option tries to revoke this role from himself. Seince fixed GH-8462 this is NOT so. + 3. Replaced hard-coded names/passwords with variables that are provided by fixtures (tmp_usr*, tmp_role*). + Checked on 6.0.0.660; 5.0.3.1624; 4.0.6.3189. + + [01.07.2025] pzotov + Added f-notation into expected_* in order to make proper content in FB 6.x (role name is enclosed in quotes there). + Checked on 6.0.0.884; 5.0.3.1668; 4.0.6.3214. """ +import locale + import pytest from firebird.qa import * db = db_factory() -usr0 = user_factory('db', name='tmp$c5248_usr0', password='c5248$u0') -usr1 = user_factory('db', name='tmp$c5248_usr1', password='c5248$u1') -usr2 = user_factory('db', name='tmp$c5248_usr2', password='c5248$u2') -usr3 = user_factory('db', name='tmp$c5248_usr3', password='c5248$u3') -usrx = user_factory('db', name='tmp$c5248_usrx', password='c5248$ux') -test_role = role_factory('db', name='test_role1', do_not_create=True) - -# version: 3.0.1 - -test_script_1 = """ - set list on; - set autoddl off; - grant create role to user tmp$c5248_usr0; - commit; - - connect '$(DSN)' user tmp$c5248_usr0 password 'c5248$u0'; - create role test_role1; -- tmp$c5248_usr0 is owner of role test_role1 - commit; - - connect '$(DSN)' user tmp$c5248_usrx password 'c5248$ux'; - - -- Statement failed, SQLSTATE = 28000 - -- unsuccessful metadata update - -- -DROP ROLE TEST_ROLE1 failed - -- -no permission for DROP access to ROLE TEST_ROLE1 - - drop role test_role1; -- should fail: this user is not owner of this role and he was not granted to use it with admin option - select count(*) from rdb$roles where rdb$role_name = 'TEST_ROLE1'; - rollback; -""" - -act_1 = isql_act('db', test_script_1) - -expected_stdout_1 = """ - COUNT 1 -""" - -expected_stderr_1 = """ - Statement failed, SQLSTATE = 28000 - unsuccessful metadata update - -DROP ROLE TEST_ROLE1 failed - -no permission for DROP access to ROLE TEST_ROLE1 -""" - -@pytest.mark.version('>=3.0.1,<4.0') -def test_1(act_1: Action, usr0: User, usrx: User, test_role: Role): - act_1.expected_stdout = expected_stdout_1 - act_1.expected_stderr = expected_stderr_1 - act_1.execute() - assert (act_1.clean_stderr == act_1.clean_expected_stderr and - act_1.clean_stdout == act_1.clean_expected_stdout) - -# version: 4.0 +tmp_usr0 = user_factory('db', name='tmp$c5248_usr0', password='c5248$u0') +tmp_usr1 = user_factory('db', name='tmp$c5248_usr1', password='c5248$u1') +tmp_usr2 = user_factory('db', name='tmp$c5248_usr2', password='c5248$u2') +tmp_usr3 = user_factory('db', name='tmp$c5248_usr3', password='c5248$u3') +tmp_usr4 = user_factory('db', name='tmp$c5248_usr4', password='c5248$u4') +tmp_role = role_factory('db', name='tmp_role1', do_not_create=True) substitutions = [('-TMP\\$C5248_USR1 is not grantor of (role|ROLE|Role) on TEST_ROLE1 to TMP\\$C5248_USR1.', '-TMP$C5248_USR1 is not grantor of ROLE on TEST_ROLE1 to TMP$C5248_USR1.'), ('-Effective user is.*', '')] -test_script_2 = """ - set list on; - set count on; - set autoddl off; -/* - connect '$(DSN)' user sysdba password 'masterkey'; - create or alter user tmp$c5248_usr0 password 'c5248$u0'; - create or alter user tmp$c5248_usr1 password 'c5248$u1'; - create or alter user tmp$c5248_usr2 password 'c5248$u2'; - create or alter user tmp$c5248_usr3 password 'c5248$u3'; - create or alter user tmp$c5248_usrx password 'c5248$ux'; - commit; */ - grant create role to user tmp$c5248_usr0; - commit; -/* - set term ^; - execute block as - begin - execute statement 'drop role test_role1'; - when any do begin end - end^ - set term ;^ - commit; -*/ - recreate view v_grants as - select - current_user as who_am_i - ,p.RDB$USER as who_was_granted - ,p.RDB$PRIVILEGE as privilege_type - ,p.RDB$RELATION_NAME as role_name - ,r.RDB$OWNER_NAME as role_owner - ,p.RDB$GRANTOR as granted_by - ,p.RDB$GRANT_OPTION as grant_option - from rdb$user_privileges p - left join rdb$roles r on p.rdb$relation_name = r.rdb$role_name - where p.rdb$object_type=13 - ; - commit; - grant select on v_grants to public; - commit; - - connect '$(DSN)' user tmp$c5248_usr0 password 'c5248$u0'; - create role test_role1; -- tmp$c5248_usr0 is owner of role test_role1 - commit; - - connect '$(DSN)' user sysdba password 'masterkey'; - grant test_role1 to tmp$c5248_usr1 with admin option; - grant test_role1 to tmp$c5248_usr3; - commit; - - connect '$(DSN)' user tmp$c5248_usr1 password 'c5248$u1'; - grant test_role1 to tmp$c5248_usr2; ----------------------- tmp$c5248_usr1 grants role to tmp$c5248_usr2 - commit; - - -- 1. revoke - avoid cascade grants delete - - connect '$(DSN)' user sysdba password 'masterkey'; - - select * from v_grants where upper(who_was_granted) in ( upper('tmp$c5248_usr1'), upper('tmp$c5248_usr2') ); -- must contain 2 records - - revoke test_role1 from tmp$c5248_usr1; -- Q: whether grant on role 'test_role1' remains to user 'tmp$c5248_usr2' after revoking from 'tmp$c5248_usr1' ? - - select * from v_grants where upper(who_was_granted) in ( upper('tmp$c5248_usr1'), upper('tmp$c5248_usr2') ); -- must contain 1 record for tmp$c5248_usr2 - - -- return grant to tmp$c5248_usr1 because it was revoked just now: - rollback; - --grant test_role1 to tmp$c5248_usr1 with admin option; - --commit; - - -- 2. revoke: user who has 'admin option' can revoke role from anyone EXCEPT himself - connect '$(DSN)' user tmp$c5248_usr1 password 'c5248$u1'; - - -- Following REVOKE should fail with: - -- Statement failed, SQLSTATE = 42000 - -- unsuccessful metadata update - -- -REVOKE failed - -- -tmp$c5248_usr1 is not grantor of Role on TEST_ROLE1 to tmp$c5248_usr1. - revoke test_role1 from tmp$c5248_usr1; - - select * from v_grants where upper(who_was_granted) = upper('tmp$c5248_usr1'); -- record should remain - rollback; - - -- 3. revoke - check role owner rights - connect '$(DSN)' user tmp$c5248_usr0 password 'c5248$u0'; - - select * from v_grants where upper(who_was_granted) = upper('tmp$c5248_usr3'); - - -- current user = tmp$c5248_usr0 - is owner of role test_role1, but this role was granted to tmp$c5248_usr3 by SYSDBA. - -- Q: should user 'c5248$u0' (current) be able to revoke role which he did NOT grant but owns ? - -- A: yes. - - revoke test_role1 from tmp$c5248_usr3; -- NO error/warning should be here - - select * from v_grants where upper(who_was_granted) = upper('tmp$c5248_usr3'); -- record should NOT appear. - rollback; - - -- 4. revoke - check admin option - connect '$(DSN)' user tmp$c5248_usr1 password 'c5248$u1'; - - select * from v_grants where upper(who_was_granted) in ( upper('tmp$c5248_usr1'), upper('tmp$c5248_usr3') ); -- two records should be here - - -- current user = tmp$c5248_usr1 - is NOT owner of role TEST_ROLE1 but he was granted to use it WITH ADMIN option - -- (grant test_role1 to tmp$c5248_usr1 with admin option). - -- Q: should user 'tmp$c5248_usr1' (current) be able to revoke role which he neither did grant nor owns but has admin option ? - -- A: yes. - - revoke test_role1 from tmp$c5248_usr3; - - select * from v_grants where upper(who_was_granted) in (upper('tmp$c5248_usr1'), upper('tmp$c5248_usr3')); -- only one record should be here - rollback; - - -- 5a. drop role - should fail - connect '$(DSN)' user tmp$c5248_usrx password 'c5248$ux'; - - -- Statement failed, SQLSTATE = 28000 - -- unsuccessful metadata update - -- -DROP ROLE TEST_ROLE1 failed - -- -no permission for DROP access to ROLE TEST_ROLE1 - - drop role test_role1; -- should fail: this user is not owner of this role and he was not granted to use it with admin option - - set count off; - select count(*) from rdb$roles where rdb$role_name = 'TEST_ROLE1'; - set count on; - rollback; - connect '$(DSN)' user tmp$c5248_usr0 password 'c5248$u0'; +act = python_act('db', substitutions = substitutions) - select * from v_grants where upper(role_name) = upper('TEST_ROLE1'); -- should output 3 records +@pytest.mark.version('>=4.0') +def test_2(act: Action, tmp_usr0: User, tmp_usr1: User, tmp_usr2: User, tmp_usr3: User, tmp_usr4: User, tmp_role: Role): - drop role test_role1; -- current user: 'tmp$c5248_usr0' - is owner of role test_role1 + test_sql = f""" + set list on; + set count on; + -- ############# + set autoddl OFF; + -- ############# + connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; - select * from rdb$roles where upper(rdb$role_name) = upper('TEST_ROLE1'); -- should output 0 records - select * from v_grants where upper(role_name) = upper('TEST_ROLE1'); -- should output 0 records - rollback; + recreate view v_grants as + select + current_user as who_am_i + ,p.RDB$USER as who_was_granted + ,p.RDB$PRIVILEGE as privilege_type + ,p.RDB$RELATION_NAME as role_name + ,r.RDB$OWNER_NAME as role_owner + ,p.RDB$GRANTOR as granted_by + ,p.RDB$GRANT_OPTION as grant_option + from rdb$user_privileges p + left join rdb$roles r on p.rdb$relation_name = r.rdb$role_name + where p.rdb$object_type=13 + ; + commit; + grant select on v_grants to public; + commit; - -- 6. drop role - check admin option - connect '$(DSN)' user tmp$c5248_usr1 password 'c5248$u1'; + grant create role to user {tmp_usr0.name}; + commit; - -- current user: 'tmp$c5248_usr1' - HAS grant on role TEST_ROLE1 with admin option (but he is NOT owner of this role). + connect '{act.db.dsn}' user {tmp_usr0.name} password '{tmp_usr0.password}'; + create role {tmp_role.name}; -- {tmp_usr0.name} is owner of role {tmp_role.name} + commit; - select * from v_grants where upper(role_name) = upper('TEST_ROLE1'); -- should output 3 records + connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; + grant {tmp_role.name} to {tmp_usr1.name} with admin option; + grant {tmp_role.name} to {tmp_usr3.name}; + commit; - drop role test_role1; -- current user: 'tmp$c5248_usr0' - is owner of role test_role1 + connect '{act.db.dsn}' user {tmp_usr1.name} password '{tmp_usr1.password}'; + grant {tmp_role.name} to {tmp_usr2.name}; ----------------------- {tmp_usr1.name} grants role to {tmp_usr2.name} + commit; - select * from rdb$roles where upper(rdb$role_name) = upper('TEST_ROLE1'); -- should output 0 records - select * from v_grants where upper(role_name) = upper('TEST_ROLE1'); -- should output 0 records - rollback; + -- 1. revoke - avoid cascade grants delete -/* - connect '$(DSN)' user sysdba password 'masterkey'; - drop user tmp$c5248_usr0; - drop user tmp$c5248_usr1; - drop user tmp$c5248_usr2; - drop user tmp$c5248_usr3; - drop user tmp$c5248_usrx; - commit; -*/ -""" + connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; -act_2 = isql_act('db', test_script_2, substitutions=substitutions) - -expected_stdout_2 = """ - WHO_AM_I SYSDBA - WHO_WAS_GRANTED TMP$C5248_USR1 - PRIVILEGE_TYPE M - ROLE_NAME TEST_ROLE1 - ROLE_OWNER TMP$C5248_USR0 - GRANTED_BY SYSDBA - GRANT_OPTION 2 - - WHO_AM_I SYSDBA - WHO_WAS_GRANTED TMP$C5248_USR2 - PRIVILEGE_TYPE M - ROLE_NAME TEST_ROLE1 - ROLE_OWNER TMP$C5248_USR0 - GRANTED_BY TMP$C5248_USR1 - GRANT_OPTION 0 - - - Records affected: 2 - - WHO_AM_I SYSDBA - WHO_WAS_GRANTED TMP$C5248_USR2 - PRIVILEGE_TYPE M - ROLE_NAME TEST_ROLE1 - ROLE_OWNER TMP$C5248_USR0 - GRANTED_BY TMP$C5248_USR1 - GRANT_OPTION 0 - - - Records affected: 1 - - WHO_AM_I TMP$C5248_USR1 - WHO_WAS_GRANTED TMP$C5248_USR1 - PRIVILEGE_TYPE M - ROLE_NAME TEST_ROLE1 - ROLE_OWNER TMP$C5248_USR0 - GRANTED_BY SYSDBA - GRANT_OPTION 2 - - - Records affected: 1 - - WHO_AM_I TMP$C5248_USR0 - WHO_WAS_GRANTED TMP$C5248_USR3 - PRIVILEGE_TYPE M - ROLE_NAME TEST_ROLE1 - ROLE_OWNER TMP$C5248_USR0 - GRANTED_BY SYSDBA - GRANT_OPTION 0 - - - Records affected: 1 - Records affected: 0 - - WHO_AM_I TMP$C5248_USR1 - WHO_WAS_GRANTED TMP$C5248_USR1 - PRIVILEGE_TYPE M - ROLE_NAME TEST_ROLE1 - ROLE_OWNER TMP$C5248_USR0 - GRANTED_BY SYSDBA - GRANT_OPTION 2 - - WHO_AM_I TMP$C5248_USR1 - WHO_WAS_GRANTED TMP$C5248_USR3 - PRIVILEGE_TYPE M - ROLE_NAME TEST_ROLE1 - ROLE_OWNER TMP$C5248_USR0 - GRANTED_BY SYSDBA - GRANT_OPTION 0 - - - Records affected: 2 - - WHO_AM_I TMP$C5248_USR1 - WHO_WAS_GRANTED TMP$C5248_USR1 - PRIVILEGE_TYPE M - ROLE_NAME TEST_ROLE1 - ROLE_OWNER TMP$C5248_USR0 - GRANTED_BY SYSDBA - GRANT_OPTION 2 - - - Records affected: 1 - - COUNT 1 - - WHO_AM_I TMP$C5248_USR0 - WHO_WAS_GRANTED TMP$C5248_USR1 - PRIVILEGE_TYPE M - ROLE_NAME TEST_ROLE1 - ROLE_OWNER TMP$C5248_USR0 - GRANTED_BY SYSDBA - GRANT_OPTION 2 - - WHO_AM_I TMP$C5248_USR0 - WHO_WAS_GRANTED TMP$C5248_USR3 - PRIVILEGE_TYPE M - ROLE_NAME TEST_ROLE1 - ROLE_OWNER TMP$C5248_USR0 - GRANTED_BY SYSDBA - GRANT_OPTION 0 - - WHO_AM_I TMP$C5248_USR0 - WHO_WAS_GRANTED TMP$C5248_USR2 - PRIVILEGE_TYPE M - ROLE_NAME TEST_ROLE1 - ROLE_OWNER TMP$C5248_USR0 - GRANTED_BY TMP$C5248_USR1 - GRANT_OPTION 0 - - - Records affected: 3 - Records affected: 0 - Records affected: 0 - - WHO_AM_I TMP$C5248_USR1 - WHO_WAS_GRANTED TMP$C5248_USR1 - PRIVILEGE_TYPE M - ROLE_NAME TEST_ROLE1 - ROLE_OWNER TMP$C5248_USR0 - GRANTED_BY SYSDBA - GRANT_OPTION 2 - - WHO_AM_I TMP$C5248_USR1 - WHO_WAS_GRANTED TMP$C5248_USR3 - PRIVILEGE_TYPE M - ROLE_NAME TEST_ROLE1 - ROLE_OWNER TMP$C5248_USR0 - GRANTED_BY SYSDBA - GRANT_OPTION 0 - - WHO_AM_I TMP$C5248_USR1 - WHO_WAS_GRANTED TMP$C5248_USR2 - PRIVILEGE_TYPE M - ROLE_NAME TEST_ROLE1 - ROLE_OWNER TMP$C5248_USR0 - GRANTED_BY TMP$C5248_USR1 - GRANT_OPTION 0 - - Records affected: 3 - Records affected: 0 - Records affected: 0 -""" + select 'Point-1' as msg, v.* from v_grants v where upper(v.who_was_granted) in ( upper('{tmp_usr1.name}'), upper('{tmp_usr2.name}') ); -- must contain 2 records -expected_stderr_2 = """ - Statement failed, SQLSTATE = 42000 - unsuccessful metadata update - -REVOKE failed - -TMP$C5248_USR1 is not grantor of ROLE on TEST_ROLE1 to TMP$C5248_USR1. + revoke {tmp_role.name} from {tmp_usr1.name}; -- Q: whether grant on role '{tmp_role.name}' remains to user '{tmp_usr2.name}' after revoking from '{tmp_usr1.name}' ? - Statement failed, SQLSTATE = 28000 - unsuccessful metadata update - -DROP ROLE TEST_ROLE1 failed - -no permission for DROP access to ROLE TEST_ROLE1 -""" + select 'Point-2' as msg, v.* from v_grants v where upper(v.who_was_granted) in ( upper('{tmp_usr1.name}'), upper('{tmp_usr2.name}') ); -- must contain 1 record for {tmp_usr2.name} + -- return grant to {tmp_usr1.name} because it was revoked just now: + rollback; -@pytest.mark.version('>=4.0') -def test_2(act_2: Action, usr0: User, usr1: User, usr2: User, usr3: User, usrx: User, test_role: Role): - act_2.expected_stdout = expected_stdout_2 - act_2.expected_stderr = expected_stderr_2 - act_2.execute() - assert (act_2.clean_stderr == act_2.clean_expected_stderr and - act_2.clean_stdout == act_2.clean_expected_stdout) + /*********************************************** + ############################################ + DISABLED 08.03.2025, after GH-8462 was fixed + ############################################ + -- 2. revoke: user who has 'admin option' can revoke role from anyone EXCEPT himself + connect '{act.db.dsn}' user {tmp_usr1.name} password '{tmp_usr1.password}'; + -- Following REVOKE should fail with: + -- Statement failed, SQLSTATE = 42000 + -- unsuccessful metadata update + -- -REVOKE failed + -- -{tmp_usr1.name} is not grantor of Role on {tmp_role.name} to {tmp_usr1.name}. + revoke {tmp_role.name} from {tmp_usr1.name}; + + select * from v_grants where upper(who_was_granted) = upper('{tmp_usr1.name}'); -- record should remain + rollback; + ***********************************************/ + + -- 3. revoke - check role owner rights + connect '{act.db.dsn}' user {tmp_usr0.name} password '{tmp_usr0.password}'; + + select 'Point-3' as msg, v.* from v_grants v where upper(v.who_was_granted) = upper('{tmp_usr3.name}'); + + -- current user = {tmp_usr0.name} - is owner of role {tmp_role.name}, but this role was granted to {tmp_usr3.name} by SYSDBA. + -- Q: should user '{tmp_usr0.password}' (current) be able to revoke role which he did NOT grant but owns ? + -- A: yes. + + revoke {tmp_role.name} from {tmp_usr3.name}; -- NO error/warning should be here + + select 'Point-4' as msg, v.* from v_grants v where upper(v.who_was_granted) = upper('{tmp_usr3.name}'); -- record should NOT appear. + rollback; + + -- 4. revoke - check admin option + connect '{act.db.dsn}' user {tmp_usr1.name} password '{tmp_usr1.password}'; + + select 'Point-5' as msg, v.* from v_grants v where upper(v.who_was_granted) in ( upper('{tmp_usr1.name}'), upper('{tmp_usr3.name}') ); -- two records should be here + + -- current user = {tmp_usr1.name} - is NOT owner of role {tmp_role.name} but he was granted to use it WITH ADMIN option + -- (grant {tmp_role.name} to {tmp_usr1.name} with admin option). + -- Q: should user '{tmp_usr1.name}' (current) be able to revoke role which he neither did grant nor owns but has admin option ? + -- A: yes. + + revoke {tmp_role.name} from {tmp_usr3.name}; + + select 'Point-6' as msg, v.* from v_grants v where upper(v.who_was_granted) in (upper('{tmp_usr1.name}'), upper('{tmp_usr3.name}')); -- only one record should be here + rollback; + + -- 5a. drop role - should fail + connect '{act.db.dsn}' user {tmp_usr4.name} password '{tmp_usr4.password}'; + + -- Statement failed, SQLSTATE = 28000 + -- unsuccessful metadata update + -- -DROP ROLE {tmp_role.name} failed + -- -no permission for DROP access to ROLE {tmp_role.name} + + drop role {tmp_role.name}; -- should fail: this user is not owner of this role and he was not granted to use it with admin option + + set count off; + select count(*) from rdb$roles where rdb$role_name = '{tmp_role.name}'; + set count on; + rollback; + + connect '{act.db.dsn}' user {tmp_usr0.name} password '{tmp_usr0.password}'; + + select 'Point-6' as msg, v.* from v_grants v where upper(v.role_name) = upper('{tmp_role.name}'); -- should output 3 records + + drop role {tmp_role.name}; -- current user: '{tmp_usr0.name}' - is owner of role {tmp_role.name} + + select 'Point-7' as msg, r.* from rdb$database d left join rdb$roles r on upper(r.rdb$role_name) = upper('{tmp_role.name}'); -- should output NULLs + + select 'Point-8' as msg, v.* from rdb$database d left join v_grants v on upper(v.role_name) = upper('{tmp_role.name}'); -- should output NULLs + rollback; + + -- 6. drop role - check admin option + connect '{act.db.dsn}' user {tmp_usr1.name} password '{tmp_usr1.password}'; + + -- current user: '{tmp_usr1.name}' - HAS grant on role {tmp_role.name} with admin option (but he is NOT owner of this role). + + select 'Point-9' as msg, v.* from v_grants v where upper(v.role_name) = upper('{tmp_role.name}'); -- should output 3 records + + drop role {tmp_role.name}; -- current user: '{tmp_usr0.name}' - is owner of role {tmp_role.name} + + select 'Point-10' as msg, r.* from rdb$database d left join rdb$roles r on upper(r.rdb$role_name) = upper('{tmp_role.name}'); -- should output NULLs + select 'Point-11' as msg, v.* from rdb$database d left join v_grants v on upper(role_name) = upper('{tmp_role.name}'); -- should output NULLs + rollback; + """ + + + ROLE_NAME = 'TMP_ROLE1' if act.is_version('<6') else '"TMP_ROLE1"' + act.expected_stdout = f""" + MSG Point-1 + WHO_AM_I SYSDBA + WHO_WAS_GRANTED TMP$C5248_USR1 + PRIVILEGE_TYPE M + ROLE_NAME TMP_ROLE1 + ROLE_OWNER TMP$C5248_USR0 + GRANTED_BY SYSDBA + GRANT_OPTION 2 + MSG Point-1 + WHO_AM_I SYSDBA + WHO_WAS_GRANTED TMP$C5248_USR2 + PRIVILEGE_TYPE M + ROLE_NAME TMP_ROLE1 + ROLE_OWNER TMP$C5248_USR0 + GRANTED_BY TMP$C5248_USR1 + GRANT_OPTION 0 + Records affected: 2 + MSG Point-2 + WHO_AM_I SYSDBA + WHO_WAS_GRANTED TMP$C5248_USR2 + PRIVILEGE_TYPE M + ROLE_NAME TMP_ROLE1 + ROLE_OWNER TMP$C5248_USR0 + GRANTED_BY TMP$C5248_USR1 + GRANT_OPTION 0 + Records affected: 1 + MSG Point-3 + WHO_AM_I TMP$C5248_USR0 + WHO_WAS_GRANTED TMP$C5248_USR3 + PRIVILEGE_TYPE M + ROLE_NAME TMP_ROLE1 + ROLE_OWNER TMP$C5248_USR0 + GRANTED_BY SYSDBA + GRANT_OPTION 0 + Records affected: 1 + Records affected: 0 + MSG Point-5 + WHO_AM_I TMP$C5248_USR1 + WHO_WAS_GRANTED TMP$C5248_USR1 + PRIVILEGE_TYPE M + ROLE_NAME TMP_ROLE1 + ROLE_OWNER TMP$C5248_USR0 + GRANTED_BY SYSDBA + GRANT_OPTION 2 + MSG Point-5 + WHO_AM_I TMP$C5248_USR1 + WHO_WAS_GRANTED TMP$C5248_USR3 + PRIVILEGE_TYPE M + ROLE_NAME TMP_ROLE1 + ROLE_OWNER TMP$C5248_USR0 + GRANTED_BY SYSDBA + GRANT_OPTION 0 + Records affected: 2 + MSG Point-6 + WHO_AM_I TMP$C5248_USR1 + WHO_WAS_GRANTED TMP$C5248_USR1 + PRIVILEGE_TYPE M + ROLE_NAME TMP_ROLE1 + ROLE_OWNER TMP$C5248_USR0 + GRANTED_BY SYSDBA + GRANT_OPTION 2 + Records affected: 1 + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -DROP ROLE TMP_ROLE1 failed + -no permission for DROP access to ROLE {ROLE_NAME} + COUNT 1 + MSG Point-6 + WHO_AM_I TMP$C5248_USR0 + WHO_WAS_GRANTED TMP$C5248_USR1 + PRIVILEGE_TYPE M + ROLE_NAME TMP_ROLE1 + ROLE_OWNER TMP$C5248_USR0 + GRANTED_BY SYSDBA + GRANT_OPTION 2 + MSG Point-6 + WHO_AM_I TMP$C5248_USR0 + WHO_WAS_GRANTED TMP$C5248_USR3 + PRIVILEGE_TYPE M + ROLE_NAME TMP_ROLE1 + ROLE_OWNER TMP$C5248_USR0 + GRANTED_BY SYSDBA + GRANT_OPTION 0 + MSG Point-6 + WHO_AM_I TMP$C5248_USR0 + WHO_WAS_GRANTED TMP$C5248_USR2 + PRIVILEGE_TYPE M + ROLE_NAME TMP_ROLE1 + ROLE_OWNER TMP$C5248_USR0 + GRANTED_BY TMP$C5248_USR1 + GRANT_OPTION 0 + Records affected: 3 + MSG Point-7 + RDB$ROLE_NAME + RDB$OWNER_NAME + RDB$DESCRIPTION + RDB$SYSTEM_FLAG + RDB$SECURITY_CLASS + RDB$SYSTEM_PRIVILEGES + Records affected: 1 + MSG Point-8 + WHO_AM_I + WHO_WAS_GRANTED + PRIVILEGE_TYPE + ROLE_NAME + ROLE_OWNER + GRANTED_BY + GRANT_OPTION + Records affected: 1 + MSG Point-9 + WHO_AM_I TMP$C5248_USR1 + WHO_WAS_GRANTED TMP$C5248_USR1 + PRIVILEGE_TYPE M + ROLE_NAME TMP_ROLE1 + ROLE_OWNER TMP$C5248_USR0 + GRANTED_BY SYSDBA + GRANT_OPTION 2 + MSG Point-9 + WHO_AM_I TMP$C5248_USR1 + WHO_WAS_GRANTED TMP$C5248_USR3 + PRIVILEGE_TYPE M + ROLE_NAME TMP_ROLE1 + ROLE_OWNER TMP$C5248_USR0 + GRANTED_BY SYSDBA + GRANT_OPTION 0 + MSG Point-9 + WHO_AM_I TMP$C5248_USR1 + WHO_WAS_GRANTED TMP$C5248_USR2 + PRIVILEGE_TYPE M + ROLE_NAME TMP_ROLE1 + ROLE_OWNER TMP$C5248_USR0 + GRANTED_BY TMP$C5248_USR1 + GRANT_OPTION 0 + Records affected: 3 + MSG Point-10 + RDB$ROLE_NAME + RDB$OWNER_NAME + RDB$DESCRIPTION + RDB$SYSTEM_FLAG + RDB$SECURITY_CLASS + RDB$SYSTEM_PRIVILEGES + Records affected: 1 + MSG Point-11 + WHO_AM_I + WHO_WAS_GRANTED + PRIVILEGE_TYPE + ROLE_NAME + ROLE_OWNER + GRANTED_BY + GRANT_OPTION + Records affected: 1 + """ + act.isql(switches = ['-q'], input = test_sql, combine_output = True, connect_db = False, io_enc = locale.getpreferredencoding()) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5269_test.py b/tests/bugs/core_5269_test.py index 1256aa86..e85839cc 100644 --- a/tests/bugs/core_5269_test.py +++ b/tests/bugs/core_5269_test.py @@ -61,6 +61,7 @@ where p.rdb$user = upper('TMP$C5269_2'); """ +@pytest.mark.trace @pytest.mark.version('>=4.0') def test_1(act: Action, user_a: User, user_b: User, test_role: Role): with act.db.connect() as con: diff --git a/tests/bugs/core_5273_test.py b/tests/bugs/core_5273_test.py index 139e7b70..f51a59e7 100644 --- a/tests/bugs/core_5273_test.py +++ b/tests/bugs/core_5273_test.py @@ -3,8 +3,7 @@ """ ID: issue-5551 ISSUE: 5551 -TITLE: Crash when attempt to create database with running trace ( internal Firebird - consistency check (cannot find tip page (165), file: tra.cpp line: 2233) ) +TITLE: Attempt to create database with running trace leads to consistency check (cannot find tip page (165), file: tra.cpp line: 2233) DESCRIPTION: 1. Get the content of firebird.log before test. 2. Make config file and launch trace session, with separate logging of its STDOUT and STDERR. @@ -43,22 +42,26 @@ 'max_log_size = 5000000', ] +@pytest.mark.trace @pytest.mark.version('>=4.0') def test_1(act: Action, temp_db: Path): sql_ddl = f""" - set list on; - set bail on; - create database 'localhost:{temp_db}'; - select mon$database_name from mon$database; - commit; - drop database; + set list on; + set bail on; + create database 'localhost:{temp_db}'; + select mon$database_name from mon$database; + commit; + drop database; """ # Get content of firebird.log BEFORE test log_before = act.get_firebird_log() + # Start trace with act.trace(db_events=trace, keep_log=False, database=temp_db.name): - act.isql(switches=[], input=sql_ddl) + act.isql(switches = ['-q'], input = sql_ddl, connect_db = False, combine_output = True) + # Get content of firebird.log AFTER test log_after = act.get_firebird_log() + # Check assert list(unified_diff(log_before, log_after)) == [] diff --git a/tests/bugs/core_5275_test.py b/tests/bugs/core_5275_test.py index 5969ad03..e0d4e11f 100644 --- a/tests/bugs/core_5275_test.py +++ b/tests/bugs/core_5275_test.py @@ -27,7 +27,6 @@ gen_id(,1) will return 0 (ZERO!) rather than 1. See also CORE-6084 and its fix: https://github.com/FirebirdSQL/firebird/commit/23dc0c6297825b2e9006f4d5a2c488702091033d This is considered as *expected* and is noted in doc/README.incompatibilities.3to4.txt - Because of this, it was decided to replace 'alter sequence restart...' with subtraction of two gen values: c = gen_id(, -gen_id(, 0)) -- see procedure sp_restart_sequences. [15.09.2022] pzotov @@ -42,14 +41,18 @@ This test FAILS on Linux when running against FB 4.x (almost every run on Classic, but also it can fail on Super). Connection that is waiting for COMMIT during index creation for some reason can finish its work successfully, despite the fact that we issue 'delete from mon$attachments' and all transactions have to be rolled back. - Issue that was described in the ticket can be reproduced if attachment will be killed during creation of SECOND (non-computed) index for big table within the same transaction that creates first (computed-by) index. Perhaps, one need to query IndexRoot Page in some other ('monitoring') connection and run 'delete from mon$attachments' command exactly at the moment when result of parsing shows that we have only 1st index for selected relation. Discussed with dimitr et al, letters ~20-mar-2023. - Test needs to be fully re-implemented, but it remains avaliable for Windows because it shows stable results there. + + [01.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' and variables - to be substituted in expected_* on FB 6.x + Separated expected output for FB major versions prior/since 6.x. + + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import platform @@ -197,40 +200,6 @@ delete from mon$attachments where mon$attachment_id<>current_connection; """ -expected_stdout = """ - 0: BULK INSERTS LOG: BULK_INSERT_START - 0: BULK INSERTS LOG: STATEMENT FAILED, SQLSTATE = 08003 - 0: BULK INSERTS LOG: CONNECTION SHUTDOWN - 0: BULK INSERTS LOG: AFTER LINE - 0: CREATE INDEX LOG: INSERTS_STATE OK, IS RUNNING - 0: CREATE INDEX LOG: CREATE_INDX_START - 0: CREATE INDEX LOG: SET TRANSACTION WAIT; - 0: CREATE INDEX LOG: CREATE INDEX TEST_WAIT ON TEST COMPUTED BY('WAIT' || S); - 0: CREATE INDEX LOG: SET ECHO OFF; - 0: CREATE INDEX LOG: STATEMENT FAILED, SQLSTATE = 08003 - 0: CREATE INDEX LOG: CONNECTION SHUTDOWN - 0: CREATE INDEX LOG: AFTER LINE - 0: KILL ATTACH LOG: RECORDS AFFECTED: - 1: BULK INSERTS LOG: BULK_INSERT_START - 1: BULK INSERTS LOG: STATEMENT FAILED, SQLSTATE = 08003 - 1: BULK INSERTS LOG: CONNECTION SHUTDOWN - 1: BULK INSERTS LOG: AFTER LINE - 1: CREATE INDEX LOG: INSERTS_STATE OK, IS RUNNING - 1: CREATE INDEX LOG: CREATE_INDX_START - 1: CREATE INDEX LOG: SET TRANSACTION WAIT; - 1: CREATE INDEX LOG: CREATE INDEX TEST_WAIT ON TEST COMPUTED BY('WAIT' || S); - 1: CREATE INDEX LOG: SET ECHO OFF; - 1: CREATE INDEX LOG: STATEMENT FAILED, SQLSTATE = 08003 - 1: CREATE INDEX LOG: CONNECTION SHUTDOWN - 1: CREATE INDEX LOG: AFTER LINE - 1: KILL ATTACH LOG: RECORDS AFFECTED: - VALIDATION STDOUT: 20:05:26.86 VALIDATION STARTED - VALIDATION STDOUT: 20:05:26.86 RELATION 128 (TEST) - VALIDATION STDOUT: 20:05:26.86 PROCESS POINTER PAGE 0 OF 1 - VALIDATION STDOUT: 20:05:26.86 INDEX 1 (TEST_X) - VALIDATION STDOUT: 20:05:26.86 RELATION 128 (TEST) IS OK - VALIDATION STDOUT: 20:05:26.86 VALIDATION FINISHED -""" def print_validation(line: str) -> None: if line.strip(): @@ -287,6 +256,45 @@ def test_1(act: Action, bulk_insert_script: Path, bulk_insert_output: Path, with act.connect_server() as srv: srv.database.validate(database=act.db.db_path, callback=print_validation) + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + TABLE_NAME = 'TEST' if act.is_version('<6') else '"TEST"' + INDEX_NAME = 'TEST_X' if act.is_version('<6') else '"TEST_X"' + + expected_stdout = f""" + 0: BULK INSERTS LOG: BULK_INSERT_START + 0: BULK INSERTS LOG: STATEMENT FAILED, SQLSTATE = 08003 + 0: BULK INSERTS LOG: CONNECTION SHUTDOWN + 0: BULK INSERTS LOG: AFTER LINE + 0: CREATE INDEX LOG: INSERTS_STATE OK, IS RUNNING + 0: CREATE INDEX LOG: CREATE_INDX_START + 0: CREATE INDEX LOG: SET TRANSACTION WAIT; + 0: CREATE INDEX LOG: CREATE INDEX TEST_WAIT ON TEST COMPUTED BY('WAIT' || S); + 0: CREATE INDEX LOG: SET ECHO OFF; + 0: CREATE INDEX LOG: STATEMENT FAILED, SQLSTATE = 08003 + 0: CREATE INDEX LOG: CONNECTION SHUTDOWN + 0: CREATE INDEX LOG: AFTER LINE + 0: KILL ATTACH LOG: RECORDS AFFECTED: + 1: BULK INSERTS LOG: BULK_INSERT_START + 1: BULK INSERTS LOG: STATEMENT FAILED, SQLSTATE = 08003 + 1: BULK INSERTS LOG: CONNECTION SHUTDOWN + 1: BULK INSERTS LOG: AFTER LINE + 1: CREATE INDEX LOG: INSERTS_STATE OK, IS RUNNING + 1: CREATE INDEX LOG: CREATE_INDX_START + 1: CREATE INDEX LOG: SET TRANSACTION WAIT; + 1: CREATE INDEX LOG: CREATE INDEX TEST_WAIT ON TEST COMPUTED BY('WAIT' || S); + 1: CREATE INDEX LOG: SET ECHO OFF; + 1: CREATE INDEX LOG: STATEMENT FAILED, SQLSTATE = 08003 + 1: CREATE INDEX LOG: CONNECTION SHUTDOWN + 1: CREATE INDEX LOG: AFTER LINE + 1: KILL ATTACH LOG: RECORDS AFFECTED: + VALIDATION STDOUT: 20:05:26.86 VALIDATION STARTED + VALIDATION STDOUT: 20:05:26.86 RELATION 128 ({SQL_SCHEMA_PREFIX}{TABLE_NAME}) + VALIDATION STDOUT: 20:05:26.86 PROCESS POINTER PAGE 0 OF 1 + VALIDATION STDOUT: 20:05:26.86 INDEX 1 ({SQL_SCHEMA_PREFIX}{INDEX_NAME}) + VALIDATION STDOUT: 20:05:26.86 RELATION 128 ({SQL_SCHEMA_PREFIX}{TABLE_NAME}) IS OK + VALIDATION STDOUT: 20:05:26.86 VALIDATION FINISHED + """ + # Check act.expected_stdout = expected_stdout act.stdout = capsys.readouterr().out diff --git a/tests/bugs/core_5313_test.py b/tests/bugs/core_5313_test.py index e7c08387..54dd7acb 100644 --- a/tests/bugs/core_5313_test.py +++ b/tests/bugs/core_5313_test.py @@ -7,6 +7,14 @@ DESCRIPTION: JIRA: CORE-5313 FBTEST: bugs.core_5313 +NOTES: + [01.07.2025] pzotov + Refactored: we have to check only rows which contain either 'sqltype' or 'SQLSTATE'. + Added appropriate substitutions. + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -17,38 +25,31 @@ test_script = """ set planonly; set sqlda_display on; - select list(trim(rdb$relation_name), ?) from rdb$relations; + select list(trim(rdb$relation_name), ?) from rdb$relations rows 0; """ -act = isql_act('db', test_script) +substitutions=[('^((?!(SQLSTATE|sqltype)).)*$', ''), ('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ - INPUT message field count: 1 - 01: sqltype: 452 TEXT scale: 0 subtype: 0 len: 4 charset: 4 UTF8 - : name: alias: - : table: owner: - - PLAN (RDB$RELATIONS NATURAL) +expected_stdout_3x = """ + 01: sqltype: 452 TEXT scale: 0 subtype: 0 len: 3 charset: 3 UNICODE_FSS + 01: sqltype: 520 BLOB Nullable scale: 0 subtype: 1 len: 8 charset: 3 UNICODE_FSS +""" - OUTPUT message field count: 1 +expected_stdout_5x = """ + 01: sqltype: 452 TEXT scale: 0 subtype: 0 len: 4 charset: 4 UTF8 01: sqltype: 520 BLOB Nullable scale: 0 subtype: 1 len: 8 charset: 4 UTF8 - : name: LIST alias: LIST - : table: owner: """ -# version: 3.0 +expected_stdout_6x = """ + 01: sqltype: 452 TEXT scale: 0 subtype: 0 len: 4 charset: 4 SYSTEM.UTF8 + 01: sqltype: 520 BLOB Nullable scale: 0 subtype: 1 len: 8 charset: 4 SYSTEM.UTF8 +""" -@pytest.mark.version('>=3.0.1,<4.0') -def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute(charset='utf8') - assert act.clean_stdout == act.clean_expected_stdout -# version: 4.0 +@pytest.mark.version('>=3.0') +def test_1(act: Action): -@pytest.mark.version('>=4.0') -def test_2(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_3x if act.is_version('<4') else expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_5325_test.py b/tests/bugs/core_5325_test.py index a63e4878..e536bd03 100644 --- a/tests/bugs/core_5325_test.py +++ b/tests/bugs/core_5325_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-5325 FBTEST: bugs.core_5325 + [01.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' and variables - to be substituted in expected_* on FB 6.x + Separated expected output for FB major versions prior/since 6.x. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -22,32 +27,34 @@ act = python_act('db', substitutions=substitutions) test_script = """ - create exception error_test 'йцукенг'; + create exception exc_qwerty 'йцукенг'; commit; set term ^; execute block as begin - exception error_test 'йцу' || _win1251 x'0d0a' || 'кенг'; + exception exc_qwerty 'йцу' || _win1251 x'0d0a' || 'кенг'; end^ set term ;^ """ script_file = temp_file('test-script.sql') -expected_stderr_1 = """ - Statement failed, SQLSTATE = HY000 - exception 1 - -ERROR_TEST - -йцу - - кенг -""" - @pytest.mark.version('>=3.0') def test_1(act: Action, script_file: Path): script_file.write_text(test_script, encoding='cp1251') - act.expected_stderr = expected_stderr_1 - act.isql(switches=['-q'], input_file=script_file, charset='WIN1251') - assert act.clean_stderr == act.clean_expected_stderr + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + EXCEPTION_NAME = ('exc_qwerty' if act.is_version('<6') else '"exc_qwerty"').upper() + expected_out = f""" + Statement failed, SQLSTATE = HY000 + exception 1 + -{SQL_SCHEMA_PREFIX}{EXCEPTION_NAME} + -йцу + + кенг + """ + act.expected_stdout = expected_out + act.isql(switches=['-q'], input_file=script_file, charset='WIN1251', combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5330_test.py b/tests/bugs/core_5330_test.py index d5c370ca..89a52ab1 100644 --- a/tests/bugs/core_5330_test.py +++ b/tests/bugs/core_5330_test.py @@ -3,8 +3,7 @@ """ ID: issue-5606 ISSUE: 5606 -TITLE: Trace session leads FB 4.0 to hang after 2nd launch of trivial .sql script. - Neither attach to any database nor regular restart of FB service can be done. +TITLE: Trace session leads FB 4.0 to hang after 2nd launch of trivial .sql script. Neither attach to any database nor regular restart of FB service can be done. DESCRIPTION: Ticket issue was reproduced on trivial trace config with single line ("enabled = true"). We prepare such config, launch trace session in async mode and run THREE times isql with logging its output. @@ -87,6 +86,7 @@ '}'] +@pytest.mark.trace @pytest.mark.version('>=4.0') def test_1(act: Action, capsys): with act.trace(config=trace, keep_log=False): diff --git a/tests/bugs/core_5367_test.py b/tests/bugs/core_5367_test.py index d8f7808f..970e676a 100644 --- a/tests/bugs/core_5367_test.py +++ b/tests/bugs/core_5367_test.py @@ -7,46 +7,38 @@ DESCRIPTION: JIRA: CORE-5367 FBTEST: bugs.core_5367 +NOTES: + [01.07.2025] pzotov + Refactored: we have to check only rows which contain either 'sqltype' or 'SQLSTATE'. + Added appropriate substitutions. + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -init_script = """ - recreate table test(id int,boo boolean); -""" - -db = db_factory(init=init_script) +db = db_factory() test_script = """ + recreate table test(id int, boo boolean); set sqlda_display on; set planonly; select * from test where ?; - set planonly; """ -act = isql_act('db', test_script) + +substitutions=[('^((?!(SQLSTATE|sqltype)).)*$', ''), ('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ - INPUT message field count: 1 01: sqltype: 32764 BOOLEAN scale: 0 subtype: 0 len: 1 - : name: alias: - : table: owner: - - PLAN (TEST NATURAL) - - OUTPUT message field count: 2 01: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 - : name: ID alias: ID - : table: TEST owner: SYSDBA 02: sqltype: 32764 BOOLEAN Nullable scale: 0 subtype: 0 len: 1 - : name: BOO alias: BOO - : table: TEST owner: SYSDBA """ @pytest.mark.version('>=3.0.2') def test_1(act: Action): + act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_5380_test.py b/tests/bugs/core_5380_test.py index c0b9f7dc..58960c53 100644 --- a/tests/bugs/core_5380_test.py +++ b/tests/bugs/core_5380_test.py @@ -8,6 +8,12 @@ We check not only ability of recursive calls but also max depth of them. It should be equal to 1000. JIRA: CORE-5380 FBTEST: bugs.core_5380 + [30.06.2025] pzotov + Part of call stack ('At sub function line X col Y') must be supressed because its length is limited to 1024 characters + and number of lines (together with interrupting marker '...') depends on length of function name that is called recursively. + It is enough for this test to check only presense of 'SQLSTATE = 54001' and 'Too many concurrent executions' in STDERR. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -41,40 +47,18 @@ -- (a1 + an) * n / 2 """ -act = isql_act('db', test_script, substitutions=[('line:\\s[0-9]+,', 'line: x'), - ('col:\\s[0-9]+', 'col: y')]) +substitutions = [ ('^((?!(SQLSTATE|Too many concurrent executions|ARITHMETIC_PROGRESSION_TOTAL)).)*$', ''), ('[ \t]+', ' ') ] +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ - ARITHMETIC_PROGRESSION_TOTAL 501501 -""" - -expected_stderr = """ + ARITHMETIC_PROGRESSION_TOTAL 501501 Statement failed, SQLSTATE = 54001 Too many concurrent executions of the same request - -At sub function 'GET_SUB_TOTAL_RECURSIVELY' line: 7, col: 13 - At sub function 'GET_SUB_TOTAL_RECURSIVELY' line: 7, col: 13 - At sub function 'GET_SUB_TOTAL_RECURSIVELY' line: 7, col: 13 - At sub function 'GET_SUB_TOTAL_RECURSIVELY' line: 7, col: 13 - At sub function 'GET_SUB_TOTAL_RECURSIVELY' line: 7, col: 13 - At sub function 'GET_SUB_TOTAL_RECURSIVELY' line: 7, col: 13 - At sub function 'GET_SUB_TOTAL_RECURSIVELY' line: 7, col: 13 - At sub function 'GET_SUB_TOTAL_RECURSIVELY' line: 7, col: 13 - At sub function 'GET_SUB_TOTAL_RECURSIVELY' line: 7, col: 13 - At sub function 'GET_SUB_TOTAL_RECURSIVELY' line: 7, col: 13 - At sub function 'GET_SUB_TOTAL_RECURSIVELY' line: 7, col: 13 - At sub function 'GET_SUB_TOTAL_RECURSIVELY' line: 7, col: 13 - At sub function 'GET_SUB_TOTAL_RECURSIVELY' line: 7, col: 13 - At sub function 'GET_SUB_TOTAL_RECURSIVELY' line: 7, col: 13 - At sub function 'GET_SUB_TOTAL_RECURSIVELY' line: 7, col: 13 - At sub function 'GET_SUB_TOTAL_RECURSIVELY' line: 7, col: 13 - At sub function 'GET_SUB_TOTAL_RECURSIVELY'... """ @pytest.mark.version('>=4.0') def test_1(act: Action): act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5381_test.py b/tests/bugs/core_5381_test.py index 0577b6d5..ccc31232 100644 --- a/tests/bugs/core_5381_test.py +++ b/tests/bugs/core_5381_test.py @@ -2,104 +2,151 @@ """ ID: issue-5654 -ISSUE: 5654 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/5654 TITLE: Regression: could not execute query (select from view with nested view) DESCRIPTION: + Test uses queries from ticket but creates user tables instead of rdb$ ones. We ask FB only to prepare query. + Before fix this query preparation lasted ~130 seconds with raising '335544382 : request size limit exceeded. JIRA: CORE-5381 FBTEST: bugs.core_5381 -""" +NOTES: + [17.11.2024] pzotov + Re-implemented. No sense to check execution time or execution plan. + We have to ensure only ability of engine to complete prerape_statement. -import pytest -from firebird.qa import * + [19.11.2024] pzotov + Added max allowed time for prepare duration and appropriate check (suggested by dimitr). -db = db_factory() + Confirmed bug on 3.0.1.32609 (27-sep-2016), got in trace: + 2024-11-08T00:42:49.8710 ERROR AT JStatement::prepare + 335544382 : request size limit exceeded -test_script = """ - create table t1(ID bigint not null primary key); - create table t2(ID bigint not null primary key); - create table t3(ID bigint not null primary key); - create table t4(ID bigint not null primary key); - create table t5(ID bigint not null primary key); - create table t6(ID bigint not null primary key); - create table t7(ID bigint not null primary key); - create table t8(ID bigint not null primary key); + Checked on 3.0.13.33794, 4.0.6.3168, 5.0.2.1553, 6.0.0.520 +""" +import datetime as py_dt +from datetime import timedelta - create view inner_view(ID) - as - select t1.ID - from t1 - inner join t8 B on B.ID = t1.ID - inner join t2 C on C.ID = t1.ID - left join t4 D on D.ID = t1.ID - inner join t5 E on E.ID = t1.ID - left join t6 F on F.ID = t1.ID - - inner join RDB$TYPES G1 on G1.rdb$type = t1.ID - inner join RDB$RELATIONS G2 on G2.rdb$relation_id = t1.ID - inner join RDB$DEPENDENCIES G3 on G3.rdb$dependent_type = t1.ID - inner join RDB$COLLATIONS G4 on G4.rdb$collation_id = t1.ID - inner join RDB$FIELDS G5 on G5.rdb$field_type = t1.ID - inner join RDB$CHARACTER_SETS G6 on G6.rdb$character_set_id = t1.ID +import pytest +from firebird.qa import * +from firebird.driver import DatabaseError + +################### +MAX_PREPARE_TIME_MS=1000 +################### + +init_sql = """ + recreate view test_view as select 1 x from rdb$database; + recreate view inner_view as select 1 x from rdb$database; + + recreate table rdb_types( + id int generated by default as identity constraint pk_rdb_types primary key + ,type_id int + ); + + recreate table rdb_rels( + id int generated by default as identity constraint pk_rdb_rels primary key + ,rel_id int + ); + + recreate table rdb_deps( + id int generated by default as identity constraint pk_rdb_deps primary key + ,dep_type int + ); + + recreate table rdb_colls( + id int generated by default as identity constraint pk_rdb_colls primary key + ,coll_id int + ); + + recreate table rdb_flds( + id int generated by default as identity constraint pk_rdb_flds primary key + ,fld_type_id int + ); + + recreate table rdb_csets( + id int generated by default as identity constraint pk_rdb_csets primary key + ,cset_id int + ); + + + recreate table t1(id bigint not null primary key using index pk_t1_id); + recreate table t2(id bigint not null primary key using index pk_t2_id); + recreate table t3(id bigint not null primary key using index pk_t3_id); + recreate table t4(id bigint not null primary key using index pk_t4_id); + recreate table t5(id bigint not null primary key using index pk_t5_id); + recreate table t6(id bigint not null primary key using index pk_t6_id); + recreate table t7(id bigint not null primary key using index pk_t7_id); + recreate table t8(id bigint not null primary key using index pk_t8_id); + + recreate view inner_view as + select t1.id + from t1 + inner join t8 b on b.id = t1.id + inner join t2 c on c.id = t1.id + left join t4 d on d.id = t1.id + inner join t5 e on e.id = t1.id + left join t6 f on f.id = t1.id + inner join rdb_types g1 on g1.type_id = t1.id + inner join rdb_rels g2 on g2.rel_id = t1.id + inner join rdb_deps g3 on g3.dep_type = t1.id + inner join rdb_colls g4 on g4.coll_id = t1.id + inner join rdb_flds g5 on g5.fld_type_id = t1.id + inner join rdb_csets g6 on g6.cset_id = t1.id ; - create view test_view(ID) + recreate view test_view as - select t1.ID - from t1 - inner join inner_view on inner_view.ID = t1.ID - inner join t7 on t7.ID = t1.ID - left join t3 on t3.ID = t1.ID - - inner join RDB$TYPES D1 on D1.rdb$type = t1.ID - inner join RDB$RELATIONS D2 on D2.rdb$relation_id = t1.ID - inner join RDB$DEPENDENCIES D3 on D3.rdb$dependent_type = t1.ID - inner join RDB$COLLATIONS D4 on D4.rdb$collation_id = t1.ID - inner join RDB$FIELDS D5 on D5.rdb$field_type = t1.ID + select t1.id + from t1 + inner join inner_view on inner_view.id = t1.id + inner join t7 on t7.id = t1.id + left join t3 on t3.id = t1.id + + inner join rdb_types d1 on d1.type_id = t1.id + inner join rdb_rels d2 on d2.rel_id = t1.id + inner join rdb_deps d3 on d3.dep_type = t1.id + inner join rdb_colls d4 on d4.coll_id = t1.id + inner join rdb_flds d5 on d5.fld_type_id = t1.id ; commit; - - set list on; - - set term ^; - execute block returns( result varchar(128) ) as - declare dts_beg timestamp; - declare c int; - declare elap_ms int; - declare max_allowed_ms int = 1000; - -- ##### - -- ^ - -- | - -- ########################### - -- ### T H R E S H O L D ### - -- ########################### - begin - dts_beg ='now'; - - select A.ID - from test_view A - inner join RDB$TYPES D1 on D1.rdb$type = A.ID - inner join RDB$RELATIONS D2 on D2.rdb$relation_id = A.ID - inner join RDB$DEPENDENCIES D3 on D3.rdb$dependent_type = A.ID - where A.ID = 1 - into c; - elap_ms = datediff(millisecond from dts_beg to cast('now' as timestamp)); - result = iif(elap_ms <= max_allowed_ms, 'Acceptable.', 'TOO LONG: ' || elap_ms || ' ms - more than max allowed ' || max_allowed_ms || ' ms.' ); - suspend; - end - ^ - set term ;^ - -""" - -act = isql_act('db', test_script) - -expected_stdout = """ - RESULT Acceptable. """ - -@pytest.mark.version('>=3.0.2') -def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() +db = db_factory(init = init_sql) + +act = python_act('db') + +#----------------------------------------------------------- + +@pytest.mark.version('>=3.0.1') +def test_1(act: Action, capsys): + + test_sql = """ + select count(*) + from test_view a + inner join rdb_types d1 on d1.type_id = a.id + inner join rdb_rels d2 on d2.rel_id = a.id + inner join rdb_deps d3 on d3.dep_type = a.id + where a.id = 1 + ; + """ + td = 86400000 + with act.db.connect() as con: + cur = con.cursor() + ps = None + try: + t1=py_dt.datetime.now() + ps = cur.prepare(test_sql) + t2=py_dt.datetime.now() + td = int((t2-t1).total_seconds() * 1000) # milliseconds + print('Completed.') + except DatabaseError as e: + print(e.__str__()) + print(e.gds_codes) + finally: + if ps: + ps.free() + + act.expected_stdout = 'Completed.' + act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout + assert td < MAX_PREPARE_TIME_MS, f'Prepare time: {td} ms - greater than max allowed {MAX_PREPARE_TIME_MS} ms.' diff --git a/tests/bugs/core_5383_test.py b/tests/bugs/core_5383_test.py index 6b757777..3b5429e9 100644 --- a/tests/bugs/core_5383_test.py +++ b/tests/bugs/core_5383_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-5383 FBTEST: bugs.core_5383 +NOTES: + [01.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -117,45 +122,63 @@ execute procedure pg_03.p03(1); """ -act = isql_act('db', test_script) +substitutions = [] # [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ - - RDB$DEPENDENT_NAME PG_03 - RDB$DEPENDED_ON_NAME TEST01 - - RDB$DEPENDENT_NAME PG_03 - RDB$DEPENDED_ON_NAME TEST02 - - RDB$DEPENDENT_NAME PG_03 - RDB$DEPENDED_ON_NAME TEST03 - - Records affected: 3 - - O_Y 111 - O_Y 222 - O_Y 333 +expected_stdout_5x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -cannot delete + -COLUMN TEST01.ID1 + -there are 1 dependencies + + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -cannot delete + -COLUMN TEST02.ID2 + -there are 1 dependencies + + RDB$DEPENDENT_NAME PG_03 + RDB$DEPENDED_ON_NAME TEST01 + RDB$DEPENDENT_NAME PG_03 + RDB$DEPENDED_ON_NAME TEST02 + RDB$DEPENDENT_NAME PG_03 + RDB$DEPENDED_ON_NAME TEST03 + Records affected: 3 + + O_Y 111 + O_Y 222 + O_Y 333 """ -expected_stderr = """ +expected_stdout_6x = """ Statement failed, SQLSTATE = 42000 unsuccessful metadata update -cannot delete - -COLUMN TEST01.ID1 + -COLUMN "PUBLIC"."TEST01"."ID1" -there are 1 dependencies Statement failed, SQLSTATE = 42000 unsuccessful metadata update -cannot delete - -COLUMN TEST02.ID2 + -COLUMN "PUBLIC"."TEST02"."ID2" -there are 1 dependencies + + RDB$DEPENDENT_NAME PG_03 + RDB$DEPENDED_ON_NAME TEST01 + RDB$DEPENDENT_NAME PG_03 + RDB$DEPENDED_ON_NAME TEST02 + RDB$DEPENDENT_NAME PG_03 + RDB$DEPENDED_ON_NAME TEST03 + Records affected: 3 + + O_Y 111 + O_Y 222 + O_Y 333 """ @pytest.mark.version('>=3.0.2') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5389_test.py b/tests/bugs/core_5389_test.py index 8f9a1b8e..aab83154 100644 --- a/tests/bugs/core_5389_test.py +++ b/tests/bugs/core_5389_test.py @@ -45,6 +45,7 @@ script_file = temp_file('test-script.sql') +@pytest.mark.intl @pytest.mark.version('>=4.0') def test_1(act: Action, script_file: Path): script_file.write_text(test_script, encoding='cp1251') diff --git a/tests/bugs/core_5404_test.py b/tests/bugs/core_5404_test.py index 6d483722..d9edfc7c 100644 --- a/tests/bugs/core_5404_test.py +++ b/tests/bugs/core_5404_test.py @@ -5,11 +5,16 @@ ISSUE: 5677 TITLE: Inconsistent column/line references when PSQL definitions return errors DESCRIPTION: - ### WARNING ### - Following code is intentionaly aborted in the middle point because some cases are not - covered by fix of this ticket (see also issue in the ticket, 22/Nov/16 06:10 PM). + ### WARNING ### + Following code is intentionaly aborted in the middle point because some cases are not + covered by fix of this ticket (see also issue in the ticket, 22/Nov/16 06:10 PM). JIRA: CORE-5404 FBTEST: bugs.core_5404 +NOTES: + [01.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -93,7 +98,7 @@ substitutions=[('-At line[:]{0,1}[\\s]+[\\d]+,[\\s]+column[:]{0,1}[\\s]+[\\d]+', '-At line: column:')]) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 42000 Dynamic SQL Error -SQL error code = -104 @@ -110,9 +115,25 @@ -At line 4, column 10 """ +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Zero length identifiers are not allowed + -At line: column: + + Statement failed, SQLSTATE = 42S22 + unsuccessful metadata update + -CREATE OR ALTER PROCEDURE "PUBLIC"."DSQL_FIELD_ERR2" failed + -Dynamic SQL Error + -SQL error code = -206 + -Column unknown + -"FOO" + -At line: column: +""" + @pytest.mark.version('>=4.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5410_test.py b/tests/bugs/core_5410_test.py index 8a2422c4..587afdb2 100644 --- a/tests/bugs/core_5410_test.py +++ b/tests/bugs/core_5410_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-5410 FBTEST: bugs.core_5410 +NOTES: + [01.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -109,36 +114,50 @@ select pg_test.fn_outer_packaged() as min_f02 from rdb$database; """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ - MAX_F01 100 - MIN_F01 1 +expected_stdout_5x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -cannot delete + -COLUMN TEST_1.F01 + -there are 2 dependencies - MAX_F02 200 - MIN_F02 2 + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -cannot delete + -COLUMN TEST_2.F02 + -there are 1 dependencies + MAX_F01 100 + MIN_F01 1 + MAX_F02 200 + MIN_F02 2 """ -expected_stderr = """ +expected_stdout_6x = """ Statement failed, SQLSTATE = 42000 unsuccessful metadata update -cannot delete - -COLUMN TEST_1.F01 + -COLUMN "PUBLIC"."TEST_1"."F01" -there are 2 dependencies Statement failed, SQLSTATE = 42000 unsuccessful metadata update -cannot delete - -COLUMN TEST_2.F02 + -COLUMN "PUBLIC"."TEST_2"."F02" -there are 1 dependencies + + MAX_F01 100 + MIN_F01 1 + MAX_F02 200 + MIN_F02 2 """ @pytest.mark.version('>=3.0.2') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5421_test.py b/tests/bugs/core_5421_test.py index ef5c233a..52f867c5 100644 --- a/tests/bugs/core_5421_test.py +++ b/tests/bugs/core_5421_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-5421 FBTEST: bugs.core_5421 +NOTES: + [01.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -62,17 +67,23 @@ """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ +expected_stdout_5x = """ PLAN SORT (JOIN (C INDEX (C5421_TMAIN_EKEY), D INDEX (C5421_TDETL_DOC_ID))) - DOC_ID 0 + DOC_ID 0 + Records affected: 1 +""" + +expected_stdout_6x = """ + PLAN SORT (JOIN ("C" INDEX ("PUBLIC"."C5421_TMAIN_EKEY"), "D" INDEX ("PUBLIC"."C5421_TDETL_DOC_ID"))) + DOC_ID 0 Records affected: 1 """ @pytest.mark.version('>=3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_5423_test.py b/tests/bugs/core_5423_test.py index 3680951c..b3960eed 100644 --- a/tests/bugs/core_5423_test.py +++ b/tests/bugs/core_5423_test.py @@ -7,6 +7,10 @@ DESCRIPTION: JIRA: CORE-5423 FBTEST: bugs.core_5423 +NOTES: + [01.07.2025] pzotov + Refactored: suppress output of column name ('foo') that is unknown - it has no matter for this test. + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -21,30 +25,21 @@ select 2 k from rdb$database where 1 between 0 and 2 and foo is not null; """ -act = isql_act('db', test_script, - substitutions=[('-At line[:]{0,1}[\\s]+[\\d]+,[\\s]+column[:]{0,1}[\\s]+[\\d]+', - '-At line: column:')]) +substitutions = [ ('[ \t]+', ' '), ('(-)?(")?FOO(")?', ''), (r'(-)?At line(:)?\s+\d+.*', '') ] +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ - K 1 - + K 1 Records affected: 1 -""" - -expected_stderr = """ Statement failed, SQLSTATE = 42S22 Dynamic SQL Error -SQL error code = -206 -Column unknown - -FOO - -At line: column: """ @pytest.mark.version('>=3.0') def test_1(act: Action): act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5431_test.py b/tests/bugs/core_5431_test.py index d8cd8935..3d293f45 100644 --- a/tests/bugs/core_5431_test.py +++ b/tests/bugs/core_5431_test.py @@ -12,6 +12,10 @@ Adjusted expected_stderr according to notes in #7229. Removed unneeded substitutions and old comments. Confirmed problem on 5.0.0.573 Checked on 5.0.0.958, 4.0.3.2903 - all OK. + + [01.07.2025] pzotov + Refactored: suppress output of column name (TEST1.ID) related to validation error - it has no matter for this test. + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -49,23 +53,21 @@ insert into test1 default values returning id as test1_id; """ -act = isql_act('db', test_script) +substitutions = [ ('[ \t]+', ' '), ('validation error for column .*', 'validation error for column') ] +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ - IDENTITY_SEQUENCES_COUNT_1 1 - TEST1_ID 32767 - IDENTITY_SEQUENCES_COUNT_2 0 -""" -expected_stderr = """ + IDENTITY_SEQUENCES_COUNT_1 1 + TEST1_ID 32767 + IDENTITY_SEQUENCES_COUNT_2 0 + Statement failed, SQLSTATE = 23000 - validation error for column "TEST1"."ID", value "*** null ***" + validation error for column """ @pytest.mark.version('>=4.0') def test_1(act: Action): act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5435_test.py b/tests/bugs/core_5435_test.py index 88de6dff..0ffe3bd0 100644 --- a/tests/bugs/core_5435_test.py +++ b/tests/bugs/core_5435_test.py @@ -25,10 +25,10 @@ JIRA: CORE-5435 FBTEST: bugs.core_5435 NOTES: - [25.11.2023] pzotov - Writing code requires more care since 6.0.0.150: ISQL does not allow to specify THE SAME terminator twice, - i.e. - set term @; select 1 from rdb$database @ set term @; - will not compile ("Unexpected end of command" raises). + [01.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -40,10 +40,6 @@ FETCHES_THRESHOLD = 80 -expected_stdout = """ - PLAN (TEST ORDER TEST_F01_ID) -""" - async_init_script = """ recreate table test ( @@ -95,6 +91,7 @@ 'log_statement_finish = true', ] +@pytest.mark.trace @pytest.mark.version('>=3.0.2') def test_1(act: Action): act.isql(switches=[], input=async_init_script) @@ -114,6 +111,11 @@ def test_1(act: Action): if words[k].startswith('fetch'): num_of_fetches = int(words[k - 1]) break - # Check - assert run_with_plan == 'PLAN (TEST ORDER TEST_F01_ID)' + + if act.is_version('<6'): + expected_plan = 'PLAN (TEST ORDER TEST_F01_ID)' + else: + expected_plan = 'PLAN ("PUBLIC"."TEST" ORDER "PUBLIC"."TEST_F01_ID")' + + assert run_with_plan == expected_plan assert num_of_fetches < FETCHES_THRESHOLD diff --git a/tests/bugs/core_5441_test.py b/tests/bugs/core_5441_test.py index 88332365..7133e8e9 100644 --- a/tests/bugs/core_5441_test.py +++ b/tests/bugs/core_5441_test.py @@ -64,6 +64,7 @@ 'include_filter = "%(select % from test where x = ?)%"', ] +@pytest.mark.trace @pytest.mark.version('>=3.0.2') def test_1(act: Action, capsys): with act.trace(db_events=trace), act.db.connect() as con: diff --git a/tests/bugs/core_5463_test.py b/tests/bugs/core_5463_test.py index 2a75f86d..4a650963 100644 --- a/tests/bugs/core_5463_test.py +++ b/tests/bugs/core_5463_test.py @@ -17,6 +17,11 @@ "OVERRIDING SYSTEM VALUE should be used to override the value of an identity column defined as 'GENERATED ALWAYS' in table/view ..." New text (after #7638 was fixed): "OVERRIDING clause should be used when an identity column defined as 'GENERATED ALWAYS' is present in the INSERT's field list for table table/view ..." + + [01.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -93,33 +98,41 @@ insert into test_default(id_default) overriding user value values(-7654322) returning id_default; -- expected: -121 """ -act = isql_act('db', test_script) - -expected_stdout = """ - ID_DEFAULT -11 - ID_ALWAYS 11 - ID_DEFAULT -7654321 - ID_ALWAYS 7654321 - ID_DEFAULT -33 - ID_DEFAULT -55 -""" +substitutions = [ ('[ \t]+', ' ') ] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stderr = """ +expected_stdout_5x = """ + ID_DEFAULT -11 + ID_ALWAYS 11 Statement failed, SQLSTATE = 42000 OVERRIDING clause should be used when an identity column defined as 'GENERATED ALWAYS' is present in the INSERT's field list for table table/view TEST_ALWAYS - Statement failed, SQLSTATE = 23000 validation error for column "TEST_DEFAULT"."ID_DEFAULT", value "*** null ***" - + ID_DEFAULT -7654321 Statement failed, SQLSTATE = 23000 validation error for column "TEST_ALWAYS"."ID_ALWAYS", value "*** null ***" + ID_ALWAYS 7654321 + ID_DEFAULT -33 + ID_DEFAULT -55 +""" + +expected_stdout_6x = """ + ID_DEFAULT -11 + ID_ALWAYS 11 + Statement failed, SQLSTATE = 42000 + OVERRIDING clause should be used when an identity column defined as 'GENERATED ALWAYS' is present in the INSERT's field list for table table/view "PUBLIC"."TEST_ALWAYS" + Statement failed, SQLSTATE = 23000 + validation error for column "PUBLIC"."TEST_DEFAULT"."ID_DEFAULT", value "*** null ***" + ID_DEFAULT -7654321 + Statement failed, SQLSTATE = 23000 + validation error for column "PUBLIC"."TEST_ALWAYS"."ID_ALWAYS", value "*** null ***" + ID_ALWAYS 7654321 + ID_DEFAULT -33 + ID_DEFAULT -55 """ @pytest.mark.version('>=4.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5464_test.py b/tests/bugs/core_5464_test.py index fa3855c5..8949a8c3 100644 --- a/tests/bugs/core_5464_test.py +++ b/tests/bugs/core_5464_test.py @@ -5,66 +5,107 @@ ISSUE: 5734 TITLE: AV in fbclient when reading blob stored in incompatible encoding DESCRIPTION: + Domain description contains non-ascii text in Latvian + and is created using charset = win1257. + Subsequent connect which tries to get this description uses cp1253 (Greek). + Commit that fixed ticket: 0fab1a85597baa5054a34cae437f5da6096580b0 (20.01.2017 00:43) JIRA: CORE-5464 FBTEST: bugs.core_5464 NOTES: - [06.10.2022] pzotov - Could not complete adjusting for LINUX in new-qa. - DEFERRED. + [30.10.2024] pzotov + Crash *not* occurs but one may note different behaviour of snapshots before and after fix. + + Snapshot before fix (e.g. 90a46fa3, 06-jan-2017) for query to rdb$fields (see view v_domain_descr) + behave differently depending on connection protocol: + * for TCP is does not return any record for query to view 'v_conn_cset'; + * for LOCAL protocol its returns weird 'RDB$SYSTEM_FLAG 18775' and error 'SQLSTATE = 42000 / invalid BLOB ID'. + + Also, error message for query to view 'v_domain_descr' (before fix) was: + Statement failed, SQLSTATE = HY000 + Cannot transliterate character between character sets + request synchronization error + + Discussed with Vlad, letters date: 29-oct-2024. + Checked on 6.0.0.511 (Windows/Linux); 5.0.2.1550; 4.0.6.3165; 3.0.13.33793; 3.0.2.32670-0fab1a8. """ -import platform +import locale +from pathlib import Path + import pytest from firebird.qa import * -init_script = """ - create domain d_int int; - comment on domain d_int is - '*Лев Николаевич Толстой * *Анна Каренина * /Мне отмщение, и аз воздам/ *ЧАСТЬ ПЕРВАЯ* *I * - Все счастливые семьи похожи друг на друга, каждая несчастливая - семья несчастлива по-своему. - Все смешалось в доме Облонских. Жена узнала, что муж был в связи - с бывшею в их доме француженкою-гувернанткой, и объявила мужу, что - не может жить с ним в одном доме. Положение это продолжалось уже - третий день и мучительно чувствовалось и самими супругами, и всеми - членами семьи, и домочадцами. Все члены семьи и домочадцы - чувствовали, что нет смысла в их сожительстве и что на каждом - п1 - '; - commit; -""" +db = db_factory(charset='win1257') +act = isql_act('db', substitutions = [('TCPv(4|6)', 'TCP')]) -db_1 = db_factory(charset='WIN1251', init=init_script) +tmp_sql = temp_file('tmp_core_5464.sql') -test_script = """ - set blob all; - set list on; +@pytest.mark.intl +@pytest.mark.version('>=3.0.1') +def test_1(act: Action, tmp_sql: Path, capsys): - select c.rdb$character_set_name as connection_cset, r.rdb$character_set_name as db_default_cset - from mon$attachments a - join rdb$character_sets c on a.mon$character_set_id = c.rdb$character_set_id - cross join rdb$database r where a.mon$attachment_id=current_connection; + non_ascii_txt = """ + Oblonsku mājā viss bija sajaukts. + Sieva uzzināja, ka viņas vīram ir attiecības ar franču guvernanti, + kas atradās viņu mājā, un paziņoja vīram, ka nevar dzīvot ar viņu vienā mājā. + """ - select rdb$field_name, rdb$system_flag, rdb$description - from rdb$fields where rdb$description is not null; -""" + init_script = f""" + create domain dm_test int; + comment on domain dm_test is '{non_ascii_txt}'; + commit; + create view v_conn_cset as + select + rdb$get_context('SYSTEM', 'NETWORK_PROTOCOL') as conn_protocol + ,c.rdb$character_set_name as connection_cset + ,r.rdb$character_set_name as db_default_cset + from mon$attachments a + join rdb$character_sets c on a.mon$character_set_id = c.rdb$character_set_id + cross join rdb$database r where a.mon$attachment_id=current_connection; -act = isql_act('db_1', test_script) + create view v_domain_descr as + select f.rdb$field_name, f.rdb$system_flag, f.rdb$description + from rdb$database d + left join rdb$fields f on f.rdb$description is not null; + commit; + """ + tmp_sql.write_bytes(init_script.encode('cp1257')) + act.isql(switches=['-q'], input_file = tmp_sql, charset='win1257', combine_output = True, io_enc = locale.getpreferredencoding()) + assert act.return_code == 0 -expected_stdout = """ - CONNECTION_CSET WIN1250 - DB_DEFAULT_CSET WIN1251 -""" + test_sql = f""" + set blob all; + set list on; + set count on; + connect '{act.db.dsn}'; + select v1.* from v_conn_cset as v1; + select v2.* from v_domain_descr as v2; + commit; -expected_stderr = """ - Statement failed, SQLSTATE = 22018 - Cannot transliterate character between character sets -""" + connect '{act.db.db_path}'; + select v3.* from v_conn_cset as v3; + select v4.* from v_domain_descr as v4; + commit; + """ + act.isql(switches=['-q'], connect_db = False, input = test_sql, charset='win1253', combine_output = True, io_enc = locale.getpreferredencoding()) + + act.expected_stdout = """ + CONN_PROTOCOL TCP + CONNECTION_CSET WIN1253 + DB_DEFAULT_CSET WIN1257 + Records affected: 1 + + Statement failed, SQLSTATE = 22018 + Cannot transliterate character between character sets + Records affected: 0 + + + CONN_PROTOCOL + CONNECTION_CSET WIN1253 + DB_DEFAULT_CSET WIN1257 + Records affected: 1 -@pytest.mark.skipif(platform.system() != 'Windows', reason='FIXME: see notes') -@pytest.mark.version('>=3.0.2') -def test_1(act: Action): - act.expected_stderr = expected_stderr - act.expected_stdout = expected_stdout - act.isql(switches=['-q'], input=test_script, charset='WIN1250') - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + Statement failed, SQLSTATE = 22018 + Cannot transliterate character between character sets + Records affected: 0 + """ + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5470_addi_test.py b/tests/bugs/core_5470_addi_test.py index 1c12a711..e46686f2 100644 --- a/tests/bugs/core_5470_addi_test.py +++ b/tests/bugs/core_5470_addi_test.py @@ -77,6 +77,8 @@ expected_stdout_trace = test_sql.replace('set list on;', '').replace(';','') +@pytest.mark.intl +@pytest.mark.trace @pytest.mark.version('>=3.0.6') @pytest.mark.platform('Windows') def test_1(act: Action, capsys): diff --git a/tests/bugs/core_5470_test.py b/tests/bugs/core_5470_test.py index 0c9012d3..7d611d3f 100644 --- a/tests/bugs/core_5470_test.py +++ b/tests/bugs/core_5470_test.py @@ -98,6 +98,7 @@ rdb$database /* ddl_3 line_8 */ """ +@pytest.mark.trace @pytest.mark.version('>=3.0.2') def test_1(act: Action, tmp_file: Path, capsys): with act.trace(db_events=trace), act.db.connect() as con: diff --git a/tests/bugs/core_5475_test.py b/tests/bugs/core_5475_test.py index fff8b6f7..669ca2e2 100644 --- a/tests/bugs/core_5475_test.py +++ b/tests/bugs/core_5475_test.py @@ -25,6 +25,9 @@ See letter from Vlad, 31-mar-2020 14:29. JIRA: CORE-5475 FBTEST: bugs.core_5475 +NOTES: + [01.07.2025] pzotov + Adjusted "with pytest.raises(DatabaseError, '...')": skip check for matching table name (FB 6.x). """ import pytest @@ -107,6 +110,7 @@ # 2) 335544913 : value exceeds the range for valid timestamps ] +@pytest.mark.trace @pytest.mark.version('>=3.0') def test_1(act: Action, capsys): with act.trace(db_events=trace): @@ -117,7 +121,7 @@ def test_1(act: Action, capsys): # with act.db.connect(sql_dialect=1) as con: c = con.cursor() - with pytest.raises(DatabaseError, match='.*violation of PRIMARY or UNIQUE KEY constraint "TEST_PK" on table "TEST".*'): + with pytest.raises(DatabaseError, match='.*violation of PRIMARY or UNIQUE KEY constraint.*'): c.execute('insert into test(id,tiny_num) values(?, ?)', [1, 1]) # with act.db.connect(sql_dialect=1) as con: diff --git a/tests/bugs/core_5477_test.py b/tests/bugs/core_5477_test.py index 913ca35a..3c964d45 100644 --- a/tests/bugs/core_5477_test.py +++ b/tests/bugs/core_5477_test.py @@ -42,6 +42,7 @@ 'log_statement_finish = true', ] +@pytest.mark.trace @pytest.mark.version('>=3.0.2') @pytest.mark.platform('Windows') def test_1(act: Action, tmp_file: Path, capsys): diff --git a/tests/bugs/core_5480_test.py b/tests/bugs/core_5480_test.py index 4e23bfa6..7c639ef7 100644 --- a/tests/bugs/core_5480_test.py +++ b/tests/bugs/core_5480_test.py @@ -56,6 +56,7 @@ N06 |€| """ +@pytest.mark.intl @pytest.mark.version('>=4.0') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_5481_test.py b/tests/bugs/core_5481_test.py index 0570115e..d3d006bc 100644 --- a/tests/bugs/core_5481_test.py +++ b/tests/bugs/core_5481_test.py @@ -12,6 +12,12 @@ Execution plan changed in FB 5.x since build 5.0.0.1211 (14-sep-2023). Expected output has been splitted on that remains actual for FB 4.x and one that issued for 5.x+. Confirmed by dimitr, letter 24.09.2023 13:30 + + [01.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.884; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -74,9 +80,15 @@ PLAN (V_TEST B ORDER BALANCES_BALANCEDATE_DESC INDEX (FK_BALANCES_ORGACCOUNTS)) """ +expected_stdout_6x = """ + PLAN SORT ("PUBLIC"."V_TEST" "B" INDEX ("PUBLIC"."BALANCES_BALANCEDATE_ORGACCOUNT")) + PLAN ("PUBLIC"."V_TEST" "B" ORDER "PUBLIC"."BALANCES_BALANCEDATE_DESC" INDEX ("PUBLIC"."FK_BALANCES_ORGACCOUNTS")) +""" + + @pytest.mark.version('>=3.0.4') def test_1(act: Action): - act.expected_stdout = expected_stdout_4x if act.is_version('<5') else expected_stdout_5x - act.execute() + act.expected_stdout = expected_stdout_4x if act.is_version('<5') else expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5488_running_sttm_test.py b/tests/bugs/core_5488_running_sttm_test.py index 033bf751..eadc4003 100644 --- a/tests/bugs/core_5488_running_sttm_test.py +++ b/tests/bugs/core_5488_running_sttm_test.py @@ -185,6 +185,7 @@ -Attachment level timeout expired. """ +@pytest.mark.es_eds @pytest.mark.version('>=4.0') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_5488_session_idle_test.py b/tests/bugs/core_5488_session_idle_test.py index 997595e1..532fa0fd 100644 --- a/tests/bugs/core_5488_session_idle_test.py +++ b/tests/bugs/core_5488_session_idle_test.py @@ -86,6 +86,7 @@ 'log_statement_finish = true', ] +@pytest.mark.trace @pytest.mark.version('>=4.0') def test_1(act: Action, capsys): trace_dts_pattern = re.compile('.*(ATTACH_DATABASE|START_TRANSACTION|EXECUTE_STATEMENT_FINISH|ROLLBACK_TRANSACTION|DETACH_DATABASE)') diff --git a/tests/bugs/core_5489_test.py b/tests/bugs/core_5489_test.py index edeff15a..1634dbfd 100644 --- a/tests/bugs/core_5489_test.py +++ b/tests/bugs/core_5489_test.py @@ -27,8 +27,10 @@ JIRA: CORE-5489 FBTEST: bugs.core_5489 NOTES: - [25.11.2023] pzotov - Writing code requires more care since 6.0.0.150: ISQL does not allow specifying duplicate delimiters without any statements between them (two semicolon, two carets etc). + [01.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -93,6 +95,7 @@ 'log_initfini = false', ] +@pytest.mark.trace @pytest.mark.version('>=3.0.2') def test_1(act: Action): @@ -114,6 +117,10 @@ def test_1(act: Action): for k in range(len(words)): if words[k].startswith('fetch'): num_of_fetches = int(words[k-1]) - # Check - assert run_with_plan == 'PLAN (TEST ORDER TEST_F01_ID)' + if act.is_version('<6'): + expected_plan = 'PLAN (TEST ORDER TEST_F01_ID)' + else: + expected_plan = 'PLAN ("PUBLIC"."TEST" ORDER "PUBLIC"."TEST_F01_ID")' + + assert run_with_plan == expected_plan assert num_of_fetches < FETCHES_THRESHOLD diff --git a/tests/bugs/core_5494_test.py b/tests/bugs/core_5494_test.py index 566568ed..cdb5d24c 100644 --- a/tests/bugs/core_5494_test.py +++ b/tests/bugs/core_5494_test.py @@ -7,6 +7,14 @@ DESCRIPTION: JIRA: CORE-5494 FBTEST: bugs.core_5494 +NOTES: + [01.07.2025] pzotov + Refactored: we have to check only rows which contain either 'sqltype' or 'SQLSTATE'. + Added appropriate substitutions. + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -17,9 +25,9 @@ test_script = """ set bail on; recreate table test ( - c1 binary(8), - v1 varbinary(8), - b1 blob sub_type binary + c1 binary(8), + v1 varbinary(8), + b1 blob sub_type binary ); insert into test(c1, b1) values('', ''); @@ -30,28 +38,23 @@ select * from test; """ -act = isql_act('db', test_script) +substitutions=[('^((?!(SQLSTATE|sqltype)).)*$', ''), ('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ - INPUT message field count: 0 - - PLAN (TEST NATURAL) - - OUTPUT message field count: 3 +expected_stdout_5x = """ 01: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 8 charset: 1 OCTETS - : name: C1 alias: C1 - : table: TEST owner: SYSDBA 02: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 8 charset: 1 OCTETS - : name: V1 alias: V1 - : table: TEST owner: SYSDBA 03: sqltype: 520 BLOB Nullable scale: 0 subtype: 0 len: 8 - : name: B1 alias: B1 - : table: TEST owner: SYSDBA +""" + +expected_stdout_6x = """ + 01: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 8 charset: 1 SYSTEM.OCTETS + 02: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 8 charset: 1 SYSTEM.OCTETS + 03: sqltype: 520 BLOB Nullable scale: 0 subtype: 0 len: 8 """ @pytest.mark.version('>=4.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout - diff --git a/tests/bugs/core_5495_test.py b/tests/bugs/core_5495_test.py index 3b70731e..fef018db 100644 --- a/tests/bugs/core_5495_test.py +++ b/tests/bugs/core_5495_test.py @@ -17,38 +17,35 @@ JIRA: CORE-5495 FBTEST: bugs.core_5495 """ +import locale import pytest from firebird.qa import * db = db_factory() +act = python_act('db', substitutions = [('TCPv(4|6)', 'TCP'),('[ \t]+', ' ')]) -test_user = user_factory('db', name='tmp$c5495', password='123', plugin='Legacy_UserManager') - -test_script = """ - set list on; - set bail on; - connect '$(DSN)' user tmp$c5495 password '123'; - --select mon$user,mon$remote_address,mon$remote_protocol,mon$client_version,mon$remote_version,mon$auth_method from mon$attachments - select mon$user,mon$remote_protocol,mon$auth_method from mon$attachments - where mon$attachment_id=current_connection; - commit; - connect '$(DSN)' user SYSDBA password 'masterkey'; - commit; -""" - -act = isql_act('db', test_script, substitutions=[('TCPv.*', 'TCP'), - ('Commit current transaction \\(y/n\\)\\?', '')]) - -expected_stdout = """ - MON$USER TMP$C5495 - MON$REMOTE_PROTOCOL TCP - MON$AUTH_METHOD Legacy_Auth -""" +tmp_user = user_factory('db', name='tmp$c5495', password='123', plugin='Legacy_UserManager') @pytest.mark.version('>=4.0') -def test_1(act: Action, test_user: User): +def test_1(act: Action, tmp_user: User): + + test_script = f""" + set list on; + set bail on; + connect '{act.db.dsn}' user {tmp_user.name} password '{tmp_user.password}'; + select mon$user,mon$remote_protocol,mon$auth_method from mon$attachments + where mon$attachment_id=current_connection; + commit; + """ + + expected_stdout = f""" + MON$USER {tmp_user.name.upper()} + MON$REMOTE_PROTOCOL TCP + MON$AUTH_METHOD Legacy_Auth + """ + act.expected_stdout = expected_stdout - act.execute() + act.isql(switches = ['-q'], input = test_script, connect_db = False, combine_output = True, io_enc = locale.getpreferredencoding()) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5536_test.py b/tests/bugs/core_5536_test.py index 19ba1676..07a5287d 100644 --- a/tests/bugs/core_5536_test.py +++ b/tests/bugs/core_5536_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-5536 FBTEST: bugs.core_5536 +NOTES: + [01.07.2025] pzotov + Refactored: we have to check only rows which contain either 'sqltype' or 'SQLSTATE'. + Added appropriate substitutions. + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -15,36 +20,24 @@ db = db_factory() test_script = """ - set planonly; set sqlda_display on; select mon$wire_compressed, mon$wire_encrypted from mon$attachments where mon$attachment_id = current_connection ; - -- Fields that were used before: - -- MON$CONNECTION_COMPRESSED in (false, true) - -- and MON$CONNECTION_ENCRYPTED in (false, true) """ -act = isql_act('db', test_script) +substitutions=[('^((?!(SQLSTATE|sqltype)).)*$', ''), ('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ - INPUT message field count: 0 - - PLAN (MON$ATTACHMENTS NATURAL) - - OUTPUT message field count: 2 01: sqltype: 32764 BOOLEAN Nullable scale: 0 subtype: 0 len: 1 - : name: MON$WIRE_COMPRESSED alias: MON$WIRE_COMPRESSED - : table: MON$ATTACHMENTS owner: SYSDBA 02: sqltype: 32764 BOOLEAN Nullable scale: 0 subtype: 0 len: 1 - : name: MON$WIRE_ENCRYPTED alias: MON$WIRE_ENCRYPTED - : table: MON$ATTACHMENTS owner: SYSDBA """ @pytest.mark.version('>=4.0') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5555_test.py b/tests/bugs/core_5555_test.py index 3c8824fa..c5f1b76c 100644 --- a/tests/bugs/core_5555_test.py +++ b/tests/bugs/core_5555_test.py @@ -87,6 +87,7 @@ GDS_ON_SELECT_WITH_LOCK 335544336 """ +@pytest.mark.es_eds @pytest.mark.version('>=3.0.3') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_5567_test.py b/tests/bugs/core_5567_test.py index 9a09ca43..2869232b 100644 --- a/tests/bugs/core_5567_test.py +++ b/tests/bugs/core_5567_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-5567 FBTEST: bugs.core_5567 +NOTES: + [01.07.2025] pzotov + Refactored: suppressed name of system table as it has no matter for this test. + Added appropriate substitutions. + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -49,23 +54,18 @@ """ -act = isql_act('db', test_script, substitutions=[('line: [\\d]+, col: [\\d]+', ''), ('.*At block.*', '')]) +substitutions = [('[ \t]+', ' '), (r'line(:)?\s+\d+', ''), ('.*At block.*', ''), ('(-)?At sub procedure.*', ''), ('for system table .*', 'for system table')] +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ - DOMAIN_PRECISION -2 -""" - -expected_stderr = """ Statement failed, SQLSTATE = 42000 - UPDATE operation is not allowed for system table RDB$FIELDS - -At sub procedure 'HACK' + UPDATE operation is not allowed for system table + DOMAIN_PRECISION -2 """ @pytest.mark.version('>=3.0.3') def test_1(act: Action): act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5576_test.py b/tests/bugs/core_5576_test.py index 73baa6fe..54e1edf0 100644 --- a/tests/bugs/core_5576_test.py +++ b/tests/bugs/core_5576_test.py @@ -12,6 +12,12 @@ Neither test query nor validation should raise any output in the STDERR. JIRA: CORE-5576 FBTEST: bugs.core_5576 +NOTES: + [01.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -37,7 +43,7 @@ X1 1 """ -expected_stdout_b = """ +expected_stdout_b_5x = """ Validation started Relation 128 (TEST) process pointer page 0 of 1 @@ -46,6 +52,15 @@ Validation finished """ +expected_stdout_b_6x = """ + Validation started + Relation ("PUBLIC"."TEST") + process pointer page 0 of 1 + Index 1 ("PUBLIC"."RDB$PRIMARY1") + Relation ("PUBLIC"."TEST") is ok + Validation finished +""" + fbk_file = temp_file('core_5576.fbk') fdb_file = temp_file('core_5576.fdb') @@ -63,7 +78,9 @@ def test_1(act: Action, fbk_file: Path, fdb_file: Path): assert act.clean_stdout == act.clean_expected_stdout # Validate the database act.reset() - act.expected_stdout = expected_stdout_b + + act.expected_stdout = expected_stdout_b_5x if act.is_version('<6') else expected_stdout_b_6x + with act.connect_server() as srv: srv.database.validate(database=fdb_file) act.stdout = ''.join(srv.readlines()) diff --git a/tests/bugs/core_5580_test.py b/tests/bugs/core_5580_test.py index 2ec41bfc..0b39f795 100644 --- a/tests/bugs/core_5580_test.py +++ b/tests/bugs/core_5580_test.py @@ -7,6 +7,12 @@ DESCRIPTION: JIRA: CORE-5580 FBTEST: bugs.core_5580 +NOTES: + [01.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -47,19 +53,34 @@ act = isql_act('db', test_script) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 42000 unsuccessful metadata update -RECREATE PACKAGE BODY PK1 failed -Function F1 has a signature mismatch on package body PK1 + Statement failed, SQLSTATE = 2F000 Cannot execute function F1 of the unimplemented package PK1 + Statement failed, SQLSTATE = 2F000 Cannot execute function F2 of the unimplemented package PK1 """ +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -RECREATE PACKAGE BODY "PUBLIC"."PK1" failed + -Function F1 has a signature mismatch on package body "PUBLIC"."PK1" + + Statement failed, SQLSTATE = 2F000 + Cannot execute function "F1" of the unimplemented package "PUBLIC"."PK1" + + Statement failed, SQLSTATE = 2F000 + Cannot execute function "F2" of the unimplemented package "PUBLIC"."PK1" +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5598_test.py b/tests/bugs/core_5598_test.py index bb5e5bf1..db88d82c 100644 --- a/tests/bugs/core_5598_test.py +++ b/tests/bugs/core_5598_test.py @@ -3,8 +3,7 @@ """ ID: issue-5864 ISSUE: 5864 -TITLE: Error "block size exceeds implementation restriction" while inner joining large - datasets with a long key using the HASH JOIN plan +TITLE: Error "block size exceeds implementation restriction" while inner joining large datasets with a long key using the HASH JOIN plan DESCRIPTION: Hash join have to operate with keys of total length >= 1 Gb if we want to reproduce runtime error "Statement failed, SQLSTATE = HY001 / unable to allocate memory from operating system" @@ -30,7 +29,7 @@ ======== (Captured ISQL stderr call): Statement failed, SQLSTATE = 08001 - I/O error during "CreateFile (create)" operation for file "R:\RAMDISK\fb_recbuf_py6oyh" + I/O error during "CreateFile (create)" operation for file "\fb_recbuf_py6oyh" -Error while trying to create file -[ The system cannot find the path specified ] -- CAN BE LOCALIZED. ======== @@ -39,6 +38,12 @@ act.isql(switches = ..., input = ..., charset = ..., io_enc=locale.getpreferredencoding()) NOTE: specifying 'encoding_errors = ignore' in the DEFAULT section of firebird-driver.conf does not prevent from UnicodeDecode error in this case. + + [01.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -60,6 +65,10 @@ PLAN HASH (A NATURAL, B NATURAL) """ +fb6x_checked_stdout = """ + PLAN HASH ("A" NATURAL, "B" NATURAL) +""" + MIN_RECS_TO_ADD = 17000 test_script = """ @@ -79,7 +88,7 @@ def test_1(act: Action): c.execute(f"insert into test(id, s) select row_number()over(), lpad('', 8191, 'Алексей, Łukasz, Máté, François, Jørgen, Νικόλαος') from rdb$types,rdb$types rows {MIN_RECS_TO_ADD}") con.commit() - act.expected_stdout = fb3x_checked_stdout if act.is_version('<5') else fb5x_checked_stdout + act.expected_stdout = fb3x_checked_stdout if act.is_version('<5') else fb5x_checked_stdout if act.is_version('<6') else fb6x_checked_stdout # NB: FIREBIRD_TMP must point do accessible directory here! act.isql(switches=['-q'], input=test_script, charset='UTF8', io_enc=locale.getpreferredencoding()) diff --git a/tests/bugs/core_5606_test.py b/tests/bugs/core_5606_test.py index 26bed86e..d1ba319c 100644 --- a/tests/bugs/core_5606_test.py +++ b/tests/bugs/core_5606_test.py @@ -5,16 +5,20 @@ ISSUE: 5872 TITLE: Add expression index name to exception message if computation failed DESCRIPTION: -NOTES: -[24.08.2020] - changed sequence of actions: one statement must violate requirements of only ONE index. - Before this statement: - insert into test(id, x, y, s) values( 4, 3, -7, 'qwerty' ); - -- did violate TWO indices: test_eval3 and test_eval5. - The order of which of them will raise first is undefined, so this test could fail because of appearance - "wrong" index name in its STDERR. Detected on 4.0.0.2173. Discussed with hvlad, letter 23.08.2020 16:58. JIRA: CORE-5606 FBTEST: bugs.core_5606 +NOTES: + [24.08.2020] + Changed sequence of actions: one statement must violate requirements of only ONE index. + Before this statement: + insert into test(id, x, y, s) values( 4, 3, -7, 'qwerty' ); + -- did violate TWO indices: test_eval3 and test_eval5. + The order of which of them will raise first is undefined, so this test could fail because of appearance + "wrong" index name in its STDERR. Detected on 4.0.0.2173. Discussed with hvlad, letter 23.08.2020 16:58. + [30.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -61,39 +65,53 @@ act = isql_act('db', test_script) -expected_stdout = """ - Records affected: 0 -""" - -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 22012 Expression evaluation error for index "TEST_EVAL1" on table "TEST" -arithmetic exception, numeric overflow, or string truncation -Integer divide by zero. The code attempted to divide an integer value by an integer divisor of zero. - Statement failed, SQLSTATE = 42000 Expression evaluation error for index "TEST_EVAL2" on table "TEST" -expression evaluation not supported -Argument for LOG10 must be positive - Statement failed, SQLSTATE = HY000 Expression evaluation error for index "TEST_EVAL3" on table "TEST" -Context variable 'FOO_&_BAR' is not found in namespace 'SYSTEM' Statement failed, SQLSTATE = 22012 - Expression evaluation error for index "TEST_EVAL4" on table "TEST" -arithmetic exception, numeric overflow, or string truncation -Integer divide by zero. The code attempted to divide an integer value by an integer divisor of zero. - Statement failed, SQLSTATE = 22011 Expression evaluation error for index "TEST_EVAL5" on table "TEST" -Invalid length parameter -3 to SUBSTRING. Negative integers are not allowed. + Records affected: 0 +""" + +expected_stdout_6x = """ + Statement failed, SQLSTATE = 22012 + Expression evaluation error for index "PUBLIC"."TEST_EVAL1" on table "PUBLIC"."TEST" + -arithmetic exception, numeric overflow, or string truncation + -Integer divide by zero. The code attempted to divide an integer value by an integer divisor of zero. + Statement failed, SQLSTATE = 42000 + Expression evaluation error for index "PUBLIC"."TEST_EVAL2" on table "PUBLIC"."TEST" + -expression evaluation not supported + -Argument for LOG10 must be positive + Statement failed, SQLSTATE = HY000 + Expression evaluation error for index "PUBLIC"."TEST_EVAL3" on table "PUBLIC"."TEST" + -Context variable 'FOO_&_BAR' is not found in namespace 'SYSTEM' + Statement failed, SQLSTATE = 22012 + Expression evaluation error for index "PUBLIC"."TEST_EVAL4" on table "PUBLIC"."TEST" + -arithmetic exception, numeric overflow, or string truncation + -Integer divide by zero. The code attempted to divide an integer value by an integer divisor of zero. + Statement failed, SQLSTATE = 22011 + Expression evaluation error for index "PUBLIC"."TEST_EVAL5" on table "PUBLIC"."TEST" + -Invalid length parameter -3 to SUBSTRING. Negative integers are not allowed. + Records affected: 0 """ @pytest.mark.version('>=4.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5630_test.py b/tests/bugs/core_5630_test.py index e0a7545c..cfba5cd2 100644 --- a/tests/bugs/core_5630_test.py +++ b/tests/bugs/core_5630_test.py @@ -4,11 +4,17 @@ ID: issue-5896 ISSUE: 5896 TITLE: Can't create the shadow file -DESCRIPTION: - Shadow file is can not be created during restore when -use_all_space option is used +DESCRIPTION: Shadow file can not be created during restore when '-use_all_space' option is used JIRA: CORE-5630 -FBTEST: bugs.core_5630 +NOTES: + [30.12.2024] pzotov + Splitted expected out for FB 6.x because columns rdb$file_sequence, rdb$file_start and rdb$file_length + have NULLs instead of zeroes, see: + https://github.com/FirebirdSQL/firebird/commit/f0740d2a3282ed92a87b8e0547139ba8efe61173 + ("Wipe out multi-file database support (#8047)") + Checked on 6.0.0.565 """ +import locale import pytest from pathlib import Path @@ -16,24 +22,24 @@ db = db_factory() -act = python_act('db', substitutions=[('Commit current transaction \\(y/n\\)\\?', '')]) +act = python_act('db', substitutions=[('[ \t]+', ' ')]) -expected_stdout_a = """ +expected_stdout_5x = """ RDB$FILE_SEQUENCE 0 RDB$FILE_START 0 RDB$FILE_LENGTH 0 RDB$FILE_FLAGS 1 RDB$SHADOW_NUMBER 1 - S_HASH_BEFORE 1499836372373901520 + S_HASH 1499836372373901520 """ -expected_stdout_b = """ - RDB$FILE_SEQUENCE 0 - RDB$FILE_START 0 - RDB$FILE_LENGTH 0 +expected_stdout_6x = """ + RDB$FILE_SEQUENCE + RDB$FILE_START + RDB$FILE_LENGTH RDB$FILE_FLAGS 1 RDB$SHADOW_NUMBER 1 - S_HASH_AFTER 1499836372373901520 + S_HASH 1499836372373901520 """ fdb_file = temp_file('core_5630.fdb') @@ -43,37 +49,38 @@ @pytest.mark.version('>=3.0.3') def test_1(act: Action, fdb_file: Path, fbk_file: Path, shd_file: Path): init_ddl = f""" - set bail on; - set list on; - - create database 'localhost:{fdb_file}' user '{act.db.user}' password '{act.db.password}'; - - recreate table test(s varchar(30)); - commit; - - create or alter view v_shadow_info as - select - rdb$file_sequence -- 0 - ,rdb$file_start -- 0 - ,rdb$file_length -- 0 - ,rdb$file_flags -- 1 - ,rdb$shadow_number -- 1 - from rdb$files - where lower(rdb$file_name) containing lower('core_5630.shd') - ; - - insert into test select 'line #' || lpad(row_number()over(), 3, '0' ) from rdb$types rows 200; - commit; - - create shadow 1 '{shd_file}'; - commit; - set list on; - select * from v_shadow_info; - select hash( list(s) ) as s_hash_before from test; - quit; + set bail on; + set list on; + + create database 'localhost:{fdb_file}' user {act.db.user} password '{act.db.password}'; + + recreate table test(s varchar(30)); + commit; + + create or alter view v_shadow_info as + select + rdb$file_sequence -- 0 + ,rdb$file_start -- 0 + ,rdb$file_length -- 0 + ,rdb$file_flags -- 1 + ,rdb$shadow_number -- 1 + from rdb$files + where lower(rdb$file_name) containing lower('core_5630.shd') + ; + + insert into test select 'line #' || lpad(row_number()over(), 3, '0' ) from rdb$types rows 200; + commit; + + create shadow 1 '{shd_file}'; + commit; + set list on; + select * from v_shadow_info; + select hash( list(s) ) as s_hash from test; + quit; """ - act.expected_stdout = expected_stdout_a - act.isql(switches=['-q'], input=init_ddl) + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.isql(switches=['-q'], input = init_ddl, connect_db=False, combine_output = True, io_enc = locale.getpreferredencoding()) assert act.clean_stdout == act.clean_expected_stdout # with act.connect_server() as srv: @@ -84,14 +91,20 @@ def test_1(act: Action, fdb_file: Path, fbk_file: Path, shd_file: Path): shd_file.unlink() # act.reset() + + #----------------------------------------------------------------------------------- + act.gbak(switches=['-c', '-use_all_space', str(fbk_file), act.get_dsn(fdb_file)]) # Check that we have the same data in DB tables sql_text = """ set list on; select * from v_shadow_info; - select hash( list(s) ) as s_hash_after from test; + select hash( list(s) ) as s_hash from test; """ act.reset() - act.expected_stdout = expected_stdout_b - act.isql(switches=['-q', act.get_dsn(fdb_file)], input=sql_text, connect_db=False) + + #----------------------------------------------------------------------------------- + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.isql(switches=['-q', act.get_dsn(fdb_file)], input=sql_text, connect_db=False, combine_output = True, io_enc = locale.getpreferredencoding()) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5646_test.py b/tests/bugs/core_5646_test.py index ccfd5b64..fba60da1 100644 --- a/tests/bugs/core_5646_test.py +++ b/tests/bugs/core_5646_test.py @@ -5,87 +5,92 @@ ISSUE: 5912 TITLE: Parse error when compiling a statement causes memory leak until attachment is disconnected DESCRIPTION: - Test uses DDL and query for well known Einstein task. - SQL query for solution of this task is quite complex and must consume some memory resources. - This query intentionally is made non-compilable, so actually it will not ever run. - We query mon$memory_usage before running this query and after it, with storing values mon$memory* fields in the dictionary. - We do runs and after this evaluate differences of comsumed memory for each run. - Build 4.0.0.483 (05-jan-2017) shows that: - * mon$memory_used increased for ~227 Kb each run; - * mon$memory_allocated increased for ~960Kb. - Build 4.0.0.840 (02-jan-2018) shows that mon$memory_used increased for 2Kb - but only at FIRST measure. - Since 2nd measure memory consumption did not increase (neither mon$memory_used nor mon$memory_allocated). - But mon$max_memory_used can slightly increase, only ONCE, at 3rd run, for about 1Kb. - - Test evaluates MEDIANS for differences of mon$memory* fields. - All these medians must be ZERO otherwise test is considered as failed. + Test uses DDL and query for well known Einstein task. + SQL query for solution of this task is quite complex and must consume some memory resources. + This query intentionally is made non-compilable, so actually it will not ever run. + We query mon$memory_usage before running this query and after it, with storing values mon$memory* fields in the dictionary. + We do runs and after this evaluate differences of comsumed memory for each run. + Build 4.0.0.483 (05-jan-2017) shows that: + * mon$memory_used increased for ~227 Kb each run; + * mon$memory_allocated increased for ~960Kb. + Build 4.0.0.840 (02-jan-2018) shows that mon$memory_used increased for 2Kb - but only at FIRST measure. + Since 2nd measure memory consumption did not increase (neither mon$memory_used nor mon$memory_allocated). + But mon$max_memory_used can slightly increase, only ONCE, at 3rd run, for about 1Kb. + + Test evaluates MEDIANS for differences of mon$memory* fields. + All these medians must be ZERO otherwise test is considered as failed. JIRA: CORE-5646 FBTEST: bugs.core_5646 NOTES: [27.11.2023] pzotov - commit to FB 4.x: 26-nov-2017 - https://github.com/FirebirdSQL/firebird/commit/5e1b5e172e95388f8f2f236b89295bb915aef397 - - commit to FB 3.x: 06-oct-2021 - https://github.com/FirebirdSQL/firebird/commit/ed585ab09fdad63551c48d1ce392c810b5cef4a8 - (since build 3.0.8.33519, date: 07-oct-2021) - - 4.0.0.483, 05-jan-2017 - ====================== - absolute values: - 1 ::: (2329392, 2818048, 2569880, 2818048) - 2 ::: (2565264, 4456448, 3380832, 4653056) - 3 ::: (2565264, 4456448, 3380832, 4653056) - 4 ::: (2798240, 5373952, 3614280, 5636096) - 5 ::: (2798240, 5373952, 3614280, 5636096) - 6 ::: (3031216, 6356992, 3846784, 6619136) - 7 ::: (3031216, 6356992, 3846784, 6619136) - 8 ::: (3264192, 7340032, 4079760, 7602176) - 9 ::: (3264192, 7340032, 4079760, 7602176) - 10 ::: (3497400, 8323072, 4312968, 8585216) - 11 ::: (3497400, 8323072, 4312968, 8585216) - 12 ::: (3730376, 9240576, 4545944, 9502720) - 13 ::: (3730376, 9240576, 4545944, 9502720) - 14 ::: (3963352, 10223616, 4778920, 10485760) - 15 ::: (3963352, 10223616, 4778920, 10485760) - 16 ::: (4196328, 11141120, 5011896, 11403264) - 17 ::: (4196328, 11141120, 5011896, 11403264) - 18 ::: (4429304, 12124160, 5244872, 12386304) - 19 ::: (4429304, 12124160, 5244872, 12386304) - 20 ::: (4662280, 13041664, 5477848, 13303808) - 21 ::: (4662280, 13041664, 5477848, 13303808) - 22 ::: (4895256, 14024704, 5710824, 14286848) - - differences: - 0 ::: [235872, 1638400, 810952, 1835008] - 1 ::: [232976, 917504, 233448, 983040] - 2 ::: [232976, 983040, 232504, 983040] - 3 ::: [232976, 983040, 232976, 983040] - 4 ::: [233208, 983040, 233208, 983040] - 5 ::: [232976, 917504, 232976, 917504] - 6 ::: [232976, 983040, 232976, 983040] - 7 ::: [232976, 917504, 232976, 917504] - 8 ::: [232976, 983040, 232976, 983040] - 9 ::: [232976, 917504, 232976, 917504] - 10 ::: [232976, 983040, 232976, 983040] - + commit to FB 4.x: 26-nov-2017 + https://github.com/FirebirdSQL/firebird/commit/5e1b5e172e95388f8f2f236b89295bb915aef397 + + commit to FB 3.x: 06-oct-2021 + https://github.com/FirebirdSQL/firebird/commit/ed585ab09fdad63551c48d1ce392c810b5cef4a8 + (since build 3.0.8.33519, date: 07-oct-2021) + + 4.0.0.483, 05-jan-2017 + ====================== + absolute values: + 1 ::: (2329392, 2818048, 2569880, 2818048) + 2 ::: (2565264, 4456448, 3380832, 4653056) + 3 ::: (2565264, 4456448, 3380832, 4653056) + 4 ::: (2798240, 5373952, 3614280, 5636096) + 5 ::: (2798240, 5373952, 3614280, 5636096) + 6 ::: (3031216, 6356992, 3846784, 6619136) + 7 ::: (3031216, 6356992, 3846784, 6619136) + 8 ::: (3264192, 7340032, 4079760, 7602176) + 9 ::: (3264192, 7340032, 4079760, 7602176) + 10 ::: (3497400, 8323072, 4312968, 8585216) + 11 ::: (3497400, 8323072, 4312968, 8585216) + 12 ::: (3730376, 9240576, 4545944, 9502720) + 13 ::: (3730376, 9240576, 4545944, 9502720) + 14 ::: (3963352, 10223616, 4778920, 10485760) + 15 ::: (3963352, 10223616, 4778920, 10485760) + 16 ::: (4196328, 11141120, 5011896, 11403264) + 17 ::: (4196328, 11141120, 5011896, 11403264) + 18 ::: (4429304, 12124160, 5244872, 12386304) + 19 ::: (4429304, 12124160, 5244872, 12386304) + 20 ::: (4662280, 13041664, 5477848, 13303808) + 21 ::: (4662280, 13041664, 5477848, 13303808) + 22 ::: (4895256, 14024704, 5710824, 14286848) + + differences: + 0 ::: [235872, 1638400, 810952, 1835008] + 1 ::: [232976, 917504, 233448, 983040] + 2 ::: [232976, 983040, 232504, 983040] + 3 ::: [232976, 983040, 232976, 983040] + 4 ::: [233208, 983040, 233208, 983040] + 5 ::: [232976, 917504, 232976, 917504] + 6 ::: [232976, 983040, 232976, 983040] + 7 ::: [232976, 917504, 232976, 917504] + 8 ::: [232976, 983040, 232976, 983040] + 9 ::: [232976, 917504, 232976, 917504] + 10 ::: [232976, 983040, 232976, 983040] 4.0.0.840, 02-jan-2018 ====================== - differences: - 0 ::: [2896, 65536, 876800, 1966080] - 1 ::: [ 0, 0, 0, 0] - 2 ::: [ 0, 0, 0, 0] - 3 ::: [ 0, 0, 0, 0] - 4 ::: [ 0, 0, 0, 0] - 5 ::: [ 0, 0, 0, 0] - 6 ::: [ 0, 0, 0, 0] - 7 ::: [ 0, 0, 0, 0] - 8 ::: [ 0, 0, 0, 0] - 9 ::: [ 0, 0, 0, 0] - 10 ::: [ 0, 0, 0, 0] - + differences: + 0 ::: [2896, 65536, 876800, 1966080] + 1 ::: [ 0, 0, 0, 0] + 2 ::: [ 0, 0, 0, 0] + 3 ::: [ 0, 0, 0, 0] + 4 ::: [ 0, 0, 0, 0] + 5 ::: [ 0, 0, 0, 0] + 6 ::: [ 0, 0, 0, 0] + 7 ::: [ 0, 0, 0, 0] + 8 ::: [ 0, 0, 0, 0] + 9 ::: [ 0, 0, 0, 0] + 10 ::: [ 0, 0, 0, 0] + + [18.01.2025] pzotov + Resultset of cursor that executes using instance of selectable PreparedStatement must be stored + in some variable in order to have ability close it EXPLICITLY (before PS will be freed). + Otherwise access violation raises during Python GC and pytest hangs at final point (does not return control to OS). + This occurs at least for: Python 3.11.2 / pytest: 7.4.4 / firebird.driver: 1.10.6 / Firebird.Qa: 0.19.3 + The reason of that was explained by Vlad, 26.10.24 17:42 ("oddities when use instances of selective statements"). """ import pytest @@ -418,29 +423,44 @@ def test_1(act: Action, capsys): """ with con_worker.cursor() as cur_worker, con_dba.cursor() as cur_dba: - - ps = cur_dba.prepare(sql_memory_usage) - for i in range(N_MEASURES): - - cur_dba.execute(ps) - for r in cur_dba: - memory_diff[ i ] = r[1:] - - try: - # nb: EVERY run we force engine to compile NEW query because of changing 'i': - px = cur_worker.prepare(bad_sql % locals()) - except DatabaseError as e: - pass - - con_dba.commit() - cur_dba.execute(ps) - for r in cur_dba: - # Make subtraction for elements with same indices. - # This is DIFFERENCE between values in mon$memory_usage columns - # gathered after and before each measure: - # - memory_diff[ i ] = [a - b for a, b in zip(r[1:], memory_diff[ i ])] - + ps, rs = None, None + try: + ps = cur_dba.prepare(sql_memory_usage) + for i in range(N_MEASURES): + + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + rs = cur_dba.execute(ps) + for r in rs: + memory_diff[ i ] = r[1:] + + try: + # nb: EVERY run we force engine to compile NEW query because of changing 'i': + px = cur_worker.prepare(bad_sql % locals()) + except DatabaseError as x: + pass + + con_dba.commit() + rs = cur_dba.execute(ps) + for r in rs: + # Make subtraction for elements with same indices. + # This is DIFFERENCE between values in mon$memory_usage columns + # gathered after and before each measure: + # + memory_diff[ i ] = [a - b for a, b in zip(r[1:], memory_diff[ i ])] + + except DatabaseError as e: + print( e.__str__() ) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() memo_used_diff_list = [ v[0] for v in memory_diff.values() ] memo_allo_diff_list = [ v[1] for v in memory_diff.values() ] diff --git a/tests/bugs/core_5648_test.py b/tests/bugs/core_5648_test.py index 8d546c97..205d257b 100644 --- a/tests/bugs/core_5648_test.py +++ b/tests/bugs/core_5648_test.py @@ -40,6 +40,7 @@ eds_script = temp_file('eds_script.sql') +@pytest.mark.es_eds @pytest.mark.version('>=3.0.3') def test_1(act: Action, eds_script: Path): eds_sql = f""" diff --git a/tests/bugs/core_5659_test.py b/tests/bugs/core_5659_test.py index 96fd4122..f0735931 100644 --- a/tests/bugs/core_5659_test.py +++ b/tests/bugs/core_5659_test.py @@ -5,10 +5,15 @@ ISSUE: 5925 TITLE: Bad PLAN generated for query on Firebird v3.0 DESCRIPTION: - Test is based on data from original database that was provided in the ticket by its author. - Lot of data from tables were removed in order to reduce DB size. + Test is based on data from original database that was provided in the ticket by its author. + Lot of data from tables were removed in order to reduce DB size. JIRA: CORE-5659 FBTEST: bugs.core_5659 +NOTES: + [03.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.892; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813 """ import pytest @@ -18,12 +23,16 @@ db = db_factory() -act_1 = python_act('db') +act = python_act('db') -expected_stdout = """ +expected_stdout_5x = """ PLAN JOIN (B INDEX (COM_PEDIDO_IDX1), A INDEX (FK_COM_PEDIDO_ITEM_PEDIDO), C INDEX (PK_EST_PRODUTO)) """ +expected_stdout_6x = """ + PLAN JOIN ("B" INDEX ("PUBLIC"."COM_PEDIDO_IDX1"), "A" INDEX ("PUBLIC"."FK_COM_PEDIDO_ITEM_PEDIDO"), "C" INDEX ("PUBLIC"."PK_EST_PRODUTO")) +""" + test_script = """ set planonly; select @@ -42,14 +51,14 @@ fdb_file = temp_file('bad_plan_5659.fdb') @pytest.mark.version('>=3.0') -def test_1(act_1: Action, fbk_file: Path, fdb_file: Path): - zipped_fbk_file = zipfile.Path(act_1.files_dir / 'core_5659.zip', at='core_5659.fbk') +def test_1(act: Action, fbk_file: Path, fdb_file: Path): + zipped_fbk_file = zipfile.Path(act.files_dir / 'core_5659.zip', at='core_5659.fbk') fbk_file.write_bytes(zipped_fbk_file.read_bytes()) # - with act_1.connect_server() as srv: + with act.connect_server() as srv: srv.database.restore(backup=fbk_file, database=fdb_file) srv.wait() - # - act_1.expected_stdout = expected_stdout - act_1.isql(switches=['-q', act_1.get_dsn(fdb_file)], input=test_script, connect_db=False) - assert act_1.clean_stdout == act_1.clean_expected_stdout + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.isql(switches=['-q', act.get_dsn(fdb_file)], input=test_script, connect_db=False, combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5673_test.py b/tests/bugs/core_5673_test.py index 0416abc7..b7566600 100644 --- a/tests/bugs/core_5673_test.py +++ b/tests/bugs/core_5673_test.py @@ -5,7 +5,6 @@ ISSUE: 5939 TITLE: Unique constraint not working in encrypted database on first command DESCRIPTION: - Test uses Firebird built-in encryption plugin wich actually does encryption using trivial algorithm. Before running this test following prerequisites must be met: 1. Files fbSampleDbCrypt.conf and libfbSampleDbCrypt.so/fbSampleDbCrypt.dll must present in the $FB_HOME/plugins folder; @@ -26,13 +25,19 @@ that encryption/decryption thread completed. Otherwise we loop until such conditions will raise or timeout expired. After this we make TWO attempts to insert duplicates and catch exceptions for each of them and print exception details. - Expected result: two exception must occur here -- see 'expected_stdout_uniq_violation' variable. + Expected result: two exception must occur here. JIRA: CORE-5673 FBTEST: bugs.core_5673 NOTES: [06.06.2022] pzotov Checked on 4.0.1.2692, 3.0.8.33535 - both on Linux and Windows. + + [02.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import os import time @@ -134,13 +139,6 @@ def test_1(act: Action, capsys): #------------------------------------------------------------------------------------------------------ - expected_stdout_uniq_violation = f""" - violation of PRIMARY or UNIQUE KEY constraint "TEST_UNQ" on table "TEST" - -Problematic key value is ("DB_STATE" = '{m}', "X" = 1) - violation of PRIMARY or UNIQUE KEY constraint "TEST_UNQ" on table "TEST" - -Problematic key value is ("DB_STATE" = '{m}', "X" = 1) - """ - tx2 = con.transaction_manager(default_tpb=custom_tpb.get_buffer()) tx2.begin() cur2 = tx2.cursor() @@ -156,7 +154,21 @@ def test_1(act: Action, capsys): print( e.__str__() ) - act.expected_stdout = expected_stdout_uniq_violation + expected_stdout_5x = f""" + violation of PRIMARY or UNIQUE KEY constraint "TEST_UNQ" on table "TEST" + -Problematic key value is ("DB_STATE" = '{m}', "X" = 1) + violation of PRIMARY or UNIQUE KEY constraint "TEST_UNQ" on table "TEST" + -Problematic key value is ("DB_STATE" = '{m}', "X" = 1) + """ + + expected_stdout_6x = f""" + violation of PRIMARY or UNIQUE KEY constraint "TEST_UNQ" on table "PUBLIC"."TEST" + -Problematic key value is ("DB_STATE" = '{m}', "X" = 1) + violation of PRIMARY or UNIQUE KEY constraint "TEST_UNQ" on table "PUBLIC"."TEST" + -Problematic key value is ("DB_STATE" = '{m}', "X" = 1) + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout act.reset() diff --git a/tests/bugs/core_5674_test.py b/tests/bugs/core_5674_test.py index 0959f098..7eb58cac 100644 --- a/tests/bugs/core_5674_test.py +++ b/tests/bugs/core_5674_test.py @@ -9,12 +9,15 @@ FBTEST: bugs.core_5674 NOTES: [19.07.2023] pzotov - Adjusted expected error text for FB 4.x and 5.x: it now contains not only errors but also warnings about non-used CTEs. + Adjusted expected error text for FB 4.x and 5.x: it now contains not only errors but also warnings about non-used CTEs. [12.08.2023] pzotov - Adjusted expected error text for FB 3.0.12: now it is the same as for FB 4.x+ - - Change caused by commit "Print warnings occurred during commit", date: 07-jul-2023, started on builds 4.0.3.2958 and 5.0.0.1101. - Discussed with Vlad, 10-jul-2023. + Adjusted expected error text for FB 3.0.12: now it is the same as for FB 4.x+ + Change caused by commit "Print warnings occurred during commit", date: 07-jul-2023, started on builds 4.0.3.2958 and 5.0.0.1101. + Discussed with Vlad, 10-jul-2023. + [02.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -109,15 +112,14 @@ """ -act = isql_act('db', test_script, - substitutions=[('-At line[:]{0,1}[\\s]+[\\d]+,[\\s]+column[:]{0,1}[\\s]+[\\d]+', - '-At line: column:')]) +substitutions = [ ('[ \t]+', ' '), ('(-)?At line \\d+.*', '') ] +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ - Y 2 - X 4 + Y 2 + X 4 - X 0 + X 0 """ @pytest.mark.version('>=3.0.3') @@ -146,7 +148,7 @@ def test_1(act: Action): -SQL error code = -104 -CTE 'C' has cyclic dependencies """ - else: + elif act.is_version('<6'): act.expected_stderr = """ SQL warning code = -104 -CTE "X" is not used in query @@ -175,6 +177,33 @@ def test_1(act: Action): -CTE "B" is not used in query -CTE "C" is not used in query """ + else: + act.expected_stderr = """ + SQL warning code = -104 + -CTE "X" is not used in query + -CTE "Y" is not used in query + SQL warning code = -104 + -CTE "B" is not used in query + -CTE "C" is not used in query + -CTE "D" is not used in query + + Statement failed, SQLSTATE = 42S02 + Dynamic SQL Error + -SQL error code = -204 + -Table unknown + -"FOO" + SQL warning code = -104 + -CTE "B" is not used in query + -CTE "C" is not used in query + + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -CTE '"C"' has cyclic dependencies + SQL warning code = -104 + -CTE "B" is not used in query + -CTE "C" is not used in query + """ act.execute() assert (act.clean_stderr == act.clean_expected_stderr and diff --git a/tests/bugs/core_5676_test.py b/tests/bugs/core_5676_test.py index 50263d01..6b347185 100644 --- a/tests/bugs/core_5676_test.py +++ b/tests/bugs/core_5676_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-5676 FBTEST: bugs.core_5676 +NOTES: + [02.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -51,16 +56,24 @@ act = isql_act('db', test_script) -expected_stdout = """ - PLAN JOIN (DOC_IP_DOC ORDER PK_DOC_IP_DOC, DOCUMENT INDEX (PK_DOCUMENT)) - PLAN JOIN (DOC_IP_DOC ORDER PK_DOC_IP_DOC, DOCUMENT INDEX (PK_DOCUMENT)) - PLAN JOIN (DOC_IP_DOC ORDER PK_DOC_IP_DOC, DOCUMENT INDEX (PK_DOCUMENT)) - PLAN JOIN (DOC_IP_DOC ORDER PK_DOC_IP_DOC, DOCUMENT INDEX (PK_DOCUMENT)) -""" - @pytest.mark.version('>=3.0.3') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + + expected_stdout_5x = """ + PLAN JOIN (DOC_IP_DOC ORDER PK_DOC_IP_DOC, DOCUMENT INDEX (PK_DOCUMENT)) + PLAN JOIN (DOC_IP_DOC ORDER PK_DOC_IP_DOC, DOCUMENT INDEX (PK_DOCUMENT)) + PLAN JOIN (DOC_IP_DOC ORDER PK_DOC_IP_DOC, DOCUMENT INDEX (PK_DOCUMENT)) + PLAN JOIN (DOC_IP_DOC ORDER PK_DOC_IP_DOC, DOCUMENT INDEX (PK_DOCUMENT)) + """ + + expected_stdout_6x = """ + PLAN JOIN ("PUBLIC"."DOC_IP_DOC" ORDER "PUBLIC"."PK_DOC_IP_DOC", "PUBLIC"."DOCUMENT" INDEX ("PUBLIC"."PK_DOCUMENT")) + PLAN JOIN ("PUBLIC"."DOC_IP_DOC" ORDER "PUBLIC"."PK_DOC_IP_DOC", "PUBLIC"."DOCUMENT" INDEX ("PUBLIC"."PK_DOCUMENT")) + PLAN JOIN ("PUBLIC"."DOC_IP_DOC" ORDER "PUBLIC"."PK_DOC_IP_DOC", "PUBLIC"."DOCUMENT" INDEX ("PUBLIC"."PK_DOCUMENT")) + PLAN JOIN ("PUBLIC"."DOC_IP_DOC" ORDER "PUBLIC"."PK_DOC_IP_DOC", "PUBLIC"."DOCUMENT" INDEX ("PUBLIC"."PK_DOCUMENT")) + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5685_test.py b/tests/bugs/core_5685_test.py index 998dc02c..1f4631f5 100644 --- a/tests/bugs/core_5685_test.py +++ b/tests/bugs/core_5685_test.py @@ -1,207 +1,224 @@ -#coding:utf-8 - -""" -ID: issue-5951 -ISSUE: 5951 -TITLE: Sometime it is impossible to cancel/kill connection executing external query -DESCRIPTION: - Problem did appear when host "A" established connection to host "B" but could not get completed reply from this "B". - This can be emulated by following steps: - 1. We establich new remote connection to the same database using EDS mechanism and supply completely new ROLE to force new attachment be created; - 2. Within this EDS we do query to selectable procedure (with name 'sp_unreachable') which surely will not produce any result. - Bogon IP '192.0.2.2' is used in order to make this SP hang for sufficient time (on Windows it is about 20, on POSIX - about 44 seconds). - Steps 1 and 2 are implemented by asynchronous call of ISQL: we must have ability to kill its process after. - When this 'hanging ISQL' is launched, we wait 1..2 seconds and run one more ISQL, which has mission to KILL all attachments except his own. - This ISQL session is named 'killer', and it writes result of actions to log. - This "killer-ISQL" does TWO iterations with the same code which looks like 'select ... from mon$attachments' and 'delete from mon$attachments'. - First iteration must return data of 'hanging ISQL' and also this session must be immediately killed. - Second iteration must NOT return any data - and this is main check in this test. - - For builds which had bug (before 25.12.2017) one may see that second iteration STILL RETURNS the same data as first one: - ==== - ITERATION_NO 1 - HANGING_ATTACH_CONNECTION 1 - HANGING_ATTACH_PROTOCOL TCP - HANGING_STATEMENT_STATE 1 - HANGING_STATEMENT_BLOB_ID 0:3 - select * from sp_get_data - Records affected: 1 - - ITERATION_NO 2 - HANGING_ATTACH_CONNECTION 1 - HANGING_ATTACH_PROTOCOL TCP - HANGING_STATEMENT_STATE 1 - HANGING_STATEMENT_BLOB_ID 0:1 - select * from sp_get_data - Records affected: 1 - ==== - (expected: all fields in ITER #2 must be NULL) -JIRA: CORE-5685 -FBTEST: bugs.core_5685 -NOTES: - [06.10.2022] pzotov - Fails on Linux when run in 'batch' mode (i.e. when pytest has to perform the whole tests set). - Can not reproduce fail when run this test 'separately': it passes, but lasts too longm, ~130 s. - Test will be re-implemented. - DEFERRED. -""" -import platform -import pytest -import re -import subprocess -import time -from pathlib import Path -from firebird.qa import * -from firebird.driver import ShutdownMode, ShutdownMethod - -substitutions = [('.*After line.*', ''), ('.*Data source.*', '.*Data source'), - ('.*HANGING_STATEMENT_BLOB_ID.*', '')] - -init_script = """ - create sequence g; - commit; - set term ^; - create or alter procedure sp_unreachable returns( unreachable_address varchar(50) ) as - begin - for - execute statement ('select mon$remote_address from mon$attachments a where a.mon$attachment_id = current_connection') - on external '192.0.2.2:' || rdb$get_context('SYSTEM', 'DB_NAME') - as user 'SYSDBA' password 'masterkey' role left(replace( uuid_to_char(gen_uuid()), '-', ''), 31) - into unreachable_address - do - suspend; - end - ^ - - create or alter procedure sp_get_data returns( unreachable_address varchar(50) ) as - begin - for - execute statement ('select u.unreachable_address from sp_unreachable as u') - on external 'localhost:' || rdb$get_context('SYSTEM', 'DB_NAME') - as user 'SYSDBA' password 'masterkey' role left(replace( uuid_to_char(gen_uuid()), '-', ''), 31) - into unreachable_address - do - suspend; - end - ^ - set term ;^ - commit; -""" - -db = db_factory(init=init_script) - -act = python_act('db', substitutions=substitutions) - -expected_stdout = """ - HANGED ATTACH, STDOUT: Records affected: 0 - HANGED ATTACH, STDERR: Statement failed, SQLSTATE = 42000 - HANGED ATTACH, STDERR: Execute statement error at isc_dsql_fetch : - HANGED ATTACH, STDERR: - HANGED ATTACH, STDERR: Statement : select u.unreachable_address from sp_unreachable as u - .*Data source - HANGED ATTACH, STDERR: -At procedure 'SP_GET_DATA' line: 3, col: 9 - HANGED ATTACH, STDERR: - HANGED ATTACH, STDERR: - HANGED ATTACH, STDERR: - HANGED ATTACH, STDERR: - KILLER ATTACH, STDOUT: ITERATION_NO 1 - KILLER ATTACH, STDOUT: HANGING_ATTACH_CONNECTION 1 - KILLER ATTACH, STDOUT: HANGING_ATTACH_PROTOCOL TCP - KILLER ATTACH, STDOUT: HANGING_STATEMENT_STATE 1 - KILLER ATTACH, STDOUT: select * from sp_get_data - KILLER ATTACH, STDOUT: Records affected: 1 - KILLER ATTACH, STDOUT: ITERATION_NO 2 - KILLER ATTACH, STDOUT: HANGING_ATTACH_CONNECTION - KILLER ATTACH, STDOUT: HANGING_ATTACH_PROTOCOL - KILLER ATTACH, STDOUT: HANGING_STATEMENT_STATE - KILLER ATTACH, STDOUT: Records affected: 0 -""" - - -kill_script = """ - set list on; - set blob all; - select gen_id(g,1) as ITERATION_NO from rdb$database; - commit; - - select - sign(a.mon$attachment_id) as hanging_attach_connection - ,left(a.mon$remote_protocol,3) as hanging_attach_protocol - ,s.mon$state as hanging_statement_state - ,s.mon$sql_text as hanging_statement_blob_id - from rdb$database d - left join mon$attachments a on a.mon$remote_process containing 'isql' - -- do NOT use, field not existed in 2.5.x: and a.mon$system_flag is distinct from 1 - and a.mon$attachment_id is distinct from current_connection - left join mon$statements s on - a.mon$attachment_id = s.mon$attachment_id - and s.mon$state = 1 -- 4.0 Classic: 'SELECT RDB$MAP_USING, RDB$MAP_PLUGIN, ... FROM RDB$AUTH_MAPPING', mon$state = 0 - ; - - set count on; - delete from mon$attachments a - where - a.mon$attachment_id <> current_connection - and a.mon$remote_process containing 'isql' - ; - commit; -""" - -hang_script = temp_file('hang_script.sql') -hang_stdout = temp_file('hang_script.out') -hang_stderr = temp_file('hang_script.err') - -@pytest.mark.skipif(platform.system() != 'Windows', reason='FIXME: see notes') -@pytest.mark.version('>=3.0.3') -def test_1(act: Action, hang_script: Path, hang_stdout: Path, hang_stderr: Path, - capsys): - hang_script.write_text('set list on; set count on; select * from sp_get_data;') - pattern_for_failed_statement = re.compile('Statement failed, SQLSTATE = (08006|08003)') - pattern_for_connection_close = re.compile('(Error (reading|writing) data (from|to) the connection)|(connection shutdown)') - pattern_for_ignored_messages = re.compile('(-send_packet/send)|(-Killed by database administrator.)') - killer_output = [] - # - with open(hang_stdout, mode='w') as hang_out, open(hang_stderr, mode='w') as hang_err: - p_hang_sql = subprocess.Popen([act.vars['isql'], '-i', str(hang_script), - '-user', act.db.user, - '-password', act.db.password, act.db.dsn], - stdout=hang_out, stderr=hang_err) - try: - time.sleep(4) - for i in range(2): - act.reset() - act.isql(switches=[], input=kill_script) - killer_output.append(act.stdout) - finally: - p_hang_sql.terminate() - # Ensure that database is not busy - with act.connect_server() as srv: - srv.database.shutdown(database=act.db.db_path, mode=ShutdownMode.FULL, - method=ShutdownMethod.FORCED, timeout=0) - time.sleep(2) - srv.database.bring_online(database=act.db.db_path) - # - output = [] - for line in hang_stdout.read_text().splitlines(): - if line.strip(): - output.append(f'HANGED ATTACH, STDOUT: {line}') - for line in hang_stderr.read_text().splitlines(): - if line.strip(): - if pattern_for_ignored_messages.search(line): - continue - elif pattern_for_failed_statement.search(line): - msg = '' - elif pattern_for_connection_close.search(line): - msg = '' - else: - msg = line - output.append(f'HANGED ATTACH, STDERR: {msg}') - for step in killer_output: - for line in act.clean_string(step).splitlines(): - if line.strip(): - output.append(f"KILLER ATTACH, STDOUT: {' '.join(line.split())}") - # Check - act.reset() - act.expected_stdout = expected_stdout - act.stdout = '\n'.join(output) - assert act.clean_stdout == act.clean_expected_stdout +#coding:utf-8 + +""" +ID: issue-5951 +ISSUE: 5951 +TITLE: Sometime it is impossible to cancel/kill connection executing external query +DESCRIPTION: + Problem did appear when host "A" established connection to host "B" but could not get completed reply from this "B". + This can be emulated by following steps: + 1. We establich new remote connection to the same database using EDS mechanism and supply completely new ROLE to force new attachment be created; + 2. Within this EDS we do query to selectable procedure (with name 'sp_unreachable') which surely will not produce any result. + Bogon IP '192.0.2.2' is used in order to make this SP hang for sufficient time (on Windows it is about 20, on POSIX - about 44 seconds). + Steps 1 and 2 are implemented by asynchronous call of ISQL: we must have ability to kill its process after. + When this 'hanging ISQL' is launched, we wait 1..2 seconds and run one more ISQL, which has mission to KILL all attachments except his own. + This ISQL session is named 'killer', and it writes result of actions to log. + This "killer-ISQL" does TWO iterations with the same code which looks like 'select ... from mon$attachments' and 'delete from mon$attachments'. + First iteration must return data of 'hanging ISQL' and also this session must be immediately killed. + Second iteration must NOT return any data - and this is main check in this test. + + For builds which had bug (before 25.12.2017) one may see that second iteration STILL RETURNS the same data as first one: + ==== + ITERATION_NO 1 + HANGING_ATTACH_CONNECTION 1 + HANGING_ATTACH_PROTOCOL TCP + HANGING_STATEMENT_STATE 1 + HANGING_STATEMENT_BLOB_ID 0:3 + select * from sp_get_data + Records affected: 1 + + ITERATION_NO 2 + HANGING_ATTACH_CONNECTION 1 + HANGING_ATTACH_PROTOCOL TCP + HANGING_STATEMENT_STATE 1 + HANGING_STATEMENT_BLOB_ID 0:1 + select * from sp_get_data + Records affected: 1 + ==== + (expected: all fields in ITER #2 must be NULL) +JIRA: CORE-5685 +FBTEST: bugs.core_5685 +NOTES: + [06.10.2022] pzotov + Fails on Linux when run in 'batch' mode (i.e. when pytest has to perform the whole tests set). + Can not reproduce fail when run this test 'separately': it passes, but lasts too longm, ~130 s. + Test will be re-implemented. + DEFERRED. + [09.07.2024] pzotov + Added item to substitutions related to 'port detached' message that raises in dev build. + Fixed wrong logic because of missed indentation, see hang_stderr. + [01.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' and variables - to be substituted in expected_* on FB 6.x + Separated expected output for FB major versions prior/since 6.x. + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. +""" +import platform +import pytest +import re +import subprocess +import time +from pathlib import Path +from firebird.qa import * +from firebird.driver import ShutdownMode, ShutdownMethod + +substitutions = [ ('.*After line.*', '') + ,('.*Data source.*', '.*Data source') + ,('.*HANGING_STATEMENT_BLOB_ID.*', '') + ,('.*-port detached', '') + ] + +init_script = """ + create sequence g; + commit; + set term ^; + create or alter procedure sp_unreachable returns( unreachable_address varchar(50) ) as + begin + for + execute statement ('select mon$remote_address from mon$attachments a where a.mon$attachment_id = current_connection') + on external '192.0.2.2:' || rdb$get_context('SYSTEM', 'DB_NAME') + as user 'SYSDBA' password 'masterkey' role left(replace( uuid_to_char(gen_uuid()), '-', ''), 31) + into unreachable_address + do + suspend; + end + ^ + + create or alter procedure sp_get_data returns( unreachable_address varchar(50) ) as + begin + for + execute statement ('select u.unreachable_address from sp_unreachable as u') + on external 'localhost:' || rdb$get_context('SYSTEM', 'DB_NAME') + as user 'SYSDBA' password 'masterkey' role left(replace( uuid_to_char(gen_uuid()), '-', ''), 31) + into unreachable_address + do + suspend; + end + ^ + set term ;^ + commit; +""" + +db = db_factory(init=init_script) + +act = python_act('db', substitutions=substitutions) + +kill_script = """ + set list on; + set blob all; + select gen_id(g,1) as ITERATION_NO from rdb$database; + commit; + + select + sign(a.mon$attachment_id) as hanging_attach_connection + ,left(a.mon$remote_protocol,3) as hanging_attach_protocol + ,s.mon$state as hanging_statement_state + ,s.mon$sql_text as hanging_statement_blob_id + from rdb$database d + left join mon$attachments a on a.mon$remote_process containing 'isql' + -- do NOT use, field not existed in 2.5.x: and a.mon$system_flag is distinct from 1 + and a.mon$attachment_id is distinct from current_connection + left join mon$statements s on + a.mon$attachment_id = s.mon$attachment_id + and s.mon$state = 1 -- 4.0 Classic: 'SELECT RDB$MAP_USING, RDB$MAP_PLUGIN, ... FROM RDB$AUTH_MAPPING', mon$state = 0 + ; + + set count on; + delete from mon$attachments a + where + a.mon$attachment_id <> current_connection + and a.mon$remote_process containing 'isql' + ; + commit; +""" + +hang_script = temp_file('hang_script.sql') +hang_stdout = temp_file('hang_script.out') +hang_stderr = temp_file('hang_script.err') + +@pytest.mark.skipif(platform.system() != 'Windows', reason='FIXME: see notes') +@pytest.mark.es_eds +@pytest.mark.version('>=3.0.3') +def test_1(act: Action, hang_script: Path, hang_stdout: Path, hang_stderr: Path, + capsys): + hang_script.write_text('set list on; set count on; select * from sp_get_data;') + pattern_for_failed_statement = re.compile('Statement failed, SQLSTATE = (08006|08003)') + pattern_for_connection_close = re.compile('(Error (reading|writing) data (from|to) the connection)|(connection shutdown)') + pattern_for_ignored_messages = re.compile('(-send_packet/send)|(-Killed by database administrator.)') + killer_output = [] + # + with open(hang_stdout, mode='w') as hang_out, open(hang_stderr, mode='w') as hang_err: + p_hang_sql = subprocess.Popen([act.vars['isql'], '-i', str(hang_script), + '-user', act.db.user, + '-password', act.db.password, act.db.dsn], + stdout=hang_out, stderr=hang_err) + try: + time.sleep(4) + for i in range(2): + act.reset() + act.isql(switches=[], input=kill_script) + killer_output.append(act.stdout) + finally: + p_hang_sql.terminate() + # Ensure that database is not busy + with act.connect_server() as srv: + srv.database.shutdown(database=act.db.db_path, mode=ShutdownMode.FULL, + method=ShutdownMethod.FORCED, timeout=0) + time.sleep(2) + srv.database.bring_online(database=act.db.db_path) + # + output = [] + for line in hang_stdout.read_text().splitlines(): + if line.strip(): + output.append(f'HANGED ATTACH, STDOUT: {line}') + for line in hang_stderr.read_text().splitlines(): + if line.strip(): + msg = '' + if pattern_for_ignored_messages.search(line): + continue + elif pattern_for_failed_statement.search(line): + msg = '' + elif pattern_for_connection_close.search(line): + msg = '' + else: + msg = line + + if msg.strip(): + output.append(f'HANGED ATTACH, STDERR: {msg}') + + for step in killer_output: + for line in act.clean_string(step).splitlines(): + if line.strip(): + output.append(f"KILLER ATTACH, STDOUT: {' '.join(line.split())}") + + act.reset() + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + STORED_PROC_NAME = "'SP_GET_DATA'" if act.is_version('<6') else '"SP_GET_DATA"' + expected_stdout = f""" + HANGED ATTACH, STDOUT: Records affected: 0 + HANGED ATTACH, STDERR: Statement failed, SQLSTATE = 42000 + HANGED ATTACH, STDERR: Execute statement error at isc_dsql_fetch : + HANGED ATTACH, STDERR: + HANGED ATTACH, STDERR: Statement : select u.unreachable_address from sp_unreachable as u + .*Data source + HANGED ATTACH, STDERR: -At procedure {SQL_SCHEMA_PREFIX}{STORED_PROC_NAME} line: 3, col: 9 + HANGED ATTACH, STDERR: + HANGED ATTACH, STDERR: + HANGED ATTACH, STDERR: + HANGED ATTACH, STDERR: + KILLER ATTACH, STDOUT: ITERATION_NO 1 + KILLER ATTACH, STDOUT: HANGING_ATTACH_CONNECTION 1 + KILLER ATTACH, STDOUT: HANGING_ATTACH_PROTOCOL TCP + KILLER ATTACH, STDOUT: HANGING_STATEMENT_STATE 1 + KILLER ATTACH, STDOUT: select * from sp_get_data + KILLER ATTACH, STDOUT: Records affected: 1 + KILLER ATTACH, STDOUT: ITERATION_NO 2 + KILLER ATTACH, STDOUT: HANGING_ATTACH_CONNECTION + KILLER ATTACH, STDOUT: HANGING_ATTACH_PROTOCOL + KILLER ATTACH, STDOUT: HANGING_STATEMENT_STATE + KILLER ATTACH, STDOUT: Records affected: 0 + """ + + act.expected_stdout = expected_stdout + act.stdout = '\n'.join(output) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5691_test.py b/tests/bugs/core_5691_test.py index b3b255d1..c3c71ed9 100644 --- a/tests/bugs/core_5691_test.py +++ b/tests/bugs/core_5691_test.py @@ -22,8 +22,17 @@ [28.03.2024] pzotov Removed loop with search for 'file description' attribute because it is useless in case when system locale differs from English (e.g. russian etc). Defined constant with value = 34. + + [01.08.2024] pzotov + re.sub() calls must be applied to every obtained binary file description because it my contain "(NN-bit debug)" + suffix if we run test against dev-build, e.g.: + 'fbclient.dll' : firebird client library (64-bit debug) + We have to remove such suffix (together with "NN-bit" and parenthesis). + Noted by Dimitry Sibiryakov, https://github.com/FirebirdSQL/firebird-qa/issues/29 """ + import os +import re from pathlib import Path import subprocess import time @@ -158,10 +167,11 @@ def test_1(act: Action, tmp_vbs: Path, tmp_log: Path, capsys): for x in sorted(f_list): subprocess.call( [ 'cscript', '//nologo', str(tmp_vbs), act.vars['bin-dir'] / x ], stdout = vbs_log, stderr = subprocess.STDOUT) - + # 'fbclient.dll' : firebird client library (64-bit debug) with open( tmp_log,'r') as f: for line in f: - print( line.lower() ) + # 01.08.2024: added 're.sub()' here, see notes: + print( re.sub(r'\s+\(\d+(\s+|-)bit debug\)', '', line.lower()) ) expected_stdout = f""" 'fb_lock_print.exe' : firebird lock print tool diff --git a/tests/bugs/core_5695_test.py b/tests/bugs/core_5695_test.py index 02e458a9..dc77d9b6 100644 --- a/tests/bugs/core_5695_test.py +++ b/tests/bugs/core_5695_test.py @@ -44,6 +44,7 @@ RES 1 """ +@pytest.mark.intl @pytest.mark.version('>=3.0.3') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_5704_test.py b/tests/bugs/core_5704_test.py index 38c27f95..164e1538 100644 --- a/tests/bugs/core_5704_test.py +++ b/tests/bugs/core_5704_test.py @@ -16,7 +16,15 @@ FBTEST: bugs.core_5704 NOTES: [25.11.2023] pzotov - Writing code requires more care since 6.0.0.150: ISQL does not allow specifying duplicate delimiters without any statements between them (two semicolon, two carets etc). + Writing code requires more care since 6.0.0.150: ISQL does not allow specifying duplicate delimiters without any + statements between them (two semicolon, two carets etc). + + [29.12.2024] pzotov + Splitted code and expected output for FB versions upto 5.x vs 6.x. + FB 6.x does not support statements "alter database add file ..." (issuing 'token unknown: file'). + See: https://github.com/FirebirdSQL/firebird/commit/f0740d2a3282ed92a87b8e0547139ba8efe61173 + ("Wipe out multi-file database support (#8047)") + """ import pytest @@ -30,10 +38,16 @@ act = python_act('db') -expected_stdout = """ +expected_stdout_5x = """ + CHECK_EDS_RESULT 1 + Records affected: 1 + Records affected: 0 CHECK_EDS_RESULT 1 Records affected: 1 Records affected: 0 +""" + +expected_stdout_6x = """ CHECK_EDS_RESULT 1 Records affected: 1 Records affected: 0 @@ -44,7 +58,8 @@ new_diff_file = temp_file('_new_diff_5704.tmp') new_main_file = temp_file('new_main_5704.tmp') -@pytest.mark.version('>=3.0.3') +@pytest.mark.es_eds +@pytest.mark.version('>=3.0.3,<6') def test_1(act: Action, eds_script: Path, eds_output: Path, new_diff_file: Path, new_main_file: Path): eds_script.write_text(f""" @@ -103,6 +118,63 @@ def test_1(act: Action, eds_script: Path, eds_output: Path, new_diff_file: Path, method=ShutdownMethod.FORCED, timeout=0) srv.database.bring_online(database=act.db.db_path) # Check - act.expected_stdout = expected_stdout + act.expected_stdout = expected_stdout_5x + act.stdout = eds_output.read_text() + assert act.clean_stdout == act.clean_expected_stdout + + +@pytest.mark.version('>=6') +def test_2(act: Action, eds_script: Path, eds_output: Path, new_diff_file: Path, + new_main_file: Path): + eds_script.write_text(f""" + set count on; + set list on; + set autoddl off; + + set term ^; + create or alter procedure sp_connect returns(check_eds_result int) as + declare usr varchar(31); + declare pwd varchar(31); + declare v_sttm varchar(255) = 'select 1 from rdb$database'; + begin + usr ='{act.db.user}'; + pwd = '{act.db.password}'; + execute statement v_sttm + on external 'localhost:' || rdb$get_context('SYSTEM','DB_NAME') + as user usr password pwd + into check_eds_result; + suspend; + end + ^ + set term ;^ + + commit; + set transaction read committed no record_version lock timeout 1; + + alter database add difference file '{new_diff_file}'; + select * from sp_connect; + + rollback; + select * from rdb$files; + rollback; + + """) + # + with open(eds_output, mode='w') as eds_out: + p_eds_sql = subprocess.Popen([act.vars['isql'], '-i', str(eds_script), + '-user', act.db.user, + '-password', act.db.password, act.db.dsn], + stdout=eds_out, stderr=subprocess.STDOUT) + try: + time.sleep(4) + finally: + p_eds_sql.terminate() + # Ensure that database is not busy + with act.connect_server() as srv: + srv.database.shutdown(database=act.db.db_path, mode=ShutdownMode.FULL, + method=ShutdownMethod.FORCED, timeout=0) + srv.database.bring_online(database=act.db.db_path) + # Check + act.expected_stdout = expected_stdout_6x act.stdout = eds_output.read_text() assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5706_test.py b/tests/bugs/core_5706_test.py index 620dc3c9..95889640 100644 --- a/tests/bugs/core_5706_test.py +++ b/tests/bugs/core_5706_test.py @@ -57,6 +57,7 @@ } """ +@pytest.mark.trace @pytest.mark.version('>=3.0.3') def test_1(act: Action): log_before = act.get_firebird_log() diff --git a/tests/bugs/core_5737_test.py b/tests/bugs/core_5737_test.py index 248cf72d..17495a06 100644 --- a/tests/bugs/core_5737_test.py +++ b/tests/bugs/core_5737_test.py @@ -17,6 +17,11 @@ Confirmed bug on 3.0.3.32837 and 4.0.0.800 (ISQL did hang when issued any of 'SHOW TABLE' / 'SHOW INDEX' copmmand). JIRA: CORE-5737 FBTEST: bugs.core_5737 +NOTES: + [02.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -29,11 +34,6 @@ act = python_act('db') -expected_stdout = """ - TEST1 - TEST1_ID_PK_DESC UNIQUE DESCENDING INDEX ON TEST1(ID) -""" - show_script = temp_file('show_script.sql') show_output = temp_file('show_script.out') @@ -56,7 +56,17 @@ def test_1(act: Action, show_script: Path, show_output: Path): time.sleep(4) finally: p_show_sql.terminate() - # - act.expected_stdout = expected_stdout + + expected_stdout_5x = """ + TEST1 + TEST1_ID_PK_DESC UNIQUE DESCENDING INDEX ON TEST1(ID) + """ + + expected_stdout_6x = """ + PUBLIC.TEST1 + PUBLIC.TEST1_ID_PK_DESC UNIQUE DESCENDING INDEX ON TEST1(ID) + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.stdout = show_output.read_text() assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5743_test.py b/tests/bugs/core_5743_test.py index 89c57f61..4a2232f0 100644 --- a/tests/bugs/core_5743_test.py +++ b/tests/bugs/core_5743_test.py @@ -32,24 +32,31 @@ (periode = ?) group by 1 ; + set planonly; -- sample from CORE-5749: select 'my constant ' as dsc, count( * ) from rdb$relations a - where a.rdb$system_flag = 99 + where a.rdb$system_flag = -2147483648 group by 1 ; """ act = isql_act('db', test_script) -expected_stdout = """ +expected_stdout_5x = """ PLAN SORT (JOURNAL_CAISSE INDEX (JOURNAL_CAISSE_IDX)) PLAN SORT (A NATURAL) """ +expected_stdout_6x = """ + PLAN SORT ("PUBLIC"."JOURNAL_CAISSE" INDEX ("PUBLIC"."JOURNAL_CAISSE_IDX")) + PLAN SORT ("A" NATURAL) +""" + + @pytest.mark.version('>=3.0.4') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5746_test.py b/tests/bugs/core_5746_test.py index 3507684c..7f0b3dca 100644 --- a/tests/bugs/core_5746_test.py +++ b/tests/bugs/core_5746_test.py @@ -16,95 +16,119 @@ Restictions about create/alter/drop indexes on system tables that are checked by test for CORE-4731 should be removed. JIRA: CORE-5746 FBTEST: bugs.core_5746 +NOTES: + [03.07.2025] pzotov + 1. ::: NB ::: + For ALTER, DROP, and others statements, Firebird searches for the specified object across all schemas in the search path. + The reference is bound to the first matching object found. + Because of that, on 6.x following syntax should be used for GRANT ALTER ANY TABLE: + grant alter any table on schema system to tmp$c5746; + Explained by Adriano, letter 03.07.2025 + 2. Added 'SQL_SCHEMA_PREFIX' and variables - to be substituted in expected_* on FB 6.x + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.892; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813 """ - +import locale import pytest from firebird.qa import * db = db_factory() -test_script = """ - set list on; - set plan on; - create descending index systable_comm_idx on rdb$relations(rdb$format); - create descending index systable_calc_idx on rdb$relations computed by ( 1 + rdb$format ); - set statistics index systable_comm_idx; - set statistics index systable_calc_idx; +tmp_user = user_factory('db', name = 'tmp$c5746', password = '123') +act = isql_act('db', substitutions=[('-Effective user is.*', '')]) + +expected_stdout_5x = """ + PLAN (RDB$RELATIONS INDEX (SYSTABLE_COMM_IDX)) + SIGN_COUNT 1 + PLAN (RDB$RELATIONS INDEX (SYSTABLE_CALC_IDX)) + SIGN_COUNT 1 + + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -CREATE INDEX SYSTABLE_COMM_IDX failed + -no permission for ALTER access to TABLE RDB$RELATIONS - select sign(count(*)) as sign_count from rdb$relations where rdb$format < 65537; - select sign(count(*)) as sign_count from rdb$relations where 1 + rdb$format < 65537; + PLAN (RDB$RELATIONS INDEX (SYSTABLE_COMM_IDX)) + SIGN_COUNT 1 + PLAN (RDB$RELATIONS INDEX (SYSTABLE_CALC_IDX)) + SIGN_COUNT 1 +""" - alter index systable_calc_idx inactive; - alter index systable_calc_idx active; +expected_stdout_6x = """ + PLAN ("SYSTEM"."RDB$RELATIONS" INDEX ("SYSTEM"."SYSTABLE_COMM_IDX")) + SIGN_COUNT 1 + PLAN ("SYSTEM"."RDB$RELATIONS" INDEX ("SYSTEM"."SYSTABLE_CALC_IDX")) + SIGN_COUNT 1 - drop index systable_comm_idx; - drop index systable_calc_idx; - commit; + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -CREATE INDEX "SYSTEM"."SYSTABLE_COMM_IDX" failed + -no permission for ALTER access to TABLE "SYSTEM"."RDB$RELATIONS" - create or alter user tmp$c5746 password '123'; - commit; + PLAN ("SYSTEM"."RDB$RELATIONS" INDEX ("SYSTEM"."SYSTABLE_COMM_IDX")) + SIGN_COUNT 1 + PLAN ("SYSTEM"."RDB$RELATIONS" INDEX ("SYSTEM"."SYSTABLE_CALC_IDX")) + SIGN_COUNT 1 +""" - connect '$(DSN)' user tmp$c5746 password '123'; - -- this should FAIL: - create descending index systable_comm_idx on rdb$relations(rdb$format); - commit; +@pytest.mark.version('>=3.0.4') +def test_1(act: Action, tmp_user: User): + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'ON SCHEMA SYSTEM' + GRANT_ALTER_ANY_TABLE_EXPR = f'grant alter any table {SQL_SCHEMA_PREFIX} to {tmp_user.name};' + # grant alter any table to tmp$c5746; + # grant alter any table on schema system to tmp$c5746; - connect '$(DSN)' user 'SYSDBA' password 'masterkey'; - grant alter any table to tmp$c5746; - commit; + test_script = f""" + set list on; + set plan on; + create descending index systable_comm_idx on rdb$relations(rdb$format); + create descending index systable_calc_idx on rdb$relations computed by ( 1 + rdb$format ); + set statistics index systable_comm_idx; + set statistics index systable_calc_idx; + select sign(count(*)) as sign_count from rdb$relations where rdb$format < 65537; + select sign(count(*)) as sign_count from rdb$relations where 1 + rdb$format < 65537; - connect '$(DSN)' user tmp$c5746 password '123'; - -- All following statements should PASS: - create descending index systable_comm_idx on rdb$relations(rdb$format); - create descending index systable_calc_idx on rdb$relations computed by ( 1 + rdb$format ); - set statistics index systable_comm_idx; - set statistics index systable_calc_idx; + alter index systable_calc_idx inactive; + alter index systable_calc_idx active; - select sign(count(*)) as sign_count from rdb$relations where rdb$format < 65537; - select sign(count(*)) as sign_count from rdb$relations where 1 + rdb$format < 65537; + drop index systable_comm_idx; + drop index systable_calc_idx; + commit; - alter index systable_calc_idx inactive; - alter index systable_calc_idx active; + connect '{act.db.dsn}' user {tmp_user.name} password '{tmp_user.password}'; + -- this should FAIL: + create descending index systable_comm_idx on rdb$relations(rdb$format); + commit; - drop index systable_comm_idx; - drop index systable_calc_idx; - commit; + connect '{act.db.dsn}' user '{act.db.user}' password '{act.db.password}'; - connect '$(DSN)' user 'SYSDBA' password 'masterkey'; - drop user tmp$c5746; - commit; + {GRANT_ALTER_ANY_TABLE_EXPR}; + commit; -""" -act = isql_act('db', test_script, substitutions=[('-Effective user is.*', '')]) + connect '{act.db.dsn}' user {tmp_user.name} password '{tmp_user.password}'; + -- All following statements should PASS: + create descending index systable_comm_idx on rdb$relations(rdb$format); + create descending index systable_calc_idx on rdb$relations computed by ( 1 + rdb$format ); + set statistics index systable_comm_idx; + set statistics index systable_calc_idx; -expected_stdout = """ - PLAN (RDB$RELATIONS INDEX (SYSTABLE_COMM_IDX)) - SIGN_COUNT 1 - PLAN (RDB$RELATIONS INDEX (SYSTABLE_CALC_IDX)) - SIGN_COUNT 1 + select sign(count(*)) as sign_count from rdb$relations where rdb$format < 65537; + select sign(count(*)) as sign_count from rdb$relations where 1 + rdb$format < 65537; - PLAN (RDB$RELATIONS INDEX (SYSTABLE_COMM_IDX)) - SIGN_COUNT 1 - PLAN (RDB$RELATIONS INDEX (SYSTABLE_CALC_IDX)) - SIGN_COUNT 1 -""" + alter index systable_calc_idx inactive; + alter index systable_calc_idx active; -expected_stderr = """ - Statement failed, SQLSTATE = 28000 - unsuccessful metadata update - -CREATE INDEX SYSTABLE_COMM_IDX failed - -no permission for ALTER access to TABLE RDB$RELATIONS -""" + drop index systable_comm_idx; + drop index systable_calc_idx; + commit; -@pytest.mark.version('>=3.0.4') -def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + """ + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.isql(switches=['-q'], input=test_script, combine_output = True, io_enc = locale.getpreferredencoding()) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5747_test.py b/tests/bugs/core_5747_test.py index 1d58be5e..b3fbd589 100644 --- a/tests/bugs/core_5747_test.py +++ b/tests/bugs/core_5747_test.py @@ -8,15 +8,16 @@ beside generator we also have to check the same issue about grant usage on exception. JIRA: CORE-5747 FBTEST: bugs.core_5747 +NOTES: + [02.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -substitutions = [('no G privilege with grant option on object .*', - 'no USAGE privilege with grant option on object'), - ('GEN_FOR_DBA_ONLY', ''), ('EXC_FOR_DBA_ONLY', '')] - db = db_factory() test_script = """ @@ -56,11 +57,13 @@ """ -act = isql_act('db', test_script, substitutions=substitutions) +substitutions = [('no G privilege with grant option on object .*', + 'no USAGE privilege with grant option on object'), + ('GEN_FOR_DBA_ONLY', ''), ('EXC_FOR_DBA_ONLY', '')] -# version: 3.0 +act = isql_act('db', test_script, substitutions=substitutions) -expected_stderr_1 = """ +expected_stdout_3x = """ Statement failed, SQLSTATE = 42000 unsuccessful metadata update -GRANT failed @@ -75,36 +78,44 @@ no permission for USAGE access to EXCEPTION """ -@pytest.mark.version('>=3.0.4,<4.0') -def test_1(act: Action): - act.expected_stderr = expected_stderr_1 - act.execute() - assert act.clean_stderr == act.clean_expected_stderr - -# version: 4.0 - -expected_stderr_2 = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 42000 unsuccessful metadata update -GRANT failed -no USAGE privilege with grant option on object - Statement failed, SQLSTATE = 42000 unsuccessful metadata update -GRANT failed -no USAGE privilege with grant option on object - Statement failed, SQLSTATE = 28000 no permission for USAGE access to GENERATOR -Effective user is TMP$C5747 - Statement failed, SQLSTATE = 28000 no permission for USAGE access to EXCEPTION -Effective user is TMP$C5747 """ -@pytest.mark.version('>=4.0') +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -GRANT failed + -no USAGE privilege with grant option on object "PUBLIC"."" + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -GRANT failed + -no USAGE privilege with grant option on object "PUBLIC"."" + Statement failed, SQLSTATE = 28000 + no permission for USAGE access to GENERATOR "PUBLIC"."" + -Effective user is TMP$C5747 + Statement failed, SQLSTATE = 28000 + no permission for USAGE access to EXCEPTION "PUBLIC"."" + -Effective user is TMP$C5747 +""" + + +@pytest.mark.version('>=3.0') def test_2(act: Action): - act.expected_stderr = expected_stderr_2 - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + + act.expected_stdout = expected_stdout_3x if act.is_version('<4') else expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5755_test.py b/tests/bugs/core_5755_test.py index 0ff6da02..df750a85 100644 --- a/tests/bugs/core_5755_test.py +++ b/tests/bugs/core_5755_test.py @@ -5,10 +5,15 @@ ISSUE: 6018 TITLE: No error if the GRANT target object does not exist DESCRIPTION: - grant execute on proc|func|package and grant usage on sequence|exception -- still does NOT produce error/warning. - These statements temply disabled until some additional comments in tracker. + grant execute on proc|func|package and grant usage on sequence|exception -- still does NOT produce error/warning. + These statements temply disabled until some additional comments in tracker. JIRA: CORE-5755 FBTEST: bugs.core_5755 +NOTES: + [02.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -53,11 +58,6 @@ grant select on table_test to function wrong_func; - /************ - - TEMPLY DISABLED, SEE ISSUE IN THE TICKET, 02-JUN-2018 08:20 - =============== - grant execute on procedure sp_test to wrong_func; grant execute on function fn_test to wrong_func; @@ -68,24 +68,36 @@ grant usage on exception x_test to wrong_func; - **************/ """ act = isql_act('db', test_script) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 42000 unsuccessful metadata update -GRANT failed -Function WRONG_FUNC does not exist + Statement failed, SQLSTATE = 42000 unsuccessful metadata update -GRANT failed -Function WRONG_FUNC does not exist """ +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -GRANT failed + -Function "PUBLIC"."WRONG_FUNC" does not exist + + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -GRANT failed + -Function "PUBLIC"."WRONG_FUNC" does not exist +""" + @pytest.mark.version('>=3.0.4') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5756_test.py b/tests/bugs/core_5756_test.py index d5a10c13..f5d71591 100644 --- a/tests/bugs/core_5756_test.py +++ b/tests/bugs/core_5756_test.py @@ -7,6 +7,13 @@ DESCRIPTION: JIRA: CORE-5756 FBTEST: bugs.core_5756 + [02.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Difference between current_connection values must be checked to be sure that there was no crash, see 'ATT_DIFF' + + Confirmed bug (crash) on 3.0.3.32900. + Checked on 6.0.0.889; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -19,28 +26,42 @@ recreate table test(x int); insert into test values(1); select * from test; + set term ^; + execute block as + begin + rdb$set_context('USER_SESSION', 'INIT_ATT', current_connection); + end ^ + set term ;^ + recreate table test(x int, y int); -- this led to crash - commit; + select * from test; + select current_connection - cast( rdb$get_context('USER_SESSION', 'INIT_ATT') as int) as att_diff from rdb$database; """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ - X 1 - X 1 +expected_stdout_5x = """ + X 1 + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -object TABLE "TEST" is in use + X 1 + ATT_DIFF 0 """ -expected_stderr = """ +expected_stdout_6x = """ + X 1 Statement failed, SQLSTATE = 42000 unsuccessful metadata update - -object TABLE "TEST" is in use + -object TABLE "PUBLIC"."TEST" is in use + X 1 + ATT_DIFF 0 """ @pytest.mark.version('>=3.0.4') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5794_test.py b/tests/bugs/core_5794_test.py index 23b0ff38..7548f375 100644 --- a/tests/bugs/core_5794_test.py +++ b/tests/bugs/core_5794_test.py @@ -13,6 +13,11 @@ "Since Firebird3 <..> cursor doesn't see the changes made by "inner" statements." JIRA: CORE-5794 FBTEST: bugs.core_5794 +NOTES: + [02.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.889; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -67,30 +72,41 @@ select mon$variable_name as ctx_var, mon$variable_value as ctx_value from mon$context_variables; """ -act = isql_act('db', test_script, substitutions=[('line:\\s[0-9]+,', 'line: x'), - ('col:\\s[0-9]+', 'col: y')]) +substitutions = [ ('[ \t]+', ' '), ( r'line(:)?\s+\d+.*', '' ) ] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ - CTX_VAR TRIGGER_OLD_ID - CTX_VALUE 1 +expected_stdout_5x = """ + Statement failed, SQLSTATE = HY000 + exception 1 + -TEST_EXCEPTION + -it is forbidden to delete row with val>0 (id = 1, val=10) + -At trigger 'TEST_TABLE_BD' + At block - CTX_VAR TRIGGER_OLD_VAL - CTX_VALUE 10 + CTX_VAR TRIGGER_OLD_ID + CTX_VALUE 1 + + CTX_VAR TRIGGER_OLD_VAL + CTX_VALUE 10 """ -expected_stderr = """ +expected_stdout_6x = """ Statement failed, SQLSTATE = HY000 exception 1 - -TEST_EXCEPTION + -"PUBLIC"."TEST_EXCEPTION" -it is forbidden to delete row with val>0 (id = 1, val=10) - -At trigger 'TEST_TABLE_BD' line: 6, col: 9 - At block line: 16, col: 9 + -At trigger "PUBLIC"."TEST_TABLE_BD" + At block + + CTX_VAR TRIGGER_OLD_ID + CTX_VALUE 1 + + CTX_VAR TRIGGER_OLD_VAL + CTX_VALUE 10 """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5840_test.py b/tests/bugs/core_5840_test.py index 3e9a6959..92f723ff 100644 --- a/tests/bugs/core_5840_test.py +++ b/tests/bugs/core_5840_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-5840 FBTEST: bugs.core_5840 +NOTES: + [02.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.889; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -63,77 +68,84 @@ commit; """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ - RDB$RELATION_NAME TEST1 - RDB$RELATION_NAME TEST3 -""" - -# version: 3.0.4 - -expected_stderr_1 = """ +expected_stdout_3x = """ Statement failed, SQLSTATE = 28000 unsuccessful metadata update -CREATE TABLE TEST2 failed -no permission for REFERENCES access to TABLE TEST1 - Statement failed, SQLSTATE = 28000 unsuccessful metadata update -ALTER TABLE TEST3 failed -no permission for ALTER access to TABLE TEST3 - Statement failed, SQLSTATE = 28000 unsuccessful metadata update -ALTER TABLE TEST1 failed -no permission for ALTER access to TABLE TEST1 - Statement failed, SQLSTATE = 28000 unsuccessful metadata update -ALTER TABLE TEST1 failed -no permission for ALTER access to TABLE TEST1 + RDB$RELATION_NAME TEST1 + RDB$RELATION_NAME TEST3 """ -@pytest.mark.version('>=3.0.4,<4.0') -def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr_1 - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) - -# version: 4.0 - -expected_stderr_2 = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 28000 unsuccessful metadata update -CREATE TABLE TEST2 failed -no permission for REFERENCES access to TABLE TEST1 -Effective user is TMP$C5840 - Statement failed, SQLSTATE = 28000 unsuccessful metadata update -ALTER TABLE TEST3 failed -no permission for ALTER access to TABLE TEST3 -Effective user is TMP$C5840 - Statement failed, SQLSTATE = 28000 unsuccessful metadata update -ALTER TABLE TEST1 failed -no permission for ALTER access to TABLE TEST1 -Effective user is TMP$C5840 - Statement failed, SQLSTATE = 28000 unsuccessful metadata update -ALTER TABLE TEST1 failed -no permission for ALTER access to TABLE TEST1 -Effective user is TMP$C5840 + RDB$RELATION_NAME TEST1 + RDB$RELATION_NAME TEST3 """ -@pytest.mark.version('>=4.0') +expected_stdout_6x = """ + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -CREATE TABLE "PUBLIC"."TEST2" failed + -no permission for REFERENCES access to TABLE "PUBLIC"."TEST1" + -Effective user is TMP$C5840 + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER TABLE "PUBLIC"."TEST3" failed + -no permission for ALTER access to TABLE "PUBLIC"."TEST3" + -Effective user is TMP$C5840 + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER TABLE "PUBLIC"."TEST1" failed + -no permission for ALTER access to TABLE "PUBLIC"."TEST1" + -Effective user is TMP$C5840 + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER TABLE "PUBLIC"."TEST1" failed + -no permission for ALTER access to TABLE "PUBLIC"."TEST1" + -Effective user is TMP$C5840 + RDB$RELATION_NAME TEST1 + RDB$RELATION_NAME TEST3 +""" + + +@pytest.mark.version('>=3.0') def test_2(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr_2 - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + + act.expected_stdout = expected_stdout_3x if act.is_version('<4') else expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5843_test.py b/tests/bugs/core_5843_test.py index 056f3406..873c237d 100644 --- a/tests/bugs/core_5843_test.py +++ b/tests/bugs/core_5843_test.py @@ -8,18 +8,15 @@ JIRA: CORE-5843 FBTEST: bugs.core_5843 NOTES: - [25.11.2023] pzotov - Writing code requires more care since 6.0.0.150: ISQL does not allow specifying duplicate delimiters without any statements between them (two semicolon, two carets etc). + [02.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.889; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * -substitutions = [('line[:]{0,1}[\\s]+[\\d]+.*', 'line'), - ('transaction[\\s]+[\\d]+[\\s]+aborted', 'transaction aborted'), - ('tx=[\\d]+', 'tx='), ('TX_ID[\\s]+[\\d]+', 'TX_ID'), - ('exception[\\s]+[\\d]+.*', 'exception')] - init_script = """ --set echo on; set bail on; @@ -157,50 +154,79 @@ """ +substitutions = [('[ \t]+', ' '), + ('line[:]{0,1}[\\s]+[\\d]+.*', 'line'), + ('transaction[\\s]+[\\d]+[\\s]+aborted', 'transaction aborted'), + ('tx=[\\d]+', 'tx='), ('TX_ID[\\s]+[\\d]+', 'TX_ID'), + ('exception[\\s]+[\\d]+.*', 'exception')] + act = isql_act('db', test_script, substitutions=substitutions) -expected_stdout = """ +expected_stdout_5x = """ MSG test_1, point-1 - TX_ID 22 + TX_ID 19 TX_STATE 1 - + Statement failed, SQLSTATE = HY000 + exception 1 + -EX_TRA_START + -transaction 20 aborted + -At trigger 'TX_START' line: 10, col: 13 MSG test_1, point-3 - TX_ID 25 + TX_ID 22 TX_STATE 1 Records affected: 1 - MSG test2, point-a - TX_ID 28 + TX_ID 24 TX_STATE 1 + Statement failed, SQLSTATE = HY000 + exception 1 + -EX_TRA_START + -transaction 25 aborted + -At trigger 'TX_START' line: 10, col: 13 + At procedure 'SP_USE_ATX' line: 5, col: 9 RDB$GET_CONTEXT - start: tx=26 - trigger on commit, current tx=26 - trigger on transaction start, current tx=28 - sp_use_atx, point_a, current tx=28 - trigger on transaction start, current tx=29 - exception on tx start, current tx=29 - + start: tx=23 + trigger on commit, current tx=23 + trigger on transaction start, current tx=24 + sp_use_atx, point_a, current tx=24 + trigger on transaction start, current tx=25 + exception on tx start, current tx=25 """ -expected_stderr = """ +expected_stdout_6x = """ + MSG test_1, point-1 + TX_ID 19 + TX_STATE 1 Statement failed, SQLSTATE = HY000 exception 1 - -EX_TRA_START - -transaction 23 aborted - -At trigger 'TX_START' line - + -"PUBLIC"."EX_TRA_START" + -transaction 20 aborted + -At trigger "PUBLIC"."TX_START" line: 10, col: 13 + MSG test_1, point-3 + TX_ID 22 + TX_STATE 1 + Records affected: 1 + MSG test2, point-a + TX_ID 24 + TX_STATE 1 Statement failed, SQLSTATE = HY000 exception 1 - -EX_TRA_START - -transaction 29 aborted - -At trigger 'TX_START' line - At procedure 'SP_USE_ATX' line + -"PUBLIC"."EX_TRA_START" + -transaction 25 aborted + -At trigger "PUBLIC"."TX_START" line: 10, col: 13 + At procedure "PUBLIC"."SP_USE_ATX" line: 5, col: 9 + RDB$GET_CONTEXT + start: tx=23 + trigger on commit, current tx=23 + trigger on transaction start, current tx=24 + sp_use_atx, point_a, current tx=24 + trigger on transaction start, current tx=25 + exception on tx start, current tx=25 """ @pytest.mark.version('>=3.0.4') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5845_test.py b/tests/bugs/core_5845_test.py index 309e4a8c..0a37afbe 100644 --- a/tests/bugs/core_5845_test.py +++ b/tests/bugs/core_5845_test.py @@ -9,25 +9,33 @@ FBTEST: bugs.core_5845 NOTES: [12.09.2023] pzotov - 1. Refactored: use firebird-driver ability to show plan (instead of call ISQL), removed hard-coded index names. - Expected result is "accumulated" by traversing through dictionary items (see 'chk_qry_map') instead of be written beforehand. - Added several queries provided by dimitr, letter 12-sep-2023. - - 2. ATTENTION: plan for query "select ... where t.id1 = 1 and t.x > 0 and t.id2 = 0" - has been changed in intermediate snapshot + 1. Added several queries provided by dimitr. + 2. Plan for query "select ... where t.id1 = 1 and t.x > 0 and t.id2 = 0" - has been changed in intermediate snapshot 5.0.0.1204 (timestamp: 20230912 08:00). One need to split expected results for FB 5.x+ and older versions. See: https://github.com/FirebirdSQL/firebird/commit/022f09287747dd05753bd11acd3b3fe4b0756f6e https://github.com/FirebirdSQL/firebird/compare/252c5b2b2f88...784f7bd8a6f5 - Checked on: 3.0.12.33707; 4.0.4.2986; 5.0.0.1204 (nightly build and intermediate snapshot of 12-sep-2023 08:00). + [25.06.2025] pzotov + 1. Re-implemented: use same Python dictionary which stores QUERIES as its KEYS and execution plans as values, + depending on major FB version. + 2. ::: ACHTUNG ::: + We have to call .free() in order to prevent from pytest hanging after all tests completed. + Workaround was provided by Vlad, letter 25.06.2025 13:36. + See also explaination by Vlad: 26.10.24 17:42 ("oddities when use instances of selective statements"). + + 3. Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + + Checked on 6.0.0.863; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * P_KEY_IDX = "test_pk_id1_id2_id3".upper() -SINGLE_X_IDX = "test__x_only".upper() -COMPOUND_IDX = "test__id1_x".upper() +SINGLE_X_IDX = "test_single".upper() +COMPOUND_IDX = "test_compound".upper() init_sql = f""" recreate table test @@ -43,34 +51,61 @@ commit; """ -chk_qry_map_fb4 = { - "select * from test t where t.id1=1 and t.x>0" : f"PLAN (T INDEX ({COMPOUND_IDX}))" - ,"select * from test t where t.id1=1 and t.x>0 order by t.id1, t.id2, t.id3" : f"PLAN SORT (T INDEX ({COMPOUND_IDX}))" - ,"select * from test t where t.id1=1 and t.x>0 order by t.id1+0, t.id2, t.id3" : f"PLAN SORT (T INDEX ({COMPOUND_IDX}))" - - # following examples were provided by dimitr, 12-sep-2023: - ,"select * from test t where t.id1 = 1 and t.x > 0 and t.id2 = 0" : f"PLAN (T INDEX ({COMPOUND_IDX}))" - ,"select * from test t where t.id1 = 1 and t.x = 0" : f"PLAN (T INDEX ({COMPOUND_IDX}))" - ,"select * from test t where t.id1 = 1 and t.id2 = 2" : f"PLAN (T INDEX ({P_KEY_IDX}))" - ,"select * from test t where t.id1 = 1 and t.id2 = 2 and t.id3 = 3" : f"PLAN (T INDEX ({P_KEY_IDX}))" - ,"select * from test t where t.id1 = 1 and t.x = 0 and t.id2 = 0" : f"PLAN (T INDEX ({COMPOUND_IDX}))" -} - -chk_qry_map_fb5 = { - "select * from test t where t.id1=1 and t.x>0" : f"PLAN (T INDEX ({COMPOUND_IDX}))" - ,"select * from test t where t.id1=1 and t.x>0 order by t.id1, t.id2, t.id3" : f"PLAN SORT (T INDEX ({COMPOUND_IDX}))" - ,"select * from test t where t.id1=1 and t.x>0 order by t.id1+0, t.id2, t.id3" : f"PLAN SORT (T INDEX ({COMPOUND_IDX}))" +chk_qry_map = { + "select * from test ta where ta.id1=1 and ta.x>0" : + ( + f"PLAN (TA INDEX ({COMPOUND_IDX}))", # 3.x ... 4.x + f"PLAN (TA INDEX ({COMPOUND_IDX}))", # 5.x + f'PLAN ("TA" INDEX ("PUBLIC"."{COMPOUND_IDX}"))', # 6.x+ + ) + ,"select * from test tb where tb.id1=1 and tb.x>0 order by tb.id1, tb.id2, tb.id3" : + ( + f"PLAN SORT (TB INDEX ({COMPOUND_IDX}))", + f"PLAN SORT (TB INDEX ({COMPOUND_IDX}))", + f'PLAN SORT ("TB" INDEX ("PUBLIC"."{COMPOUND_IDX}"))', + ) + ,"select * from test tc where tc.id1=1 and tc.x>0 order by tc.id1+0, tc.id2, tc.id3" : + ( + f"PLAN SORT (TC INDEX ({COMPOUND_IDX}))", + f"PLAN SORT (TC INDEX ({COMPOUND_IDX}))", + f'PLAN SORT ("TC" INDEX ("PUBLIC"."{COMPOUND_IDX}"))', + ) # following examples were provided by dimitr, 12-sep-2023: ########################################################## # ::: NB ::: Since 5.0.0.1204 (intermediate snapshot with timestamp 20230912 08:00) plan for following query # "select ... where t.id1 = 1 and t.x > 0 and t.id2 = 0" -- has been changed to: PLAN (T INDEX (TEST_PK_ID1_ID2_ID3)) ########################################################## - ,"select * from test t where t.id1 = 1 and t.x > 0 and t.id2 = 0" : f"PLAN (T INDEX ({P_KEY_IDX}))" - ,"select * from test t where t.id1 = 1 and t.x = 0" : f"PLAN (T INDEX ({COMPOUND_IDX}))" - ,"select * from test t where t.id1 = 1 and t.id2 = 2" : f"PLAN (T INDEX ({P_KEY_IDX}))" - ,"select * from test t where t.id1 = 1 and t.id2 = 2 and t.id3 = 3" : f"PLAN (T INDEX ({P_KEY_IDX}))" - ,"select * from test t where t.id1 = 1 and t.x = 0 and t.id2 = 0" : f"PLAN (T INDEX ({COMPOUND_IDX}))" + ,"select * from test td where td.id1 = 1 and td.x > 0 and td.id2 = 0" : + ( + f"PLAN (TD INDEX ({COMPOUND_IDX}))", + f"PLAN (TD INDEX ({P_KEY_IDX}))", + f'PLAN ("TD" INDEX ("PUBLIC"."{P_KEY_IDX}"))', + ) + ,"select * from test te where te.id1 = 1 and te.x = 0" : + ( + f"PLAN (TE INDEX ({COMPOUND_IDX}))", + f"PLAN (TE INDEX ({COMPOUND_IDX}))", + f'PLAN ("TE" INDEX ("PUBLIC"."{COMPOUND_IDX}"))', + ) + ,"select * from test tf where tf.id1 = 1 and tf.id2 = 2" : + ( + f"PLAN (TF INDEX ({P_KEY_IDX}))", + f"PLAN (TF INDEX ({P_KEY_IDX}))", + f'PLAN ("TF" INDEX ("PUBLIC"."{P_KEY_IDX}"))', + ) + ,"select * from test tg where tg.id1 = 1 and tg.id2 = 2 and tg.id3 = 3" : + ( + f"PLAN (TG INDEX ({P_KEY_IDX}))", + f"PLAN (TG INDEX ({P_KEY_IDX}))", + f'PLAN ("TG" INDEX ("PUBLIC"."{P_KEY_IDX}"))', + ) + ,"select * from test th where th.id1 = 1 and th.x = 0 and th.id2 = 0" : + ( + f"PLAN (TH INDEX ({COMPOUND_IDX}))", + f"PLAN (TH INDEX ({COMPOUND_IDX}))", + f'PLAN ("TH" INDEX ("PUBLIC"."{COMPOUND_IDX}"))', + ) } db = db_factory(init = init_sql) @@ -79,18 +114,22 @@ @pytest.mark.version('>=3.0.4') def test_1(act: Action, capsys): - if act.is_version('<5'): - chk_qry_map = chk_qry_map_fb4 - else: - chk_qry_map = chk_qry_map_fb5 - expected_plans_lst = [ '\n'.join((k,v)) for k,v in chk_qry_map.items() ] + expected_plans_lst = [] with act.db.connect() as con: cur = con.cursor() - for q in chk_qry_map.keys(): + #for q in chk_qry_map.keys(): + for q, v in chk_qry_map.items(): + cur.execute(q) ps = cur.prepare(q) - print( q ) + print(q) print( ps.plan ) + + ps.free() # ::: achtung ::: 25.06.2024, need to prevent from pytest hanging after all tests completed. + + expected_qry_plan = v[0] if act.is_version('<5') else v[1] if act.is_version('<6') else v[2] + expected_plans_lst.append( q ) + expected_plans_lst.append( expected_qry_plan + '\n' ) act.expected_stdout = '\n'.join(expected_plans_lst) act.stdout = capsys.readouterr().out diff --git a/tests/bugs/core_5852_test.py b/tests/bugs/core_5852_test.py index 515c78b1..93e223a0 100644 --- a/tests/bugs/core_5852_test.py +++ b/tests/bugs/core_5852_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-5852 FBTEST: bugs.core_5852 +NOTES: + [02.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.889; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -62,31 +67,40 @@ act = isql_act('db', test_script) -expected_stdout = """ - Records affected: 0 -""" - -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 42000 unsuccessful metadata update -GRANT failed -Exception NO_SUCH_EXC does not exist - Statement failed, SQLSTATE = 42000 unsuccessful metadata update -GRANT failed -Generator/Sequence NO_SUCH_GEN does not exist - Statement failed, SQLSTATE = 42000 unsuccessful metadata update -GRANT failed -Generator/Sequence NO_SUCH_SEQ does not exist + Records affected: 0 +""" + +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -GRANT failed + -Exception "PUBLIC"."NO_SUCH_EXC" does not exist + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -GRANT failed + -Generator/Sequence "PUBLIC"."NO_SUCH_GEN" does not exist + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -GRANT failed + -Generator/Sequence "PUBLIC"."NO_SUCH_SEQ" does not exist + Records affected: 0 """ @pytest.mark.version('>=3.0.4') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5853_test.py b/tests/bugs/core_5853_test.py index c347c14c..dc832c3a 100644 --- a/tests/bugs/core_5853_test.py +++ b/tests/bugs/core_5853_test.py @@ -7,6 +7,10 @@ DESCRIPTION: JIRA: CORE-5853 FBTEST: bugs.core_5853 +NOTES: + [03.07.2025] pzotov + Added substitution to suppress all except sqltype and fields name from SQLDA output. + Checked on 6.0.0.892; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813 """ import pytest @@ -16,44 +20,24 @@ # version: 3.0 -test_script_1 = """ - set planonly; - select current_time, current_timestamp from rdb$database; +test_script = """ + set list on; + set sqlda_display on; select localtime from rdb$database; select localtimestamp from rdb$database; """ -act_1 = isql_act('db', test_script_1) +act = isql_act('db', test_script, substitutions=[('^((?!SQLSTATE|sqltype:|name:).)*$',''),('[ \t]+',' ')]) -expected_stdout_1 = """ - PLAN (RDB$DATABASE NATURAL) - PLAN (RDB$DATABASE NATURAL) - PLAN (RDB$DATABASE NATURAL) +expected_stdout = """ + 01: sqltype: 560 TIME scale: 0 subtype: 0 len: 4 + : name: LOCALTIME alias: LOCALTIME + 01: sqltype: 510 TIMESTAMP scale: 0 subtype: 0 len: 8 + : name: LOCALTIMESTAMP alias: LOCALTIMESTAMP """ -@pytest.mark.version('>=2.5.9,<4.0') -def test_1(act_1: Action): - act_1.expected_stdout = expected_stdout_1 - act_1.execute() - assert act_1.clean_stdout == act_1.clean_expected_stdout - -# version: 4.0 - -test_script_2 = """ - set planonly; - select current_time, current_timestamp from rdb$database; - --select localtime from rdb$database; - --select localtimestamp from rdb$database; -""" - -act_2 = isql_act('db', test_script_2) - -expected_stdout_2 = """ - PLAN (RDB$DATABASE NATURAL) -""" - -@pytest.mark.version('>=4.0') -def test_2(act_2: Action): - act_2.expected_stdout = expected_stdout_2 - act_2.execute() - assert act_2.clean_stdout == act_2.clean_expected_stdout +@pytest.mark.version('>=3.0') +def test_2(act: Action): + act.expected_stdout = expected_stdout + act.execute() + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5861_test.py b/tests/bugs/core_5861_test.py index c983286f..00553ca0 100644 --- a/tests/bugs/core_5861_test.py +++ b/tests/bugs/core_5861_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-5861 FBTEST: bugs.core_5861 +NOTES: + [02.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.889; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -124,60 +129,88 @@ commit;*/ """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ +expected_stdout_5x = """ RDB$ROLE_NAME ROLE1 F GEN_ID 1 -""" - -expected_stderr = """ Statement failed, SQLSTATE = 42000 unsuccessful metadata update -GRANT failed -no CREATE privilege with grant option on DDL SQL$TABLES - Statement failed, SQLSTATE = 42000 unsuccessful metadata update -GRANT failed -no EXECUTE privilege with grant option on object P - Statement failed, SQLSTATE = 42000 unsuccessful metadata update -GRANT failed -no EXECUTE privilege with grant option on object F - Statement failed, SQLSTATE = 42000 unsuccessful metadata update -GRANT failed -no USAGE privilege with grant option on object G - Statement failed, SQLSTATE = 42000 unsuccessful metadata update -GRANT failed -no USAGE privilege with grant option on object E - Statement failed, SQLSTATE = 42000 unsuccessful metadata update -GRANT failed -no grant option for privilege SELECT on table/view TAB - Statement failed, SQLSTATE = 42000 unsuccessful metadata update -GRANT failed -no grant option for privilege UPDATE on column ID of table/view TAB - Statement failed, SQLSTATE = 42000 unsuccessful metadata update -GRANT failed -no EXECUTE privilege with grant option on object PAK """ +expected_stdout_6x = """ + RDB$ROLE_NAME ROLE1 + F + GEN_ID 1 + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -GRANT failed + -no CREATE privilege with grant option on DDL "PUBLIC"."SQL$TABLES" + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -GRANT failed + -no EXECUTE privilege with grant option on object "PUBLIC"."P" + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -GRANT failed + -no EXECUTE privilege with grant option on object "PUBLIC"."F" + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -GRANT failed + -no USAGE privilege with grant option on object "PUBLIC"."G" + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -GRANT failed + -no USAGE privilege with grant option on object "PUBLIC"."E" + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -GRANT failed + -no grant option for privilege SELECT on table/view "PUBLIC"."TAB" + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -GRANT failed + -no grant option for privilege UPDATE on column "ID" of table/view "PUBLIC"."TAB" + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -GRANT failed + -no EXECUTE privilege with grant option on object "PUBLIC"."PAK" +""" + @pytest.mark.version('>=4.0') def test_1(act: Action, user_1, user_2, user_3, role_1, role_2, role_3): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5874_test.py b/tests/bugs/core_5874_test.py index 2410bb8a..941b5484 100644 --- a/tests/bugs/core_5874_test.py +++ b/tests/bugs/core_5874_test.py @@ -10,6 +10,11 @@ All cases should produce STDERR with specifying table name and R/O column after dot. JIRA: CORE-5874 FBTEST: bugs.core_5874 +NOTES: + [02.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.889; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -38,7 +43,7 @@ act = isql_act('db', test_script) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 42000 attempted update of read-only column TEST.hozzáadása @@ -49,8 +54,20 @@ attempted update of read-only column TEST.hozzáadása """ +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + attempted update of read-only column "PUBLIC"."TEST"."hozzáadása" + + Statement failed, SQLSTATE = 42000 + attempted update of read-only column "PUBLIC"."TEST"."hozzáadása" + + Statement failed, SQLSTATE = 42000 + attempted update of read-only column "PUBLIC"."TEST"."hozzáadása" +""" + @pytest.mark.version('>=4.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5876_test.py b/tests/bugs/core_5876_test.py index 75682097..4b2b51d8 100644 --- a/tests/bugs/core_5876_test.py +++ b/tests/bugs/core_5876_test.py @@ -19,6 +19,11 @@ in UDR library "udf_compat", see it in folder: ../plugins/udr/ JIRA: CORE-5876 FBTEST: bugs.core_5876 +NOTES: + [02.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.889; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -87,15 +92,23 @@ def test_1(act_1: Action): act_2 = isql_act('db', test_script_2) -expected_stderr_2 = """ +expected_stderr_5x = """ Statement failed, SQLSTATE = 22012 arithmetic exception, numeric overflow, or string truncation -Integer divide by zero. The code attempted to divide an integer value by an integer divisor of zero. -At function 'UDR40_DIV' """ +expected_stderr_6x = """ + Statement failed, SQLSTATE = 22012 + arithmetic exception, numeric overflow, or string truncation + -Integer divide by zero. The code attempted to divide an integer value by an integer divisor of zero. + -At function "PUBLIC"."UDR40_DIV" +""" + + @pytest.mark.version('>=4.0') def test_2(act_2: Action): - act_2.expected_stderr = expected_stderr_2 + act_2.expected_stderr = expected_stderr_5x if act_2.is_version('<6') else expected_stderr_6x act_2.execute() assert act_2.clean_stderr == act_2.clean_expected_stderr diff --git a/tests/bugs/core_5887_trusted_role_test.py b/tests/bugs/core_5887_trusted_role_test.py index 2b375ab1..0419c774 100644 --- a/tests/bugs/core_5887_trusted_role_test.py +++ b/tests/bugs/core_5887_trusted_role_test.py @@ -1,151 +1,185 @@ -#coding:utf-8 - -""" -ID: issue-6145-B -ISSUE: 6145 -TITLE: Allow the use of management statements in PSQL blocks -DESCRIPTION: - Role can be set as TRUSTED when following conditions are true: - * BOTH AuthServer and AuthClient parameters from firebird.conf contain 'Win_Sspi' as plugin, in any place; - * current OS user has admin rights; - * OS environment has *no* variables ISC_USER and ISC_PASSWORD (i.e. they must be UNSET); - * Two mappings are created (both uses plugin win_sspi): - ** from any user to user; - ** from predefined_group domain_any_rid_admins to role - - Connect to database should be done in form: CONNECT ':' role ', - and after this we can user 'SET TRUSTED ROLE' statement. - - This test checks that statement 'SET TRUSTED ROLE' can be used within PSQL block rather than as DSQL. -JIRA: CORE-5887 -FBTEST: bugs.core_5887_trusted_role -NOTES: - [15.08.2022] pzotov - Checked on 5.0.0.623, 4.0.1.2692. - [04.03.2023] pzotov - Computer name must be converted to UPPERCASE, otherwise test fails. -""" - -import os -import socket -import getpass - -import pytest -from firebird.qa import * - -for v in ('ISC_USER','ISC_PASSWORD'): - try: - del os.environ[ v ] - except KeyError as e: - pass - -THIS_COMPUTER_NAME = socket.gethostname().upper() -CURRENT_WIN_ADMIN = getpass.getuser() - -db = db_factory() -act = python_act('db', substitutions=[('\t+', ' '), ('TCPv(4|6)', 'TCP')]) - -tmp_role_senior = role_factory('db', name='tmp_role_5887_senior') -tmp_role_junior = role_factory('db', name='tmp_role_5887_junior') - -@pytest.mark.version('>=4.0') -@pytest.mark.platform('Windows') -def test_1(act: Action, tmp_role_junior: Role, tmp_role_senior: Role, capsys): - - sql_init = f""" - create table test(id int); - grant select on test to role {tmp_role_senior.name}; - commit; - - -- We have to use here "create mapping trusted_auth ... from any user to user" otherwise get - -- Statement failed, SQLSTATE = 28000 /Missing security context for - -- on connect statement which specifies COMPUTERNAME:USERNAME instead path to DB: - create or alter mapping trusted_auth using plugin win_sspi from any user to user; - - -- We have to use here "create mapping win_admins ... DOMAIN_ANY_RID_ADMINS" otherwise get - -- Statement failed, SQLSTATE = 0P000 / Your attachment has no trusted role - - create or alter mapping win_admins1 using plugin win_sspi from predefined_group domain_any_rid_admins to role {tmp_role_junior.name}; - commit; - create view v_info as - select a.mon$user, a.mon$role, a.mon$remote_protocol, a.mon$auth_method from mon$attachments a where mon$attachment_id = current_connection - ; - grant select on v_info to public; - commit; - """ - act.isql(switches=['-q'], input = sql_init) - assert act.clean_stdout == '' - act.reset() - - sql_check = f""" - -- This will make connection with tole = {tmp_role_junior.name} - connect '{THIS_COMPUTER_NAME}:{act.db.db_path}'; - - set list on; - select 'point-1' as msg, v.* from v_info v; - - -- MUST FAIL because neither user nor its role has no access rights to the 'TEST' table: - select count(*) as test_rows from test; - commit; - - -- Make temporary connection as SYSDBA and change mapping from predefined_group domain_any_rid_admins - -- so that any connection can get {tmp_role_senior.name} role as trusted role: - connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; - - drop mapping win_admins1; - grant {tmp_role_junior.name} to public; - - -- Following statement means that any attachment will be granted with role - -- which, in turn was granted for SELECT from table 'test': - create or alter mapping win_admins2 using plugin win_sspi from predefined_group domain_any_rid_admins to role {tmp_role_senior.name}; - commit; - - connect '{THIS_COMPUTER_NAME}:{act.db.db_path}' role {tmp_role_junior.name.upper()}; - - select 'point-2' as msg, v.* from v_info v; - - set term ^; - execute block as - begin - -- Following statement: - -- 1) must pass without any error; - -- 2) leads to change effective role from {tmp_role_junior.name} to {tmp_role_senior.name}: - set trusted role; - end - ^ - set term ;^ - commit; - - select 'point-3' as msg, v.* from v_info v; - -- this MUST PASS because of trusted role {tmp_role_senior.name} whic has needed access rights: - select count(*) as test_rows from test; - commit; - """ - - expected_out = f""" - MSG point-1 - MON$USER {THIS_COMPUTER_NAME}\\{CURRENT_WIN_ADMIN.upper()} - MON$ROLE {tmp_role_junior.name.upper()} - MON$REMOTE_PROTOCOL TCP - MON$AUTH_METHOD Mapped from Win_Sspi - Statement failed, SQLSTATE = 28000 - no permission for SELECT access to TABLE TEST - -Effective user is {THIS_COMPUTER_NAME}\\{CURRENT_WIN_ADMIN.upper()} - - MSG point-2 - MON$USER {THIS_COMPUTER_NAME}\\{CURRENT_WIN_ADMIN.upper()} - MON$ROLE {tmp_role_junior.name.upper()} - MON$REMOTE_PROTOCOL TCP - MON$AUTH_METHOD Mapped from Win_Sspi - - MSG point-3 - MON$USER {THIS_COMPUTER_NAME}\\{CURRENT_WIN_ADMIN.upper()} - MON$ROLE {tmp_role_senior.name.upper()} - MON$REMOTE_PROTOCOL TCP - MON$AUTH_METHOD Mapped from Win_Sspi - TEST_ROWS 0 - """ - - act.expected_stdout = expected_out - act.isql(switches=['-q'], input = sql_check, connect_db=False, credentials = False, combine_output = True) - assert act.clean_stdout == act.clean_expected_stdout +#coding:utf-8 + +""" +ID: issue-6145-B +ISSUE: 6145 +TITLE: Allow the use of management statements in PSQL blocks +DESCRIPTION: + Role can be set as TRUSTED when following conditions are true: + * BOTH AuthServer and AuthClient parameters from firebird.conf contain 'Win_Sspi' as plugin, in any place; + * current OS user has admin rights; + * OS environment has *no* variables ISC_USER and ISC_PASSWORD (i.e. they must be UNSET); + * Two mappings are created (both uses plugin win_sspi): + ** from any user to user; + ** from predefined_group domain_any_rid_admins to role + + Connect to database should be done in form: CONNECT ':' role ', + and after this we can user 'SET TRUSTED ROLE' statement. + + This test checks that statement 'SET TRUSTED ROLE' can be used within PSQL block rather than as DSQL. +JIRA: CORE-5887 +FBTEST: bugs.core_5887_trusted_role +NOTES: + [15.08.2022] pzotov + Checked on 5.0.0.623, 4.0.1.2692. + [04.03.2023] pzotov + Computer name must be converted to UPPERCASE, otherwise test fails. + [02.08.2024] pzotov + One need to check for admin rights of current OS user (noted by Dimitry Sibiryakov). + Checked on Windows 6.0.0.406, 5.0.1.1469, 4.0.5.3139 + [01.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.884; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. +""" + +import os +import ctypes +import socket +import getpass + +import pytest +from firebird.qa import * + +for v in ('ISC_USER','ISC_PASSWORD'): + try: + del os.environ[ v ] + except KeyError as e: + pass + +THIS_COMPUTER_NAME = socket.gethostname().upper() +CURRENT_WIN_ADMIN = getpass.getuser() + +db = db_factory() +act = python_act('db', substitutions=[('\t+', ' '), ('TCPv(4|6)', 'TCP')]) + +tmp_role_senior = role_factory('db', name='tmp_role_5887_senior') +tmp_role_junior = role_factory('db', name='tmp_role_5887_junior') + +#---------------------------------------------------------- + +def is_admin(): + # https://serverfault.com/questions/29659/crossplatform-way-to-check-admin-rights-in-python-script + # Checked on Windows 10. + try: + is_admin = os.getuid() == 0 + except AttributeError: + is_admin = ctypes.windll.shell32.IsUserAnAdmin() + + return is_admin + +#---------------------------------------------------------- + +@pytest.mark.version('>=4.0') +@pytest.mark.platform('Windows') +def test_1(act: Action, tmp_role_junior: Role, tmp_role_senior: Role, capsys): + + if not is_admin(): + pytest.skip("Current OS user must have admin rights.") + + sql_init = f""" + set bail on; + create table test(id int); + grant select on test to role {tmp_role_senior.name}; + commit; + + -- We have to use here "create mapping trusted_auth ... from any user to user" otherwise get + -- Statement failed, SQLSTATE = 28000 /Missing security context for + -- on connect statement which specifies COMPUTERNAME:USERNAME instead path to DB: + create or alter mapping trusted_auth using plugin win_sspi from any user to user; + commit; + -- We have to use here "create mapping win_admins ... DOMAIN_ANY_RID_ADMINS" otherwise get + -- Statement failed, SQLSTATE = 0P000 / Your attachment has no trusted role + + create or alter mapping win_admins1 using plugin win_sspi from predefined_group domain_any_rid_admins to role {tmp_role_junior.name}; + + create view v_info as + select a.mon$user, a.mon$role, a.mon$remote_protocol, a.mon$auth_method from mon$attachments a where mon$attachment_id = current_connection + ; + grant select on v_info to public; + commit; + """ + + act.isql(switches=['-q'], input = sql_init, combine_output = True) + assert act.clean_stdout == '' + act.reset() + + sql_check = f""" + -- DO NOT add 'set bail' here! + -- This will make connection with tole = {tmp_role_junior.name} + connect '{THIS_COMPUTER_NAME}:{act.db.db_path}'; + + set list on; + select 'point-1' as msg, v.* from v_info v; + + -- MUST FAIL because neither user nor its role has no access rights to the 'TEST' table: + select count(*) as test_rows from test; + commit; + + -- Make temporary connection as SYSDBA and change mapping from predefined_group domain_any_rid_admins + -- so that any connection can get {tmp_role_senior.name} role as trusted role: + connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; + + drop mapping win_admins1; + grant {tmp_role_junior.name} to public; + + -- Following statement means that any attachment will be granted with role + -- which, in turn was granted for SELECT from table 'test': + create or alter mapping win_admins2 using plugin win_sspi from predefined_group domain_any_rid_admins to role {tmp_role_senior.name}; + commit; + + connect '{THIS_COMPUTER_NAME}:{act.db.db_path}' role {tmp_role_junior.name.upper()}; + + select 'point-2' as msg, v.* from v_info v; + + set bail on; + set term ^; + execute block as + begin + -- Following statement: + -- 1) must pass without any error; + -- 2) leads to change effective role from {tmp_role_junior.name} to {tmp_role_senior.name}: + -- NB: if current OS user has no admin rights then following error will raise at this point: + -- Statement failed, SQLSTATE = 0P000 + -- Your attachment has no trusted role + set trusted role; + end + ^ + set term ;^ + commit; + set bail off; + + select 'point-3' as msg, v.* from v_info v; + -- this MUST PASS because of trusted role {tmp_role_senior.name} whic has needed access rights: + select count(*) as test_rows from test; + commit; + """ + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + TABLE_NAME = 'TEST' if act.is_version('<6') else f'{SQL_SCHEMA_PREFIX}"TEST"' + expected_out = f""" + MSG point-1 + MON$USER {THIS_COMPUTER_NAME}\\{CURRENT_WIN_ADMIN.upper()} + MON$ROLE {tmp_role_junior.name.upper()} + MON$REMOTE_PROTOCOL TCP + MON$AUTH_METHOD Mapped from Win_Sspi + Statement failed, SQLSTATE = 28000 + no permission for SELECT access to TABLE {TABLE_NAME} + -Effective user is {THIS_COMPUTER_NAME}\\{CURRENT_WIN_ADMIN.upper()} + + MSG point-2 + MON$USER {THIS_COMPUTER_NAME}\\{CURRENT_WIN_ADMIN.upper()} + MON$ROLE {tmp_role_junior.name.upper()} + MON$REMOTE_PROTOCOL TCP + MON$AUTH_METHOD Mapped from Win_Sspi + + MSG point-3 + MON$USER {THIS_COMPUTER_NAME}\\{CURRENT_WIN_ADMIN.upper()} + MON$ROLE {tmp_role_senior.name.upper()} + MON$REMOTE_PROTOCOL TCP + MON$AUTH_METHOD Mapped from Win_Sspi + TEST_ROWS 0 + """ + + act.expected_stdout = expected_out + act.isql(switches=['-q'], input = sql_check, connect_db=False, credentials = False, combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5898_test.py b/tests/bugs/core_5898_test.py index 8cb7770a..5879b2df 100644 --- a/tests/bugs/core_5898_test.py +++ b/tests/bugs/core_5898_test.py @@ -99,6 +99,7 @@ WHAT_PROTOCOL_IM_USING """ +@pytest.mark.es_eds @pytest.mark.version('>=3.0.4') def test_1(act: Action, tmp_user, tmp_role): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_5905_test.py b/tests/bugs/core_5905_test.py index c95c9161..173d8b00 100644 --- a/tests/bugs/core_5905_test.py +++ b/tests/bugs/core_5905_test.py @@ -19,6 +19,11 @@ (4.x - output phrase "UDF THE_FRAC" instead of "Function THE_FRAC" on attempt to drop function). JIRA: CORE-5905 FBTEST: bugs.core_5905 +NOTES: + [02.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.889; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -113,6 +118,7 @@ def test_1(act_1: Action): assert (act_1.clean_stderr == act_1.clean_expected_stderr and act_1.clean_stdout == act_1.clean_expected_stdout) +########################################################################## # version: 4.0 test_script_2 = """ @@ -185,40 +191,57 @@ def test_1(act_1: Action): commit; """ -act_2 = isql_act('db', test_script_2) +substitutions = [('[ \t]+', ' ')] +act_2 = isql_act('db', test_script_2, substitutions = substitutions) -expected_stdout_2 = """ +expected_stdout_5x = """ THE_FRAC_1 -0.1415926535897931 - FUNC_NAME THE_FRAC LEGACY_FLAG 0 + Statement failed, SQLSTATE = 38000 + unsuccessful metadata update + -cannot delete + -Function THE_FRAC + -there are 1 dependencies + FUNC_NAME THE_FRAC LEGACY_FLAG 0 - THE_FRAC_2 7.062513305931052 - THE_FRAC_3 -0.1415926535897931 -""" -expected_stderr_2 = """ Statement failed, SQLSTATE = 38000 unsuccessful metadata update -cannot delete -Function THE_FRAC -there are 1 dependencies +""" + +expected_stdout_6x = """ + THE_FRAC_1 -0.1415926535897931 + FUNC_NAME THE_FRAC + LEGACY_FLAG 0 + + Statement failed, SQLSTATE = 38000 + unsuccessful metadata update + -cannot delete + -Function "PUBLIC"."THE_FRAC" + -there are 1 dependencies + + FUNC_NAME THE_FRAC + LEGACY_FLAG 0 + THE_FRAC_2 7.062513305931052 + THE_FRAC_3 -0.1415926535897931 Statement failed, SQLSTATE = 38000 unsuccessful metadata update -cannot delete - -Function THE_FRAC + -Function "PUBLIC"."THE_FRAC" -there are 1 dependencies """ @pytest.mark.version('>=4.0') def test_2(act_2: Action): - act_2.expected_stdout = expected_stdout_2 - act_2.expected_stderr = expected_stderr_2 - act_2.execute() - assert (act_2.clean_stderr == act_2.clean_expected_stderr and - act_2.clean_stdout == act_2.clean_expected_stdout) + act_2.expected_stdout = expected_stdout_5x if act_2.is_version('<6') else expected_stdout_6x + act_2.execute(combine_output = True) + assert act_2.clean_stdout == act_2.clean_expected_stdout diff --git a/tests/bugs/core_5907_test.py b/tests/bugs/core_5907_test.py index 0fa2091f..e3f19380 100644 --- a/tests/bugs/core_5907_test.py +++ b/tests/bugs/core_5907_test.py @@ -42,6 +42,7 @@ '}' ] +@pytest.mark.trace @pytest.mark.version('>=4.0') def test_1(act: Action, capsys): with act.trace(config=trace_conf): diff --git a/tests/bugs/core_5926_test.py b/tests/bugs/core_5926_test.py index 2a72675d..33eec9c7 100644 --- a/tests/bugs/core_5926_test.py +++ b/tests/bugs/core_5926_test.py @@ -32,6 +32,7 @@ test_script = temp_file('test_script.sql') +@pytest.mark.intl @pytest.mark.version('>=3.0.4') def test_1(act: Action, test_script: Path): if act.is_version('<4'): diff --git a/tests/bugs/core_5965_test.py b/tests/bugs/core_5965_test.py index 680e5d6d..51699621 100644 --- a/tests/bugs/core_5965_test.py +++ b/tests/bugs/core_5965_test.py @@ -5,11 +5,16 @@ ISSUE: 6219 TITLE: FB3 Optimiser chooses less efficient plan than FB2.5 optimiser DESCRIPTION: - Filling of database with data from ticket can take noticable time. - Instead of this it was decided to extract form ZIP archieve .fbk and then to restore it. - Instead of actual execution we can only obtain PLAN by querying cursor read-only property "plan" + Filling of database with data from ticket can take noticable time. + Instead of this it was decided to extract from ZIP archieve .fbk and restore it. + Instead of actual execution we can only obtain PLAN by querying cursor read-only property "plan" JIRA: CORE-5965 FBTEST: bugs.core_5965 +NOTES: + [02.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.889; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -22,11 +27,6 @@ act = python_act('db') -expected_stdout = """ - PLAN SORT (OPT_TEST INDEX (O_CLID_CUSTTY_CUSTID)) - PLAN SORT (OPT_TEST INDEX (O_CLID_CUSTTY_CUSTID)) -""" - fbk_file = temp_file('core_5965.fbk') @pytest.mark.version('>=3.0') @@ -46,7 +46,17 @@ def test_1(act: Action, fbk_file: Path, db_tmp: Database, capsys): # c2.execute("select 2 from opt_test where sysid = 1 and clid = 23 and cust_type = 1 and cust_id = 73 order by order_no desc") print(c2.statement.plan) - # Check - act.expected_stdout = expected_stdout + + expected_stdout_5x = """ + PLAN SORT (OPT_TEST INDEX (O_CLID_CUSTTY_CUSTID)) + PLAN SORT (OPT_TEST INDEX (O_CLID_CUSTTY_CUSTID)) + """ + + expected_stdout_6x = """ + PLAN SORT ("PUBLIC"."OPT_TEST" INDEX ("PUBLIC"."O_CLID_CUSTTY_CUSTID")) + PLAN SORT ("PUBLIC"."OPT_TEST" INDEX ("PUBLIC"."O_CLID_CUSTTY_CUSTID")) + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5970_test.py b/tests/bugs/core_5970_test.py index 0fe4849d..5bf592be 100644 --- a/tests/bugs/core_5970_test.py +++ b/tests/bugs/core_5970_test.py @@ -10,6 +10,10 @@ Also, it checks that -> encrypt() -> decrypt(encrypted_source) gives the same . JIRA: CORE-5970 FBTEST: bugs.core_5970 +NOTES: + [02.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' and variables - to be substituted in expected_* on FB 6.x + Checked on 6.0.0.889; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -328,1060 +332,896 @@ """ -act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) - -expected_stdout = """ - ENCRYPTION_ALGORITHM AES - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM AES - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM AES - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM AES - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM AES - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM AES - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 11 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM AES - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 11 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM AES - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 11 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM AES - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 11 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM AES - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 11 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM AES - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 11 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM AES - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 11 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM AES - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 11 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM AES - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 11 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM AES - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 11 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - - ENCRYPTION_ALGORITHM ANUBIS - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM ANUBIS - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM ANUBIS - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM ANUBIS - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM ANUBIS - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM ANUBIS - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 12 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM ANUBIS - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 12 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM ANUBIS - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 12 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM ANUBIS - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 12 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM ANUBIS - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 12 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM ANUBIS - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 13 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM ANUBIS - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 13 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM ANUBIS - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 13 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM ANUBIS - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 13 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM ANUBIS - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 13 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - - ENCRYPTION_ALGORITHM BLOWFISH - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM BLOWFISH - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM BLOWFISH - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM BLOWFISH - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM BLOWFISH - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM BLOWFISH - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 13 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM BLOWFISH - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 13 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM BLOWFISH - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 13 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM BLOWFISH - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 13 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM BLOWFISH - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 13 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM BLOWFISH - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 15 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM BLOWFISH - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 15 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM BLOWFISH - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 15 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM BLOWFISH - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 15 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM BLOWFISH - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 15 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - - ENCRYPTION_ALGORITHM KHAZAD - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM KHAZAD - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM KHAZAD - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM KHAZAD - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM KHAZAD - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM KHAZAD - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 14 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM KHAZAD - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 14 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM KHAZAD - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 14 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM KHAZAD - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 14 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM KHAZAD - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 14 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM KHAZAD - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 17 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM KHAZAD - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 17 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM KHAZAD - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 17 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM KHAZAD - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 17 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM KHAZAD - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 17 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - - ENCRYPTION_ALGORITHM RC5 - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM RC5 - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM RC5 - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM RC5 - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM RC5 - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM RC5 - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 15 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM RC5 - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 15 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM RC5 - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 15 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM RC5 - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 15 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM RC5 - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 15 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM RC5 - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 19 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM RC5 - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 19 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM RC5 - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 19 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM RC5 - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 19 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM RC5 - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 19 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - - ENCRYPTION_ALGORITHM RC6 - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM RC6 - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM RC6 - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM RC6 - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM RC6 - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM RC6 - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 17 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM RC6 - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 17 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM RC6 - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 17 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM RC6 - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 17 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM RC6 - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 17 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM RC6 - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 21 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM RC6 - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 21 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM RC6 - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 21 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM RC6 - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 21 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM RC6 - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 21 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - - ENCRYPTION_ALGORITHM "SAFER+" - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM "SAFER+" - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM "SAFER+" - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM "SAFER+" - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM "SAFER+" - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM "SAFER+" - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 18 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM "SAFER+" - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 18 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM "SAFER+" - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 18 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM "SAFER+" - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 18 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM "SAFER+" - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 18 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM "SAFER+" - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 23 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM "SAFER+" - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 23 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM "SAFER+" - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 23 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM "SAFER+" - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 23 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM "SAFER+" - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 23 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - - ENCRYPTION_ALGORITHM TWOFISH - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM TWOFISH - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM TWOFISH - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM TWOFISH - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM TWOFISH - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM TWOFISH - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 19 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM TWOFISH - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 19 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM TWOFISH - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 19 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM TWOFISH - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 19 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM TWOFISH - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 19 - ENC_INIT_VECTOR_OCTET_LENGTH 16 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM TWOFISH - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 25 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM TWOFISH - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 25 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM TWOFISH - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 25 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM TWOFISH - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 25 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM TWOFISH - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 25 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - - ENCRYPTION_ALGORITHM XTEA - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM XTEA - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM XTEA - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM XTEA - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM XTEA - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM XTEA - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 20 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM XTEA - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 20 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM XTEA - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 20 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM XTEA - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 20 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM XTEA - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 20 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM XTEA - ENCRYPTION_MODE CBC - ENC_KEY_OCTET_LENGTH 26 - ENC_INIT_VECTOR_OCTET_LENGTH 27 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM XTEA - ENCRYPTION_MODE CFB - ENC_KEY_OCTET_LENGTH 26 - ENC_INIT_VECTOR_OCTET_LENGTH 27 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM XTEA - ENCRYPTION_MODE CTR - ENC_KEY_OCTET_LENGTH 26 - ENC_INIT_VECTOR_OCTET_LENGTH 27 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - ENCRYPTION_ALGORITHM XTEA - ENCRYPTION_MODE ECB - ENC_KEY_OCTET_LENGTH 26 - ENC_INIT_VECTOR_OCTET_LENGTH 27 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545224 - - ENCRYPTION_ALGORITHM XTEA - ENCRYPTION_MODE OFB - ENC_KEY_OCTET_LENGTH 26 - ENC_INIT_VECTOR_OCTET_LENGTH 27 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545229 - - - ENCRYPTION_ALGORITHM CHACHA20 - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM CHACHA20 - ENC_KEY_OCTET_LENGTH 21 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545250 - - ENCRYPTION_ALGORITHM CHACHA20 - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 29 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545240 - - - ENCRYPTION_ALGORITHM RC4 - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 0 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM RC4 - ENC_KEY_OCTET_LENGTH 22 - ENC_INIT_VECTOR_OCTET_LENGTH 0 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM RC4 - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 0 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - - ENCRYPTION_ALGORITHM SOBER128 - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE - - ENCRYPTION_ALGORITHM SOBER128 - ENC_KEY_OCTET_LENGTH 23 - ENC_INIT_VECTOR_OCTET_LENGTH 8 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - ENCRYPTION_ALGORITHM SOBER128 - ENC_KEY_OCTET_LENGTH 16 - ENC_INIT_VECTOR_OCTET_LENGTH 33 - ENCRYPTED_EQUALS_TO_DECRYPTED - ENCRYPTION_FINISH_GDSCODE 335545230 - - - CTR_CLAUSE_CASE_2 E813A50C069FC418AA - CTR_CLAUSE_CASE_3 E813A50C069FC418AA - ENCRYPT 8E709DDA89912F172C - ENCRYPT BC3604C147B53D3BDD - ENCRYPT C8051FB1A2581EA9A1 - ENCRYPT 2E2298CF4C2B81AD54 - - INPUT message field count: 0 - - OUTPUT message field count: 3 - 01: sqltype: 520 BLOB scale: 0 subtype: 0 len: 8 - : name: ENCRYPT alias: E_BLOB - : table: owner: - 02: sqltype: 448 VARYING scale: 0 subtype: 0 len: 255 charset: 1 OCTETS - : name: ENCRYPT alias: E_CHAR - : table: owner: - 03: sqltype: 448 VARYING scale: 0 subtype: 0 len: 6 charset: 1 OCTETS - : name: DECRYPT alias: D_BIN - : table: owner: - -""" - -expected_stderr = """ - Statement failed, SQLSTATE = 22023 - Too big counter value -123, maximum 16 can be used - - Statement failed, SQLSTATE = 22023 - Too big counter value 123, maximum 16 can be used - - Statement failed, SQLSTATE = 22023 - Counter length/value parameter is not used with mode OFB - - Statement failed, SQLSTATE = 22023 - Invalid key length 9, need 16 or 32 -""" +substitutions = [ ('table:.*', '') ] # [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) @pytest.mark.version('>=4.0') def test_1(act: Action): + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'SYSTEM.' + expected_stdout = f""" + ENCRYPTION_ALGORITHM AES + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM AES + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM AES + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM AES + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM AES + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM AES + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 11 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM AES + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 11 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM AES + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 11 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM AES + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 11 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM AES + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 11 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM AES + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 11 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM AES + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 11 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM AES + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 11 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM AES + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 11 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM AES + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 11 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM ANUBIS + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM ANUBIS + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM ANUBIS + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM ANUBIS + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM ANUBIS + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM ANUBIS + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 12 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM ANUBIS + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 12 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM ANUBIS + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 12 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM ANUBIS + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 12 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM ANUBIS + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 12 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM ANUBIS + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 13 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM ANUBIS + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 13 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM ANUBIS + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 13 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM ANUBIS + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 13 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM ANUBIS + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 13 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM BLOWFISH + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM BLOWFISH + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM BLOWFISH + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM BLOWFISH + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM BLOWFISH + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM BLOWFISH + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 13 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM BLOWFISH + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 13 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM BLOWFISH + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 13 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM BLOWFISH + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 13 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM BLOWFISH + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 13 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM BLOWFISH + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 15 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM BLOWFISH + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 15 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM BLOWFISH + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 15 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM BLOWFISH + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 15 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM BLOWFISH + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 15 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM KHAZAD + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM KHAZAD + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM KHAZAD + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM KHAZAD + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM KHAZAD + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM KHAZAD + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 14 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM KHAZAD + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 14 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM KHAZAD + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 14 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM KHAZAD + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 14 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM KHAZAD + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 14 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM KHAZAD + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 17 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM KHAZAD + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 17 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM KHAZAD + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 17 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM KHAZAD + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 17 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM KHAZAD + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 17 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM RC5 + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM RC5 + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM RC5 + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM RC5 + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM RC5 + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM RC5 + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 15 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM RC5 + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 15 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM RC5 + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 15 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM RC5 + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 15 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM RC5 + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 15 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM RC5 + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 19 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM RC5 + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 19 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM RC5 + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 19 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM RC5 + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 19 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM RC5 + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 19 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM RC6 + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM RC6 + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM RC6 + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM RC6 + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM RC6 + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM RC6 + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 17 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM RC6 + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 17 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM RC6 + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 17 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM RC6 + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 17 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM RC6 + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 17 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM RC6 + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 21 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM RC6 + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 21 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM RC6 + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 21 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM RC6 + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 21 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM RC6 + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 21 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM "SAFER+" + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM "SAFER+" + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM "SAFER+" + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM "SAFER+" + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM "SAFER+" + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM "SAFER+" + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 18 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM "SAFER+" + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 18 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM "SAFER+" + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 18 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM "SAFER+" + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 18 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM "SAFER+" + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 18 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM "SAFER+" + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 23 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM "SAFER+" + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 23 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM "SAFER+" + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 23 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM "SAFER+" + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 23 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM "SAFER+" + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 23 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM TWOFISH + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM TWOFISH + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM TWOFISH + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM TWOFISH + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM TWOFISH + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM TWOFISH + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 19 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM TWOFISH + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 19 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM TWOFISH + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 19 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM TWOFISH + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 19 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM TWOFISH + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 19 + ENC_INIT_VECTOR_OCTET_LENGTH 16 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM TWOFISH + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 25 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM TWOFISH + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 25 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM TWOFISH + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 25 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM TWOFISH + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 25 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM TWOFISH + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 25 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM XTEA + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM XTEA + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM XTEA + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM XTEA + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM XTEA + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM XTEA + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 20 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM XTEA + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 20 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM XTEA + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 20 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM XTEA + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 20 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM XTEA + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 20 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM XTEA + ENCRYPTION_MODE CBC + ENC_KEY_OCTET_LENGTH 26 + ENC_INIT_VECTOR_OCTET_LENGTH 27 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM XTEA + ENCRYPTION_MODE CFB + ENC_KEY_OCTET_LENGTH 26 + ENC_INIT_VECTOR_OCTET_LENGTH 27 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM XTEA + ENCRYPTION_MODE CTR + ENC_KEY_OCTET_LENGTH 26 + ENC_INIT_VECTOR_OCTET_LENGTH 27 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM XTEA + ENCRYPTION_MODE ECB + ENC_KEY_OCTET_LENGTH 26 + ENC_INIT_VECTOR_OCTET_LENGTH 27 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545224 + ENCRYPTION_ALGORITHM XTEA + ENCRYPTION_MODE OFB + ENC_KEY_OCTET_LENGTH 26 + ENC_INIT_VECTOR_OCTET_LENGTH 27 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545229 + ENCRYPTION_ALGORITHM CHACHA20 + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM CHACHA20 + ENC_KEY_OCTET_LENGTH 21 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545250 + ENCRYPTION_ALGORITHM CHACHA20 + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 29 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545240 + ENCRYPTION_ALGORITHM RC4 + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 0 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM RC4 + ENC_KEY_OCTET_LENGTH 22 + ENC_INIT_VECTOR_OCTET_LENGTH 0 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM RC4 + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 0 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM SOBER128 + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE + ENCRYPTION_ALGORITHM SOBER128 + ENC_KEY_OCTET_LENGTH 23 + ENC_INIT_VECTOR_OCTET_LENGTH 8 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + ENCRYPTION_ALGORITHM SOBER128 + ENC_KEY_OCTET_LENGTH 16 + ENC_INIT_VECTOR_OCTET_LENGTH 33 + ENCRYPTED_EQUALS_TO_DECRYPTED + ENCRYPTION_FINISH_GDSCODE 335545230 + Statement failed, SQLSTATE = 22023 + Too big counter value -123, maximum 16 can be used + CTR_CLAUSE_CASE_2 E813A50C069FC418AA + CTR_CLAUSE_CASE_3 E813A50C069FC418AA + Statement failed, SQLSTATE = 22023 + Too big counter value 123, maximum 16 can be used + Statement failed, SQLSTATE = 22023 + Counter length/value parameter is not used with mode OFB + ENCRYPT 8E709DDA89912F172C + ENCRYPT BC3604C147B53D3BDD + ENCRYPT C8051FB1A2581EA9A1 + ENCRYPT 2E2298CF4C2B81AD54 + Statement failed, SQLSTATE = 22023 + Invalid key length 9, need 16 or 32 + INPUT message field count: 0 + OUTPUT message field count: 3 + 01: sqltype: 520 BLOB scale: 0 subtype: 0 len: 8 + : name: ENCRYPT alias: E_BLOB + : table: owner: + 02: sqltype: 448 VARYING scale: 0 subtype: 0 len: 255 charset: 1 {SQL_SCHEMA_PREFIX}OCTETS + : name: ENCRYPT alias: E_CHAR + : table: owner: + 03: sqltype: 448 VARYING scale: 0 subtype: 0 len: 6 charset: 1 {SQL_SCHEMA_PREFIX}OCTETS + : name: DECRYPT alias: D_BIN + : table: owner: + """ + act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5972_test.py b/tests/bugs/core_5972_test.py index 29bebb97..27bc9def 100644 --- a/tests/bugs/core_5972_test.py +++ b/tests/bugs/core_5972_test.py @@ -21,28 +21,17 @@ 335544382 : COMP 336397208 : At line 1, column 57 Statement : insert into "PERSONS" ("ID", "NAME", "ADDRESS", "INFO", "COMP") values (?, ?, ?, ?, ?) - Data source : Firebird::C:\\FBTESTING\\qa\\misc\\tmprepl.fdb + Data source : Firebird:: -At block line: ... -At trigger 'PERSONS_REPLICATE' We expect appearing of this exception (see try/except block): check its class and content of message. -NOTES: - [08.02.2022] pcisar - Fails on Windows 3.0.8 due to malformed error message: - Got exception: - + Execute statement error at isc_dsql_prepare :335544359 : attempted update of read-only column - - Execute statement error at isc_dsql_prepare : - - 335544359 : attempted update of read-only column - Statement - - Data source - -At block line: 9, col: 5 - -At trigger 'PERSONS_REPLICATE' - [08.04.2022] pzotov - CAN NOT REPRODUCE FAIL! - Test PASSES on FB 3.0.8 Rls, 4.0.1 RLs and 5.0.0.467. - JIRA: CORE-5972 FBTEST: bugs.core_5972 +NOTES: + [02.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' and variables - to be substituted in expected_* on FB 6.x + Checked on 6.0.0.889; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -50,10 +39,11 @@ from firebird.qa import * from firebird.driver import DatabaseError -substitutions = [('[ \t]+', ' '), ('.* At block line.*', 'At block'), - ('read-only column.*', 'read-only column'), - ('Statement.*', 'Statement'), ('Data source.*', 'Data source'), - ('.* At trigger.*', 'At trigger')] +substitutions = [ ('[ \t]+', ' '), + ('(-)?At block line.*', 'At block'), + ('read-only column.*', 'read-only column'), + ('Statement.*', 'Statement'), ('Data source.*', 'Data source'), + ] init_script = """ create table persons ( @@ -70,16 +60,6 @@ act = python_act('db', substitutions=substitutions) -expected_stdout = """ - Got exception: - Execute statement error at isc_dsql_prepare : - 335544359 : attempted update of read-only column PERSONS.COMP - Statement : insert into "PERSONS" ("ID", "NAME", "ADDRESS", "INFO", "COMP") values (?, ?, ?, ?, ?) - Data source : Firebird::C:\\FBTESTING\\qa\\fbt-repo\\tmp\\tmp_5972_repl.fdb - -At block line: 9, col: 5 - -At trigger 'PERSONS_REPLICATE' -""" - @pytest.mark.version('>=3.0.6') def test_1(act: Action, db_repl: Database, capsys): ddl_for_replication = f""" @@ -118,6 +98,20 @@ def test_1(act: Action, db_repl: Database, capsys): act.isql(switches=['-q'], input='ALTER EXTERNAL CONNECTIONS POOL CLEAR ALL;') # act.reset() + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + TRIGGER_NAME = "'PERSONS_REPLICATE'" if act.is_version('<6') else '"PERSONS_REPLICATE"' + + expected_stdout = f""" + Got exception: + Execute statement error at isc_dsql_prepare : + 335544359 : attempted update of read-only column PERSONS.COMP + Statement : insert into "PERSONS" ("ID", "NAME", "ADDRESS", "INFO", "COMP") values (?, ?, ?, ?, ?) + Data source : Firebird:: + -At block line: 9, col: 5 + -At trigger {SQL_SCHEMA_PREFIX}{TRIGGER_NAME} + """ + act.expected_stdout = expected_stdout act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5973_test.py b/tests/bugs/core_5973_test.py index 844e4be5..f6e93530 100644 --- a/tests/bugs/core_5973_test.py +++ b/tests/bugs/core_5973_test.py @@ -9,7 +9,11 @@ [05.03.2021] added subst.: max. floating point precision on Linux is 15 rather than on Windows (16 digits). JIRA: CORE-5973 -FBTEST: bugs.core_5973 +NOTES: + [24.05.2025] pzotov + Splitted expected* variables for versions up to 5.x and 6.x+ + This is needed after 11d5d5 ("Fix for #8082 ... user buffers directly (#8145)") by Dmitry Sibiryakov. + Discussed in email 24.05.2025 22:06, subj: "one more consequence of 11d5d5 ..." (since 15.05.2025 17:25). """ import pytest @@ -66,14 +70,7 @@ act = isql_act('db', test_script, substitutions=[('0.0000000000000000', '0.000000000000000')]) -expected_stdout = """ - GREATEST_DF34_FOR_POS_SCOPE Infinity - GREATEST_DF34_FOR_NEG_SCOPE -Infinity - APPROX_ZERO_DF34_FOR_POS_SCOPE 0.0000000000000000 - APPROX_ZERO_DF34_FOR_NEG_SCOPE 0.0000000000000000 -""" - -expected_stderr = """ +expected_out_5x = """ Statement failed, SQLSTATE = 22003 Dynamic SQL Error -SQL error code = -303 @@ -93,12 +90,34 @@ Dynamic SQL Error -SQL error code = -303 -Floating-point underflow. The exponent of a floating-point operation is less than the magnitude allowed. + + GREATEST_DF34_FOR_POS_SCOPE Infinity + GREATEST_DF34_FOR_NEG_SCOPE -Infinity + APPROX_ZERO_DF34_FOR_POS_SCOPE 0.000000000000000 + APPROX_ZERO_DF34_FOR_NEG_SCOPE 0.000000000000000 +""" + +expected_out_6x = """ + Statement failed, SQLSTATE = 22003 + Floating-point overflow. The exponent of a floating-point operation is greater than the magnitude allowed. + + Statement failed, SQLSTATE = 22003 + Floating-point overflow. The exponent of a floating-point operation is greater than the magnitude allowed. + + Statement failed, SQLSTATE = 22003 + Floating-point underflow. The exponent of a floating-point operation is less than the magnitude allowed. + + Statement failed, SQLSTATE = 22003 + Floating-point underflow. The exponent of a floating-point operation is less than the magnitude allowed. + + GREATEST_DF34_FOR_POS_SCOPE Infinity + GREATEST_DF34_FOR_NEG_SCOPE -Infinity + APPROX_ZERO_DF34_FOR_POS_SCOPE 0.000000000000000 + APPROX_ZERO_DF34_FOR_NEG_SCOPE 0.000000000000000 """ @pytest.mark.version('>=4.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + act.expected_stdout = expected_out_5x if act.is_version('<6') else expected_out_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_5985_test.py b/tests/bugs/core_5985_test.py index 122bfe47..e9056066 100644 --- a/tests/bugs/core_5985_test.py +++ b/tests/bugs/core_5985_test.py @@ -85,6 +85,7 @@ WHATS_MY_ROLE WORKER """ +@pytest.mark.es_eds @pytest.mark.version('>=3.0') def test_1(act: Action, user_foo, user_bar): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_5990_test.py b/tests/bugs/core_5990_test.py index e70a35bc..0d88ff3a 100644 --- a/tests/bugs/core_5990_test.py +++ b/tests/bugs/core_5990_test.py @@ -165,6 +165,7 @@ DUP_CNT 5 """ +@pytest.mark.es_eds @pytest.mark.version('>=4.0') def test_1(act: Action): if int(act.get_config('ExtConnPoolSize')) < 6 or int(act.get_config('ExtConnPoolLifeTime')) < 10: diff --git a/tests/bugs/core_5991_test.py b/tests/bugs/core_5991_test.py index fc8719ca..19cf7836 100644 --- a/tests/bugs/core_5991_test.py +++ b/tests/bugs/core_5991_test.py @@ -95,6 +95,7 @@ '}' ] +@pytest.mark.trace @pytest.mark.version('>=3.0.5') def test_1(act: Action, capsys): trace.insert(0, f"database = '{act.db.db_path}'") diff --git a/tests/bugs/core_5995_test.py b/tests/bugs/core_5995_test.py index c157c1cb..af374076 100644 --- a/tests/bugs/core_5995_test.py +++ b/tests/bugs/core_5995_test.py @@ -25,6 +25,7 @@ 'log_statement_finish = true', ] +@pytest.mark.trace @pytest.mark.version('>=3.0.5') def test_1(act: Action): with act.trace(db_events=trace), act.connect_server() as srv: diff --git a/tests/bugs/core_6015_test.py b/tests/bugs/core_6015_test.py index e6e19f79..c6b906dc 100644 --- a/tests/bugs/core_6015_test.py +++ b/tests/bugs/core_6015_test.py @@ -26,6 +26,11 @@ Also, error messages differ because CORE-5606 was not backported to FB 3.x. JIRA: CORE-6015 FBTEST: bugs.core_6015 +NOTES: + [02.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.889; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813 """ import pytest @@ -107,43 +112,36 @@ act = isql_act('db', test_script, substitutions=[('(-)?At procedure .*', '')]) -# version: 3.0.8 - -expected_stderr_1 = """ +expected_stdout_3x = """ Statement failed, SQLSTATE = 40001 lock conflict on no wait transaction - -At procedure 'SP_EVAL_STATIC_PSQL' line: 3, col: 8 - At procedure 'SP_EVAL_STATIC_PSQL' line: 3, col: 8 - + Statement failed, SQLSTATE = 40001 Attempt to evaluate index expression recursively - -At procedure 'SP_EVAL_DYNAMIC_SQL' line: 3, col: 8 -lock conflict on no wait transaction """ -@pytest.mark.version('>=3.0.8,<4.0') -def test_1(act: Action): - act.expected_stderr = expected_stderr_1 - act.execute() - assert act.clean_stderr == act.clean_expected_stderr - -# version: 4.0 - -expected_stderr_2 = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 42000 Expression evaluation error for index "TEST_STATIC_PSQL_EVAL" on table "TEST_STATIC_PSQL" -Attempt to evaluate index expression recursively - -At procedure 'SP_EVAL_STATIC_PSQL' line: 3, col: 8 - At procedure 'SP_EVAL_STATIC_PSQL' line: 3, col: 8 - Statement failed, SQLSTATE = 42000 Expression evaluation error for index "TEST_DYNAMIC_SQL_EVAL" on table "TEST_DYNAMIC_SQL" -Attempt to evaluate index expression recursively - -At procedure 'SP_EVAL_DYNAMIC_SQL' line: 3, col: 8 """ -@pytest.mark.version('>=4.0') +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + Expression evaluation error for index "PUBLIC"."TEST_STATIC_PSQL_EVAL" on table "PUBLIC"."TEST_STATIC_PSQL" + -Attempt to evaluate index expression recursively + Statement failed, SQLSTATE = 42000 + Expression evaluation error for index "PUBLIC"."TEST_DYNAMIC_SQL_EVAL" on table "PUBLIC"."TEST_DYNAMIC_SQL" + -Attempt to evaluate index expression recursively +""" + + +@pytest.mark.version('>=3.0') def test_2(act: Action): - act.expected_stderr = expected_stderr_2 - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + act.expected_stdout = expected_stdout_3x if act.is_version('<4') else expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_6024_test.py b/tests/bugs/core_6024_test.py index 2d63363f..1aeeec65 100644 --- a/tests/bugs/core_6024_test.py +++ b/tests/bugs/core_6024_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-6024 FBTEST: bugs.core_6024 +NOTES: + [02.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.889; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813 """ import pytest @@ -81,13 +86,19 @@ act = isql_act('db', test_script) -expected_stdout = """ +expected_stdout_5x = """ PLAN SORT (W INDEX (IXA_WPLATA__KONTRAHENT__PK)) PLAN (W ORDER PK_WPLATA INDEX (IXA_WPLATA__KONTRAHENT__PK)) """ +expected_stdout_6x = """ + PLAN SORT ("W" INDEX ("PUBLIC"."IXA_WPLATA__KONTRAHENT__PK")) + PLAN ("W" ORDER "PUBLIC"."PK_WPLATA" INDEX ("PUBLIC"."IXA_WPLATA__KONTRAHENT__PK")) +""" + @pytest.mark.version('>=3.0.5') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_6038_test.py b/tests/bugs/core_6038_test.py index df83a78b..aa048483 100644 --- a/tests/bugs/core_6038_test.py +++ b/tests/bugs/core_6038_test.py @@ -4,6 +4,7 @@ ID: issue-6288 ISSUE: 6288 TITLE: Srp user manager sporadically creates users which can not attach +DESCRIPTION: Explanation of bug nature was provided by Alex, see letter 05-jun-19 13:51. Some iteration failed with probability equal to occurence of 0 (zero) in the highest BYTE of some number. Byte is 8 bit ==> this probability is 1/256. @@ -12,12 +13,10 @@ Because of time (speed) it was decided to run only 256 iterations. If bug will be 'raised' somewhere then this number is enough to catch it after 2-3 times of test run. - +NOTES: Reproduced on WI-V3.0.5.33118, date: 11-apr-19 (got fails not late than on 250th iteration). Works fine on WI-V3.0.5.33139, date: 04-apr-19. -NOTES: A new bug was found during this test implementation, affected 4.0 Classic only: CORE-6080. -DESCRIPTION: JIRA: CORE-6038 FBTEST: bugs.core_6038 """ diff --git a/tests/bugs/core_6043_test.py b/tests/bugs/core_6043_test.py index f592875a..bece765d 100644 --- a/tests/bugs/core_6043_test.py +++ b/tests/bugs/core_6043_test.py @@ -76,7 +76,7 @@ def test_1(act: Action, capsys): # tmp_fdb = Path( act.vars['sample_dir'], 'qa', fname_in_dbconf ) - # PermissionError: [Errno 13] Permission denied --> probably because + # Permiss. Error: [Errno 13] Permiss. denied --> probably because # Firebird was started by root rather than current (non-privileged) user. # tmp_fdb.write_bytes(act.db.db_path.read_bytes()) diff --git a/tests/bugs/core_6044_test.py b/tests/bugs/core_6044_test.py index 2233bda5..8d917380 100644 --- a/tests/bugs/core_6044_test.py +++ b/tests/bugs/core_6044_test.py @@ -7,13 +7,19 @@ DESCRIPTION: JIRA: CORE-6044 FBTEST: bugs.core_6044 +NOTES: + [02.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.889; 5.0.3.1668; 4.0.6.3214 """ import pytest from firebird.qa import * substitutions = [ ('current value.*', 'current value') - ,('COLL-VERSION=\\d+.\\d+(;ICU-VERSION=\\d+.\\d+)?.*', '') + ,("'COLL-VERSION.*", "''") + #,('COLL-VERSION=\\d+.\\d+(;ICU-VERSION=\\d+.\\d+)?.*', '') ] db = db_factory(charset='UTF8') @@ -57,14 +63,16 @@ """ expected_stdout_6x = """ - ДоменДляХраненияСтроковыхДанныхКоторыеПредставимыДляСортировкии - ИсключениеДляСообщенияПользователюОНевозможностиПреобразованияя; Msg: Ваша строка не может быть преобразована в число. - КоллацияДляСортировкиСтроковыхДанныхКоторыеПредставимыКакЧислаа, CHARACTER SET UTF8, FROM EXTERNAL ('UNICODE'), PAD SPACE, CASE INSENSITIVE, ' - ТаблицаКотораяВсегдаДолжнаСодержатьТолькоСамуюСвежуюИнформациюю - Generator ГенераторКоторыйДолженСодержатьНомераПоследнихУдаленнДокументов, current value - СтолбецКоторыйВсегдаДолжнаСодержатьТолькоСамуюСвежуюИнформациюю (ДоменДляХраненияСтроковыхДанныхКоторыеПредставимыДляСортировкии) VARCHAR(160) CHARACTER SET UTF8 COLLATE КоллацияДляСортировкиСтроковыхДанныхКоторыеПредставимыКакЧислаа Not Null - CONSTRAINT ПервичныйКлючНаТаблицуКотораяВсегдаДолжнаСодержатьСвежайшуюИнфу: - Primary key (СтолбецКоторыйВсегдаДолжнаСодержатьТолькоСамуюСвежуюИнформациюю) + PUBLIC."ДоменДляХраненияСтроковыхДанныхКоторыеПредставимыДляСортировкии" + PUBLIC."ИсключениеДляСообщенияПользователюОНевозможностиПреобразованияя"; Msg: Ваша строка не может быть преобразована в число. + PUBLIC."КоллацияДляСортировкиСтроковыхДанныхКоторыеПредставимыКакЧислаа", CHARACTER SET SYSTEM.UTF8, FROM EXTERNAL ('UNICODE'), PAD SPACE, CASE INSENSITIVE, '' + PUBLIC."ТаблицаКотораяВсегдаДолжнаСодержатьТолькоСамуюСвежуюИнформациюю" + Generator PUBLIC."ГенераторКоторыйДолженСодержатьНомераПоследнихУдаленнДокументов", current value + Table: PUBLIC."ТаблицаКотораяВсегдаДолжнаСодержатьТолькоСамуюСвежуюИнформациюю" + "СтолбецКоторыйВсегдаДолжнаСодержатьТолькоСамуюСвежуюИнформациюю" (PUBLIC."ДоменДляХраненияСтроковыхДанныхКоторыеПредставимыДляСортировкии") VARCHAR(160) CHARACTER SET SYSTEM.UTF8 COLLATE PUBLIC."КоллацияДляСортировкиСтроковыхДанныхКоторыеПредставимыКакЧислаа" Not Null + CONSTRAINT "ПервичныйКлючНаТаблицуКотораяВсегдаДолжнаСодержатьСвежайшуюИнфу": + Primary key ("СтолбецКоторыйВсегдаДолжнаСодержатьТолькоСамуюСвежуюИнформациюю") + """ @pytest.mark.version('>=4.0') diff --git a/tests/bugs/core_6048_test.py b/tests/bugs/core_6048_test.py index b83265e6..e0acd313 100644 --- a/tests/bugs/core_6048_test.py +++ b/tests/bugs/core_6048_test.py @@ -33,6 +33,13 @@ ::: NB-2 ::: Careful tuning required on each tesing box for this test. + + [18.01.2025] pzotov + Resultset of cursor that executes using instance of selectable PreparedStatement must be stored + in some variable in order to have ability close it EXPLICITLY (before PS will be freed). + Otherwise access violation raises during Python GC and pytest hangs at final point (does not return control to OS). + This occurs at least for: Python 3.11.2 / pytest: 7.4.4 / firebird.driver: 1.10.6 / Firebird.Qa: 0.19.3 + The reason of that was explained by Vlad, 26.10.24 17:42 ("oddities when use instances of selective statements"). """ import os @@ -125,39 +132,59 @@ def test_1(act: Action, capsys): print( e.__str__() ) cur2 = con2.cursor() - ps = cur2.prepare('select mon$crypt_page, mon$crypt_state from mon$database') - - # This will store different number of pages which are currently encrypted. - # When length of this set will exceed ENCRYPTING_PAGES_MIN_CNT then we break from loop: - # - encrypting_pages_set = set() - waiting_in_loop = -1 - while encryption_started: - t2=py_dt.datetime.now() - d1=t2-t1 - waiting_in_loop = d1.seconds*1000 + d1.microseconds//1000 - if waiting_in_loop > MAX_WAITING_ENCR_FINISH: - print(f'TIMEOUT EXPIRATION: encryption took {d1.seconds*1000 + d1.microseconds//1000} ms which exceeds limit = {MAX_WAITING_ENCR_FINISH} ms.') - break - - cur2.execute(ps) - crypt_page, crypt_state = cur2.fetchone() - con2.commit() - - # 0 = non crypted; - # 1 = has been encrypted; - # 2 = is DEcrypting; - # 3 = is Encrypting; - if crypt_state == RUNNING_ENCRYPTING_STATE: - encrypting_pages_set.add(crypt_page,) - - if crypt_state == COMPLETED_ENCRYPTION_STATE: - encryption_finished = True - break - elif len(encrypting_pages_set) > ENCRYPTING_PAGES_MIN_CNT: - break - else: - time.sleep(0.5) + ps, rs = None, None + + try: + ps = cur2.prepare('select mon$crypt_page, mon$crypt_state from mon$database') + + # This will store different number of pages which are currently encrypted. + # When length of this set will exceed ENCRYPTING_PAGES_MIN_CNT then we break from loop: + # + encrypting_pages_set = set() + waiting_in_loop = -1 + while encryption_started: + t2=py_dt.datetime.now() + d1=t2-t1 + waiting_in_loop = d1.seconds*1000 + d1.microseconds//1000 + if waiting_in_loop > MAX_WAITING_ENCR_FINISH: + print(f'TIMEOUT EXPIRATION: encryption took {d1.seconds*1000 + d1.microseconds//1000} ms which exceeds limit = {MAX_WAITING_ENCR_FINISH} ms.') + break + + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + rs = cur2.execute(ps) + for r in rs: + crypt_page, crypt_state = r[:2] + + con2.commit() + + # 0 = non crypted; + # 1 = has been encrypted; + # 2 = is DEcrypting; + # 3 = is Encrypting; + if crypt_state == RUNNING_ENCRYPTING_STATE: + encrypting_pages_set.add(crypt_page,) + + if crypt_state == COMPLETED_ENCRYPTION_STATE: + encryption_finished = True + break + elif len(encrypting_pages_set) > ENCRYPTING_PAGES_MIN_CNT: + break + else: + time.sleep(0.1) + + except DatabaseError as e: + print( e.__str__() ) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() # --------------------------------------------------------- diff --git a/tests/bugs/core_6049_test.py b/tests/bugs/core_6049_test.py index 2768f63f..c76444dc 100644 --- a/tests/bugs/core_6049_test.py +++ b/tests/bugs/core_6049_test.py @@ -5,11 +5,11 @@ ISSUE: 6299 TITLE: Builtin functions converting binary string to hexadecimal representation and vice versa DESCRIPTION: - Test may need to be more complex. Currently only basic operations are checked: - * ability to insert into binary field result of hex_decode() - * result of double conversion: bin_data -> base64_encode -> base64_decode - - must be equal to initial bin_data (and similar for bin_data -> hex_encode -> hex_decode) - We get columns type details using sqlda_display in order to fix them in expected_stdout. + Test may need to be more complex. Currently only basic operations are checked: + * ability to insert into binary field result of hex_decode() + * result of double conversion: bin_data -> base64_encode -> base64_decode + - must be equal to initial bin_data (and similar for bin_data -> hex_encode -> hex_decode) + We get columns type details using sqlda_display in order to fix them in expected_stdout. JIRA: CORE-6049 FBTEST: bugs.core_6049 NOTES: @@ -25,6 +25,9 @@ Added 'SQLSTATE' in substitutions: runtime error must not be filtered out by '?!(...)' pattern ("negative lookahead assertion", see https://docs.python.org/3/library/re.html#regular-expression-syntax). Added 'combine_output = True' in order to see SQLSTATE if any error occurs. + [02.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.889; 5.0.3.1668; 4.0.6.3214 """ import pytest @@ -70,26 +73,28 @@ hex_dec(hex_enc(uid)) result """ -expected_stdout = f""" - 01: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 20 charset: 1 OCTETS - : name: UID alias: UID - 02: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 28 charset: 2 ASCII - : name: BASE64_ENCODE alias: b64_encode(uid) - 03: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 21 charset: 1 OCTETS - : name: BASE64_DECODE alias: b64_decode(b64_encode(uid)) - 04: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 40 charset: 2 ASCII - : name: HEX_ENCODE alias: hex_encode(uid) - 05: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 20 charset: 1 OCTETS - : name: HEX_DECODE alias: hex_decode(hex_encode(uid)) - 06: sqltype: 32764 BOOLEAN Nullable scale: 0 subtype: 0 len: 1 - : name: alias: b64_dec(b64_enc(uid)) result - 07: sqltype: 32764 BOOLEAN Nullable scale: 0 subtype: 0 len: 1 - : name: alias: hex_dec(hex_enc(uid)) result - {COMMON_OUTPUT} -""" - @pytest.mark.version('>=4.0') def test_1(act: Action): + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'SYSTEM.' + expected_stdout = f""" + 01: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 20 charset: 1 {SQL_SCHEMA_PREFIX}OCTETS + : name: UID alias: UID + 02: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 28 charset: 2 {SQL_SCHEMA_PREFIX}ASCII + : name: BASE64_ENCODE alias: b64_encode(uid) + 03: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 21 charset: 1 {SQL_SCHEMA_PREFIX}OCTETS + : name: BASE64_DECODE alias: b64_decode(b64_encode(uid)) + 04: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 40 charset: 2 {SQL_SCHEMA_PREFIX}ASCII + : name: HEX_ENCODE alias: hex_encode(uid) + 05: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 20 charset: 1 {SQL_SCHEMA_PREFIX}OCTETS + : name: HEX_DECODE alias: hex_decode(hex_encode(uid)) + 06: sqltype: 32764 BOOLEAN Nullable scale: 0 subtype: 0 len: 1 + : name: alias: b64_dec(b64_enc(uid)) result + 07: sqltype: 32764 BOOLEAN Nullable scale: 0 subtype: 0 len: 1 + : name: alias: hex_dec(hex_enc(uid)) result + {COMMON_OUTPUT} + """ + act.expected_stdout = expected_stdout act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_6054_test.py b/tests/bugs/core_6054_test.py index 89f83ada..b429bb56 100644 --- a/tests/bugs/core_6054_test.py +++ b/tests/bugs/core_6054_test.py @@ -5,8 +5,15 @@ ISSUE: 6304 TITLE: Random crash 64bit fb_inet_server. Possible collation issue DESCRIPTION: + Only *one* error message should raise. + Output should finish on: 'Records affected: 0', see: + https://github.com/FirebirdSQL/firebird/issues/6304#issuecomment-826244780 JIRA: CORE-6054 FBTEST: bugs.core_6054 +NOTES: + [02.07.2025] pzotov + Refactored: added subst to suppress name of non-existing column as it has no matter for this test. + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -16,32 +23,26 @@ test_script = """ recreate table c (id int, f1 varchar(32) character set win1251 collate win1251); - select * from c where f2 collate win1251_ua = 'x'; + select * from c where non_existing_column collate win1251_ua = 'x'; set count on; select * from c where f1 = _utf8 'x'; """ -act = isql_act('db', test_script, - substitutions=[('-At line[:]{0,1}[\\s]+[\\d]+,[\\s]+column[:]{0,1}[\\s]+[\\d]+', - '-At line: column:')]) +substitutions = [('(-)?At line.*', ''), ('(-)?(")?NON_EXISTING_COLUMN(")?', '')] +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ - Records affected: 0 -""" - -expected_stderr = """ Statement failed, SQLSTATE = 42S22 Dynamic SQL Error -SQL error code = -206 -Column unknown - -F2 - -At line: column: + + Records affected: 0 """ @pytest.mark.version('>=2.5.9') def test_1(act: Action): + act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_6078_test.py b/tests/bugs/core_6078_test.py index 6df20b30..4620bb2b 100644 --- a/tests/bugs/core_6078_test.py +++ b/tests/bugs/core_6078_test.py @@ -10,6 +10,11 @@ privilege to do this. JIRA: CORE-6078 FBTEST: bugs.core_6078 +NOTES: + [02.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.889; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813 """ import pytest @@ -189,9 +194,11 @@ def test_1(act: Action, user_0: User, user_1: User, user_2: User): act.isql(switches=['-q'], input=script, combine_output=True) assert act.clean_stdout == act.clean_expected_stdout -# version: 4.0 +############################################################################ -expected_stdout_2 = """ +# version: 4.0+ + +expected_stdout_5x = """ Statement failed, SQLSTATE = 28000 modify record error -no permission for UPDATE access to COLUMN PLG$SRP_VIEW.PLG$ACTIVE @@ -346,6 +353,123 @@ def test_1(act: Action, user_0: User, user_1: User, user_2: User): -System privilege CHANGE_MAPPING_RULES is missing """ +expected_stdout_6x = """ + Statement failed, SQLSTATE = 28000 + modify record error + -no permission for UPDATE access to COLUMN "PLG$SRP"."PLG$SRP_VIEW"."PLG$ACTIVE" + -Effective user is TMP$C6078_0 + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER DATABASE failed + -no permission for ALTER access to DATABASE + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER DOMAIN "PUBLIC"."DM_TEST" failed + -no permission for ALTER access to DOMAIN "PUBLIC"."DM_TEST" + -Effective user is TMP$C6078_0 + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER TABLE "PUBLIC"."TEST" failed + -no permission for ALTER access to TABLE "PUBLIC"."TEST" + -Effective user is TMP$C6078_0 + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER TABLE "PUBLIC"."TEST" failed + -no permission for ALTER access to TABLE "PUBLIC"."TEST" + -Effective user is TMP$C6078_0 + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER INDEX "PUBLIC"."TEST_UID" failed + -no permission for ALTER access to TABLE "PUBLIC"."TEST" + -Effective user is TMP$C6078_0 + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -COMMENT ON "PUBLIC"."TEST" failed + -no permission for ALTER access to TABLE "PUBLIC"."TEST" + -Effective user is TMP$C6078_0 + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -CREATE OR ALTER TRIGGER "PUBLIC"."TEST_BI" failed + -no permission for ALTER access to TABLE "PUBLIC"."TEST" + -Effective user is TMP$C6078_0 + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -CREATE OR ALTER TRIGGER "PUBLIC"."TRG$START" failed + -no permission for ALTER access to DATABASE + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -CREATE OR ALTER TRIGGER "PUBLIC"."TRIG_DDL_SP" failed + -no permission for ALTER access to DATABASE + ALTERED_TRIGGER_NAME TEST_BI + ALTERED_TRIGGER_SOURCE + as + begin + new.uid = gen_uuid(); + end + Records affected: 1 + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -CREATE OR ALTER PACKAGE "PUBLIC"."PKG_TEST" failed + -No permission for CREATE PACKAGE operation + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -RECREATE PACKAGE BODY "PUBLIC"."PKG_TEST" failed + -No permission for CREATE PACKAGE operation + ALTERED_PKG_NAME + Records affected: 1 + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -CREATE OR ALTER FUNCTION "PUBLIC"."FN_C6078" failed + -No permission for CREATE FUNCTION operation + ALTERED_STANDALONE_FUNC + Records affected: 1 + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -CREATE OR ALTER PROCEDURE "PUBLIC"."SP_C6078" failed + -No permission for CREATE PROCEDURE operation + ALTERED_STANDALONE_PROC + Records affected: 1 + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -CREATE OR ALTER VIEW "PUBLIC"."V_C6078" failed + -No permission for CREATE VIEW operation + ALTERED_VIEW_NAME + Records affected: 1 + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -ALTER SEQUENCE "PUBLIC"."SQ_C6078" failed + -No permission for CREATE GENERATOR operation + ALTERED_SEQUENCE_NAME + Records affected: 1 + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -CREATE OR ALTER EXCEPTION "PUBLIC"."EX_C6078" failed + -No permission for CREATE EXCEPTION operation + ALTERED_EXCEPTION_NAME + Records affected: 1 + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -CREATE OR ALTER FUNCTION "PUBLIC"."WAIT_EVENT" failed + -No permission for CREATE FUNCTION operation + ALTERED_UDR_BASED_FUNC + Records affected: 1 + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER CHARACTER SET "SYSTEM"."UTF8" failed + -no permission for ALTER access to CHARACTER SET "SYSTEM"."UTF8" + -Effective user is TMP$C6078_0 + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER MAPPING LOCAL_MAP_C6078 failed + -Unable to perform operation + -System privilege CHANGE_MAPPING_RULES is missing + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER MAPPING GLOBAL_MAP_C6078 failed + -Unable to perform operation + -System privilege CHANGE_MAPPING_RULES is missing +""" + @pytest.mark.version('>=4.0') def test_2(act: Action, user_0: User, user_1: User, user_2: User): script_vars = {'dsn': act.db.dsn, @@ -353,6 +477,7 @@ def test_2(act: Action, user_0: User, user_1: User, user_2: User): 'user_password': act.db.password,} script_file = act.files_dir / 'core_6078.sql' script = script_file.read_text() % script_vars - act.expected_stdout = expected_stdout_2 - act.isql(switches=['-q'], input=script, combine_output=True) + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.isql(switches=['-q'], input = script, combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_6095_test.py b/tests/bugs/core_6095_test.py index c7f4282e..26a96220 100644 --- a/tests/bugs/core_6095_test.py +++ b/tests/bugs/core_6095_test.py @@ -61,6 +61,7 @@ ,re.compile('\\s*New\\s+number\\s+\\d+\\s*', re.IGNORECASE) ] +@pytest.mark.trace @pytest.mark.version('>=3.0.6') def test_1(act: Action, capsys): diff --git a/tests/bugs/core_6142_test.py b/tests/bugs/core_6142_test.py index e2eb46e1..d84c82ae 100644 --- a/tests/bugs/core_6142_test.py +++ b/tests/bugs/core_6142_test.py @@ -21,18 +21,44 @@ FBTEST: bugs.core_6142 NOTES: [20.08.2022] pzotov - Confirmed again problem with 4.0.0.1598, 3.0.5.33166. - Checked on 5.0.0.591, 4.0.1.2692, 3.0.8.33535 + Confirmed again problem with 4.0.0.1598, 3.0.5.33166. + Checked on 5.0.0.591, 4.0.1.2692, 3.0.8.33535 + + [02.06.2025] pzotov + Re-implemented in order to see details about failed attachments (accumulate them in separate logs and print at final of test). + Checked on 6.0.0.795 + + ::: NB ::: + Special setting DEBUG_KIND_OF_INVALID_DATA presents in this test in order to force connection to be failed and check output. + When this setting is 'invalid_pass' then weird message after test summary: + ========== + Exception ignored in atexit callback: + Traceback (most recent call last): + File "C:/Python3x/Lib/site-packages/firebird/driver/core.py", line 161, in _api_shutdown + provider.shutdown(0, -3) # fb_shutrsn_app_stopped + ^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:/Python3x/Lib/site-packages/firebird/driver/interfaces.py", line 1315, in shutdown + self._check() + File "C:/Python3x/Lib/site-packages/firebird/driver/interfaces.py", line 113, in _check + raise self.__report(DatabaseError, self.status.get_errors()) + firebird.driver.types.DatabaseError: connection shutdown + ========== + Problem exists on 4.x ... 6.x (SS/CS). + No such message if we set DEBUG_KIND_OF_INVALID_DATA = 'not_valid_db' and try to make connection to some file that for is not valid .fdb + + The reason currently remains unknown. """ import os import threading import datetime as py_dt +from typing import List +from pathlib import Path import time import pytest from firebird.qa import * -from firebird.driver import connect, driver_config, NetProtocol +from firebird.driver import connect, driver_config, NetProtocol, DatabaseError ########################### ### S E T T I N G S ### @@ -44,89 +70,106 @@ # Number of iterations to make connect / disconnect for every started thread: LOOP_CNT = 10 +############################## +DEBUG_KIND_OF_INVALID_DATA = '' # 'not_valid_db' / 'invalid_pass' +#DEBUG_KIND_OF_INVALID_DATA = 'not_valid_db' +#DEBUG_KIND_OF_INVALID_DATA = 'invalid_pass' +############################## + +tmp_logs = temp_files( [ f'tmp_6142.{i}.log' for i in range(THREADS_CNT) ] ) tmp_user = user_factory('db', name='tmp$core_6142', password='123', plugin = 'Srp') db = db_factory() -act = python_act('db', substitutions=[('[ \t]+', ' '), ('^((?!SQLCODE|SQLSTATE|[Cc]onnection|OVERALL RESULT).)*$', '')]) +act = python_act('db') #--------------------- def showtime(): - return ''.join( (py_dt.datetime.now().strftime("%H:%M:%S.%f")[:11],'.') ) - -#--------------------- - -class workerThread(threading.Thread): - def __init__(self, db_cfg_object, thr_idx, threads_cnt, num_of_iterations, usr): - threading.Thread.__init__(self) - self.db_cfg_object = db_cfg_object - self.thr_idx = thr_idx - self.threads_cnt = threads_cnt - self.num_of_iterations = num_of_iterations - self.usr = usr - - self.results_dict = { thr_idx : [0,0] } - #fb_cset_lst = ['dos437', 'dos850', 'dos865', 'dos852', 'dos857', 'dos860','dos861', 'dos863', 'dos737', 'dos775', 'dos858', 'dos862', 'dos864', 'dos866', 'dos869', 'win1250', 'win1251', 'win1252', 'win1253', 'win1254', 'win1255', 'win1256', 'win1257', 'iso_8859_1', 'iso_8859_2', 'iso_8859_3', 'iso_8859_4', 'iso_8859_5', 'iso_8859_6', 'iso_8859_7', 'iso_8859_8', 'iso_8859_9'] - #self.db_cfg_object.charset.value = fb_cset_lst[thr_idx] - - def run(self): - print( showtime(), f"Starting thread {self.thr_idx} / {self.threads_cnt}" ) - make_db_attach(self) - print( showtime(), f"Exiting thread {self.thr_idx} / {self.threads_cnt}" ) - - def show_results(self): - for k,v in sorted( self.results_dict.items() ): - print( "ID of thread: %3d. OVERALL RESULT: PASSED=%d, FAILED=%d" % ( k, v[0], v[1] ) ) + return ''.join( (py_dt.datetime.now().strftime("%H:%M:%S.%f")[:11],'. ') ) #--------------------- def make_db_attach(thread_object): - i = 0 - mon_sql = f"select count(*) from mon$attachments where mon$user = '{thread_object.usr.name.upper()}'" - - while i < thread_object.num_of_iterations: + mon_sql = f"select count(*) from mon$attachments where mon$user = '{thread_object.tmp_user.name.upper()}'" + + with open(thread_object.tmp_log,'a') as f_thread_log: + + for iter in range(thread_object.num_of_iterations): + msg_prefix = f"Thread {thread_object.thr_idx}, iter {iter}/{thread_object.num_of_iterations-1}" + f_thread_log.write( showtime() + f"{msg_prefix} - trying to make connection\n" ) + + db_bak = thread_object.db_cfg_object.database.value # need for DEBUG_KIND_OF_INVALID_DATA == 'not_valid_db' + try: + a_pass = thread_object.tmp_user.password + if DEBUG_KIND_OF_INVALID_DATA and thread_object.thr_idx == 2 and iter % 3 == 0: + if DEBUG_KIND_OF_INVALID_DATA == 'not_valid_db': + thread_object.db_cfg_object.database.value = str(thread_object.tmp_act.vars['bin-dir'] / 'fbclient.dll') + elif DEBUG_KIND_OF_INVALID_DATA == 'invalid_pass': + a_pass = 't0ta11y@wrong' + else: + pass + + with connect( thread_object.db_cfg_object.name, user = thread_object.tmp_user.name, password = a_pass ) as con: + f_thread_log.write( showtime() + f"{msg_prefix} - established, {con.info.id=}\n" ) + + # Accumulate counter of SUCCESSFULY established attachments: + thread_object.pass_lst.append(iter) + + except DatabaseError as e: + # Accumulate counter of FAILED attachments: + thread_object.fail_lst.append(iter) + f_thread_log.write(f'### EXCEPTION ###\n') + f_thread_log.write(e.__str__() + '\n') + for x in e.gds_codes: + f_thread_log.write(str(x) + '\n') + finally: + thread_object.db_cfg_object.database.value = db_bak - con = None - att = 0 - - msg_prefix = f"Thread {thread_object.thr_idx}, iter {i}/{thread_object.num_of_iterations-1}" - print( showtime(), f"{msg_prefix} - trying to connect" ) - - try: - with connect( thread_object.db_cfg_object.name, user = thread_object.usr.name, password = thread_object.usr.password ) as con: - print( showtime(), f"{msg_prefix}: created att = {con.info.id}" ) # , charset = {con.charset}" ) +#--------------------- - # Accumulate counter of SUCCESSFULY established attachments: - thread_object.results_dict[ thread_object.thr_idx ][0] += 1 +class workerThread(threading.Thread): + def __init__(self, act: Action, db_cfg_object, thr_idx, threads_cnt, num_of_iterations, tmp_user, tmp_logs): + threading.Thread.__init__(self) + self.db_cfg_object = db_cfg_object + self.thr_idx = thr_idx + self.threads_cnt = threads_cnt + self.num_of_iterations = num_of_iterations + self.tmp_user = tmp_user + self.tmp_log = tmp_logs[thr_idx] + + self.tmp_act = act + self.pass_lst = [] + self.fail_lst = [] - except Exception as e: - # Accumulate counter of FAILED attachments: - thread_object.results_dict[ thread_object.thr_idx ][1] += 1 - print(e) + def run(self): + with open(self.tmp_log, 'w') as f_thread_log: + f_thread_log.write( showtime() + f"Starting thread {self.thr_idx} / {self.threads_cnt-1}\n" ) + + make_db_attach(self) + with open(self.tmp_log, 'a') as f_thread_log: + f_thread_log.write( showtime() + f"Exiting thread {self.thr_idx} / {self.threads_cnt-1}\n" ) - i += 1 #--------------------- - @pytest.mark.version('>=3.0.5') @pytest.mark.platform('Windows') -def test_1(act: Action, tmp_user: User, capsys): +def test_1(act: Action, tmp_user: User, tmp_logs: List[Path], capsys): srv_config = driver_config.register_server(name = 'test_srv_core_6142', config = '') # Create new threads: # ################### threads_list=[] - for thr_idx in range(0, THREADS_CNT): + for thr_idx in range(THREADS_CNT): db_cfg_object = driver_config.register_database(name = f'test_db_core_6142_{thr_idx}') db_cfg_object.database.value = str(act.db.db_path) db_cfg_object.server.value = 'test_srv_core_6142' db_cfg_object.protocol.value = NetProtocol.XNET - threads_list.append( workerThread( db_cfg_object, thr_idx, THREADS_CNT, LOOP_CNT, tmp_user ) ) + threads_list.append( workerThread( act, db_cfg_object, thr_idx, THREADS_CNT, LOOP_CNT, tmp_user, tmp_logs ) ) # Start new Threads # ################# @@ -137,12 +180,23 @@ def test_1(act: Action, tmp_user: User, capsys): for t in threads_list: t.join() - act.expected_stdout = '' - for t in threads_list: - t.show_results() - act.expected_stdout += 'ID of thread: %d. OVERALL RESULT: PASSED=%d, FAILED=%d\n' % (t.thr_idx, LOOP_CNT, 0) - - #print( showtime(), "##### Exiting Main Thread #####\\n") - + # not helps -- time.sleep(151) + + if set([len(t.pass_lst) for t in threads_list]) == set((LOOP_CNT,)): + # All threads could establish connections using XNET on iterations. + print('Expected.') + else: + for t in threads_list: + if t.fail_lst: + print(f'Thread {t.thr_idx} - failed attempts to make connection on iterations:') + print(t.fail_lst) + print('Check log:') + with open(t.tmp_log, 'r') as f: + print(f.read()) + print('*' * 50) + + act.expected_stdout = """ + Expected. + """ act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_6160_test.py b/tests/bugs/core_6160_test.py index 642c2068..cda2e18c 100644 --- a/tests/bugs/core_6160_test.py +++ b/tests/bugs/core_6160_test.py @@ -17,6 +17,9 @@ Added 'SQLSTATE' in substitutions: runtime error must not be filtered out by '?!(...)' pattern ("negative lookahead assertion", see https://docs.python.org/3/library/re.html#regular-expression-syntax). Added 'combine_output = True' in order to see SQLSTATE if any error occurs. + [03.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.889; 5.0.3.1668; 4.0.6.3214 """ import pytest @@ -34,14 +37,16 @@ act = isql_act('db', test_script, substitutions=[('^((?!SQLSTATE|charset).)*$', ''), ('[ \t]+', ' '), ('.*charset: [\\d]+', 'charset:')]) - -expected_stdout = """ - charset: ASCII - charset: ASCII -""" - @pytest.mark.version('>=4.0') def test_1(act: Action): + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'SYSTEM.' + expected_stdout = f""" + charset: {SQL_SCHEMA_PREFIX}ASCII + charset: {SQL_SCHEMA_PREFIX}ASCII + """ + + act.expected_stdout = expected_stdout act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_6166_test.py b/tests/bugs/core_6166_test.py index fec5456c..5659cbae 100644 --- a/tests/bugs/core_6166_test.py +++ b/tests/bugs/core_6166_test.py @@ -14,6 +14,11 @@ of max allowed len (63 characters). JIRA: CORE-6166 FBTEST: bugs.core_6166 +NOTES: + [03.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.889; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -23,13 +28,6 @@ act = python_act('db') -expected_stdout = """ - ПакетДляРешенияЛинейныхГиперболическихИТрансцендентныхУравнений - КоэффициентыЛинейныхГиперболическихИТрансцендентныхУравненийЦЫЧ UNIQUE INDEX ON КоэффициентыДляЛинейныхГиперболическихИТрансцендентныхУравнений(КоэффициентЦДляЛинейныхГиперболическихИТрансцендентныхУравнений, КоэффициентЫДляЛинейныхГиперболическихИТрансцендентныхУравнений, КоэффициентЧДляЛинейныхГиперболическихИТрансцендентныхУравнений) - МетодЗейделяДляЛинейныхГиперболическихИТрансцендентныхУравнений 123 - МетодНьютонаДляЛинейныхГиперболическихИТрансцендентныхУравнений 456 -""" - ddl_script = """ set term ^; recreate package "ПакетДляРешенияЛинейныхГиперболическихИТрансцендентныхУравнений" as @@ -76,11 +74,29 @@ rollback; """ +@pytest.mark.intl @pytest.mark.version('>=4.0') def test_1(act: Action): + + expected_stdout_5x = """ + ПакетДляРешенияЛинейныхГиперболическихИТрансцендентныхУравнений + КоэффициентыЛинейныхГиперболическихИТрансцендентныхУравненийЦЫЧ UNIQUE INDEX ON КоэффициентыДляЛинейныхГиперболическихИТрансцендентныхУравнений(КоэффициентЦДляЛинейныхГиперболическихИТрансцендентныхУравнений, КоэффициентЫДляЛинейныхГиперболическихИТрансцендентныхУравнений, КоэффициентЧДляЛинейныхГиперболическихИТрансцендентныхУравнений) + МетодЗейделяДляЛинейныхГиперболическихИТрансцендентныхУравнений 123 + МетодНьютонаДляЛинейныхГиперболическихИТрансцендентныхУравнений 456 + """ + + expected_stdout_6x = """ + PUBLIC."ПакетДляРешенияЛинейныхГиперболическихИТрансцендентныхУравнений" + PUBLIC."КоэффициентыЛинейныхГиперболическихИТрансцендентныхУравненийЦЫЧ" UNIQUE INDEX ON "КоэффициентыДляЛинейныхГиперболическихИТрансцендентныхУравнений"("КоэффициентЦДляЛинейныхГиперболическихИТрансцендентныхУравнений", "КоэффициентЫДляЛинейныхГиперболическихИТрансцендентныхУравнений", "КоэффициентЧДляЛинейныхГиперболическихИТрансцендентныхУравнений") + МетодЗейделяДляЛинейныхГиперболическихИТрансцендентныхУравнений 123 + МетодНьютонаДляЛинейныхГиперболическихИТрансцендентныхУравнений 456 + """ + expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.expected_stdout = expected_stdout act.isql(switches=[], input=ddl_script + test_script, charset='utf8') assert act.clean_stdout == act.clean_expected_stdout + # Extract metadata act.reset() act.isql(switches=['-x'], charset='utf8') @@ -91,6 +107,7 @@ def test_1(act: Action): # Recereate metadata act.reset() act.isql(switches=[], input=meta, charset='utf8') + # Check 2 act.reset() act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_6171_test.py b/tests/bugs/core_6171_test.py index 6332ac56..fe92901d 100644 --- a/tests/bugs/core_6171_test.py +++ b/tests/bugs/core_6171_test.py @@ -37,8 +37,6 @@ commit; set heading off; - set plan on; - select r.s from tmain r where @@ -55,9 +53,6 @@ act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) expected_stdout = """ - PLAN (D NATURAL) - PLAN (R NATURAL) - foo bar rio @@ -67,5 +62,5 @@ @pytest.mark.version('>=3.0.5') def test_1(act: Action): act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_6182_test.py b/tests/bugs/core_6182_test.py index 6169931c..0e85e364 100644 --- a/tests/bugs/core_6182_test.py +++ b/tests/bugs/core_6182_test.py @@ -105,7 +105,7 @@ def init_main_db(act: Action, eds_user: User): """ act.isql(switches=[], input=ddl_script) - +@pytest.mark.es_eds @pytest.mark.version('>=4.0') def test_1(act: Action, db_a: Database, db_b: Database, db_c: Database, db_d: Database, eds_user: User, capsys): diff --git a/tests/bugs/core_6211_test.py b/tests/bugs/core_6211_test.py index c31e2029..e0f30f0e 100644 --- a/tests/bugs/core_6211_test.py +++ b/tests/bugs/core_6211_test.py @@ -36,6 +36,7 @@ select rdb$role_name as r_name from rdb$roles where rdb$system_flag is distinct from 1; """ +@pytest.mark.intl @pytest.mark.version('>=4.0') def test_1(act: Action): act.isql(switches=[], input=ddl_script) diff --git a/tests/bugs/core_6218_test.py b/tests/bugs/core_6218_test.py index ed5b8610..a88195a2 100644 --- a/tests/bugs/core_6218_test.py +++ b/tests/bugs/core_6218_test.py @@ -7,6 +7,10 @@ DESCRIPTION: JIRA: CORE-6218 FBTEST: bugs.core_6218 +NOTES: + [03.07.2025] pzotov + Difference of transactions before and after queries must be checked to be sure that there was no crash. + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -15,6 +19,7 @@ db = db_factory() test_script = """ + set list on; recreate table test(n decfloat); commit; @@ -22,43 +27,32 @@ insert into test values( 0 ); commit; - set list on; - set explain on; + set term ^; + execute block as + begin + rdb$set_context('USER_TRANSACTION', 'INIT_TX', current_transaction); + end ^ + set term ;^ select n as n_grouped_from_test0 from test group by 1; --- [ 1 ] select distinct n as n_uniq_from_test0 from test; -- [ 2 ] select count(distinct n) as count_uniq_from_test0 from test; -- [ 3 ] + select current_transaction - cast( rdb$get_context('USER_TRANSACTION', 'INIT_TX') as int) as tx_diff from rdb$database; """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) expected_stdout = """ - Select Expression - -> Aggregate - -> Sort (record length: 68, key length: 24) - -> Table "TEST" Full Scan - - N_GROUPED_FROM_TEST0 0 - - - - Select Expression - -> Unique Sort (record length: 68, key length: 24) - -> Table "TEST" Full Scan - - N_UNIQ_FROM_TEST0 0 - - - - Select Expression - -> Aggregate - -> Table "TEST" Full Scan - - COUNT_UNIQ_FROM_TEST0 1 + N_GROUPED_FROM_TEST0 0 + N_UNIQ_FROM_TEST0 0 + COUNT_UNIQ_FROM_TEST0 1 + TX_DIFF 0 """ @pytest.mark.version('>=4.0') def test_1(act: Action): + act.expected_stdout = expected_stdout - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_6239_test.py b/tests/bugs/core_6239_test.py index 1b42fe80..0e6c2ba6 100644 --- a/tests/bugs/core_6239_test.py +++ b/tests/bugs/core_6239_test.py @@ -2,11 +2,20 @@ """ ID: issue-6483 -ISSUE: 6483 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/6483 TITLE: Procedures and EXECUTE BLOCK without RETURNS should not be allowed to use SUSPEND DESCRIPTION: JIRA: CORE-6239 FBTEST: bugs.core_6239 +NOTES: + [05.02.2020] pzotov + Fix was done by commit https://github.com/FirebirdSQL/firebird/commit/b2b5f9a87cea26a9f12fa231804dba9d0426d3fa + (can be checked by 4.0.0.1763+, date of build since 05-feb-2020). + + [02.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.889; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -91,7 +100,7 @@ act = isql_act('db', test_script) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 42000 Dynamic SQL Error -SQL error code = -104 @@ -131,8 +140,43 @@ -SUSPEND could not be used without RETURNS clause in PROCEDURE or EXECUTE BLOCK """ +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -SUSPEND could not be used without RETURNS clause in PROCEDURE or EXECUTE BLOCK + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -SUSPEND could not be used without RETURNS clause in PROCEDURE or EXECUTE BLOCK + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -CREATE OR ALTER PROCEDURE "PUBLIC"."SP_MISSED_RETURNS_IN_ITS_HEADER_1" failed + -Dynamic SQL Error + -SQL error code = -104 + -SUSPEND could not be used without RETURNS clause in PROCEDURE or EXECUTE BLOCK + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -CREATE OR ALTER PROCEDURE "PUBLIC"."SP_MISSED_RETURNS_IN_ITS_HEADER_2" failed + -Dynamic SQL Error + -SQL error code = -104 + -SUSPEND could not be used without RETURNS clause in PROCEDURE or EXECUTE BLOCK + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -CREATE OR ALTER PROCEDURE "PUBLIC"."SP_MISSED_RETURNS_IN_ITS_HEADER_3" failed + -Dynamic SQL Error + -SQL error code = -104 + -SUSPEND could not be used without RETURNS clause in PROCEDURE or EXECUTE BLOCK + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -CREATE PACKAGE BODY "PUBLIC"."PG_TEST_1" failed + -Dynamic SQL Error + -SQL error code = -104 + -SUSPEND could not be used without RETURNS clause in PROCEDURE or EXECUTE BLOCK +""" + @pytest.mark.version('>=4.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_6248_test.py b/tests/bugs/core_6248_test.py index 809b8dcc..8e15301a 100644 --- a/tests/bugs/core_6248_test.py +++ b/tests/bugs/core_6248_test.py @@ -53,6 +53,13 @@ Because of this, subprocess.run() is used to invoke fbsvcmgr Checked on Windows and Linux, 4.0.1.2692 (SS/CS), 5.0.0.736 (SS/CS). + + [25.02.2025] pzotov + Order of attributes changed since 93db88 ("ODS14: header page refactoring (#8401)"): + mon_read_only is displayed now PRIOR to mon_shutdown_mode. + Re-implmented code for building 'Attributes' line using mon$database values, see func check_db_hdr_info. + Checked on Windows, 6.0.0.652, 5.0.3.1622 + JIRA: CORE-6248 FBTEST: bugs.core_6248 """ @@ -64,7 +71,8 @@ import platform from pathlib import Path #from difflib import unified_diff -#from firebird.driver import SrvRepairFlag + +from firebird.driver import connect import pytest from firebird.qa import * @@ -82,48 +90,40 @@ def check_db_hdr_info(act: Action, db_file_chk:Path, interested_patterns, capsys # 1. Obtain attributes from mon$database: get page buffers, 'build' attributes row and get sweep interval. # These values will be displayed in the form of three separate LINES, without column names. # Content of this output must be equal to gstat filtered values, with exception of leading spaces: - sql_txt = f""" - set list on; - - -- Make connect using local protocol. - -- NOTE: 'command error' raises here if length of '{db_file_chk}' (including qutes!) greater than 255. - connect '{db_file_chk}' user {act.db.user}; - - select - 'Page buffers ' || mon$page_buffers as " " - ,'Attributes ' || iif(trim(attr_list) = '', '', substring(attr_list from 2)) - as " " - ,'Sweep interval: ' || mon$sweep_interval as " " - from ( + + expected_attr_from_mon_db = '' + with connect(str(db_file_chk), user = act.db.user) as con: + cur = con.cursor() + sql = """ select - mon$page_buffers - ,mon$forced_writes - ,mon$backup_state - ,mon$reserve_space - ,mon$shutdown_mode - ,mon$read_only - ,mon$replica_mode - ,mon$sweep_interval - ,iif(mon$forced_writes = 1, ', force write', '') - || iif(mon$reserve_space = 0, ', no reserve', '') - || decode(mon$backup_state, 2, ', merge', 1, ', backup lock', '') - || decode(mon$shutdown_mode, 3, 'full shutdown', 2, ', single-user maintenance', 1, ', multi-user maintenance', '') - -- !! NEED TRIM() !! otherwise 10 spaces will be inserted if mon$read_only=0. - -- Discussed with Vladet al, letters since 23.09.2022 10:57. - || trim(iif(mon$read_only<>0, ', read only', '')) - || decode(mon$replica_mode, 2, ', read-write replica', 1, ', read-only replica', '') - as attr_list + rdb$get_context('SYSTEM', 'ENGINE_VERSION') as engine_ver + ,mon$page_buffers as mon_page_buffers + ,mon$sweep_interval as mon_sweep_interval + ,trim(iif(mon$forced_writes = 1, ', force write', '')) as mon_force_write + ,trim(iif(mon$reserve_space = 0, ', no reserve', '')) as mon_reserve_space + ,trim(decode(mon$backup_state, 2, ', merge', 1, ', backup lock', '')) as mon_backup_state + ,trim(decode(mon$shutdown_mode, 3, 'full shutdown', 2,', single-user maintenance', 1,', multi-user maintenance', '')) as mon_shutdown_mode + -- ::: NEED TRIM() ::: otherwise 10 spaces will be inserted if mon$read_only=0. + -- Discussed with Vlad et al, letters since 23.09.2022 10:57. + ,trim(iif(mon$read_only = 1, ', read only', '')) as mon_read_only + ,trim(decode(mon$replica_mode, 2,', read-write replica', 1,', read-only replica', '')) as mon_replica_mode from mon$database - ); - commit; - """ - # Example of output: - # Page buffers 3791 - # Attributes force write, no reserve, single-user maintenance, read only, read-write replica - # Sweep interval: 5678 - - act.isql(switches = ['-q'], input = sql_txt, connect_db=False, credentials = False, combine_output = True, io_enc = locale.getpreferredencoding()) - expected_attr_from_mon_db = act.stdout + """ + cur.execute(sql) + for r in cur: + engine_ver, mon_page_buffers, mon_sweep_interval, mon_force_write, mon_reserve_space, mon_backup_state, mon_shutdown_mode, mon_read_only, mon_replica_mode = r[:9] + + expected_attr_from_mon_db += f'Page buffers {mon_page_buffers}' + if act.is_version('<6'): + attr_list = f'{mon_force_write}{mon_reserve_space}{mon_backup_state}{mon_shutdown_mode}{mon_read_only}{mon_replica_mode}' + else: + ### ACHTUNG, SINCE 6.0.0.647 2025.02.21 ### + # Order of attributes has changed since #93db88 ("ODS14: header page refactoring (#8401)"), 20-feb-2025: + # mon_read_only is displayed now PRIOR to mon_shutdown_mode: + attr_list = f'{mon_force_write}{mon_reserve_space}{mon_backup_state}{mon_read_only}{mon_shutdown_mode}{mon_replica_mode}' + + expected_attr_from_mon_db += f'\nAttributes {attr_list[2:]}' + expected_attr_from_mon_db += f'\nSweep interval: {mon_sweep_interval}' #------------------------------------------------------ @@ -157,6 +157,7 @@ def check_db_hdr_info(act: Action, db_file_chk:Path, interested_patterns, capsys for line in act.stdout.split('\n'): print('gstat output: ',line) assert db_found,'COULD NOT FIND NAME OF DATABASE IN THE GSTAT HEADER' + # 3. Return GUID of database (can be compared after b/r with GUID of restored database: they always must differ): return db_guid @@ -176,17 +177,17 @@ def test_1(act: Action, tmp_file:Path, capsys): # All these (cuted) strings have length = 254 bytes and do NOT contain ending double quote. # Because of this, we must include this character into the pattern only as OPTIONAL, i.e.: |'Database\s+"\S+(")?'| # - interested_patterns = ( 'Database\s+"\S+(")?', '[\t ]*Attributes([\t ]+\w+)?', '[\t ]*Page buffers([\t ]+\d+)', '[\t ]*Sweep interval(:)?([\t ]+\d+)', 'Database GUID') + interested_patterns = ( r'Database\s+"\S+(")?', r'[\t ]*Attributes([\t ]+\w+)?', r'[\t ]*Page buffers([\t ]+\d+)', r'[\t ]*Sweep interval(:)?([\t ]+\d+)', 'Database GUID') interested_patterns = [re.compile(p, re.IGNORECASE) for p in interested_patterns] protocol_list = ('', 'inet://', 'xnet://') if os.name == 'nt' else ('', 'inet://',) full_str = str(tmp_file.absolute()) - for utility in ('gfix', 'fbsvcmgr'): + for chk_mode in ('fb_util', 'fbsvcmgr'): for protocol_prefix in protocol_list: # NB: most strict limit for DB filename length origins from isql 'CONNECT' command: - # 'command error' raises there if length of '{db_file_chk}' (including qutes!) greater than 255. + # 'command error' raises there if length of '{db_file_chk}' including qutes greater than 255. # Because of this, we can not operate with files with length of full name greater than 253 bytes. # db_file_len = 253 - len(protocol_prefix) @@ -195,9 +196,11 @@ def test_1(act: Action, tmp_file:Path, capsys): db_file_dif = Path(os.path.splitext(db_file_chk)[0] + '.dif') db_file_fbk = Path(os.path.splitext(db_file_chk)[0] + '.fbk') + db_file_dif.unlink(missing_ok = True) + db_file_dsn = '' svc_call_starting_part = [] - if utility == 'gfix': + if chk_mode == 'fb_util': db_file_dsn = protocol_prefix + str(db_file_chk) else: db_file_dsn = db_file_chk @@ -222,7 +225,7 @@ def test_1(act: Action, tmp_file:Path, capsys): act.reset() svc_retcode = 0 - if utility == 'gfix': + if chk_mode == 'fb_util': act.gfix(switches=['-buffers', '3791', db_file_dsn], combine_output = True, io_enc = locale.getpreferredencoding()) else: svc_retcode = (subprocess.run( svc_call_starting_part + ['action_properties', 'prp_page_buffers', '3791', 'dbname', db_file_chk], stderr = subprocess.STDOUT)).returncode @@ -231,7 +234,7 @@ def test_1(act: Action, tmp_file:Path, capsys): act.reset() - if utility == 'gfix': + if chk_mode == 'fb_util': act.gfix(switches=['-write','sync', db_file_dsn], combine_output = True, io_enc = locale.getpreferredencoding()) else: svc_retcode = (subprocess.run( svc_call_starting_part + ['action_properties', 'prp_write_mode', 'prp_wm_sync', 'dbname', db_file_chk], stderr = subprocess.STDOUT)).returncode @@ -240,7 +243,7 @@ def test_1(act: Action, tmp_file:Path, capsys): act.reset() - if utility == 'gfix': + if chk_mode == 'fb_util': act.gfix(switches=['-housekeeping','5678', db_file_dsn], combine_output = True, io_enc = locale.getpreferredencoding()) else: svc_retcode = (subprocess.run( svc_call_starting_part + ['action_properties', 'prp_sweep_interval', '5678', 'dbname', db_file_chk], stderr = subprocess.STDOUT)).returncode @@ -249,7 +252,7 @@ def test_1(act: Action, tmp_file:Path, capsys): act.reset() - if utility == 'gfix': + if chk_mode == 'fb_util': act.gfix(switches=['-use','full', db_file_dsn], combine_output = True, io_enc = locale.getpreferredencoding()) else: svc_retcode = (subprocess.run( svc_call_starting_part + ['action_properties', 'prp_reserve_space', 'prp_res_use_full', 'dbname', db_file_chk], stderr = subprocess.STDOUT)).returncode @@ -258,7 +261,7 @@ def test_1(act: Action, tmp_file:Path, capsys): act.reset() - if utility == 'gfix': + if chk_mode == 'fb_util': act.gfix(switches=['-sweep', db_file_dsn], combine_output = True, io_enc = locale.getpreferredencoding()) else: svc_retcode = (subprocess.run( svc_call_starting_part + ['action_repair', 'rpr_sweep_db', 'dbname', db_file_chk], stderr = subprocess.STDOUT)).returncode @@ -267,7 +270,7 @@ def test_1(act: Action, tmp_file:Path, capsys): act.reset() if act.is_version('>=4'): - if utility == 'gfix': + if chk_mode == 'fb_util': act.gfix(switches=['-replica','read_write', db_file_dsn], combine_output = True, io_enc = locale.getpreferredencoding()) else: svc_retcode = (subprocess.run( svc_call_starting_part + ['action_properties', 'prp_replica_mode', 'prp_rm_readwrite', 'dbname', db_file_chk], stderr = subprocess.STDOUT)).returncode @@ -278,31 +281,38 @@ def test_1(act: Action, tmp_file:Path, capsys): sql_txt = f""" -- Make connect using local protocol. - -- NOTE: 'command error' raises here if length of '{db_file_chk}' (including qutes!) greater than 255. + -- NOTE: 'command error' raises here if length of '{db_file_chk}' including qutes greater than 255. connect '{db_file_chk}' user {act.db.user}; alter database add difference file '{db_file_dif}'; alter database begin backup; - alter database set linger to 100; + -- alter database set linger to 100; """ + + # Page buffers 3791 + # Attributes force write, no reserve, backup lock, read-write replica + # Sweep interval: 5678 + # act.isql(switches = ['-q'], input = sql_txt, connect_db=False, credentials = False, combine_output = True, io_enc = locale.getpreferredencoding()) assert act.clean_stdout == act.clean_expected_stdout + + # WRONG for 4.x ... 5.x: assert '' == act.stdout -- noise characters are in output: + # "SQL> SQL> SQL> SQL> Database: ..." + act.reset() _ = check_db_hdr_info(act, db_file_chk, interested_patterns, capsys) - sql_txt = f""" -- Make connect using local protocol. - -- NOTE: 'command error' raises here if length of '{db_file_chk}' (including qutes!) greater than 255. + -- NOTE: 'command error' raises here if length of '{db_file_chk}' including qutes greater than 255. connect '{db_file_chk}' user {act.db.user}; - alter database set linger to 0; + -- alter database set linger to 0; alter database end backup; """ act.isql(switches = ['-q'], input = sql_txt, connect_db=False, credentials = False, combine_output = True, io_enc = locale.getpreferredencoding()) assert act.clean_stdout == act.clean_expected_stdout act.reset() - - if utility == 'gfix': + if chk_mode == 'fb_util': act.gfix(switches=['-mode','read_only', db_file_dsn], combine_output = True, io_enc = locale.getpreferredencoding()) else: svc_retcode = (subprocess.run( svc_call_starting_part + ['action_properties', 'prp_access_mode', 'prp_am_readonly', 'dbname', db_file_chk], stderr = subprocess.STDOUT)).returncode @@ -312,7 +322,7 @@ def test_1(act: Action, tmp_file:Path, capsys): _ = check_db_hdr_info(act, db_file_chk, interested_patterns, capsys) - if utility == 'gfix': + if chk_mode == 'fb_util': act.gfix(switches=['-shut','single', '-at', '20', db_file_dsn], combine_output = True, io_enc = locale.getpreferredencoding()) else: svc_retcode = (subprocess.run( svc_call_starting_part + ['action_properties', 'prp_shutdown_mode', 'prp_sm_single', 'prp_deny_new_attachments', '20', 'dbname', db_file_chk], stderr = subprocess.STDOUT)).returncode @@ -323,7 +333,7 @@ def test_1(act: Action, tmp_file:Path, capsys): src_guid = check_db_hdr_info(act, db_file_chk, interested_patterns, capsys) - if utility == 'gfix': + if chk_mode == 'fb_util': act.gfix(switches=['-online', db_file_dsn], combine_output = True, io_enc = locale.getpreferredencoding()) else: svc_retcode = (subprocess.run( svc_call_starting_part + ['action_properties', 'prp_online_mode', 'prp_sm_normal', 'dbname', db_file_chk], stderr = subprocess.STDOUT)).returncode @@ -332,7 +342,7 @@ def test_1(act: Action, tmp_file:Path, capsys): act.reset() - if utility == 'gfix': + if chk_mode == 'fb_util': act.gfix(switches=['-v', '-full', db_file_dsn], combine_output = True, io_enc = locale.getpreferredencoding()) else: svc_retcode = (subprocess.run( svc_call_starting_part + ['action_repair', 'rpr_validate_db', 'rpr_full', 'dbname', db_file_chk], stderr = subprocess.STDOUT)).returncode @@ -341,7 +351,7 @@ def test_1(act: Action, tmp_file:Path, capsys): act.reset() - if utility == 'gfix': + if chk_mode == 'fb_util': act.gbak(switches=['-b', db_file_dsn, db_file_fbk], combine_output = True, io_enc = locale.getpreferredencoding()) else: svc_retcode = (subprocess.run( svc_call_starting_part + ['action_backup', 'dbname', db_file_chk, 'bkp_file', db_file_fbk], stderr = subprocess.STDOUT)).returncode @@ -350,7 +360,7 @@ def test_1(act: Action, tmp_file:Path, capsys): act.reset() - if utility == 'gfix': + if chk_mode == 'fb_util': act.gbak(switches=['-rep', db_file_fbk, db_file_dsn], combine_output = True, io_enc = locale.getpreferredencoding()) else: svc_retcode = (subprocess.run( svc_call_starting_part + ['action_restore', 'dbname', db_file_chk, 'bkp_file', db_file_fbk, 'res_replace' ], stderr = subprocess.STDOUT)).returncode diff --git a/tests/bugs/core_6252_test.py b/tests/bugs/core_6252_test.py index 55cb1be5..ded946f3 100644 --- a/tests/bugs/core_6252_test.py +++ b/tests/bugs/core_6252_test.py @@ -8,6 +8,11 @@ DESCRIPTION: JIRA: CORE-6252 FBTEST: bugs.core_6252 +NOTES: + [03.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.889; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -69,37 +74,50 @@ ; """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ - A 1 - B 1 - A 1 - B 2 - - U 1 - V 1 - U 1 - V 1 - - IDX_NAME - IDX_UNIQ -""" - -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 23000 violation of PRIMARY or UNIQUE KEY constraint "TEST1_UNQ" on table "TEST1" -Problematic key value is ("A" = 1) - Statement failed, SQLSTATE = 23000 violation of PRIMARY or UNIQUE KEY constraint "TEST2_PK" on table "TEST2" -Problematic key value is ("U" = 1) + A 1 + B 1 + A 1 + B 2 + U 1 + V 1 + U 1 + V 1 + IDX_NAME + IDX_UNIQ +""" + +expected_stdout_6x = """ + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST1_UNQ" on table "PUBLIC"."TEST1" + -Problematic key value is ("A" = 1) + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "TEST2_PK" on table "PUBLIC"."TEST2" + -Problematic key value is ("U" = 1) + A 1 + B 1 + A 1 + B 2 + U 1 + V 1 + U 1 + V 1 + IDX_NAME + IDX_UNIQ """ @pytest.mark.version('>=3.0.6') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_6272_test.py b/tests/bugs/core_6272_test.py index 789dbff6..9ca82e35 100644 --- a/tests/bugs/core_6272_test.py +++ b/tests/bugs/core_6272_test.py @@ -63,6 +63,7 @@ trace_conf = temp_file('trace.conf') +@pytest.mark.trace @pytest.mark.version('>=4.0') def test_1(act: Action, db_nonexistent: Database, trace_conf: Path, capsys): with ServerKeeper(act, None): # Use embedded server for trace diff --git a/tests/bugs/core_6278_test.py b/tests/bugs/core_6278_test.py index 982722fa..4aa4e4f9 100644 --- a/tests/bugs/core_6278_test.py +++ b/tests/bugs/core_6278_test.py @@ -14,15 +14,17 @@ into a table: smaller values of RDB$DB_KEY can appear *after* bigger ones (i.e. smaller RDB$DB_KEY will be physically closer to the end of table than bigger). Because of that, we check only EXPLAINED PLAN, without runtime statistics from trace log before. - On build 4.0.0.1865 (07-apr-2020) explained plan for scoped query (like 'rdb$db_key between ? and ?') returned "Table ... Full Scan" - WITHOUT "(lower bound, upper bound)". - Since build 4.0.0.1869 (08-apr-2020) this opewration is: "Table "TEST" Full Scan (lower bound, upper bound)". See commit: https://github.com/FirebirdSQL/firebird/commit/3ce4605e3cc9960afcf0224ea40e04f508669eca - Checked on 5.0.1.1394, 6.0.0.345. + + [03.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.884; 5.0.3.1668; 4.0.6.3214. + """ import pytest @@ -57,42 +59,43 @@ def test_1(act: Action, capsys): with cur.prepare(f'select count(s) from test where {x}') as ps: print( '\n'.join([replace_leading(s) for s in ps.detailed_plan .split('\n')]) ) - - act.expected_stdout = """ + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + act.expected_stdout = f""" Select Expression ....-> Aggregate ........-> Filter - ............-> Table "TEST" Full Scan (lower bound, upper bound) + ............-> Table {SQL_SCHEMA_PREFIX}"TEST" Full Scan (lower bound, upper bound) Select Expression ....-> Aggregate ........-> Filter - ............-> Table "TEST" Full Scan (lower bound, upper bound) + ............-> Table {SQL_SCHEMA_PREFIX}"TEST" Full Scan (lower bound, upper bound) Select Expression ....-> Aggregate ........-> Filter - ............-> Table "TEST" Full Scan (lower bound, upper bound) + ............-> Table {SQL_SCHEMA_PREFIX}"TEST" Full Scan (lower bound, upper bound) Select Expression ....-> Aggregate ........-> Filter - ............-> Table "TEST" Full Scan (lower bound) + ............-> Table {SQL_SCHEMA_PREFIX}"TEST" Full Scan (lower bound) Select Expression ....-> Aggregate ........-> Filter - ............-> Table "TEST" Full Scan (lower bound) + ............-> Table {SQL_SCHEMA_PREFIX}"TEST" Full Scan (lower bound) Select Expression ....-> Aggregate ........-> Filter - ............-> Table "TEST" Full Scan (upper bound) + ............-> Table {SQL_SCHEMA_PREFIX}"TEST" Full Scan (upper bound) Select Expression ....-> Aggregate ........-> Filter - ............-> Table "TEST" Full Scan (upper bound) + ............-> Table {SQL_SCHEMA_PREFIX}"TEST" Full Scan (upper bound) """ + act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_6285_test.py b/tests/bugs/core_6285_test.py index deb5aebe..76b40507 100644 --- a/tests/bugs/core_6285_test.py +++ b/tests/bugs/core_6285_test.py @@ -12,6 +12,11 @@ 'INCLUDE ...' and 'EXCLUDE ...' respectively (after reply from dimitr). JIRA: CORE-6285 FBTEST: bugs.core_6285 +NOTES: + [03.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.881; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -58,9 +63,10 @@ select 'rdb$pub after DROP LIST of some tables from publication' as msg, p.* from rdb$database left join RDB$PUBLICATION_TABLES p on 1=1; """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ +expected_stdout_5x = """ MSG rdb$pub: initial content for a NEW database RDB$PUBLICATION_NAME RDB$DEFAULT RDB$OWNER_NAME SYSDBA @@ -116,8 +122,64 @@ Records affected: 1 """ +expected_stdout_6x = """ + MSG rdb$pub: initial content for a NEW database + RDB$PUBLICATION_NAME RDB$DEFAULT + RDB$OWNER_NAME SYSDBA + RDB$SYSTEM_FLAG 1 + RDB$ACTIVE_FLAG 0 + RDB$AUTO_ENABLE 0 + Records affected: 1 + MSG rdb$pub after enable for the WHOLE DATABASE + RDB$PUBLICATION_NAME RDB$DEFAULT + RDB$OWNER_NAME SYSDBA + RDB$SYSTEM_FLAG 1 + RDB$ACTIVE_FLAG 1 + RDB$AUTO_ENABLE 0 + Records affected: 1 + MSG rdb$pub after disable for the WHOLE DATABASE + RDB$PUBLICATION_NAME RDB$DEFAULT + RDB$OWNER_NAME SYSDBA + RDB$SYSTEM_FLAG 1 + RDB$ACTIVE_FLAG 0 + RDB$AUTO_ENABLE 0 + Records affected: 1 + MSG rdb$pub after ADD ALL tables to publication + RDB$PUBLICATION_NAME RDB$DEFAULT + RDB$TABLE_NAME TEST1 + RDB$TABLE_SCHEMA_NAME PUBLIC + MSG rdb$pub after ADD ALL tables to publication + RDB$PUBLICATION_NAME RDB$DEFAULT + RDB$TABLE_NAME TEST2 + RDB$TABLE_SCHEMA_NAME PUBLIC + MSG rdb$pub after ADD ALL tables to publication + RDB$PUBLICATION_NAME RDB$DEFAULT + RDB$TABLE_NAME TEST3 + RDB$TABLE_SCHEMA_NAME PUBLIC + Records affected: 3 + MSG rdb$pub after DROP ALL tables from publication + RDB$PUBLICATION_NAME + RDB$TABLE_NAME + RDB$TABLE_SCHEMA_NAME + Records affected: 1 + MSG rdb$pub after ADD LIST of some tables to publication + RDB$PUBLICATION_NAME RDB$DEFAULT + RDB$TABLE_NAME TEST2 + RDB$TABLE_SCHEMA_NAME PUBLIC + MSG rdb$pub after ADD LIST of some tables to publication + RDB$PUBLICATION_NAME RDB$DEFAULT + RDB$TABLE_NAME TEST3 + RDB$TABLE_SCHEMA_NAME PUBLIC + Records affected: 2 + MSG rdb$pub after DROP LIST of some tables from publication + RDB$PUBLICATION_NAME + RDB$TABLE_NAME + RDB$TABLE_SCHEMA_NAME + Records affected: 1 +""" + @pytest.mark.version('>=4.0.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_6300_test.py b/tests/bugs/core_6300_test.py index a8587983..a3e03748 100644 --- a/tests/bugs/core_6300_test.py +++ b/tests/bugs/core_6300_test.py @@ -15,6 +15,12 @@ Added 'SQLSTATE' in substitutions: runtime error must not be filtered out by '?!(...)' pattern ("negative lookahead assertion", see https://docs.python.org/3/library/re.html#regular-expression-syntax). Added 'combine_output = True' in order to see SQLSTATE if any error occurs. + + [03.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.889; 5.0.3.1668; 4.0.6.3214. + """ import pytest @@ -31,23 +37,25 @@ act = isql_act('db', test_script, substitutions = [ ( '^((?!SQLSTATE|sqltype|name:).)*$', ''), ('[ \t]+', ' ') ] ) -expected_stdout = """ - 01: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 38 charset: 0 NONE - : name: MON$GUID alias: MON$GUID - 02: sqltype: 448 VARYING scale: 0 subtype: 0 len: 255 charset: 2 ASCII - : name: MON$FILE_ID alias: MON$FILE_ID - 03: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 - : name: MON$NEXT_ATTACHMENT alias: MON$NEXT_ATTACHMENT - 04: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 - : name: MON$NEXT_STATEMENT alias: MON$NEXT_STATEMENT - 05: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 255 charset: 0 NONE - : name: RDB$GET_CONTEXT alias: RDB$GET_CONTEXT - 06: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 255 charset: 0 NONE - : name: RDB$GET_CONTEXT alias: RDB$GET_CONTEXT -""" - @pytest.mark.version('>=4.0') def test_1(act: Action): + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'SYSTEM.' + expected_stdout = f""" + 01: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 38 charset: 0 {SQL_SCHEMA_PREFIX}NONE + : name: MON$GUID alias: MON$GUID + 02: sqltype: 448 VARYING scale: 0 subtype: 0 len: 255 charset: 2 {SQL_SCHEMA_PREFIX}ASCII + : name: MON$FILE_ID alias: MON$FILE_ID + 03: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: MON$NEXT_ATTACHMENT alias: MON$NEXT_ATTACHMENT + 04: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: MON$NEXT_STATEMENT alias: MON$NEXT_STATEMENT + 05: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 255 charset: 0 {SQL_SCHEMA_PREFIX}NONE + : name: RDB$GET_CONTEXT alias: RDB$GET_CONTEXT + 06: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 255 charset: 0 {SQL_SCHEMA_PREFIX}NONE + : name: RDB$GET_CONTEXT alias: RDB$GET_CONTEXT + """ + act.expected_stdout = expected_stdout act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_6316_test.py b/tests/bugs/core_6316_test.py index 9dcd309e..9c0481f2 100644 --- a/tests/bugs/core_6316_test.py +++ b/tests/bugs/core_6316_test.py @@ -6,10 +6,22 @@ TITLE: Unable to specify new 32k page size DESCRIPTION: NOTES: - Issues remain for some kind of commands: parser should be more rigorous. - Sent letter to Alex and Dmitry, 29.05.2020 12:28. -JIRA: CORE-6316 -FBTEST: bugs.core_6316 + [29.05.2020] pzotov + Issues remain for some kind of commands: parser should be more rigorous. + Sent letter to Alex and Dmitry, 29.05.2020 12:28. + [12.03.2025] pzotov + Separated expected_out for 6.x after commit #d7a0e694 + ("Replace 'upper' with 'nearest' rounding for the user-specified page size"). + Now, if we specify page_size = 16383 then actual page size must be 16384 (previously it was 8192). + Checked on 6.0.0.663-d7a0e69 (letter from dimitr, 12.03.2025 19:52). + [13.03.2025] pzotov + Changed expected out for 6.x after fixed https://github.com/FirebirdSQL/firebird/issues/8470. + Question about 'page size NULL' and 'page size DEFAULT' not yet resolved: whether they allowed or no. + + [03.07.2025] pzotov + Adjusted expected_output for FB 6.x: min page_size is 8192. + ::: NB ::: Test diration time is about 75 seconds. + Checked on 6.0.0.892 """ import pytest @@ -21,249 +33,6 @@ act = python_act('db', substitutions=[('Token unknown.*line.*', 'Token unknown')]) -expected_stdout = """ - create database ... page_size 9223372036854775809 default character set win1251 - Statement failed, SQLSTATE = 42000 - Dynamic SQL Error - -SQL error code = -104 - -Token unknown - -9223372036854775809 - create database ... page_size 9223372036854775809 - DB created. Actual page_size: 32768 - create database ... page_size 9223372036854775808 default character set win1251 - Statement failed, SQLSTATE = 42000 - Dynamic SQL Error - -SQL error code = -104 - -Token unknown - -9223372036854775808 - create database ... page_size 9223372036854775808 - DB created. Actual page_size: 32768 - create database ... page_size 9223372036854775807 default character set win1251 - Statement failed, SQLSTATE = 42000 - Dynamic SQL Error - -SQL error code = -104 - -Token unknown - -9223372036854775807 - create database ... page_size 9223372036854775807 - DB created. Actual page_size: 32768 - create database ... page_size 4294967297 default character set win1251 - Statement failed, SQLSTATE = 42000 - Dynamic SQL Error - -SQL error code = -104 - -Token unknown - -4294967297 - create database ... page_size 4294967297 - DB created. Actual page_size: 32768 - create database ... page_size 4294967296 default character set win1251 - Statement failed, SQLSTATE = 42000 - Dynamic SQL Error - -SQL error code = -104 - -Token unknown - -4294967296 - create database ... page_size 4294967296 - DB created. Actual page_size: 32768 - create database ... page_size 4294967295 default character set win1251 - Statement failed, SQLSTATE = 42000 - Dynamic SQL Error - -SQL error code = -104 - -Token unknown - -4294967295 - create database ... page_size 4294967295 - DB created. Actual page_size: 32768 - create database ... page_size 2147483649 default character set win1251 - Statement failed, SQLSTATE = 42000 - Dynamic SQL Error - -SQL error code = -104 - -Token unknown - -2147483649 - create database ... page_size 2147483649 - DB created. Actual page_size: 32768 - create database ... page_size 2147483648 default character set win1251 - Statement failed, SQLSTATE = 42000 - Dynamic SQL Error - -SQL error code = -104 - -Token unknown - -2147483648 - create database ... page_size 2147483648 - DB created. Actual page_size: 32768 - create database ... page_size 2147483647 default character set win1251 - DB created. Actual page_size: 32768 - create database ... page_size 2147483647 - DB created. Actual page_size: 32768 - create database ... page_size 65537 default character set win1251 - DB created. Actual page_size: 32768 - create database ... page_size 65537 - DB created. Actual page_size: 32768 - create database ... page_size 32769 default character set win1251 - DB created. Actual page_size: 32768 - create database ... page_size 32769 - DB created. Actual page_size: 32768 - create database ... page_size 32768 default character set win1251 - DB created. Actual page_size: 32768 - create database ... page_size 32768 - DB created. Actual page_size: 32768 - create database ... page_size 32767 default character set win1251 - DB created. Actual page_size: 16384 - create database ... page_size 32767 - DB created. Actual page_size: 16384 - create database ... page_size 16385 default character set win1251 - DB created. Actual page_size: 16384 - create database ... page_size 16385 - DB created. Actual page_size: 16384 - create database ... page_size 16384 default character set win1251 - DB created. Actual page_size: 16384 - create database ... page_size 16384 - DB created. Actual page_size: 16384 - create database ... page_size 16383 default character set win1251 - DB created. Actual page_size: 8192 - create database ... page_size 16383 - DB created. Actual page_size: 8192 - create database ... page_size 8193 default character set win1251 - DB created. Actual page_size: 8192 - create database ... page_size 8193 - DB created. Actual page_size: 8192 - create database ... page_size 8192 default character set win1251 - DB created. Actual page_size: 8192 - create database ... page_size 8192 - DB created. Actual page_size: 8192 - create database ... page_size 8191 default character set win1251 - DB created. Actual page_size: 4096 - create database ... page_size 8191 - DB created. Actual page_size: 4096 - create database ... page_size 4097 default character set win1251 - DB created. Actual page_size: 4096 - create database ... page_size 4097 - DB created. Actual page_size: 4096 - create database ... page_size 4096 default character set win1251 - DB created. Actual page_size: 4096 - create database ... page_size 4096 - DB created. Actual page_size: 4096 - create database ... page_size 4095 default character set win1251 - DB created. Actual page_size: 4096 - create database ... page_size 4095 - DB created. Actual page_size: 4096 - create database ... page_size 2049 default character set win1251 - DB created. Actual page_size: 4096 - create database ... page_size 2049 - DB created. Actual page_size: 4096 - create database ... page_size 2048 default character set win1251 - DB created. Actual page_size: 4096 - create database ... page_size 2048 - DB created. Actual page_size: 4096 - create database ... page_size 2047 default character set win1251 - DB created. Actual page_size: 4096 - create database ... page_size 2047 - DB created. Actual page_size: 4096 - create database ... page_size 1025 default character set win1251 - DB created. Actual page_size: 4096 - create database ... page_size 1025 - DB created. Actual page_size: 4096 - create database ... page_size 1024 default character set win1251 - DB created. Actual page_size: 4096 - create database ... page_size 1024 - DB created. Actual page_size: 4096 - create database ... page_size 1023 default character set win1251 - DB created. Actual page_size: 4096 - create database ... page_size 1023 - DB created. Actual page_size: 4096 - create database ... page_size 0 default character set win1251 - DB created. Actual page_size: 8192 - create database ... page_size 0 - DB created. Actual page_size: 8192 - create database ... page_size 0x10000 default character set win1251 - DB created. Actual page_size: 8192 - create database ... page_size 0x10000 - DB created. Actual page_size: 8192 - create database ... page_size 0xFFFF default character set win1251 - DB created. Actual page_size: 8192 - create database ... page_size 0xFFFF - DB created. Actual page_size: 8192 - create database ... page_size 0x8000 default character set win1251 - DB created. Actual page_size: 8192 - create database ... page_size 0x8000 - DB created. Actual page_size: 8192 - create database ... page_size 0x7FFF default character set win1251 - DB created. Actual page_size: 8192 - create database ... page_size 0x7FFF - DB created. Actual page_size: 8192 - create database ... page_size 0x4000 default character set win1251 - DB created. Actual page_size: 8192 - create database ... page_size 0x4000 - DB created. Actual page_size: 8192 - create database ... page_size 0x3FFF default character set win1251 - DB created. Actual page_size: 8192 - create database ... page_size 0x3FFF - DB created. Actual page_size: 8192 - create database ... page_size 0x2000 default character set win1251 - DB created. Actual page_size: 8192 - create database ... page_size 0x2000 - DB created. Actual page_size: 8192 - create database ... page_size 0x1FFF default character set win1251 - DB created. Actual page_size: 8192 - create database ... page_size 0x1FFF - DB created. Actual page_size: 8192 - create database ... page_size 0x1000 default character set win1251 - DB created. Actual page_size: 8192 - create database ... page_size 0x1000 - DB created. Actual page_size: 8192 - create database ... page_size 0xFFF default character set win1251 - DB created. Actual page_size: 8192 - create database ... page_size 0xFFF - DB created. Actual page_size: 8192 - create database ... page_size 0x800 default character set win1251 - DB created. Actual page_size: 8192 - create database ... page_size 0x800 - DB created. Actual page_size: 8192 - create database ... page_size 0x7FF default character set win1251 - DB created. Actual page_size: 8192 - create database ... page_size 0x7FF - DB created. Actual page_size: 8192 - create database ... page_size 0x400 default character set win1251 - DB created. Actual page_size: 8192 - create database ... page_size 0x400 - DB created. Actual page_size: 8192 - create database ... page_size 0x3FF default character set win1251 - DB created. Actual page_size: 8192 - create database ... page_size 0x3FF - DB created. Actual page_size: 8192 - create database ... page_size default default character set win1251 - Statement failed, SQLSTATE = 42000 - Dynamic SQL Error - -SQL error code = -104 - -Token unknown - -default - create database ... page_size default - DB created. Actual page_size: 8192 - create database ... page_size null default character set win1251 - Statement failed, SQLSTATE = 42000 - Dynamic SQL Error - -SQL error code = -104 - -Token unknown - -null - create database ... page_size null - DB created. Actual page_size: 8192 - create database ... page_size qwerty default character set win1251 - Statement failed, SQLSTATE = 42000 - Dynamic SQL Error - -SQL error code = -104 - -Token unknown - -qwerty - create database ... page_size qwerty - DB created. Actual page_size: 8192 - create database ... page_size -32768 default character set win1251 - Statement failed, SQLSTATE = 42000 - Dynamic SQL Error - -SQL error code = -104 - -Token unknown - -- - create database ... page_size -32768 - Statement failed, SQLSTATE = 42000 - Dynamic SQL Error - -SQL error code = -104 - -Token unknown - -- -""" - page_list= ['9223372036854775809', '9223372036854775808', '9223372036854775807', @@ -316,6 +85,462 @@ @pytest.mark.version('>=4.0') def test_1(act: Action, capsys): + if act.is_version('<6'): + expected_stdout = """ + create database ... page_size 9223372036854775809 default character set win1251 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Token unknown + -9223372036854775809 + create database ... page_size 9223372036854775809 + DB created. Actual page_size: 32768 + create database ... page_size 9223372036854775808 default character set win1251 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Token unknown + -9223372036854775808 + create database ... page_size 9223372036854775808 + DB created. Actual page_size: 32768 + create database ... page_size 9223372036854775807 default character set win1251 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Token unknown + -9223372036854775807 + create database ... page_size 9223372036854775807 + DB created. Actual page_size: 32768 + create database ... page_size 4294967297 default character set win1251 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Token unknown + -4294967297 + create database ... page_size 4294967297 + DB created. Actual page_size: 32768 + create database ... page_size 4294967296 default character set win1251 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Token unknown + -4294967296 + create database ... page_size 4294967296 + DB created. Actual page_size: 32768 + create database ... page_size 4294967295 default character set win1251 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Token unknown + -4294967295 + create database ... page_size 4294967295 + DB created. Actual page_size: 32768 + create database ... page_size 2147483649 default character set win1251 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Token unknown + -2147483649 + create database ... page_size 2147483649 + DB created. Actual page_size: 32768 + create database ... page_size 2147483648 default character set win1251 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Token unknown + -2147483648 + create database ... page_size 2147483648 + DB created. Actual page_size: 32768 + create database ... page_size 2147483647 default character set win1251 + DB created. Actual page_size: 32768 + create database ... page_size 2147483647 + DB created. Actual page_size: 32768 + create database ... page_size 65537 default character set win1251 + DB created. Actual page_size: 32768 + create database ... page_size 65537 + DB created. Actual page_size: 32768 + create database ... page_size 32769 default character set win1251 + DB created. Actual page_size: 32768 + create database ... page_size 32769 + DB created. Actual page_size: 32768 + create database ... page_size 32768 default character set win1251 + DB created. Actual page_size: 32768 + create database ... page_size 32768 + DB created. Actual page_size: 32768 + create database ... page_size 32767 default character set win1251 + DB created. Actual page_size: 16384 + create database ... page_size 32767 + DB created. Actual page_size: 16384 + create database ... page_size 16385 default character set win1251 + DB created. Actual page_size: 16384 + create database ... page_size 16385 + DB created. Actual page_size: 16384 + create database ... page_size 16384 default character set win1251 + DB created. Actual page_size: 16384 + create database ... page_size 16384 + DB created. Actual page_size: 16384 + create database ... page_size 16383 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 16383 + DB created. Actual page_size: 8192 + create database ... page_size 8193 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 8193 + DB created. Actual page_size: 8192 + create database ... page_size 8192 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 8192 + DB created. Actual page_size: 8192 + create database ... page_size 8191 default character set win1251 + DB created. Actual page_size: 4096 + create database ... page_size 8191 + DB created. Actual page_size: 4096 + create database ... page_size 4097 default character set win1251 + DB created. Actual page_size: 4096 + create database ... page_size 4097 + DB created. Actual page_size: 4096 + create database ... page_size 4096 default character set win1251 + DB created. Actual page_size: 4096 + create database ... page_size 4096 + DB created. Actual page_size: 4096 + create database ... page_size 4095 default character set win1251 + DB created. Actual page_size: 4096 + create database ... page_size 4095 + DB created. Actual page_size: 4096 + create database ... page_size 2049 default character set win1251 + DB created. Actual page_size: 4096 + create database ... page_size 2049 + DB created. Actual page_size: 4096 + create database ... page_size 2048 default character set win1251 + DB created. Actual page_size: 4096 + create database ... page_size 2048 + DB created. Actual page_size: 4096 + create database ... page_size 2047 default character set win1251 + DB created. Actual page_size: 4096 + create database ... page_size 2047 + DB created. Actual page_size: 4096 + create database ... page_size 1025 default character set win1251 + DB created. Actual page_size: 4096 + create database ... page_size 1025 + DB created. Actual page_size: 4096 + create database ... page_size 1024 default character set win1251 + DB created. Actual page_size: 4096 + create database ... page_size 1024 + DB created. Actual page_size: 4096 + create database ... page_size 1023 default character set win1251 + DB created. Actual page_size: 4096 + create database ... page_size 1023 + DB created. Actual page_size: 4096 + create database ... page_size 0 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0 + DB created. Actual page_size: 8192 + create database ... page_size 0x10000 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0x10000 + DB created. Actual page_size: 8192 + create database ... page_size 0xFFFF default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0xFFFF + DB created. Actual page_size: 8192 + create database ... page_size 0x8000 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0x8000 + DB created. Actual page_size: 8192 + create database ... page_size 0x7FFF default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0x7FFF + DB created. Actual page_size: 8192 + create database ... page_size 0x4000 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0x4000 + DB created. Actual page_size: 8192 + create database ... page_size 0x3FFF default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0x3FFF + DB created. Actual page_size: 8192 + create database ... page_size 0x2000 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0x2000 + DB created. Actual page_size: 8192 + create database ... page_size 0x1FFF default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0x1FFF + DB created. Actual page_size: 8192 + create database ... page_size 0x1000 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0x1000 + DB created. Actual page_size: 8192 + create database ... page_size 0xFFF default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0xFFF + DB created. Actual page_size: 8192 + create database ... page_size 0x800 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0x800 + DB created. Actual page_size: 8192 + create database ... page_size 0x7FF default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0x7FF + DB created. Actual page_size: 8192 + create database ... page_size 0x400 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0x400 + DB created. Actual page_size: 8192 + create database ... page_size 0x3FF default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0x3FF + DB created. Actual page_size: 8192 + create database ... page_size default default character set win1251 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Token unknown + -default + create database ... page_size default + DB created. Actual page_size: 8192 + create database ... page_size null default character set win1251 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Token unknown + -null + create database ... page_size null + DB created. Actual page_size: 8192 + create database ... page_size qwerty default character set win1251 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Token unknown + -qwerty + create database ... page_size qwerty + DB created. Actual page_size: 8192 + create database ... page_size -32768 default character set win1251 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Token unknown + -- + create database ... page_size -32768 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Token unknown + -- + """ + else: + expected_stdout = """ + create database ... page_size 9223372036854775809 default character set win1251 + DB created. Actual page_size: 32768 + create database ... page_size 9223372036854775809 + DB created. Actual page_size: 32768 + create database ... page_size 9223372036854775808 default character set win1251 + DB created. Actual page_size: 32768 + create database ... page_size 9223372036854775808 + DB created. Actual page_size: 32768 + create database ... page_size 9223372036854775807 default character set win1251 + DB created. Actual page_size: 32768 + create database ... page_size 9223372036854775807 + DB created. Actual page_size: 32768 + create database ... page_size 4294967297 default character set win1251 + DB created. Actual page_size: 32768 + create database ... page_size 4294967297 + DB created. Actual page_size: 32768 + create database ... page_size 4294967296 default character set win1251 + DB created. Actual page_size: 32768 + create database ... page_size 4294967296 + DB created. Actual page_size: 32768 + create database ... page_size 4294967295 default character set win1251 + DB created. Actual page_size: 32768 + create database ... page_size 4294967295 + DB created. Actual page_size: 32768 + create database ... page_size 2147483649 default character set win1251 + DB created. Actual page_size: 32768 + create database ... page_size 2147483649 + DB created. Actual page_size: 32768 + create database ... page_size 2147483648 default character set win1251 + DB created. Actual page_size: 32768 + create database ... page_size 2147483648 + DB created. Actual page_size: 32768 + create database ... page_size 2147483647 default character set win1251 + DB created. Actual page_size: 32768 + create database ... page_size 2147483647 + DB created. Actual page_size: 32768 + create database ... page_size 65537 default character set win1251 + DB created. Actual page_size: 32768 + create database ... page_size 65537 + DB created. Actual page_size: 32768 + create database ... page_size 32769 default character set win1251 + DB created. Actual page_size: 32768 + create database ... page_size 32769 + DB created. Actual page_size: 32768 + create database ... page_size 32768 default character set win1251 + DB created. Actual page_size: 32768 + create database ... page_size 32768 + DB created. Actual page_size: 32768 + create database ... page_size 32767 default character set win1251 + DB created. Actual page_size: 32768 + create database ... page_size 32767 + DB created. Actual page_size: 32768 + create database ... page_size 16385 default character set win1251 + DB created. Actual page_size: 16384 + create database ... page_size 16385 + DB created. Actual page_size: 16384 + create database ... page_size 16384 default character set win1251 + DB created. Actual page_size: 16384 + create database ... page_size 16384 + DB created. Actual page_size: 16384 + create database ... page_size 16383 default character set win1251 + DB created. Actual page_size: 16384 + create database ... page_size 16383 + DB created. Actual page_size: 16384 + create database ... page_size 8193 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 8193 + DB created. Actual page_size: 8192 + create database ... page_size 8192 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 8192 + DB created. Actual page_size: 8192 + create database ... page_size 8191 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 8191 + DB created. Actual page_size: 8192 + create database ... page_size 4097 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 4097 + DB created. Actual page_size: 8192 + create database ... page_size 4096 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 4096 + DB created. Actual page_size: 8192 + create database ... page_size 4095 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 4095 + DB created. Actual page_size: 8192 + create database ... page_size 2049 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 2049 + DB created. Actual page_size: 8192 + create database ... page_size 2048 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 2048 + DB created. Actual page_size: 8192 + create database ... page_size 2047 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 2047 + DB created. Actual page_size: 8192 + create database ... page_size 1025 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 1025 + DB created. Actual page_size: 8192 + create database ... page_size 1024 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 1024 + DB created. Actual page_size: 8192 + create database ... page_size 1023 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 1023 + DB created. Actual page_size: 8192 + create database ... page_size 0 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0 + DB created. Actual page_size: 8192 + create database ... page_size 0x10000 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0x10000 + DB created. Actual page_size: 8192 + create database ... page_size 0xFFFF default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0xFFFF + DB created. Actual page_size: 8192 + create database ... page_size 0x8000 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0x8000 + DB created. Actual page_size: 8192 + create database ... page_size 0x7FFF default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0x7FFF + DB created. Actual page_size: 8192 + create database ... page_size 0x4000 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0x4000 + DB created. Actual page_size: 8192 + create database ... page_size 0x3FFF default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0x3FFF + DB created. Actual page_size: 8192 + create database ... page_size 0x2000 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0x2000 + DB created. Actual page_size: 8192 + create database ... page_size 0x1FFF default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0x1FFF + DB created. Actual page_size: 8192 + create database ... page_size 0x1000 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0x1000 + DB created. Actual page_size: 8192 + create database ... page_size 0xFFF default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0xFFF + DB created. Actual page_size: 8192 + create database ... page_size 0x800 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0x800 + DB created. Actual page_size: 8192 + create database ... page_size 0x7FF default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0x7FF + DB created. Actual page_size: 8192 + create database ... page_size 0x400 default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0x400 + DB created. Actual page_size: 8192 + create database ... page_size 0x3FF default character set win1251 + DB created. Actual page_size: 8192 + create database ... page_size 0x3FF + DB created. Actual page_size: 8192 + create database ... page_size default default character set win1251 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Token unknown + -default + create database ... page_size default + DB created. Actual page_size: 8192 + create database ... page_size null default character set win1251 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Token unknown + -null + create database ... page_size null + DB created. Actual page_size: 8192 + create database ... page_size qwerty default character set win1251 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Token unknown + -qwerty + create database ... page_size qwerty + DB created. Actual page_size: 8192 + create database ... page_size -32768 default character set win1251 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Token unknown + -- + create database ... page_size -32768 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Token unknown + -- + """ + + with act.connect_server() as srv: for page_size in page_list: for charset in [' default character set win1251', '']: diff --git a/tests/bugs/core_6329_test.py b/tests/bugs/core_6329_test.py index b15ce2a2..87e142f9 100644 --- a/tests/bugs/core_6329_test.py +++ b/tests/bugs/core_6329_test.py @@ -51,7 +51,12 @@ def test_1(act: Action, tmp_fbk: Path, capsys): act.reset() if act_retcode == 0: + gbak_output = '' try: - act.gbak(switches=['-b', '-se', 'localhost:service_mgr', str(act.db.db_path), str(tmp_fbk)], credentials=False) + act.gbak(switches=['-b', '-se', 'localhost:service_mgr', str(act.db.db_path), str(tmp_fbk)], credentials=False, combine_output = True) + gbak_output = act.clean_stdout finally: act.isql(switches=['-q'], input='drop global mapping tmp_mapping_6329;', combine_output = True) + + assert gbak_output == '' + diff --git a/tests/bugs/core_6343_test.py b/tests/bugs/core_6343_test.py index 714d5fce..2cc9887d 100644 --- a/tests/bugs/core_6343_test.py +++ b/tests/bugs/core_6343_test.py @@ -7,6 +7,9 @@ DESCRIPTION: JIRA: CORE-6343 FBTEST: bugs.core_6343 +notes: + [03.07.2025] pzotov + Suppress name of stored procedure from output - it has no matter in this test. """ import pytest @@ -82,48 +85,33 @@ """ -act = isql_act('db', test_script, substitutions=[('line:.*', '')]) +act = isql_act('db', test_script, substitutions=[('line:.*', ''), ('(-)?At procedure .*', '')]) expected_stdout = """ - 1 - 2 - - 1 - 2 - - 1 - 2 - - 1 - 2 -""" - -expected_stderr = """ + 1 + 2 Statement failed, SQLSTATE = 22012 arithmetic exception, numeric overflow, or string truncation -Integer divide by zero. The code attempted to divide an integer value by an integer divisor of zero. - -At procedure 'SP_TEST' - + 1 + 2 Statement failed, SQLSTATE = 22012 arithmetic exception, numeric overflow, or string truncation -Integer divide by zero. The code attempted to divide an integer value by an integer divisor of zero. - -At procedure 'SP_TEST' - + 1 + 2 Statement failed, SQLSTATE = 22012 arithmetic exception, numeric overflow, or string truncation -Integer divide by zero. The code attempted to divide an integer value by an integer divisor of zero. - -At procedure 'SP_TEST' - + 1 + 2 Statement failed, SQLSTATE = 22012 arithmetic exception, numeric overflow, or string truncation -Integer divide by zero. The code attempted to divide an integer value by an integer divisor of zero. - -At procedure 'SP_TEST' """ @pytest.mark.version('>=3.0.6') def test_1(act: Action): act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr - and act.clean_stdout == act.clean_expected_stdout) + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_6353_test.py b/tests/bugs/core_6353_test.py index 22121d59..b8aff723 100644 --- a/tests/bugs/core_6353_test.py +++ b/tests/bugs/core_6353_test.py @@ -7,6 +7,11 @@ DESCRIPTION: JIRA: CORE-6353 FBTEST: bugs.core_6353 +NOTES: + [03.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.889; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -193,7 +198,7 @@ act = isql_act('db', test_script, substitutions=[('line: [\\d]+, col: [\\d]+', ''), ('[ \t]+', ' ')]) -expected_stdout = """ +expected_stdout_5x = """ ID 1 ID2 170141183460469231731687303715884105726 ID -9223372036854775808 @@ -220,30 +225,70 @@ I_MAX 170141183460469231731687303715884105727 I_MIN -170141183460469231731687303715884105728 I_MAX 170141183460469231731687303715884105727 + Statement failed, SQLSTATE = 23000 + validation error for column "TEST2"."I_MIN", value "-2" + Statement failed, SQLSTATE = 23000 + validation error for column "TEST2"."I_MIN", value "2" V_MIN -170141183460469231731687303715884105728 V_MIN 170141183460469231731687303715884105727 P_MIN 170141183460469231731687303715884105727 P_MAX -170141183460469231731687303715884105728 + Statement failed, SQLSTATE = HY000 + exception 1 + -EX_ZERO_DIV_NOT_ALLOWED + -Can not delete -170141183460469231731687303715884105728 by zero + -At procedure 'SP_ZERO_DIV' P_MIN 1 P_MIN -1 """ -expected_stderr = """ +expected_stdout_6x = """ + ID 1 + ID2 170141183460469231731687303715884105726 + ID -9223372036854775808 + ID2 -1 + ID 9223372036854775807 + ID2 -1 + I_MIN -170141183460469231731687303715884105728 + I_MAX 170141183460469231731687303715884105727 + I_MIN -170141183460469231731687303715884105728 + I_MAX 170141183460469231731687303715884105727 + PLAN ("PUBLIC"."V_TEST0" "PUBLIC"."TEST0" ORDER "PUBLIC"."TEST0_I_MIN_DEC") + MAX -170141183460469231731687303715884105728 + PLAN ("PUBLIC"."V_TEST0" "PUBLIC"."TEST0" ORDER "PUBLIC"."TEST0_I_MIN_ASC") + MIN -170141183460469231731687303715884105728 + PLAN ("PUBLIC"."V_TEST0" "PUBLIC"."TEST0" ORDER "PUBLIC"."TEST0_I_MAX_DEC") + MAX 170141183460469231731687303715884105727 + PLAN ("PUBLIC"."V_TEST0" "PUBLIC"."TEST0" ORDER "PUBLIC"."TEST0_I_MAX_ASC") + MIN 170141183460469231731687303715884105727 + I_MIN -170141183460469231731687303715884105728 + I_MAX 170141183460469231731687303715884105727 + I_MIN -170141183460469231731687303715884105728 + I_MAX 170141183460469231731687303715884105727 + I_MIN -170141183460469231731687303715884105728 + I_MAX 170141183460469231731687303715884105727 + I_MIN -170141183460469231731687303715884105728 + I_MAX 170141183460469231731687303715884105727 Statement failed, SQLSTATE = 23000 - validation error for column "TEST2"."I_MIN", value "-2" + validation error for column "PUBLIC"."TEST2"."I_MIN", value "-2" Statement failed, SQLSTATE = 23000 - validation error for column "TEST2"."I_MIN", value "2" + validation error for column "PUBLIC"."TEST2"."I_MIN", value "2" + V_MIN -170141183460469231731687303715884105728 + V_MIN 170141183460469231731687303715884105727 + P_MIN 170141183460469231731687303715884105727 + P_MAX -170141183460469231731687303715884105728 Statement failed, SQLSTATE = HY000 exception 1 - -EX_ZERO_DIV_NOT_ALLOWED + -"PUBLIC"."EX_ZERO_DIV_NOT_ALLOWED" -Can not delete -170141183460469231731687303715884105728 by zero - -At procedure 'SP_ZERO_DIV' line: 8, col: 12 + -At procedure "PUBLIC"."SP_ZERO_DIV" + P_MIN 1 + P_MIN -1 """ + @pytest.mark.version('>=4.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_6411_test.py b/tests/bugs/core_6411_test.py index 157d55f1..72d34dc3 100644 --- a/tests/bugs/core_6411_test.py +++ b/tests/bugs/core_6411_test.py @@ -5,50 +5,38 @@ ISSUE: 6649 TITLE: FB crashes on attempt to create table with number of fields greater than 5460 DESCRIPTION: - It was found that maximal number of fields with type = BIGINT that could fit in a table DDL is 8066. - If this limit is exeeded then FB raises "new record size of N bytes is too big" (where N >= 65536). - We use for-loop with two iterations, each of them does following: - 1. Create table with total number of fields = (one for 'ID primary key' plus 8064 for - 'user-data' fields with names 'F1', 'F2', ..., 'F'-1). All of them have type = BIGINT. - 2. DO RECONNECT // mandatory! otherwise crash can not be reproduced. - 3. Run UPDATE OR INSERT statement that is specified in the ticker(insert single record with ID=1). - 4. Run SELECT statement which calculates total sum on all 'user-data' fields. - When N = 8065 then these actions must complete successfully and result of final SELECT must be displayed. - When N = 8066 then we have to get exception: - Statement failed, SQLSTATE = 54000 - unsuccessful metadata update - -new record size of 65536 bytes is too big - - Confirmed bug on 4.0.0.2204: got crash when N=8065 (but still "new record size of 65536 bytes is too big" when N=8066). - Checked on 3.0.7.33368, 4.0.0.2214 - all OK. -NOTES: -[08.02.2022] pcisar - Fails on Windows 3.0.8 with diff: - step: 0, FLD_COUNT: 8064, result: FIELDS_TOTAL 32510016 - step: 1, FLD_COUNT: 8065, result: Statement failed, SQLSTATE = 54000 - step: 1, FLD_COUNT: 8065, result: unsuccessful metadata update - step: 1, FLD_COUNT: 8065, result: -new record size of 65536 bytes is too big - - step: 1, FLD_COUNT: 8065, result: -TABLE TDATA - + step: 1, FLD_COUNT: 8065, result: -TABLE TDATA - ? + - + step: 1, FLD_COUNT: 8065, result: Statement failed, SQLSTATE = 21S01 - + step: 1, FLD_COUNT: 8065, result: Dynamic SQL Error - + step: 1, FLD_COUNT: 8065, result: -SQL error code = -804 - + step: 1, FLD_COUNT: 8065, result: -Count of read-write columns does not equal count of values - -[24.03.2022] pzotov - On WINDOWS excessive lines in STDERR (after first error) appear because ISQL ignores 'SET BAIL' command or '-bail' switch - in case when it executing commands that come via PIPE mechanism. - Discussed with FB-team, letter with subj "[new-qa] ISQL "SET BAIL ON" problem on Windows when commands come from PIPE", 12-mar-2022 16:04. - It looks strange but such behaviour was considered as DESIRED feature and requested long ago (in 2011). - This means that it is very unlikely that it will be fixed and ISQL will behave like on POSIX (where no such effect occurs). - Because of this, it was decided to change test: we create temporary SQL file and provide this file as input script for launching ISQL, - see: - act.isql(..., input_file=isql_script, ...) - - + It was found that maximal number of fields with type = BIGINT that could fit in a table DDL is 8066. + If this limit is exeeded then FB raises "new record size of N bytes is too big" (where N >= 65536). + We use for-loop with two iterations, each of them does following: + 1. Create table with total number of fields = (one for 'ID primary key' plus 8064 for + 'user-data' fields with names 'F1', 'F2', ..., 'F'-1). All of them have type = BIGINT. + 2. DO RECONNECT // mandatory! otherwise crash can not be reproduced. + 3. Run UPDATE OR INSERT statement that is specified in the ticker(insert single record with ID=1). + 4. Run SELECT statement which calculates total sum on all 'user-data' fields. + When N = 8065 then these actions must complete successfully and result of final SELECT must be displayed. + When N = 8066 then we have to get exception: + Statement failed, SQLSTATE = 54000 + unsuccessful metadata update + -new record size of 65536 bytes is too big + + Confirmed bug on 4.0.0.2204: got crash when N=8065 (but still "new record size of 65536 bytes is too big" when N=8066). + Checked on 3.0.7.33368, 4.0.0.2214 - all OK. JIRA: CORE-6411 FBTEST: bugs.core_6411 +NOTES: + [24.03.2022] pzotov + On WINDOWS excessive lines in STDERR (after first error) appear because ISQL ignores 'SET BAIL' command or '-bail' switch + in case when it executing commands that come via PIPE mechanism. + Discussed with FB-team, letter with subj "[new-qa] ISQL "SET BAIL ON" problem on Windows when commands come from PIPE", 12-mar-2022 16:04. + It looks strange but such behaviour was considered as DESIRED feature and requested long ago (in 2011). + This means that it is very unlikely that it will be fixed and ISQL will behave like on POSIX (where no such effect occurs). + Because of this, it was decided to change test: we create temporary SQL file and provide this file as input script for launching ISQL, + see: + act.isql(..., input_file=isql_script, ...) + [03.07.2025] pzotov + ::: NB ::: test duration time is ~6 minutes. + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.884; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -61,18 +49,8 @@ act = python_act('db', substitutions=[('.*(-)?After line \\d+.*', ''), ('[ \t]+', ' ')]) -expected_stdout = """ - step: 0, FLD_COUNT: 8064, result: FIELDS_TOTAL 32510016 - step: 1, FLD_COUNT: 8065, result: Statement failed, SQLSTATE = 54000 - step: 1, FLD_COUNT: 8065, result: unsuccessful metadata update - step: 1, FLD_COUNT: 8065, result: -new record size of 65536 bytes is too big - step: 1, FLD_COUNT: 8065, result: -TABLE TDATA -""" - - isql_script = temp_file('test-script-6411.sql') - @pytest.mark.version('>=3.0.7') def test_1(act: Action, isql_script: Path, capsys): for step in range(0,2): @@ -95,7 +73,7 @@ def test_1(act: Action, isql_script: Path, capsys): set list on ; {sel_expr} ; quit ; - """ + """ isql_script.write_text(sql_expr) act.reset() @@ -106,6 +84,17 @@ def test_1(act: Action, isql_script: Path, capsys): print(f'step: {step}, FLD_COUNT: {FLD_COUNT}, result: {line}') act.reset() + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + TABLE_NAME = 'TDATA' if act.is_version('<6') else '"TDATA"' + expected_stdout = f""" + step: 0, FLD_COUNT: 8064, result: FIELDS_TOTAL 32510016 + step: 1, FLD_COUNT: 8065, result: Statement failed, SQLSTATE = 54000 + step: 1, FLD_COUNT: 8065, result: unsuccessful metadata update + step: 1, FLD_COUNT: 8065, result: -new record size of 65536 bytes is too big + step: 1, FLD_COUNT: 8065, result: -TABLE {SQL_SCHEMA_PREFIX}{TABLE_NAME} + """ + act.expected_stdout = expected_stdout act.stdout = capsys.readouterr().out diff --git a/tests/bugs/core_6414_test.py b/tests/bugs/core_6414_test.py index 3046673e..6d887286 100644 --- a/tests/bugs/core_6414_test.py +++ b/tests/bugs/core_6414_test.py @@ -3,8 +3,7 @@ """ ID: issue-6652 ISSUE: 6652 -TITLE: Error message "expected length N, actual M" contains wrong value of M when - charset UTF8 is used in the field declaration of a table +TITLE: Error message "expected length N, actual M" contains wrong value of M when charset UTF8 is used in the field declaration of a table DESCRIPTION: All attempts to create/alter table with not-null column with size that not enough space to fit default value must fail. Length of such column can be declared either directly or via domain - and both of these ways must fail. @@ -133,6 +132,7 @@ -expected length 1, actual 8 """ +@pytest.mark.intl @pytest.mark.version('>=4.0') def test_1(act: Action): act.expected_stderr = expected_stderr diff --git a/tests/bugs/core_6419_test.py b/tests/bugs/core_6419_test.py index aee8b911..5acb7ddd 100644 --- a/tests/bugs/core_6419_test.py +++ b/tests/bugs/core_6419_test.py @@ -67,6 +67,7 @@ Records affected: 1 """ +@pytest.mark.intl @pytest.mark.version('>=4.0') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/core_6438_test.py b/tests/bugs/core_6438_test.py index 9dee6761..4da1cc38 100644 --- a/tests/bugs/core_6438_test.py +++ b/tests/bugs/core_6438_test.py @@ -2,11 +2,16 @@ """ ID: issue-2366 -ISSUE: 2366 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/2366 TITLE: ISQL: bad headers when text columns has >= 80 characters DESCRIPTION: JIRA: CORE-6438 FBTEST: bugs.core_6438 +NOTES: + [19.03.2025] pzotov + Fix https://github.com/FirebirdSQL/firebird/commit/37a42a6093077c9156a5853cfe69bba1ea92a468 (08-nov-2020) + Confirmed bug on 3.0.7.33388 (07-nov-2020), 4.0.0.2240 (04-nov-2020). + Checked on 4.0.0.2249 (09-nov-2020) """ import pytest @@ -16,25 +21,32 @@ act = python_act('db') -expected_stdout = """ -hdr_len: 65533 -txt_len: 65533 -""" +PAD_TO_WIDTH = 65533 @pytest.mark.version('>=4.0') def test_1(act: Action, capsys): - data = '1' * 65533 - act.isql(switches=[], input=f'''select '{data}' as " ", 1 as " " from rdb$database;''', combine_output = True) + data = '1' * PAD_TO_WIDTH + test_sql = f""" + set list off; + select '{data}' as col_1, 1 as col_2 from rdb$database; + """ + act.isql(switches=[], input = test_sql, combine_output = True) hdr_len = txt_len = 0 for line in act.stdout.splitlines(): if line.startswith('='): + # before fix: hdr_len=79 instead of expected {PAD_TO_WIDTH} hdr_len = len(line.split()[0]) elif line.startswith('1'): txt_len = len(line.split()[0]) + act.reset() + print('hdr_len:', hdr_len) print('txt_len:', txt_len) - # - act.reset() + + expected_stdout = f""" + hdr_len: {PAD_TO_WIDTH} + txt_len: {PAD_TO_WIDTH} + """ act.expected_stdout = expected_stdout act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_6440_test.py b/tests/bugs/core_6440_test.py index 9833c0b2..2662b7d5 100644 --- a/tests/bugs/core_6440_test.py +++ b/tests/bugs/core_6440_test.py @@ -4,11 +4,15 @@ ID: issue-6674 ISSUE: 6674 TITLE: Expression indexes containing COALESCE inside cannot be matched by the optimizer - after migration from v2.5 to v3.0 DESCRIPTION: Test uses .fbk that was created on FB 2.5.9, file: core6440-ods11.fbk JIRA: CORE-6440 FBTEST: bugs.core_6440 +NOTES: + [03.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.892; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813 """ import pytest @@ -47,20 +51,24 @@ act = isql_act('db', test_script) -expected_stdout = """ +expected_stdout_5x = """ PLAN (TEST INDEX (PK_TEST)) - PLAN (TEST INDEX (TEST_IDX4)) - PLAN (TEST INDEX (TEST_IDX3)) - PLAN (TEST INDEX (TEST_IDX2)) - PLAN (TEST INDEX (TEST_IDX1)) """ +expected_stdout_6x = """ + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."PK_TEST")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_IDX4")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_IDX3")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_IDX2")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_IDX1")) +""" + @pytest.mark.version('>=3.0.8') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_6444_test.py b/tests/bugs/core_6444_test.py index 87757aac..d06c4847 100644 --- a/tests/bugs/core_6444_test.py +++ b/tests/bugs/core_6444_test.py @@ -15,6 +15,10 @@ Added 'SQLSTATE' in substitutions: runtime error must not be filtered out by '?!(...)' pattern ("negative lookahead assertion", see https://docs.python.org/3/library/re.html#regular-expression-syntax). Added 'combine_output = True' in order to see SQLSTATE if any error occurs. + [03.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.892; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813 """ import os import pytest @@ -31,18 +35,20 @@ @pytest.mark.version('>=4.0') def test_1(act: Action): - act.expected_stdout = """ + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'SYSTEM.' + act.expected_stdout = f""" 01: sqltype: 496 LONG scale: 0 subtype: 0 len: 4 : name: RDB$CONFIG_ID alias: RDB$CONFIG_ID - 02: sqltype: 448 VARYING scale: 0 subtype: 0 len: 63 charset: 2 ASCII + 02: sqltype: 448 VARYING scale: 0 subtype: 0 len: 63 charset: 2 {SQL_SCHEMA_PREFIX}ASCII : name: RDB$CONFIG_NAME alias: RDB$CONFIG_NAME - 03: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 1020 charset: 4 UTF8 + 03: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 1020 charset: 4 {SQL_SCHEMA_PREFIX}UTF8 : name: RDB$CONFIG_VALUE alias: RDB$CONFIG_VALUE - 04: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 1020 charset: 4 UTF8 + 04: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 1020 charset: 4 {SQL_SCHEMA_PREFIX}UTF8 : name: RDB$CONFIG_DEFAULT alias: RDB$CONFIG_DEFAULT 05: sqltype: 32764 BOOLEAN scale: 0 subtype: 0 len: 1 : name: RDB$CONFIG_IS_SET alias: RDB$CONFIG_IS_SET - 06: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 1020 charset: 4 UTF8 + 06: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 1020 charset: 4 {SQL_SCHEMA_PREFIX}UTF8 : name: RDB$CONFIG_SOURCE alias: RDB$CONFIG_SOURCE """ act.execute(combine_output = True) diff --git a/tests/bugs/core_6458_test.py b/tests/bugs/core_6458_test.py index ca6c82c8..59e2eecc 100644 --- a/tests/bugs/core_6458_test.py +++ b/tests/bugs/core_6458_test.py @@ -5,35 +5,41 @@ ISSUE: 6691 TITLE: Regression: Cancel Query function no longer works DESCRIPTION: - We create .sql script with 'heavy query' that for sure will run more than several seconds. - Then we launch asynchronously ISQL to perform this query and wait until its PID and running - query will appear in the mon$ tables (we are looking for query that containing text 'HEAVY_TAG'). - - After this we send signal CTRL_C_EVENT for emulating interruption that is done by pressing Ctrl-C. - Then we wait for process finish (call wait() method) - this is necessary if ISQL will continue - without interruprion (i.e. if something will be broken again). - - When method wait() will return control back, we can obtain info about whether child process was - terminated or no (using method poll()). If yes (expected) then it must return 1. - - Finally, we check ISQL logs for STDOUT and STDERR. They must be as follows: - * STDOUT -- must be empty - * STDERR -- must contain (at least) two phrases: - 1. Statement failed, SQLSTATE = HY008 - 2. operation was cancelled - - ::: NB ::: - Windows only: subprocess.Popen() must have argument: creationflags = subprocess.CREATE_NEW_PROCESS_GROUP - Otherwise we can not send signal Ctrl_C_EVENT to the child process. - Linux: parameter 'creationflags' must be 0, signal.SIGINT is used instead of Ctrl_C_EVENT. - - See: https://docs.python.org/2.7/library/subprocess.html - - Confirmed bug on 4.0.0.2307: query could NOT be interrupted and we had to wait until it completed. - Checked on 4.0.0.2324 (SS/CS): works OK, query can be interrupted via sending Ctrl-C signal. - -JIRA: CORE-6458 + We create .sql script with 'heavy query' that for sure will run more than several seconds. + Then we launch asynchronously ISQL to perform this query and wait until its PID and running + query will appear in the mon$ tables (we are looking for query that containing text 'HEAVY_TAG'). + + After this we send signal CTRL_C_EVENT for emulating interruption that is done by pressing Ctrl-C. + Then we wait for process finish (call wait() method) - this is necessary if ISQL will continue + without interruprion (i.e. if something will be broken again). + + When method wait() will return control back, we can obtain info about whether child process was + terminated or no (using method poll()). If yes (expected) then it must return 1. + + Finally, we check ISQL logs for STDOUT and STDERR. They must be as follows: + * STDOUT -- must be empty + * STDERR -- must contain (at least) two phrases: + 1. Statement failed, SQLSTATE = HY008 + 2. operation was cancelled FBTEST: bugs.core_6458 +NOTES: + + ::: NB ::: + Windows only: subprocess.Popen() must have argument: creationflags = subprocess.CREATE_NEW_PROCESS_GROUP + Otherwise we can not send signal Ctrl_C_EVENT to the child process. + Linux: parameter 'creationflags' must be 0, signal.SIGINT is used instead of Ctrl_C_EVENT. + + See: https://docs.python.org/2.7/library/subprocess.html + + Confirmed bug on 4.0.0.2307: query could NOT be interrupted and we had to wait until it completed. + Checked on 4.0.0.2324 (SS/CS): works OK, query can be interrupted via sending Ctrl-C signal. + + [18.01.2025] pzotov + Resultset of cursor that executes using instance of selectable PreparedStatement must be stored + in some variable in order to have ability close it EXPLICITLY (before PS will be freed). + Otherwise access violation raises during Python GC and pytest hangs at final point (does not return control to OS). + This occurs at least for: Python 3.11.2 / pytest: 7.4.4 / firebird.driver: 1.10.6 / Firebird.Qa: 0.19.3 + The reason of that was explained by Vlad, 26.10.24 17:42 ("oddities when use instances of selective statements"). """ import re @@ -100,28 +106,44 @@ def test_1(act: Action, heavy_script: Path, heavy_stdout: Path, heavy_stderr: Pa tx_watcher = con_watcher.transaction_manager(custom_tpb) cur_watcher = tx_watcher.cursor() - ps = cur_watcher.prepare(chk_mon_sql) - - i = 0 - da = dt.now() - while True: - cur_watcher.execute(ps, (p_heavy_sql.pid, HEAVY_TAG,) ) - mon_result = -1 - for r in cur_watcher: - mon_result = r[0] - - tx_watcher.commit() - db = dt.now() - diff_ms = (db-da).seconds*1000 + (db-da).microseconds//1000 - if mon_result == 1: - found_in_mon_tables = True - break - elif diff_ms > MAX_WAIT_FOR_ISQL_PID_APPEARS_MS: - break + ps, rs = None, None + try: + ps = cur_watcher.prepare(chk_mon_sql) + + i = 0 + da = dt.now() + while True: + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + rs = cur_watcher.execute(ps, (p_heavy_sql.pid, HEAVY_TAG,) ) + mon_result = -1 + for r in rs: + mon_result = r[0] + + tx_watcher.commit() + db = dt.now() + diff_ms = (db-da).seconds*1000 + (db-da).microseconds//1000 + if mon_result == 1: + found_in_mon_tables = True + break + elif diff_ms > MAX_WAIT_FOR_ISQL_PID_APPEARS_MS: + break + time.sleep(0.1) + + except DatabaseError as e: + print( e.__str__() ) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() - time.sleep(0.1) - ps.free() assert found_in_mon_tables, f'Could not find attachment in mon$ tables for {MAX_WAIT_FOR_ISQL_PID_APPEARS_MS} ms.' diff --git a/tests/bugs/core_6469_test.py b/tests/bugs/core_6469_test.py index 7bbc7d5b..193b8c5e 100644 --- a/tests/bugs/core_6469_test.py +++ b/tests/bugs/core_6469_test.py @@ -1,282 +1,314 @@ -#coding:utf-8 - -""" -ID: issue-2376 -ISSUE: 2376 -TITLE: Provide ability to see in the trace log actions related to session management (e.g. ALTER SESSION RESET) -DESCRIPTION: - Test verifies management statements which are specified in doc/sql.extensions/README.management_statements_psql.md - We launch trace session before ISQL and stop it after its finish. - Every management statement is expected to be found in the trace log. - - ATTENTION: TWO SEPARATE BRANCHES present in this test for different OS. - - NOTES FOR WINDOWS - ################# - Statement 'SET TRUSTED ROLE' is verified for appearance in the trace log. - There are several prerequisites that must be met for check SET TRUSTED ROLE statement: - * BOTH AuthServer and AuthClient parameters from firebird.conf contain 'Win_Sspi' as plugin, in any place; - * current OS user has admin rights; - * OS environment has *no* variables ISC_USER and ISC_PASSWORD (i.e. they must be UNSET); - * Two mappings are created (both uses plugin win_sspi): - ** from any user to user; - ** from predefined_group domain_any_rid_admins to role - - Connect to database should be done in form: CONNECT ':' role ', - and after this we can user 'SET TRUSTED ROLE' statement (see also: core_5887-trusted_role.fbt). - - ::: NOTE ::: - We have to remove OS-veriable 'ISC_USER' before any check of trusted role. - This variable could be set by other .fbts which was performed before current within batch mode (i.e. when fbt_run is called from ) - - NOTES FOR LINUX - ############### - Trusted role is not verified for this case. - Weird behaviour detected when test was ran on FB 4.0.0.2377 SuperServer: if we run this test several times (e.g. in loop) then *all* - statements related to session management can be missed in the trace - despite the fact that they *for sure* was performed successfully - (this can be seen in ISQL log). It seems that fail somehow related to the duration of DELAY between subsequent runs: if delay more than ~30s - then almost no fails. But if delay is small then test can fail for almost every run. - NO such trouble in the Classic. - The reason currently (03-mar-2021) remains unknown. - Sent letter to Alex et al, 03-mar-2021. -NOTES: -[09.02.2022] pcisar - Test fails on Windows as script execution fails with: - Statement failed, SQLSTATE = 0P000 - Your attachment has no trusted role - -[08.04.2022] pzotov - [WINDOWS] - 1. The 'CONNECT ...' operator, being specified without USER/PASSWORD clauses, will take in account parameters that were specified in the command line - of ISQL (confirmed by Alex, letter 03.04.2022 20:31). - This means that it will use 'SYSDBA' / 'masterkey' rather than Windows trusted auth. This, in turn, leads that SYSDBA will be current user - when following is performed: - connect '{THIS_COMPUTER_NAME}:{act.db.db_path}' role tmp$r6469; - - and it causes 'set trusted role' to fail (SQLSTATE = 0P000 / Your attachment has no trusted role). - Because of this, we have to launch ISQL without using current credentials (which is True by default) - see 'credentials = False'. - 2. One need to run ISQL with requirement do NOT establish connection to the test database because this will be done in the test script itself. - Otherwise we get 'Missing security context' *after* test finishes (the reason is unknown; instead, "Rolling back work." must be issued and redirected to STDERR). - To prevent such error, we have to specify 'connect_db = False' in db_factory() call. - - Checked on 4.0.1 Release, 5.0.0.467. - - -JIRA: CORE-6469 -FBTEST: bugs.core_6469 -""" - -import os -import pytest -import re -import socket -import getpass -from pathlib import Path -from firebird.qa import * -import time - -try: - del os.environ["ISC_USER"] -except KeyError as e: - pass - -db = db_factory() - -act = python_act('db') - -test_role = role_factory('db', name='TMP$R6469') -tmp_file = temp_file('c6469_tmp.sql') - -################################ -### W I N D O W S ### -################################ - -# version: 4.0 - Windows - -expected_stdout_win = """ - alter session reset - set session idle timeout 1800 second - set statement timeout 190 second - set bind of decfloat to double precision - set decfloat round ceiling - set decfloat traps to division_by_zero - set time zone 'america/sao_paulo' - set role tmp$r6469 - set trusted role -""" - -trace_win = ['log_initfini = false', - 'log_statement_finish = true', - 'log_errors = true', - 'time_threshold = 0', - ] - -patterns_win = [re.compile('alter session reset', re.IGNORECASE), - re.compile('set session idle timeout', re.IGNORECASE), - re.compile('set statement timeout', re.IGNORECASE), - re.compile('set bind of decfloat to double precision', re.IGNORECASE), - re.compile('set decfloat round ceiling', re.IGNORECASE), - re.compile('set decfloat traps to Division_by_zero', re.IGNORECASE), - re.compile('set time zone', re.IGNORECASE), - re.compile('set role', re.IGNORECASE), - re.compile('set trusted role', re.IGNORECASE)] - -def run_script(act: Action, tmp_file: Path): - #__tracebackhide__ = True - THIS_COMPUTER_NAME = socket.gethostname() - CURRENT_WIN_ADMIN = getpass.getuser() - script = f""" - set bail on; - set list on; - set echo on; - connect '{act.db.dsn}' user '{act.db.user}' password '{act.db.password}'; - grant tmp$r6469 to "{THIS_COMPUTER_NAME}\\{CURRENT_WIN_ADMIN}"; - commit; - - -- We have to use here "create mapping trusted_auth ... from any user to user" otherwise get - -- Statement failed, SQLSTATE = 28000 /Missing security context for C:\\FBTESTING\\QA\\MISC\\C5887.FDB - -- on connect statement which specifies COMPUTERNAME:USERNAME instead path to DB: - create or alter mapping trusted_auth using plugin win_sspi from any user to user; - - -- We have to use here "create mapping win_admins ... DOMAIN_ANY_RID_ADMINS" otherwise get - -- Statement failed, SQLSTATE = 0P000 / Your attachment has no trusted role - create or alter mapping win_admins using plugin win_sspi from predefined_group domain_any_rid_admins to role tmp$r6469; - commit; - - -- We have to GRANT ROLE, even to SYSDBA. Otherwise: - -- Statement failed, SQLSTATE = 0P000 - -- Role TMP$R6469 is invalid or unavailable - grant TMP$R6469 to sysdba; - commit; - show role; - show grants; - show mapping; - - set autoddl off; - commit; - - -- Following management statements are taken from - -- doc/sql.extensions/README.management_statements_psql.md: - -- ######################################################## - alter session reset; - set session idle timeout 1800 second; - set statement timeout 190 second; - set bind of decfloat to double precision; - set decfloat round ceiling; - set decfloat traps to Division_by_zero; - set time zone 'America/Sao_Paulo'; - set role tmp$r6469; - commit; - - connect '{THIS_COMPUTER_NAME}:{act.db.db_path}' role tmp$r6469; - - select mon$user,mon$role,mon$auth_method from mon$attachments where mon$attachment_id = current_connection; - commit; - - set trusted role; - commit; - - connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; - drop mapping trusted_auth; - drop mapping win_admins; - commit; - """ - tmp_file.write_text(script) - - act.isql(switches=['-n'], input_file = tmp_file, connect_db = False, credentials = False) - -@pytest.mark.version('>=4.0') -@pytest.mark.platform('Windows') -def test_1(act: Action, test_role: Role, tmp_file: Path, capsys): - with act.trace(db_events=trace_win): - run_script(act, tmp_file) - - # process trace - for line in act.trace_log: - if line.split(): - if act.match_any(line, patterns_win): - print(' '.join(line.split()).lower()) - # Check - act.expected_stdout = expected_stdout_win - act.stdout = capsys.readouterr().out - assert act.clean_stdout == act.clean_expected_stdout - - - -################################ -### L I N U X ### -################################ -# version: 4.0 - Linux - -expected_stdout_lin = """ - alter session reset - set session idle timeout 1800 second - set statement timeout 190 second - set bind of decfloat to double precision - set decfloat round ceiling - set decfloat traps to division_by_zero - set time zone 'america/sao_paulo' - set role tmp$r6469 -""" - -test_script_lin = """ - set bail on; - set list on; - - -- We have to GRANT ROLE, even to SYSDBA. Otherwise: - -- Statement failed, SQLSTATE = 0P000 - -- Role TMP$R6469 is invalid or unavailable - grant TMP$R6469 to sysdba; - commit; - - select current_user as who_ami, current_role as whats_my_role from rdb$database; - set autoddl off; - commit; - - -- Following management statements are taken from - -- doc/sql.extensions/README.management_statements_psql.md: - -- ######################################################## - set echo on; - alter session reset; - set session idle timeout 1800 second; - set statement timeout 190 second; - set bind of decfloat to double precision; - set decfloat round ceiling; - set decfloat traps to Division_by_zero; - set time zone 'America/Sao_Paulo'; - set role tmp$r6469; - commit; - select 'Completed' as msg from rdb$database; -""" - -trace_lin = ['log_initfini = false', - 'log_connections = true', - 'log_statement_finish = true', - 'log_errors = true', - 'time_threshold = 0', - ] - -patterns_lin = [re.compile('alter session reset', re.IGNORECASE), - re.compile('set session idle timeout', re.IGNORECASE), - re.compile('set statement timeout', re.IGNORECASE), - re.compile('set bind of decfloat to double precision', re.IGNORECASE), - re.compile('set decfloat round ceiling', re.IGNORECASE), - re.compile('set decfloat traps to Division_by_zero', re.IGNORECASE), - re.compile('set time zone', re.IGNORECASE), - re.compile('set role', re.IGNORECASE)] - -@pytest.mark.version('>=4.0') -@pytest.mark.platform('Linux') -def test_2(act: Action, test_role: Role, capsys): - with act.trace(db_events=trace_lin): - act.isql(switches=['-n'], input=test_script_lin) - - # process trace - for line in act.trace_log: - if line.split(): - if act.match_any(line, patterns_lin): - print(' '.join(line.split()).lower()) - # Check - act.expected_stdout = expected_stdout_lin - act.stdout = capsys.readouterr().out - assert act.clean_stdout == act.clean_expected_stdout - +#coding:utf-8 + +""" +ID: issue-2376 +ISSUE: 2376 +TITLE: Provide ability to see in the trace log actions related to session management (e.g. ALTER SESSION RESET) +DESCRIPTION: + Test verifies management statements which are specified in doc/sql.extensions/README.management_statements_psql.md + We launch trace session before ISQL and stop it after its finish. + Every management statement is expected to be found in the trace log. +NOTES: + [08.04.2022] pzotov + ATTENTION: TWO SEPARATE BRANCHES present in this test for different OS. + 1. NOTES FOR WINDOWS + #################### + Statement 'SET TRUSTED ROLE' is verified for appearance in the trace log. + There are several prerequisites that must be met for check SET TRUSTED ROLE statement: + * BOTH AuthServer and AuthClient parameters from firebird.conf contain 'Win_Sspi' as plugin, in any place; + * current OS user has admin rights, otherwise we get "SQLSTATE = 0P000 / Your attachment has no trusted role" + * OS environment has *no* variables ISC_USER and ISC_PASSWORD (i.e. they must be UNSET); + * Two mappings are created (both uses plugin win_sspi): + ** from any user to user; + ** from predefined_group domain_any_rid_admins to role + + Connect to database should be done in form: CONNECT ':' role ', + and after this we can user 'SET TRUSTED ROLE' statement (see also: core_5887-trusted_role.fbt). + + ::: NOTE ::: + We have to remove OS-veriable 'ISC_USER' before any check of trusted role. + This variable could be set by other .fbts which was performed before current within batch mode (i.e. when fbt_run is called from ) + + 2. NOTES FOR LINUX + ################## + Trusted role is not verified for this case. + Weird behaviour detected when test was ran on FB 4.0.0.2377 SuperServer: if we run this test several times (e.g. in loop) then *all* + statements related to session management can be missed in the trace - despite the fact that they *for sure* was performed successfully + (this can be seen in ISQL log). It seems that fail somehow related to the duration of DELAY between subsequent runs: if delay more than ~30s + then almost no fails. But if delay is small then test can fail for almost every run. + NO such trouble in the Classic. + The reason currently (03-mar-2021) remains unknown. + Sent letter to Alex et al, 03-mar-2021. + + [WINDOWS] + 1. The 'CONNECT ...' operator, being specified without USER/PASSWORD clauses, will take in account parameters that were specified in the command line + of ISQL (confirmed by Alex, letter 03.04.2022 20:31). + This means that it will use 'SYSDBA' / 'masterkey' rather than Windows trusted auth. This, in turn, leads that SYSDBA will be current user + when following is performed: + connect '{THIS_COMPUTER_NAME}:{act.db.db_path}' role tmp$r6469; + - and it causes 'set trusted role' to fail (SQLSTATE = 0P000 / Your attachment has no trusted role). + Because of this, we have to launch ISQL without using current credentials (which is True by default) - see 'credentials = False'. + 2. One need to run ISQL with requirement do NOT establish connection to the test database because this will be done in the test script itself. + Otherwise we get 'Missing security context' *after* test finishes (the reason is unknown; instead, "Rolling back work." must be issued and redirected to STDERR). + To prevent such error, we have to specify 'connect_db = False' in db_factory() call. + Checked on 4.0.1 Release, 5.0.0.467. + + [02.08.2024] pzotov + One need to check for admin rights of current OS user (noted by Dimitry Sibiryakov). + ISQL output must be checked for matching to expected before trace log (see func run_script()). + Replaced hard-coded name of role with 'f{tmp_role.name}' notation. + + Checked on Windows 6.0.0.406, 5.0.1.1469, 4.0.5.3139 + +JIRA: CORE-6469 +FBTEST: bugs.core_6469 +""" + +import os +import ctypes +import pytest +import re +import socket +import getpass +from pathlib import Path +from firebird.qa import * +import time + +try: + del os.environ["ISC_USER"] +except KeyError as e: + pass + +db = db_factory() + +act = python_act('db', substitutions = [('[ \t]+', ' '), ]) + +tmp_role = role_factory('db', name='TMP$R6469') +tmp_file = temp_file('c6469_tmp.sql') + +################################ +### W I N D O W S ### +################################ + +# version: 4.0 - Windows + +trace_win = ['log_initfini = false', + 'log_statement_finish = true', + 'log_errors = true', + 'time_threshold = 0', + ] + +patterns_win = [re.compile('alter session reset', re.IGNORECASE), + re.compile('set session idle timeout', re.IGNORECASE), + re.compile('set statement timeout', re.IGNORECASE), + re.compile('set bind of decfloat to double precision', re.IGNORECASE), + re.compile('set decfloat round ceiling', re.IGNORECASE), + re.compile('set decfloat traps to Division_by_zero', re.IGNORECASE), + re.compile('set time zone', re.IGNORECASE), + re.compile('set role', re.IGNORECASE), + re.compile('set trusted role', re.IGNORECASE)] + +#---------------------------------------------------------- + +def is_admin(): + # https://serverfault.com/questions/29659/crossplatform-way-to-check-admin-rights-in-python-script + # Checked on Windows 10. + try: + is_admin = os.getuid() == 0 + except AttributeError: + is_admin = ctypes.windll.shell32.IsUserAnAdmin() + + return is_admin + +#---------------------------------------------------------- + +def run_script(act: Action, tmp_role: Role, tmp_file: Path): + #__tracebackhide__ = True + THIS_COMPUTER_NAME = socket.gethostname() + CURRENT_WIN_ADMIN = getpass.getuser() + script = f""" + set bail on; + set list on; + -- set echo on; + connect '{act.db.dsn}' user '{act.db.user}' password '{act.db.password}'; + grant {tmp_role.name} to "{THIS_COMPUTER_NAME}\\{CURRENT_WIN_ADMIN}"; + commit; + + -- We have to use here "create mapping trusted_auth ... from any user to user" otherwise get + -- Statement failed, SQLSTATE = 28000 /Missing security context for C:\\FBTESTING\\QA\\MISC\\C5887.FDB + -- on connect statement which specifies COMPUTERNAME:USERNAME instead path to DB: + create or alter mapping trusted_auth using plugin win_sspi from any user to user; + + -- We have to use here "create mapping win_admins ... DOMAIN_ANY_RID_ADMINS" otherwise get + -- Statement failed, SQLSTATE = 0P000 / Your attachment has no trusted role + create or alter mapping win_admins using plugin win_sspi from predefined_group domain_any_rid_admins to role {tmp_role.name}; + commit; + + -- We have to GRANT ROLE, even to SYSDBA. Otherwise: + -- Statement failed, SQLSTATE = 0P000 + -- Role ... is invalid or unavailable + grant {tmp_role.name} to sysdba; + commit; + --show role; + --show grants; + --show mapping; + + set autoddl off; + commit; + + -- Following management statements are taken from + -- doc/sql.extensions/README.management_statements_psql.md: + -- ######################################################## + alter session reset; + set session idle timeout 1800 second; + set statement timeout 190 second; + set bind of decfloat to double precision; + set decfloat round ceiling; + set decfloat traps to Division_by_zero; + set time zone 'America/Sao_Paulo'; + set role {tmp_role.name}; + commit; + + connect '{THIS_COMPUTER_NAME}:{act.db.db_path}' role {tmp_role.name}; + + select upper(mon$user) as mon_user, upper(mon$role) as mon_role, mon$auth_method as mon_auth + from mon$attachments + where mon$attachment_id = current_connection; + commit; + + set trusted role; + commit; + + connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; + drop mapping trusted_auth; + drop mapping win_admins; + commit; + """ + tmp_file.write_text(script) + + act.expected_stdout = f""" + MON_USER {THIS_COMPUTER_NAME.upper()}\\{CURRENT_WIN_ADMIN.upper()} + MON_ROLE {tmp_role.name.upper()} + MON_AUTH Mapped from Win_Sspi + """ + + act.isql(switches=['-n', '-q'], input_file = tmp_file, connect_db = False, credentials = False, combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout + act.reset() + +#---------------------------------------------------------- + +@pytest.mark.trace +@pytest.mark.version('>=4.0') +@pytest.mark.platform('Windows') +def test_1(act: Action, tmp_role: Role, tmp_file: Path, capsys): + + if not is_admin(): + pytest.skip("Current OS user must have admin rights.") + + with act.trace(db_events=trace_win): + run_script(act, tmp_role, tmp_file) + + # process trace + for line in act.trace_log: + if line.split(): + if act.match_any(line, patterns_win): + print(' '.join(line.split()).lower()) + + expected_stdout_win = f""" + alter session reset + set session idle timeout 1800 second + set statement timeout 190 second + set bind of decfloat to double precision + set decfloat round ceiling + set decfloat traps to division_by_zero + set time zone 'america/sao_paulo' + set role {tmp_role.name.lower()} + set trusted role + """ + + act.expected_stdout = expected_stdout_win + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + + + +################################ +### L I N U X ### +################################ +# version: 4.0 - Linux + +trace_lin = ['log_initfini = false', + 'log_connections = true', + 'log_statement_finish = true', + 'log_errors = true', + 'time_threshold = 0', + ] + +patterns_lin = [re.compile('alter session reset', re.IGNORECASE), + re.compile('set session idle timeout', re.IGNORECASE), + re.compile('set statement timeout', re.IGNORECASE), + re.compile('set bind of decfloat to double precision', re.IGNORECASE), + re.compile('set decfloat round ceiling', re.IGNORECASE), + re.compile('set decfloat traps to Division_by_zero', re.IGNORECASE), + re.compile('set time zone', re.IGNORECASE), + re.compile('set role', re.IGNORECASE)] + +@pytest.mark.version('>=4.0') +@pytest.mark.platform('Linux') +def test_2(act: Action, tmp_role: Role, capsys): + + expected_stdout_nix = f""" + alter session reset + set session idle timeout 1800 second + set statement timeout 190 second + set bind of decfloat to double precision + set decfloat round ceiling + set decfloat traps to division_by_zero + set time zone 'america/sao_paulo' + set role {tmp_role.name.lower()} + """ + + test_script_nix = f""" + set bail on; + set list on; + + -- We have to GRANT ROLE, even to SYSDBA. Otherwise: + -- Statement failed, SQLSTATE = 0P000 + -- Role ... is invalid or unavailable + grant {tmp_role.name} to sysdba; + commit; + + select current_user as who_ami, current_role as whats_my_role from rdb$database; + set autoddl off; + commit; + + -- Following management statements are taken from + -- doc/sql.extensions/README.management_statements_psql.md: + -- ######################################################## + set echo on; + alter session reset; + set session idle timeout 1800 second; + set statement timeout 190 second; + set bind of decfloat to double precision; + set decfloat round ceiling; + set decfloat traps to Division_by_zero; + set time zone 'America/Sao_Paulo'; + set role {tmp_role.name}; + commit; + select 'Completed' as msg from rdb$database; + """ + + with act.trace(db_events=trace_lin): + act.isql(switches=['-n', '-q'], input = test_script_nix) + + # process trace + for line in act.trace_log: + if line.split(): + if act.match_any(line, patterns_lin): + print(' '.join(line.split()).lower()) + # Check + act.expected_stdout = expected_stdout_nix + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + diff --git a/tests/bugs/core_6487_test.py b/tests/bugs/core_6487_test.py index 68156a0f..a5aa77f6 100644 --- a/tests/bugs/core_6487_test.py +++ b/tests/bugs/core_6487_test.py @@ -3,11 +3,14 @@ """ ID: issue-6717 ISSUE: 6717 -TITLE: FETCH ABSOLUTE and RELATIVE beyond bounds of cursor should always position - immediately before-first or after-last +TITLE: FETCH ABSOLUTE and RELATIVE beyond bounds of cursor should always position immediately before-first or after-last DESCRIPTION: JIRA: CORE-6487 FBTEST: bugs.core_6487 +NOTES: + [03.07.2025] pzotov + Suppressed name of cursor - it has no matter in this test. + Checked on 6.0.0.892; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813 """ import pytest @@ -62,20 +65,20 @@ set term ;^ """ -act = isql_act('db', test_script, substitutions=[('-At block line:.*', '-At block line')]) +act = isql_act('db', test_script, substitutions=[('-At block line:.*', '-At block line'), ('Cursor \\S+ is not positioned', 'Cursor is not positioned')]) -expected_stderr = """ +expected_stdout = """ Statement failed, SQLSTATE = HY109 - Cursor C is not positioned in a valid record - -At block line: 14, col: 5 + Cursor is not positioned in a valid record + -At block line Statement failed, SQLSTATE = HY109 - Cursor C is not positioned in a valid record - -At block line: 14, col: 5 + Cursor is not positioned in a valid record + -At block line """ @pytest.mark.version('>=3.0.8') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_6489_test.py b/tests/bugs/core_6489_test.py index 8648fc2f..358bd24d 100644 --- a/tests/bugs/core_6489_test.py +++ b/tests/bugs/core_6489_test.py @@ -7,6 +7,10 @@ DESCRIPTION: JIRA: CORE-6489 FBTEST: bugs.core_6489 + [03.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.889; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -37,26 +41,32 @@ commit; """ -act = isql_act('db', test_script, - substitutions=[('ROLE_DESCR_BLOB_ID .*', ''), ('[\t ]+', ' '), - ('(-)?Effective user is.*', '')]) +substitutions = [ + ('[\t ]+', ' '), + ('ROLE_DESCR_BLOB_ID .*', ''), + ('(-)?Effective user is.*', ''), +] -expected_stdout = """ - Comment by tmp$c6489_senior -""" +act = isql_act('db', test_script, substitutions = substitutions) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 28000 unsuccessful metadata update -COMMENT ON RDB$ADMIN failed -no permission for ALTER access to ROLE RDB$ADMIN - -Effective user is TMP$C6489_JUNIOR + Comment by tmp$c6489_senior +""" + +expected_stdout_6x = """ + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -COMMENT ON "RDB$ADMIN" failed + -no permission for ALTER access to ROLE "RDB$ADMIN" + Comment by tmp$c6489_senior """ @pytest.mark.version('>=3.0.8') def test_1(act: Action, user_sr, user_jr): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/core_6517_test.py b/tests/bugs/core_6517_test.py index 20467b7e..db666ff2 100644 --- a/tests/bugs/core_6517_test.py +++ b/tests/bugs/core_6517_test.py @@ -3,15 +3,15 @@ """ ID: issue-6746 ISSUE: 6746 -TITLE: Regression: CREATE DATABASE fails with 'Token unknown' error when DB name is - enclosed in double quotes and 'DEFAULT CHARACTER SET' is specified after DB name +TITLE: Regression: CREATE DATABASE fails with 'Token unknown' error when DB name is enclosed in double quotes and 'DEFAULT CHARACTER SET' is specified after DB name DESCRIPTION: JIRA: CORE-6517 FBTEST: bugs.core_6517 """ +import locale +from pathlib import Path import pytest -from pathlib import Path from firebird.qa import * db = db_factory() @@ -22,5 +22,7 @@ @pytest.mark.version('>=3.0.8') def test_1(act: Action, test_db: Path): - act.isql(switches=[], input=f'create database "{act.get_dsn(test_db)}" default character set utf8;', - connect_db=False) + test_sql = f""" + create database "{act.get_dsn(test_db)}" default character set utf8; + """ + act.isql(switches = ['-q'], input = test_sql, connect_db = False, combine_output = True, io_enc = locale.getpreferredencoding()) diff --git a/tests/bugs/gh_0310_test.py b/tests/bugs/gh_0310_test.py index 08f3fffb..cbcfe677 100644 --- a/tests/bugs/gh_0310_test.py +++ b/tests/bugs/gh_0310_test.py @@ -164,6 +164,9 @@ RDB$KEYWORD_NAME BLOB RDB$KEYWORD_RESERVED + RDB$KEYWORD_NAME BLOBID + RDB$KEYWORD_RESERVED + RDB$KEYWORD_NAME BLOB_APPEND RDB$KEYWORD_RESERVED @@ -1337,6 +1340,9 @@ RDB$KEYWORD_NAME TARGET RDB$KEYWORD_RESERVED + RDB$KEYWORD_NAME TEMP + RDB$KEYWORD_RESERVED + RDB$KEYWORD_NAME TEMPORARY RDB$KEYWORD_RESERVED @@ -1515,7 +1521,7 @@ RDB$KEYWORD_RESERVED - Records affected: 496 + Records affected: 498 """ expected_stdout_6 = """ @@ -1613,6 +1619,8 @@ RDB$KEYWORD_RESERVED RDB$KEYWORD_NAME BLOB RDB$KEYWORD_RESERVED + RDB$KEYWORD_NAME BLOBID + RDB$KEYWORD_RESERVED RDB$KEYWORD_NAME BLOB_APPEND RDB$KEYWORD_RESERVED RDB$KEYWORD_NAME BLOCK @@ -1733,6 +1741,8 @@ RDB$KEYWORD_RESERVED RDB$KEYWORD_NAME CURRENT_ROLE RDB$KEYWORD_RESERVED + RDB$KEYWORD_NAME CURRENT_SCHEMA + RDB$KEYWORD_RESERVED RDB$KEYWORD_NAME CURRENT_TIME RDB$KEYWORD_RESERVED RDB$KEYWORD_NAME CURRENT_TIMESTAMP @@ -1803,6 +1813,8 @@ RDB$KEYWORD_RESERVED RDB$KEYWORD_NAME DOUBLE RDB$KEYWORD_RESERVED + RDB$KEYWORD_NAME DOWNTO + RDB$KEYWORD_RESERVED RDB$KEYWORD_NAME DROP RDB$KEYWORD_RESERVED RDB$KEYWORD_NAME ELSE @@ -1891,6 +1903,8 @@ RDB$KEYWORD_RESERVED RDB$KEYWORD_NAME GRANTED RDB$KEYWORD_RESERVED + RDB$KEYWORD_NAME GREATEST + RDB$KEYWORD_RESERVED RDB$KEYWORD_NAME GROUP RDB$KEYWORD_RESERVED RDB$KEYWORD_NAME HASH @@ -1969,6 +1983,8 @@ RDB$KEYWORD_RESERVED RDB$KEYWORD_NAME LEADING RDB$KEYWORD_RESERVED + RDB$KEYWORD_NAME LEAST + RDB$KEYWORD_RESERVED RDB$KEYWORD_NAME LEAVE RDB$KEYWORD_RESERVED RDB$KEYWORD_NAME LEFT @@ -2313,6 +2329,8 @@ RDB$KEYWORD_RESERVED RDB$KEYWORD_NAME SCROLL RDB$KEYWORD_RESERVED + RDB$KEYWORD_NAME SEARCH_PATH + RDB$KEYWORD_RESERVED RDB$KEYWORD_NAME SECOND RDB$KEYWORD_RESERVED RDB$KEYWORD_NAME SECURITY @@ -2407,6 +2425,8 @@ RDB$KEYWORD_RESERVED RDB$KEYWORD_NAME TARGET RDB$KEYWORD_RESERVED + RDB$KEYWORD_NAME TEMP + RDB$KEYWORD_RESERVED RDB$KEYWORD_NAME TEMPORARY RDB$KEYWORD_RESERVED RDB$KEYWORD_NAME THEN @@ -2465,6 +2485,8 @@ RDB$KEYWORD_RESERVED RDB$KEYWORD_NAME UNKNOWN RDB$KEYWORD_RESERVED + RDB$KEYWORD_NAME UNLIST + RDB$KEYWORD_RESERVED RDB$KEYWORD_NAME UPDATE RDB$KEYWORD_RESERVED RDB$KEYWORD_NAME UPDATING @@ -2526,12 +2548,12 @@ RDB$KEYWORD_NAME ZONE RDB$KEYWORD_RESERVED - Records affected: 503 + Records affected: 511 """ @pytest.mark.disabled_in_forks @pytest.mark.version('>=5.0') def test_1(act: Action): act.expected_stdout = expected_stdout_5 if act.is_version('<6') else expected_stdout_6 - act.execute() + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_0731_test.py b/tests/bugs/gh_0731_test.py new file mode 100644 index 00000000..13357886 --- /dev/null +++ b/tests/bugs/gh_0731_test.py @@ -0,0 +1,100 @@ +#coding:utf-8 + +""" +ID: issue-731 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/731 +TITLE: coalesce fails with data type varchar and a non ascii value [CORE388] +DESCRIPTION: +NOTES: + [04.09.2024] pzotov + The issue seems to be fixed long ago. + Old FB versions can not be checked on current firebird QA. + ISQL must work with charset = utf8. Otherwise 'Expected end of statement, encountered EOF' will raise on Linux. + + Checked on all recent 3.x ... 6.x -- all fine. +""" + +import pytest +from firebird.qa import * + +init_sql = """ + recreate table trans_table + ( + tcode smallint + not null, + code smallint + not null, + name varchar(10), + constraint trans_table_primarykey primary key + (tcode, code) + ); + + recreate table class1 + ( + class_name varchar(10) + not null, + class_num smallint + not null, + teacher_id integer, + constraint pk_class1 primary key (class_name, class_num) + ); + + recreate table class2 + ( + class_name varchar(10) + not null, + class_num smallint + not null, + teacher_id integer, + constraint pk_class2 primary key (class_name, class_num) + ); + + set term ^; + create trigger class1_bi for class1 active before insert position 0 as + declare name varchar(10); + begin + select name from trans_table c where c.tcode=2 and c.code=new.class_name + into :name; + new.class_name = case when :name is null then new.class_name else :name end; + -- new.class_name = coalesce(:name, new.class_name); + end + ^ + + create trigger class2_bi for class2 active before insert position 0 as + declare name varchar(10); + begin + select name from trans_table c where c.tcode=2 and c.code=new.class_name + into :name; + -- new.class_name = case when :name is null then new.class_name else :name end; + new.class_name = coalesce(:name, new.class_name); + end + ^ + set term ;^ + commit; +""" +db = db_factory(init = init_sql, charset='win1252') + +test_script = """ + set bail on; + set list on; + insert into trans_table(tcode, code, name) values (2, 1, 'à'); + -- passed + insert into class1(class_name, class_num, teacher_id) values (1, 1, null); + -- failed + insert into class2(class_name, class_num, teacher_id) values (1, 1, null); + select 'Passed' as msg from rdb$database; +""" + +act = isql_act('db', test_script, substitutions = [ ('[ \t]+',' ') ]) + +expected_stdout = """ + MSG Passed +""" + +@pytest.mark.intl +@pytest.mark.version('>=3.0') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(charset = 'utf8') + assert act.clean_stdout == act.clean_expected_stdout + diff --git a/tests/bugs/gh_2292_test.py b/tests/bugs/gh_2292_test.py new file mode 100644 index 00000000..5a132555 --- /dev/null +++ b/tests/bugs/gh_2292_test.py @@ -0,0 +1,57 @@ +#coding:utf-8 + +""" +ID: issue-2292 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/2292 +TITLE: Isql's extracted script is unusable with interdependent selectable procedures in FB 2.1 [CORE1862] +DESCRIPTION: + Test creates SP which has output parameter *and* SUSPEND clause. + Then we extract metadata and check whether this procedure header contains 'SUSPEND'. + On FB 2.0.7.13318 extracted metadata contains 'EXIT': + CREATE PROCEDURE SP_TEST RETURNS (O INTEGER) + AS + BEGIN EXIT; END ^ + + Although such code can be compiled, this SP could be called (and returned empty resultset) only in 2.0.7 and before. + Since 2.1 attempt to call such SP will raise: + Statement failed, SQLSTATE = 42000 + ... + -invalid request BLR at offset ... + -Procedure SP_TEST is not selectable (it does not contain a SUSPEND statement) +""" + +import re +import pytest +from firebird.qa import * + +init_sql = """ + set term ^ ; + create or alter procedure sp_test returns(out_value int) as + begin + out_value = 1; + suspend; + end + ^ + set term ;^ + commit; +""" +db = db_factory(init = init_sql) +act = python_act('db') + +@pytest.mark.version('>=3.0.0') +def test_1(act: Action, capsys): + + meta_sql = act.extract_meta() + EXPECTED_MSG = 'OK' + + p = re.compile(r'SP_TEST\s+RETURNS \(OUT_VALUE INTEGER\)\s+AS\s+BEGIN\s+SUSPEND;\s+END\s?\^', re.IGNORECASE) + + if p.search(meta_sql): + print(EXPECTED_MSG) + else: + print(f'Could not find pattern "{p.pattern}" in extracted metadata.') + print(meta_sql) + + act.expected_stdout = EXPECTED_MSG + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_3106_test.py b/tests/bugs/gh_3106_test.py index 624c2eac..1c30d693 100644 --- a/tests/bugs/gh_3106_test.py +++ b/tests/bugs/gh_3106_test.py @@ -3,24 +3,19 @@ """ ID: issue-3106 ISSUE: 3106 -TITLE: Many indexed reads in a compound index with NULLs +TITLE: Indexed reads in a compound index with NULLs present even if record does not exist DESCRIPTION: - BEFORE fix trace log was like this: - ====== - Table Natural Index - ******************************* - RDB$DATABASE 1 - TEST_TABLE 3 <<< this line must NOT present now. - ====== - AFTER fix trace must contain line only for RDB$DATABASE in the table statistics section. - - Confirmed bug on 4.0.0.2451: trace statistics contain line with three indexed reads for test table. - Checked on 4.0.0.2453 SS/CS: all OK, there are no indexed reads on test table in the trace log. JIRA: CORE-2709 FBTEST: bugs.gh_3106 NOTES: [21.06.2022] pzotov + Confirmed bug on 4.0.0.2451: table statistics has three indexed reads for 'TEST' table. Checked on 4.0.1.2692, 5.0.0.509. + + [04.07.2025] pzotov + Re-implemented using con.info.get_table_access_stats(), removed trace launch and parsing. + Confirmed problem on 4.0.0.2451. + Checked on 6.0.0.892; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import locale @@ -29,67 +24,55 @@ from firebird.qa import * init_script = ''' - recreate table test_table ( + recreate table test ( id1 int, id2 int, id3 int ); commit; - insert into test_table (id1, id2, id3) values (1, 1, null); - insert into test_table (id1, id2, id3) values (1, 2, null); - insert into test_table (id1, id2, id3) values (1, 3, null); - insert into test_table (id1, id2, id3) values (2, 1, null); - insert into test_table (id1, id2, id3) values (2, 2, null); - insert into test_table (id1, id2, id3) values (2, 3, null); + insert into test (id1, id2, id3) values (1, 1, null); + insert into test (id1, id2, id3) values (1, 2, null); + insert into test (id1, id2, id3) values (1, 3, null); + insert into test (id1, id2, id3) values (2, 1, null); + insert into test (id1, id2, id3) values (2, 2, null); + insert into test (id1, id2, id3) values (2, 3, null); commit; - create index test_table_idx1 on test_table (id1,id2,id3); + create index test_idx_compound on test (id1,id2,id3); commit; ''' db = db_factory(init = init_script) - act = python_act('db', substitutions=[('[ \t]+', ' ')]) -FOUND_EXPECTED_TAB_HEAD_MSG = 'Found table statistics header.' -FOUND_EXPECTED_RDB_STAT_MSG = 'Found expected line for rdb$database.' - -expected_stdout_trace = f""" - {FOUND_EXPECTED_TAB_HEAD_MSG} - {FOUND_EXPECTED_RDB_STAT_MSG} -""" - +expected_out = 'EXPECTED' -@pytest.mark.version('>=4.0') +@pytest.mark.trace +@pytest.mark.version('>=3.0') def test_1(act: Action, capsys): - trace_cfg_items = [ - 'time_threshold = 0', - 'log_statement_finish = true', - 'print_perf = true', - ] - sql_run=''' - set list on; - select 1 as dummy from rdb$database r left join test_table t on t.id1 = 1 and t.id2 is null; - ''' + with act.db.connect() as con: + idx_reads = 0 + cur = con.cursor() + cur.execute("select rdb$relation_id from rdb$relations where rdb$relation_name = upper('test')") + src_relation_id = cur.fetchone()[0] - with act.trace(db_events = trace_cfg_items, encoding=locale.getpreferredencoding()): - act.isql(input = sql_run, combine_output = True) + for x_table in con.info.get_table_access_stats(): + if x_table.table_id == src_relation_id: + idx_reads = -x_table.indexed if x_table.indexed else 0 - p_tablestat_head = re.compile('Table\\s+Natural\\s+Index', re.IGNORECASE) - p_tablestat_must_found = re.compile('rdb\\$database\\s+\\d+', re.IGNORECASE) - p_tablestat_must_miss = re.compile('test_table\\s+\\d+', re.IGNORECASE) + cur.execute('select 1 /* trace_me */ from test where ID1 = 1 and ID2 IS NULL') + data = cur.fetchall() + for x_table in con.info.get_table_access_stats(): + if x_table.table_id == src_relation_id: + idx_reads += x_table.indexed if x_table.indexed else 0 - for line in act.trace_log: - if p_tablestat_head.search(line): - print( FOUND_EXPECTED_TAB_HEAD_MSG ) - elif p_tablestat_must_found.search(line): - print( FOUND_EXPECTED_RDB_STAT_MSG ) - elif p_tablestat_must_miss.search(line): - print( '### FAILED ### found UNEXPECTED line:') - print(line.strip()) + if idx_reads == 0: + print(expected_out) + else: + print(f'UNEXPECTED: {data=}, {idx_reads=} ') - act.expected_stdout = expected_stdout_trace + act.expected_stdout = expected_out act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_3218_test.py b/tests/bugs/gh_3218_test.py index 3f148ef9..1fef04ed 100644 --- a/tests/bugs/gh_3218_test.py +++ b/tests/bugs/gh_3218_test.py @@ -13,11 +13,20 @@ [20.01.2024] pzotov Confirmed problem on 5.0.0.442: number of indexed reads was equal to the total count of records in rdb$relation_fields. Checked on 6.0.0.218, 5.0.1.1318. + + [18.01.2025] pzotov + Resultset of cursor that executes using instance of selectable PreparedStatement must be stored + in some variable in order to have ability close it EXPLICITLY (before PS will be freed). + Otherwise access violation raises during Python GC and pytest hangs at final point (does not return control to OS). + This occurs at least for: Python 3.11.2 / pytest: 7.4.4 / firebird.driver: 1.10.6 / Firebird.Qa: 0.19.3 + The reason of that was explained by Vlad, 26.10.24 17:42 ("oddities when use instances of selective statements"). """ -import pytest from pathlib import Path + +import pytest from firebird.qa import * +from firebird.driver import DatabaseError init_sql = """ set term ^; @@ -73,22 +82,41 @@ def test_1(act: Action, capsys): result_map = {} chk_sql = 'select 1 from sp_get_relations r join rdb$relation_fields f on f.rdb$relation_name = r.rdb$relation_name where r.rdb$relation_id < 10' - with cur.prepare(chk_sql) as ps: - cur.execute(chk_sql) + ps, rs = None, None + try: + ps = cur.prepare(chk_sql) + + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + rs = cur.execute(ps) + tabstat1 = [ p for p in con.info.get_table_access_stats() if p.table_id == rf_rel_id ] cur.fetchall() tabstat2 = [ p for p in con.info.get_table_access_stats() if p.table_id == rf_rel_id ] print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) # explained plan, with preserving indents by replacing leading spaces with '#' - idx_reads = (tabstat2[0].indexed if tabstat2[0].indexed else 0) - if tabstat1: - idx_reads -= (tabstat1[0].indexed if tabstat1[0].indexed else 0) + idx_reads = (tabstat2[0].indexed if tabstat2[0].indexed else 0) + if tabstat1: + idx_reads -= (tabstat1[0].indexed if tabstat1[0].indexed else 0) + + print('Result:') + if idx_reads == 0 or idx_reads > cnt_chk: + print(f'POOR! Number of records in rdb$relation_fields: 1) to be filtered: {cnt_chk}, 2) total: {cnt_all}. Number of indexed_reads: {idx_reads}') + else: + print('Acceptable.') - print('Result:') - if idx_reads == 0 or idx_reads > cnt_chk: - print(f'POOR! Number of records in rdb$relation_fields: 1) to be filtered: {cnt_chk}, 2) total: {cnt_all}. Number of indexed_reads: {idx_reads}') - else: - print('Acceptable.') + except DatabaseError as e: + print( e.__str__() ) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() act.expected_stdout = """ Select Expression diff --git a/tests/bugs/gh_3357_test.py b/tests/bugs/gh_3357_test.py index 89b469b4..c700eecc 100644 --- a/tests/bugs/gh_3357_test.py +++ b/tests/bugs/gh_3357_test.py @@ -6,12 +6,20 @@ TITLE: Bad execution plan if some stream depends on multiple streams via a function [CORE2975] NOTES: [04.03.2023] pzotov - Discussed with dimitr, letters 01-mar-2023 18:37 and 04-mar-2023 10:38. - Test must verify that execution plan uses NESTED LOOPS rather than HASH JOIN. - Because of this, tables must be filled with approximately equal volume of data. - Confirmed bug on 3.0.9.33548 (28-dec-2021), plan was: - PLAN HASH (JOIN (T1 INDEX (T1_COL), T2 INDEX (T2_ID)), T3 NATURAL) - Checked on 5.0.0.970, 4.0.3.2904, 3.0.11.33665. + 1. Discussed with dimitr, letters 01-mar-2023 18:37 and 04-mar-2023 10:38. + Test must verify that execution plan uses NESTED LOOPS rather than HASH JOIN. + Because of this, tables must be filled with approximately equal volume of data. + Confirmed bug on 3.0.9.33548 (28-dec-2021), plan was: + PLAN HASH (JOIN (T1 INDEX (T1_COL), T2 INDEX (T2_ID)), T3 NATURAL) + 2. Commit related to this test: + https://github.com/FirebirdSQL/firebird/commit/1b192404d43a15d403b5ff92760bc5df9d3c89c3 + (13.09.2022 19:17, "More complete solution for #3357 and #7118") + One more test that attempts to verify this commit: bugs/gh_7398_test.py + Checked on 5.0.0.970, 4.0.3.2904, 3.0.11.33665. + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -53,12 +61,16 @@ act = isql_act('db', test_script) -expected_stdout = """ +expected_stdout_5x = """ PLAN JOIN (T1 INDEX (T1_COL), T2 INDEX (T2_ID), T3 INDEX (T3_ID)) """ +expected_stdout_6x = """ + PLAN JOIN ("PUBLIC"."T1" INDEX ("PUBLIC"."T1_COL"), "PUBLIC"."T2" INDEX ("PUBLIC"."T2_ID"), "PUBLIC"."T3" INDEX ("PUBLIC"."T3_ID")) +""" + @pytest.mark.version('>=3.0.9') def test_1(act: Action): - act.expected_stdout = expected_stdout + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_3810_test.py b/tests/bugs/gh_3810_test.py index 193dcb24..674a2741 100644 --- a/tests/bugs/gh_3810_test.py +++ b/tests/bugs/gh_3810_test.py @@ -6,13 +6,16 @@ TITLE: Wrong or missing IS NULL optimization (regression) [CORE3449] NOTES: [14.04.2023] pzotov - Confirmed poor performance on 3.0.11.33678 (num of fetches = 10'099). - Checked on 3.0.11.33681 -- all fine, fetches differ for less than 20. - + Confirmed poor performance on 3.0.11.33678 (num of fetches = 10'099). + Checked on 3.0.11.33681 -- all fine, fetches differ for less than 20. [22.09.2023] pzotov - Changed name of table to simpler. - Removed usage of index test_fld2 after discuss with Anton Zuev (Red Soft) and dimitr, - see: https://github.com/FirebirdSQL/firebird-qa/pull/19 + Changed name of table to simpler. + Removed usage of index test_fld2 after discuss with Anton Zuev (Red Soft) and dimitr, + see: https://github.com/FirebirdSQL/firebird-qa/pull/19 + [04.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -95,16 +98,23 @@ ); """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ +expected_stdout_5x = """ PLAN (TEST INDEX (TEST_FLD123)) COUNT 0 FETCHES_DIFF OK, expected """ +expected_stdout_6x = """ + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_FLD123")) + COUNT 0 + FETCHES_DIFF OK, expected +""" + @pytest.mark.version('>=3.0.11') def test_1(act: Action): - act.expected_stdout = expected_stdout + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_3812_test.py b/tests/bugs/gh_3812_test.py index e855e4c0..5d806243 100644 --- a/tests/bugs/gh_3812_test.py +++ b/tests/bugs/gh_3812_test.py @@ -6,11 +6,15 @@ TITLE: Query with SP doesn't accept explicit plan [CORE3451] NOTES: [18.02.2023] pzotov - Confirmed problem on 5.0.0.743, got: - Statement failed, SQLSTATE = 42S02 - -Invalid command - -there is no alias or table named TMP_SP1 at this scope level - Checked on 5.0.0.745 - all OK. + Confirmed problem on 5.0.0.743, got: + Statement failed, SQLSTATE = 42S02 + -Invalid command + -there is no alias or table named TMP_SP1 at this scope level + Checked on 5.0.0.745 - all OK. + [04.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.876; 5.0.3.1668. """ import pytest @@ -65,12 +69,16 @@ act = isql_act('db', test_script) -expected_stdout = """ +expected_stdout_5x = """ PLAN JOIN (JOIN (TMP_SP1 NATURAL, T1 INDEX (TMP_TBL1_FLD2)), T2 INDEX (TMP_TBL2_FLD1), T3 INDEX (TMP_TBL3_FLD1)) """ +expected_stdout_6x = """ + PLAN JOIN (JOIN ("PUBLIC"."TMP_SP1" NATURAL, "T1" INDEX ("PUBLIC"."TMP_TBL1_FLD2")), "T2" INDEX ("PUBLIC"."TMP_TBL2_FLD1"), "T3" INDEX ("PUBLIC"."TMP_TBL3_FLD1")) +""" + @pytest.mark.version('>=5.0') def test_1(act: Action): - act.expected_stdout = expected_stdout + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_4314_test.py b/tests/bugs/gh_4314_test.py new file mode 100644 index 00000000..adbbb086 --- /dev/null +++ b/tests/bugs/gh_4314_test.py @@ -0,0 +1,153 @@ +#coding:utf-8 + +""" +ID: issue-4314 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/4314 +TITLE: Sub-optimal predicate checking while selecting from a view [CORE3981] +DESCRIPTION: +NOTES: + [20.08.2024] pzotov + Checked on 6.0.0.438, 5.0.2.1479, 4.0.6.3142, 3.0.12.33784. + [26.06.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. + +""" +import locale +import re + +import pytest +from firebird.qa import * + +init_sql = """ + recreate table rr( + rel_name varchar(63) + ,id int + ,fid int + ); + + recreate table rf( + rel_name varchar(63) + ,fid int + ,fnm varchar(63) + ); + insert into rr + select a.rdb$relation_name, a.rdb$relation_id, a.rdb$field_id + from rdb$relations a + ; + insert into rf select f.rdb$relation_name, f.rdb$field_id, f.rdb$field_name + from rdb$relation_fields f + ; + commit; + + alter table rr add constraint rr_rel_name_unq unique (rel_name); + create index rr_id on rr (id); + + alter table rf add constraint rf_fnm_rel_name_unq unique(fnm, rel_name); + create index rf_rel_name on rf(rel_name); + + recreate view v as + select r.rel_name, abs(r.id) as id + from rr r + left + join rf on r.rel_name = rf.rel_name and r.fid = rf.fid + where r.id < 128; + + set statistics index rr_rel_name_unq; + set statistics index rr_id; + set statistics index rf_fnm_rel_name_unq; + set statistics index rf_rel_name; + commit; +""" + +db = db_factory(init = init_sql) +act = python_act('db') + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- + +@pytest.mark.version('>=3.0') +def test_1(act: Action, capsys): + + query_from_view = """ + select /* trace_tag: VIEW */ v.rel_name as v_rel_name, v.id as v_id from v + where id = 0 + """ + query_from_dt = """ + select /* trace_tag: DERIVED TABLE */ d.rel_name as d_rel_name, d.id as d_id + from ( + select r.rel_name, abs(r.id) as id + from rr r + left + join rf on r.rel_name = rf.rel_name and r.fid = rf.fid + where r.id < 128 + ) d + where d.id = 0 + """ + + with act.db.connect() as con: + cur = con.cursor() + for test_sql in (query_from_view, query_from_dt): + ps = cur.prepare(test_sql) + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + + expected_stdout_5x = """ + Select Expression + ....-> Filter + ........-> Nested Loop Join (outer) + ............-> Filter + ................-> Table "RR" as "V R" Access By ID + ....................-> Bitmap + ........................-> Index "RR_ID" Range Scan (upper bound: 1/1) + ............-> Filter + ................-> Table "RF" as "V RF" Access By ID + ....................-> Bitmap + ........................-> Index "RF_REL_NAME" Range Scan (full match) + + Select Expression + ....-> Filter + ........-> Nested Loop Join (outer) + ............-> Filter + ................-> Table "RR" as "D R" Access By ID + ....................-> Bitmap + ........................-> Index "RR_ID" Range Scan (upper bound: 1/1) + ............-> Filter + ................-> Table "RF" as "D RF" Access By ID + ....................-> Bitmap + ........................-> Index "RF_REL_NAME" Range Scan (full match) + """ + + expected_stdout_6x = """ + Select Expression + ....-> Filter + ........-> Nested Loop Join (outer) + ............-> Filter + ................-> Table "PUBLIC"."RR" as "PUBLIC"."V" "R" Access By ID + ....................-> Bitmap + ........................-> Index "PUBLIC"."RR_ID" Range Scan (upper bound: 1/1) + ............-> Filter + ................-> Table "PUBLIC"."RF" as "PUBLIC"."V" "PUBLIC"."RF" Access By ID + ....................-> Bitmap + ........................-> Index "PUBLIC"."RF_REL_NAME" Range Scan (full match) + Select Expression + ....-> Filter + ........-> Nested Loop Join (outer) + ............-> Filter + ................-> Table "PUBLIC"."RR" as "D" "R" Access By ID + ....................-> Bitmap + ........................-> Index "PUBLIC"."RR_ID" Range Scan (upper bound: 1/1) + ............-> Filter + ................-> Table "PUBLIC"."RF" as "D" "PUBLIC"."RF" Access By ID + ....................-> Bitmap + ........................-> Index "PUBLIC"."RF_REL_NAME" Range Scan (full match) + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_4723_all_nulls_test.py b/tests/bugs/gh_4723_all_nulls_test.py new file mode 100644 index 00000000..1d7ddda2 --- /dev/null +++ b/tests/bugs/gh_4723_all_nulls_test.py @@ -0,0 +1,112 @@ +#coding:utf-8 + +""" +ID: issue-4723 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/4723 +TITLE: Optimize the record-level RLE algorithm for a denser compression of shorter-than-declared strings and sets of subsequent NULLs [CORE4401] +DESCRIPTION: + Test creates table with nullable varchar column an adds lot of rows with NULL value. + Then we run gstat in order to parse statistics related to data pages and avg fill ratio. + gstat reports for data pages and avg ratio following values: + 4.0.5.3099: + Pointer pages: 2, data page slots: 2144 + Data pages: 2144, average fill: 91% + 5.0.1.1399, 6.0.0.351: + Pointer pages: 1, data page slots: 208 + Data pages: 208, average fill: 46% + Test assumes that values returned for 5.x will not be change in too wide range for several upcoming years + in any order - see MIN_* and MAX_* thresholds. +NOTES: + [20.05.2024] pzotov + Improvement URL (27-sep-2022 15:16): + https://github.com/FirebirdSQL/firebird/commit/54f1990b98d3e510a10d06fe9ceb76456804da52 + Improved record compression (denser encoding of repeating bytes and less blocks) (#7302) + + NB: snapshots that were just before and after this commit CAN NOT be verified: + 5.0.0.745: raised BUGCHECK ("decompression overran buffer (179), file: sqz.cpp line: 293") + 5.0.0.756: crashed + Checked on 5.0.1.1399, 6.0.0.351 for DB with page_size = 8192. +""" +import re + +import pytest +import platform +from firebird.qa import * + +N_ROWS = 30000 +N_WIDT = 32760 + +MIN_DP_COUNT_THRESHOLD = 190 +MAX_DP_COUNT_THRESHOLD = 230 +MIN_AVG_FILL_THRESHOLD = 30 +MAX_AVG_FILL_THRESHOLD = 60 + +init_ddl = f""" + recreate table test (f01 varchar({N_WIDT})); + commit; + + set term ^; + execute block as + declare n int = {N_ROWS}; + begin + while (n > 0) do + begin + insert into test(f01) values(null); + n = n - 1; + end + end + ^ + set term ;^ + commit; +""" + +db = db_factory(page_size = 8192, init = init_ddl) +act = python_act('db') + +@pytest.mark.version('>=5.0') +def test_1(act: Action, capsys): + + act.gstat(switches=['-d','-t', 'TEST', '-user', act.db.user, '-pass', act.db.password]) + + # 4.x: Pointer pages: 2, data page slots: 2144 + # 5.x: Pointer pages: 1, data page slots: 208 + p_pointer_pages_data_pages_slots = re.compile( r'Pointer\s+pages(:)?\s+\d+(,)?\s+data\s+page\s+slots(:)?\s+\d+' ) + + # Data pages: 208, average fill: 46% + p_data_pages_average_fill_ratio = re.compile( r'Data\s+pages(:)?\s+\d+(,)?\s+average\s+fill(:)?\s+\d+%' ) + + data_pages_cnt = avg_fill_ratio = -1 + gstat_lines = act.stdout.splitlines() + for line in gstat_lines: + #print(line) + if p_pointer_pages_data_pages_slots.search(line): + data_pages_cnt = int(line.split()[-1]) + if p_data_pages_average_fill_ratio.search(line): + avg_fill_ratio = int(line.split()[-1].replace('%','')) + + + data_pages_cnt_expected_msg = f'data_pages_cnt: expected, within {MIN_DP_COUNT_THRESHOLD=} ... {MAX_DP_COUNT_THRESHOLD=}' + avg_fill_ratio_expected_msg = f'avg_fill_ratio: expected, within {MIN_AVG_FILL_THRESHOLD=} ... {MAX_AVG_FILL_THRESHOLD=}' + if data_pages_cnt > 0 and avg_fill_ratio > 0: + if data_pages_cnt >= MIN_DP_COUNT_THRESHOLD and data_pages_cnt <= MAX_DP_COUNT_THRESHOLD: + print(data_pages_cnt_expected_msg) + else: + print(f'data_pages_cnt UNEXPECTED: {data_pages_cnt=} -- out of scope: {MIN_DP_COUNT_THRESHOLD=} ... {MAX_DP_COUNT_THRESHOLD=}') + + if avg_fill_ratio >= MIN_AVG_FILL_THRESHOLD and avg_fill_ratio <= MAX_AVG_FILL_THRESHOLD: + print(avg_fill_ratio_expected_msg) + else: + print(f'avg_fill_ratio UNEXPECTED: {avg_fill_ratio=} -- out of scope: {MIN_AVG_FILL_THRESHOLD=} ... {MAX_AVG_FILL_THRESHOLD=}') + else: + print(f'ERROR: at least one of: {data_pages_cnt=}, {avg_fill_ratio=} is INVALID.') + print('Could not properly parse gstat output:') + for p in gstat_lines: + print(p) + + act.expected_stdout = f""" + {data_pages_cnt_expected_msg} + {avg_fill_ratio_expected_msg} + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + diff --git a/tests/bugs/gh_4723_incompressible_test.py b/tests/bugs/gh_4723_incompressible_test.py new file mode 100644 index 00000000..02f72c8c --- /dev/null +++ b/tests/bugs/gh_4723_incompressible_test.py @@ -0,0 +1,137 @@ +#coding:utf-8 + +""" +ID: issue-4723 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/4723 +TITLE: Optimize the record-level RLE algorithm for a denser compression of shorter-than-declared strings and sets of subsequent NULLs [CORE4401] +DESCRIPTION: + Test creates table with nullable varchar column an adds lot of rows with incompressible data (GEN_UUID). + Then we run gstat in order to parse statistics related to avg record length ('-r' switch). + + gstat reports for data pages and avg ratio following values: + 4.0.5.3099: + Average record length: 33018.92, total records: 10000 + Average unpacked length: 32766.00, compression ratio: 0.99 + Pointer pages: 1, data page slots: 632 + Data pages: 632, average fill: 92% + 5.0.1.1399, 6.0.0.351: + Average record length: 32757.00, total records: 10000 + Average unpacked length: 32766.00, compression ratio: 1.00 + Pointer pages: 1, data page slots: 304 + Data pages: 304, average fill: 87%. + + Test assumes that values returned for 5.x will not be change in too wide range for several upcoming years + in any order - see MIN_* and MAX_* thresholds. +NOTES: + [20.05.2024] pzotov + Improvement URL (27-sep-2022 15:16): + https://github.com/FirebirdSQL/firebird/commit/54f1990b98d3e510a10d06fe9ceb76456804da52 + Improved record compression (denser encoding of repeating bytes and less blocks) (#7302) + + Charset must be specified in db_factory, otherwise 'malformed string' will raise. + Checked on 5.0.1.1399, 6.0.0.351 for DB with page_size = 8192. +""" +import re + +import pytest +import platform +from firebird.qa import * + +N_ROWS = 10000 +N_WIDT = 32760 + +MIN_DP_COUNT_THRESHOLD = 280 +MAX_DP_COUNT_THRESHOLD = 330 + +COMPRESSION_THRESHOLD = 1.00 + +init_ddl = f""" + recreate table test (f01 varchar({N_WIDT}) character set octets not null); + commit; + + set term ^; + execute block as + declare n int = {N_ROWS}; + begin + while (n > 0) do + begin + insert into test(f01) values( lpad('', 32760, gen_uuid()) ); + n = n - 1; + end + end + ^ + set term ;^ + commit; +""" + +db = db_factory(page_size = 8192, init = init_ddl, charset = 'win1251') +act = python_act('db') + +@pytest.mark.version('>=5.0') +def test_1(act: Action, capsys): + + act.gstat(switches=['-r', '-t', 'TEST', '-user', act.db.user, '-pass', act.db.password]) + gstat_lines = act.stdout.splitlines() + + #for p in gstat_lines: + # print(p) + # + #act.expected_stdout = f""" + #""" + #act.stdout = capsys.readouterr().out + #assert act.clean_stdout == act.clean_expected_stdout + + + # Average record length: N.FF, total records: M + # NB: for improved RLE value must be LESS OR EQUAL to the table column declared length + p_average_record_length = re.compile( r'Average\s+record\s+length(:)?\s+\d+(.\d+)?' ) + + # Average unpacked length: N.FF, compression ratio: R.PP + # NB: for improved RLE value must be 1.00 because column contrains incompressible data + p_compression_ratio = re.compile( r'Average\s+unpacked\s+length(:)?\s+\d+(.\d+)?(,)?\s+compression\s+ratio:\s+\d+(.\d+)?' ) + + # Pointer pages: N, data page slots: M + p_pointer_pages_data_pages_slots = re.compile( r'Pointer\s+pages(:)?\s+\d+(,)?\s+data\s+page\s+slots(:)?\s+\d+' ) + + average_record_length = compression_ratio = data_pages_cnt = -1 + gstat_lines = act.stdout.splitlines() + for line in gstat_lines: + if p_average_record_length.search(line): + # 'Average record length: 32757.00, total records: 10000' --> 32757 + average_record_length = int(float(line.replace(',','').split()[3])) + if p_compression_ratio.search(line): + # 'Average unpacked length: 32766.00, compression ratio: 1.00' + compression_ratio = float(line.split()[-1]) + if p_pointer_pages_data_pages_slots.search(line): + data_pages_cnt = int(line.split()[-1]) + + + assert average_record_length > 0 and compression_ratio > 0 and data_pages_cnt > 0 + + avg_rec_len_expected_msg = f'average_record_length -- expected: LESS OR EQUALS to declared column length = {N_WIDT}' + if average_record_length <= N_WIDT: + print(avg_rec_len_expected_msg) + else: + print(f'average_record_length -- UNEXPECTED: {average_record_length} - more than declared withd = {N_WIDT}') + + #------------------------------------------------------------------------------------------- + compression_ratio_expected_msg = f'compression_ratio_expected_msg -- expected: >= {COMPRESSION_THRESHOLD}' + if compression_ratio >= COMPRESSION_THRESHOLD: + print(compression_ratio_expected_msg) + else: + print(f'compression_ratio -- UNEXPECTED: {compression_ratio} - less than {COMPRESSION_THRESHOLD} (wasted compression occurred)') + + #------------------------------------------------------------------------------------------- + data_pages_cnt_expected_msg = f'data_pages_cnt: expected, within {MIN_DP_COUNT_THRESHOLD=} ... {MAX_DP_COUNT_THRESHOLD=}' + if data_pages_cnt >= MIN_DP_COUNT_THRESHOLD and data_pages_cnt <= MAX_DP_COUNT_THRESHOLD: + print(data_pages_cnt_expected_msg) + else: + print(f'data_pages_cnt UNEXPECTED: {data_pages_cnt=} -- out of scope: {MIN_DP_COUNT_THRESHOLD=} ... {MAX_DP_COUNT_THRESHOLD=}') + + act.expected_stdout = f""" + {avg_rec_len_expected_msg} + {compression_ratio_expected_msg} + {data_pages_cnt_expected_msg} + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_4954_test.py b/tests/bugs/gh_4954_test.py new file mode 100644 index 00000000..8d283451 --- /dev/null +++ b/tests/bugs/gh_4954_test.py @@ -0,0 +1,175 @@ +#coding:utf-8 + +""" +ID: issue-4954 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/4954 +TITLE: subselect losing the index when where clause includes coalesce() [CORE4640] +DESCRIPTION: +NOTES: + [21.08.2024] pzotov + Confirmed bug on 2.1.7.18553. No such problem on 2.5.927156. + Checked on 6.0.0.438, 5.0.2.1479, 4.0.6.3142, 3.0.12.33784. + [04.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. +""" +import pytest +from firebird.qa import * + +init_sql = """ + recreate table t1 ( + id int not null, + vc1 varchar(1) not null, + i1 int not null, + i2 int not null, + constraint t1_pk primary key (id) using descending index t1_pk, + constraint t1_uk1 unique (i1, i2, vc1) + ); + + recreate table t2 ( + id int not null, + vc1 varchar(1) not null, + i1 int not null, + i2 int not null, + constraint t2_pk primary key (id) using descending index t2_pk, + constraint t2_uk1 unique (i1, i2, vc1) + ); + + recreate table t3 ( + id int not null, + i1_1 int, + i1_2 int + ); + + create view v1 (ID, VC1, I1, I2) as + select t1.id, t1.vc1, t1.i1, t1.i2 + from t1 + union all + select t2.id, t2.vc1, t2.i1, t2.i2 + from t2; + commit; + + insert into t1 (id, vc1, i1, i2) values (9, 'a', 1009, 1000); + insert into t1 (id, vc1, i1, i2) values (8, 'a', 1008, 1000); + insert into t1 (id, vc1, i1, i2) values (7, 'a', 1007, 1000); + insert into t1 (id, vc1, i1, i2) values (6, 'a', 1006, 1000); + insert into t1 (id, vc1, i1, i2) values (5, 'a', 1005, 1000); + insert into t1 (id, vc1, i1, i2) values (4, 'a', 1004, 1000); + insert into t1 (id, vc1, i1, i2) values (3, 'a', 1003, 1000); + insert into t1 (id, vc1, i1, i2) values (2, 'a', 1002, 1000); + insert into t1 (id, vc1, i1, i2) values (1, 'a', 1001, 1000); + + + insert into t2 (id, vc1, i1, i2) values (19, 'a', 1019, 1000); + insert into t2 (id, vc1, i1, i2) values (18, 'a', 1018, 1000); + insert into t2 (id, vc1, i1, i2) values (17, 'a', 1017, 1000); + insert into t2 (id, vc1, i1, i2) values (16, 'a', 1016, 1000); + insert into t2 (id, vc1, i1, i2) values (15, 'a', 1015, 1000); + insert into t2 (id, vc1, i1, i2) values (14, 'a', 1014, 1000); + insert into t2 (id, vc1, i1, i2) values (13, 'a', 1013, 1000); + insert into t2 (id, vc1, i1, i2) values (12, 'a', 1012, 1000); + insert into t2 (id, vc1, i1, i2) values (11, 'a', 1011, 1000); + insert into t2 (id, vc1, i1, i2) values (10, 'a', 1010, 1000); + + + insert into t3 (id, i1_1, i1_2) values (100000, null, 1010); + insert into t3 (id, i1_1, i1_2) values (100001, 1012, null); + commit; + set statistics index t1_pk; + set statistics index t2_pk; + set statistics index t1_uk1; + set statistics index t2_uk1; + commit; +""" + +db = db_factory(init = init_sql) +act = python_act('db') + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- + +@pytest.mark.version('>=3.0') +def test_1(act: Action, capsys): + + test_sql = """ + select t3.id, + (select first 1 v1.id + from v1 + where + v1.vc1 = 'A' + and v1.i2 = 1000 + and v1.i1 = coalesce(t3.i1_1, t3.i1_2) + ) + from t3 + """ + + with act.db.connect() as con: + cur = con.cursor() + ps = cur.prepare(test_sql) + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + + expected_stdout_4x = """ + Select Expression + ....-> Singularity Check + ........-> First N Records + ............-> Filter + ................-> Union + ....................-> Filter + ........................-> Table "T1" as "V1 T1" Access By ID + ............................-> Bitmap + ................................-> Index "T1_UK1" Unique Scan + ....................-> Filter + ........................-> Table "T2" as "V1 T2" Access By ID + ............................-> Bitmap + ................................-> Index "T2_UK1" Unique Scan + Select Expression + ....-> Table "T3" Full Scan + """ + + expected_stdout_5x = """ + Sub-query + ....-> Singularity Check + ........-> First N Records + ............-> Filter + ................-> Filter + ....................-> Union + ........................-> Filter + ............................-> Table "T1" as "V1 T1" Access By ID + ................................-> Bitmap + ....................................-> Index "T1_UK1" Unique Scan + ........................-> Filter + ............................-> Table "T2" as "V1 T2" Access By ID + ................................-> Bitmap + ....................................-> Index "T2_UK1" Unique Scan + Select Expression + ....-> Table "T3" Full Scan + """ + + expected_stdout_6x = """ + Sub-query + ....-> Singularity Check + ........-> First N Records + ............-> Filter + ................-> Filter + ....................-> Union + ........................-> Filter + ............................-> Table "PUBLIC"."T1" as "PUBLIC"."V1" "PUBLIC"."T1" Access By ID + ................................-> Bitmap + ....................................-> Index "PUBLIC"."T1_UK1" Unique Scan + ........................-> Filter + ............................-> Table "PUBLIC"."T2" as "PUBLIC"."V1" "PUBLIC"."T2" Access By ID + ................................-> Bitmap + ....................................-> Index "PUBLIC"."T2_UK1" Unique Scan + Select Expression + ....-> Table "PUBLIC"."T3" Full Scan + """ + + act.expected_stdout = expected_stdout_4x if act.is_version('<5') else expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_5009_test.py b/tests/bugs/gh_5009_test.py new file mode 100644 index 00000000..1d520115 --- /dev/null +++ b/tests/bugs/gh_5009_test.py @@ -0,0 +1,118 @@ +#coding:utf-8 + +""" +ID: issue-5009 +ISSUE: 5009 +TITLE: Index and blob garbage collection doesn't take into accout data in undo log [CORE4701] +DESCRIPTION: +JIRA: CORE-4701 +NOTES: + [02.11.2024] pzotov + Confirmed bug on 3.0.13.33794. + Checked on 4.0.6.3165, 5.0.2.1551, 6.0.0.415 + [04.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.894; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. + +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + create table g_test (f integer); + create index g_ind on g_test (f); + insert into g_test values (1); + commit; + update g_test set f=2; + savepoint a; + update g_test set f=3; + savepoint b; + update g_test set f=3; + savepoint c; + update g_test set f=4; + savepoint d; + update g_test set f=4; + release savepoint b only; + rollback to savepoint c; + commit; + set list on; + set count on; + set plan on; + + select g.f as f_natreads from g_test g; + + select g.f as f_idxreads from g_test g where g.f between 1 and 4; +""" + +act = isql_act('db', test_script, substitutions = [ ('[ \t]+', ' '), ]) + +@pytest.mark.version('>=4.0.0') +def test_1(act: Action, capsys): + + act.execute(combine_output = True) + + expected_stdout_5x = """ + PLAN (G NATURAL) + F_NATREADS 3 + Records affected: 1 + PLAN (G INDEX (G_IND)) + F_IDXREADS 3 + Records affected: 1 + """ + + expected_stdout_6x = """ + PLAN ("G" NATURAL) + F_NATREADS 3 + Records affected: 1 + PLAN ("G" INDEX ("PUBLIC"."G_IND")) + F_IDXREADS 3 + Records affected: 1 + """ + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + + with act.connect_server() as srv: + srv.database.validate(database = act.db.db_path) + validate_err = '\n'.join( [line for line in srv if 'ERROR' in line.upper()] ) + + expected_isql = 'ISQL output check: PASSED.' + expected_onlv = 'Online validation: FAILED.' + + if act.clean_stdout == act.clean_expected_stdout: + print(expected_isql) + else: + print( + f""" + ISQL output check: FAILED. + Actual: + {act.clean_stdout} + Expected: + {act.expected_stdout} + """ + ) + + if not validate_err: + print(expected_onlv) + else: + print( + f""" + Online validation: FAILED. + Actual: + {validate_err} + Epsected: + + """ + ) + + + act.reset() + act.expected_stdout = f""" + {expected_isql} + {expected_onlv} + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + diff --git a/tests/bugs/gh_5537_test.py b/tests/bugs/gh_5537_test.py new file mode 100644 index 00000000..114b1ed9 --- /dev/null +++ b/tests/bugs/gh_5537_test.py @@ -0,0 +1,38 @@ +#coding:utf-8 + +""" +ID: issue-5537 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/5537 +TITLE: Non US-ASCII field names treated as unicode, although charset non-unicode, lowering max field length [CORE5258] +DESCRIPTION: +NOTES: + [09.11.2024] pzotov + FB-3.x must raise "Name longer than database column size", all others must work fine. + Checked on 3.0.13.33794, 4.0.6.3165, 5.0.2.1553, 6.0.0.520 +""" +from pathlib import Path +import pytest +from firebird.qa import * + +db = db_factory() +act = python_act('db', substitutions = [(r'After line \d+ .*', '')]) +tmp_file = temp_file('tmp_5537.sql') + +@pytest.mark.intl +@pytest.mark.version('>=3') +def test_1(act: Action, tmp_file: Path): + + NON_ASCII_TXT = 'Поле в 26 символов!' + tmp_file.write_bytes(f"""set list on; select '' as "{NON_ASCII_TXT}" from rdb$database;""".encode('cp1251')) + + expected_3x = """ + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Name longer than database column size + """ + expected_4x = f"{NON_ASCII_TXT}" + + act.expected_stdout = expected_3x if act.is_version('<4') else expected_4x + act.isql(switches = ['-q'], input_file = tmp_file, charset = 'win1251', io_enc = 'cp1251', combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_5588_test.py b/tests/bugs/gh_5588_test.py index 2e998242..d7fa1b7e 100644 --- a/tests/bugs/gh_5588_test.py +++ b/tests/bugs/gh_5588_test.py @@ -2,8 +2,8 @@ """ ID: issue-5588 -ISSUE: 5588 -TITLE: upport full SQL standard binary string literal syntax +ISSUE: https://github.com/FirebirdSQL/firebird/issues/5588 +TITLE: Support full SQL standard binary string literal syntax [CORE5311] DESCRIPTION: JIRA: CORE-5311 FBTEST: bugs.gh_5588 diff --git a/tests/bugs/gh_5589_test.py b/tests/bugs/gh_5589_test.py new file mode 100644 index 00000000..0ff18e81 --- /dev/null +++ b/tests/bugs/gh_5589_test.py @@ -0,0 +1,58 @@ +#coding:utf-8 + +""" +ID: issue-5589 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/5589 +TITLE: Support full SQL standard character string literal syntax [CORE5312] +DESCRIPTION: +JIRA: CORE-5312 +NOTES: + [15.09.2024] pzotov + Commit (13.05.2021): + https://github.com/FirebirdSQL/firebird/commit/8a7927aac4fef3740e54b7941146b6d044b864b1 + + Checked on 6.0.0.457, 5.0.2.1499 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set blob all; + set list on; + select 'ab' 'cd' 'ef' as good_chr_01 from rdb$database; + select 'ab'/*comment*/ 'cd' /**/ 'ef' as good_chr_02 from rdb$database; + select 'ab'/* foo + bar */'cd' + '' + /* + */ + + 'ef' as good_chr_03 from rdb$database; + + select 'ab' -- foo + 'cd' -- bar + 'ef' as good_chr_04 from rdb$database; + + select'ab' + 'cd' + 'ef' as good_chr_05 from rdb$database; +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +expected_stdout = """ + GOOD_CHR_01 abcdef + GOOD_CHR_02 abcdef + GOOD_CHR_03 abcdef + GOOD_CHR_04 abcdef + GOOD_CHR_05 abcdef +""" + +@pytest.mark.version('>=5.0') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_5740_test.py b/tests/bugs/gh_5740_test.py new file mode 100644 index 00000000..45d174d9 --- /dev/null +++ b/tests/bugs/gh_5740_test.py @@ -0,0 +1,103 @@ +#coding:utf-8 + +""" +ID: issue-5740 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/5740 +TITLE: Trace INCLUDE_FILTER with [[:WHITESPACE:]]+ does not work when statement contains newline is issued +DESCRIPTION: + We create a list of several DDLs which all contain NEWLINE character(s) between keyword and name of DB object. + Then we launch trace session and execute all these DDLs. + Finally we check whether trace log contains every DDL or not. + Expected result: text of every DDL should be FOUND in the trace log. +JIRA: CORE-5470 +NOTES: + On Windows print(act.trace_log) displays text with EOL containing space between CR and LF, i.e.: chr(13) + space + chr(10): + ['2024-05-16T12:42:17.8040 ... EXECUTE_STATEMENT_FINISH\r \n', '\tE:\\TEMP\\QA\\FBQA\\TEST_10\\TEST.FDB ... :::1/62705)\r \n', ] + + Space between CR and LF likely is an artifact of list to string conversion done by print() using it's __str__ method. + Explanation see in reply from pcisar: + subj: "act.trace_log ends with strange EOL that is: CR + space + NL // Windows"; date: 05-MAR-2023 + In order to get trace text with normal EOLs we have to do: + trace_txt = '\n'.join( [line.rstrip() for line in act.trace_log] ) + + Confirmed bug on 4.0.0.483 (date of build: 05-jan-2017). +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +act = python_act('db') + +chk_statements_lst = ["""recreate /* ddl_1 line_1 */ + + table /* ddl_1 line_2 */ + + + + t_test /* ddl_1 line_3 */ (x int) + """, + """comment on /* ddl_2 line_1 */ + table /* ddl_2 line_2 */ + + + t_test is /* ddl_2 line_3 */ + 'foo /* ddl_2 line_4 */ + /* ddl_2 line_4 */ bar' + """, +""" + + create /* ddl_3 line_1 */ + or /* ddl_3 line_2 */ + + alter /* ddl_3 line_3 */ + view /* ddl_3 line_4 */ + + v_rio /* ddl_3 line_5 */ + + as /* ddl_3 line_6 */ + select * /* ddl_3 line_6 */ + from /* ddl_3 line_7 */ + + rdb$database /* ddl_3 line_8 */ + """] + +trace = ['time_threshold = 0', + 'log_initfini = false', + 'log_errors = true', + 'log_statement_finish = true', + 'max_sql_length = 65500', + 'include_filter = "%(ddl_[[:DIGIT:]]+[[:WHITESPACE:]]+line_[[:DIGIT:]]+)%"', + ] + +@pytest.mark.trace +@pytest.mark.version('>=3.0.2') +def test_1(act: Action, capsys): + with act.trace(db_events=trace), act.db.connect() as con: + for cmd in chk_statements_lst: + con.execute_immediate(cmd) + con.commit() + + trace_txt = '\n'.join( [line.rstrip() for line in act.trace_log] ) + missed_cnt = 0 + for sttm in [c.rstrip() for c in chk_statements_lst]: + if trace_txt.find(sttm) < 0: + missed_cnt += 1 + if missed_cnt == 1: + print('Missed in the trace log:') + + print('----- sttm start -----') + for x in [x.strip() for x in sttm.split('\n')]: + print(x) + print('----- sttm finish ----') + + if missed_cnt: + print('----- trace start -----') + for x in trace_txt.split('\n'): + print(x) + print('----- trace finish ----') + + act.expected_stdout = '' + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_5749_test.py b/tests/bugs/gh_5749_test.py index 42c702aa..cc885992 100644 --- a/tests/bugs/gh_5749_test.py +++ b/tests/bugs/gh_5749_test.py @@ -27,6 +27,11 @@ -Token unknown - line 1, column 15 - Checked on 5.0.0.742 - all OK. + + [24.06.2025] pzotov + Fixed wrong value of charset that was used to connect: "utf-8". This caused crash of isql in recent 6.x. + https://github.com/FirebirdSQL/firebird/commit/5b41342b169e0d79d63b8d2fdbc033061323fa1b + Thanks to Vlad for solved problem. """ import pytest @@ -57,7 +62,6 @@ def test_1(act: Action, tmp_file: Path): tmp_file.write_bytes(whitespace_sql.encode('utf-8')) - act.expected_stdout = expected_stdout - act.isql(switches=['-q'], input_file=tmp_file, charset='utf-8', io_enc='utf-8', combine_output = True) + act.isql(switches=['-q'], input_file = tmp_file, charset = 'utf8', combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_5867_test.py b/tests/bugs/gh_5867_test.py new file mode 100644 index 00000000..63bf28cd --- /dev/null +++ b/tests/bugs/gh_5867_test.py @@ -0,0 +1,73 @@ +#coding:utf-8 + +""" +ID: issue-5867 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/5867 +TITLE: Add details on compression and crypt status of connection (fb_info_conn_flags) to getInfo() API call [CORE5601] +DESCRIPTION: + Custom driver config object ('db_cfg_object') is used: WireCompression and WireCrypt are client-side parameters. + Test checks all combinations of these parameters and compares them with values returned by query to mon$attachments. + Also, we have to compare these values with vuts in DbInfoCode.CONN_FLAGS -- all appropriate values must be equal. +NOTES: + [22.05.2024] pzotov + FB 3.x is not checked: there is no ability to get info about wire* parameters from mon$attachments table. + Checked on 6.0.0.357, 5.0.1.1404, 4.0.5.3099. +""" + +import pytest +from firebird.qa import * +from firebird.driver import driver_config, connect, NetProtocol, DatabaseError, DbInfoCode, ConnectionFlag + +db = db_factory() +act = python_act('db') + +@pytest.mark.version('>=4.0.0') +def test_1(act: Action, capsys): + + srv_cfg = driver_config.register_server(name = 'test_srv_gh_5867', config = '') + iter = 0 + for wcompr_iter in ('True','False'): + for wcrypt_iter in ('Disabled','Enabled'): + iter += 1 + db_cfg_name = f'tmp_5867_wcompr_{wcompr_iter}_wcrypt_{wcrypt_iter}' + db_cfg_object = driver_config.register_database(name = db_cfg_name) + db_cfg_object.server.value = srv_cfg.name + db_cfg_object.protocol.value = NetProtocol.INET + db_cfg_object.database.value = str(act.db.db_path) + + db_cfg_object.config.value = f""" + WireCompression = {wcompr_iter} + WireCrypt = {wcrypt_iter} + """ + + dbcfg_wcompr = True if wcompr_iter == 'True' else False + dbcfg_wcrypt = True if wcrypt_iter == 'Enabled' else False + with connect(db_cfg_name, user = act.db.user, password = act.db.password) as con: + try: + cur = con.cursor() + cur.execute('select mon$wire_compressed, mon$wire_encrypted from mon$attachments where mon$attachment_id = current_connection') + mon_wire_compressed, mon_wire_encrypted = cur.fetchone()[:2] + con_flags_bits = con.info.get_info(DbInfoCode.CONN_FLAGS) + con_flags_wcompr = False if con_flags_bits & 0b01 == ConnectionFlag.NONE else True + con_flags_wcrypt = False if con_flags_bits & 0b10 == ConnectionFlag.NONE else True + + if dbcfg_wcompr == mon_wire_compressed and mon_wire_compressed == con_flags_wcompr \ + and dbcfg_wcrypt == mon_wire_encrypted and mon_wire_encrypted == con_flags_wcrypt: + print(f'Check # {iter}: expected.') + else: + print('Check # {iter} - MISMATCH:') + print(f"Set in db_cfg: WireCompression = {dbcfg_wcompr}, WireCrypt = {dbcfg_wcrypt}") + print(f'mon$attachments: {mon_wire_compressed=}, {mon_wire_encrypted=}') + print(f'DbInfoCode.CONN_FLAGS: {con_flags_wcompr=}, {con_flags_wcrypt=}') + except DatabaseError as exc: + print(exc.__str__()) + + act.expected_stdout = """ + Check # 1: expected. + Check # 2: expected. + Check # 3: expected. + Check # 4: expected. + """ + + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_5978_test.py b/tests/bugs/gh_5978_test.py index 6bdba7a3..c443c4ca 100644 --- a/tests/bugs/gh_5978_test.py +++ b/tests/bugs/gh_5978_test.py @@ -5,21 +5,33 @@ ISSUE: https://github.com/FirebirdSQL/firebird/issues/5978 TITLE: Access to the name of DB encryption key [CORE5712] DESCRIPTION: - Test creates temporary user with system privilege GET_DBCRYPT_INFO in order to allow him to obtain encryption info. - Then we run following: - 1) encrypt DB using plugin 'fbSampleDbCrypt' provided in every FB 4.x+ snapshot; - 2) make connection as SYSDBA and ask DB-crypt info (DbInfoCode.CRYPT_PLUGIN and DbInfoCode.CRYPT_KEY) - 3) decrypt DB - After this we repeat these actions, except that in "2)" we use temporary user ('tmp_senior') instead of SYSDBA - (he must get same info as was obtained in previous step for SYSDBA). + Test uses Firebird built-in encryption plugin wich actually does encryption using trivial algorithm. + Before running this test following prerequisites must be met: + 1. Files fbSampleKeyHolder.conf, fbSampleKeyHolder.dll, fbSampleDbCrypt.conf and fbSampleDbCrypt.dll + must be copied from $FB_HOME/examples/prebuilt/plugins/ to $FB_HOME/plugins/ + (on Linux name of binaries are: libfbSampleDbCrypt.so and libfbSampleKeyHolder.so) + 2. File fbSampleKeyHolder.conf must contain lines: Auto = true and KeyRed = + 3. File $QA_HOME/pytest.ini must contain line with 'encryption' marker declaration. + + We create temporary user with system privilege GET_DBCRYPT_INFO in order to allow him to obtain encryption info. + Then we run following: + 1) encrypt DB using plugin 'fbSampleDbCrypt' provided in every FB 4.x+ snapshot; + 2) make connection as SYSDBA and ask DB-crypt info (DbInfoCode.CRYPT_PLUGIN and DbInfoCode.CRYPT_KEY) + 3) decrypt DB + After this we repeat these actions, except that in "2)" we use temporary user ('tmp_senior') instead of SYSDBA + (he must get same info as was obtained in previous step for SYSDBA). NOTES: - [08.05.2024] pzotov + [08.05.2024] pzotov - ### ACHTUNG ### TEST REQUIRES FIREBIRD-DRIVER VERSION 1.10.4+ (date: 07-may-2024). - See reply from pcisar, letters with subj: "fb_info_crypt_key: how it can be obtained using firebird-driver ? // GH-5978, 2018" + ### ACHTUNG ### TEST REQUIRES FIREBIRD-DRIVER VERSION 1.10.4+ (date: 07-may-2024). + Thanks to pcisar for explanation of DbInfoCode usage. + See letters with subj "fb_info_crypt_key: how it can be obtained using firebird-driver ? // GH-5978, 2018" (27.04.2024 14:55). - Checked on 4.0.5.3092, 5.0.1.1395, 6.0.0.346. - FB 3.x is not checked. + Firebird 3.x can not be checked. Exception: + raise NotSupportedError(f"Info code {info_code} not supported by engine version {self.__engine_version}") + firebird.driver.types.NotSupportedError: Info code 138 not supported by engine version 3.0 + + Checked on 4.0.5.3092, 5.0.1.1395, 6.0.0.346. """ import os import locale diff --git a/tests/bugs/gh_5995_test.py b/tests/bugs/gh_5995_test.py index 5a198638..5df7b063 100644 --- a/tests/bugs/gh_5995_test.py +++ b/tests/bugs/gh_5995_test.py @@ -5,150 +5,200 @@ ISSUE: 5995 TITLE: Connection to server may hang when working with encrypted databases over non-TCP protocol DESCRIPTION: - Test implemented only to be run on Windows. - It assumes that there are files keyholder.dll and keyholder.conf in the %FIREBIRD_HOME%\\plugins dir. - These files were provided by IBSurgeon and added during fbt_run prepare phase by batch scenario (qa_rundaily). - File keyholder.conf initially contains several keys. + Test implemented only to be run on Windows. + Folder %FIREBIRD_HOME%/plugins/ must have files fbSampleKeyHolder.conf and fbSampleKeyHolder.dll which should be + copied there from %FIREBIRD_HOME%/examples/prebuilt/plugins/. + NB! These files ABSENT in FB 3.x but they can be taken from FB 4.x snapshot. + File fbSampleKeyHolder.conf must have following lines: + Auto = true + KeyRed=111 - If we make this file EMPTY then usage of XNET and WNET protocols became improssible before this ticket was fixed. - Great thanks to Alex for suggestions. - - Confirmed bug on 3.0.1.32609: ISQL hangs on attempt to connect to database when file plugins\\keyholder.conf is empty. - In order to properly finish test, we have to kill hanging ISQL and change DB state to full shutdown (with subsequent - returning it to online) - fortunately, connection using TCP remains avaliable in this case. + If we encrypt database and then make file fbSampleKeyHolder.conf EMPTY then usage of XNET and WNET protocols became + impossible before this ticket was fixed. + Great thanks to Alex for suggestions. JIRA: CORE-5730 FBTEST: bugs.gh_5995 +NOTES: + [03.06.2024] pzotov + Confirmed bug on 3.0.1.32609, 4.0.0.853: ISQL hangs on attempt to connect to database when file plugins/keyholder.conf is empty. + Checked on 6.0.0.366, 5.0.1.1411, 4.0.5.3103 (all of them were checked for ServerMode = SS and CS). + + ATTENTION: 3.x raises different SQLSTATE and error text, depending on ServerMode! + For 3.x value of SQLSTATE and error text depends on Servermode. + On Classic FB 3.x output will be almost like in FB 4.x+: + Statement failed, SQLSTATE = 08004 + Missing correct crypt key + -Plugin fbSampleDbCrypt: + -Crypt key Red not set + -IProvider::attachDatabase failed when loading mapping cache + On Super FB 3.x output is: + Statement failed, SQLSTATE = HY000 + Missing correct crypt key + -Plugin fbSampleDbCrypt: + -Crypt key Red not set + Because of that, it was decided not to check FB 3.x as this version soon will be considered as obsolete. """ +import shutil +import locale +import re +import time +import platform +import subprocess + +import datetime as py_dt +from pathlib import Path + import pytest from firebird.qa import * +from firebird.driver import DatabaseError, DbInfoCode, NetProtocol + +import time + +########################### +### S E T T I N G S ### +########################### + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +enc_settings = QA_GLOBALS['encryption'] + +# ACHTUNG: this must be carefully tuned on every new host: +# +MAX_WAITING_ENCR_FINISH = int(enc_settings['MAX_WAIT_FOR_ENCR_FINISH_WIN']) +assert MAX_WAITING_ENCR_FINISH > 0 + +ENCRYPTION_PLUGIN = enc_settings['encryption_plugin'] # fbSampleDbCrypt +ENCRYPTION_KEY = enc_settings['encryption_key'] # Red db = db_factory() +act = python_act('db', substitutions = [('After line \\d+.*', ''),('[ \t]+', ' ')]) -act = python_act('db') +kholder_cfg_bak = temp_file('fbSampleKeyHolder.bak') +tmp_sql = temp_file('tmp_5995.sql') +tmp_log = temp_file('tmp_5995.log') -expected_stdout = """ - MON$REMOTE_PROTOCOL WNET - MON$REMOTE_PROTOCOL XNET -""" +#----------------------------------------------------------------------- + +def run_encr_decr(act: Action, mode, max_wait_encr_thread_finish, capsys): + + assert mode in ('encrypt', 'decrypt') -@pytest.mark.skip('FIXME: Not IMPLEMENTED') -@pytest.mark.version('>=3.0.4') + if mode == 'encrypt': + alter_db_sttm = f'alter database encrypt with "{ENCRYPTION_PLUGIN}" key "{ENCRYPTION_KEY}"' + wait_for_state = 'Database encrypted' + elif mode == 'decrypt': + alter_db_sttm = 'alter database decrypt' + wait_for_state = 'Database not encrypted' + + e_thread_finished = False + + d1 = py_dt.timedelta(0) + with act.db.connect() as con: + #cur = con.cursor() + #ps = cur.prepare('select mon$crypt_state from mon$database') + + t1=py_dt.datetime.now() + try: + d1 = t1-t1 + con.execute_immediate(alter_db_sttm) + con.commit() + # Pattern to check for completed encryption thread: + completed_encr_pattern = re.compile(f'Attributes\\s+encrypted,\\s+plugin\\s+{ENCRYPTION_PLUGIN}', re.IGNORECASE) + while not e_thread_finished: + t2=py_dt.datetime.now() + d1=t2-t1 + if d1.seconds*1000 + d1.microseconds//1000 > max_wait_encr_thread_finish: + break + + ############################################# + ### C H E C K G S T A T A T T R. ### + ############################################# + # Invoke 'gstat -h' and read its ouput. + # Encryption can be considered as COMPLETED when we will found: + # "Attributes encrypted, plugin fbSampleDbCrypt" + # + act.gstat(switches=['-h']) + for line in act.stdout.splitlines(): + if mode == 'encrypt' and completed_encr_pattern.match(line.strip()): + e_thread_finished = True + if mode == 'decrypt' and 'Attributes' in line and not completed_encr_pattern.search(line.strip()): + e_thread_finished = True + if e_thread_finished: + break + + time.sleep(0.5) + + except DatabaseError as e: + print( e.__str__() ) + + assert e_thread_finished, f'TIMEOUT EXPIRATION: {mode=} took {d1.seconds*1000 + d1.microseconds//1000} ms which greater than {max_wait_encr_thread_finish=} ms' + +#----------------------------------------------------------------------- + +@pytest.mark.encryption +@pytest.mark.version('>=4.0') @pytest.mark.platform('Windows') -def test_1(act: Action): - pytest.fail("Not IMPLEMENTED") +def test_1(act: Action, kholder_cfg_bak: Path, tmp_sql: Path, tmp_log: Path, capsys): + kholder_cfg_file = act.vars['home-dir'] / 'plugins' / 'fbSampleKeyHolder.conf' + shutil.copy2(kholder_cfg_file, kholder_cfg_bak) + finish_encryption = False -# test_script_1 -#--- -# -# import os -# import subprocess -# from subprocess import Popen -# import datetime -# import time -# import shutil -# import re -# import fdb -# -# os.environ["ISC_USER"] = user_name -# os.environ["ISC_PASSWORD"] = user_password -# engine = db_conn.engine_version -# db_name = db_conn.database_name -# db_conn.close() -# -# svc = fdb.services.connect(host='localhost', user=user_name, password=user_password) -# FB_HOME = svc.get_home_directory() -# svc.close() -# -# #-------------------------------------------- -# -# def flush_and_close( file_handle ): -# # https://docs.python.org/2/library/os.html#os.fsync -# # If you're starting with a Python file object f, -# # first do f.flush(), and -# # then do os.fsync(f.fileno()), to ensure that all internal buffers associated with f are written to disk. -# global os -# -# file_handle.flush() -# if file_handle.mode not in ('r', 'rb') and file_handle.name != os.devnull: -# # otherwise: "OSError: [Errno 9] Bad file descriptor"! -# os.fsync(file_handle.fileno()) -# file_handle.close() -# -# #-------------------------------------------- -# -# def cleanup( f_names_list ): -# global os -# for i in range(len( f_names_list )): -# if type(f_names_list[i]) == file: -# del_name = f_names_list[i].name -# elif type(f_names_list[i]) == str: -# del_name = f_names_list[i] -# else: -# print('Unrecognized type of element:', f_names_list[i], ' - can not be treated as file.') -# print('type(f_names_list[i])=',type(f_names_list[i])) -# del_name = None -# -# if del_name and os.path.isfile( del_name ): -# os.remove( del_name ) -# -# #-------------------------------------------- -# -# -# dts = datetime.datetime.now().strftime("%y%m%d_%H%M%S") -# -# kholder_cur = os.path.join( FB_HOME, 'plugins', 'keyholder.conf') -# kholder_bak = os.path.join( context['temp_directory'], 'keyholder'+dts+'.bak') -# -# shutil.copy2( kholder_cur, kholder_bak) -# -# # Make file %FB_HOME%\\plugins\\keyholder.conf empty: -# with open(kholder_cur,'w') as f: -# pass -# -# MAX_SECONDS_TO_WAIT = 3 -# -# # Trying to establish connection to database using WNET and XNET protocols. -# # Async. launch of ISQL with check that it will finished within some reasonable time (and w/o errors). -# # If it will hang - kill (this is bug dexcribed in the ticket) -# for p in ('wnet', 'xnet'): -# f_isql_sql=open(os.path.join(context['temp_directory'],'tmp_gh_5995.'+p+'.sql'),'w') -# f_isql_sql.write('set list on; select mon$remote_protocol from mon$attachments where mon$attachment_id = current_connection;') -# flush_and_close( f_isql_sql ) -# -# protocol_conn_string = ''.join( (p, '://', db_name) ) -# f_isql_log=open( os.path.join(context['temp_directory'],'tmp_gh_5995.'+p+'.log'), 'w') -# p_isql = Popen([ context['isql_path'], protocol_conn_string, "-i", f_isql_sql.name], stdout=f_isql_log, stderr=subprocess.STDOUT ) -# -# time.sleep(0.2) -# for i in range(0,MAX_SECONDS_TO_WAIT): -# # Check if child process has terminated. Set and return returncode attribute. Otherwise, returns None. -# p_isql.poll() -# if p_isql.returncode is None: -# # A None value indicates that the process has not terminated yet. -# time.sleep(1) -# if i < MAX_SECONDS_TO_WAIT-1: -# continue -# else: -# f_isql_log.write( '\\nISQL process %d hangs for %d seconds and is forcedly killed.' % (p_isql.pid, MAX_SECONDS_TO_WAIT) ) -# p_isql.terminate() -# -# flush_and_close(f_isql_log) -# -# with open(f_isql_log.name,'r') as f: -# for line in f: -# if line: -# print(line) -# -# cleanup((f_isql_sql,f_isql_log)) -# -# shutil.move( kholder_bak, kholder_cur) -# -# # ::: NOTE ::: We have to change DB state to full shutdown and bring it back online -# # in order to prevent "Object in use" while fbtest will try to drop this DB -# ##################################### -# runProgram('gfix',[dsn,'-shut','full','-force','0']) -# runProgram('gfix',[dsn,'-online']) -# -# -#--- + protocols_list = [ NetProtocol.INET, NetProtocol.XNET, ] + if act.is_version('<5'): + protocols_list.append(NetProtocol.WNET) + + expected_output = actual_output = test_sql = '' + try: + run_encr_decr(act, 'encrypt', MAX_WAITING_ENCR_FINISH, capsys) + finish_encryption = True + with open(kholder_cfg_file,'w') as f: + pass + + for protocol_name in protocols_list: + conn_str = f"connect {protocol_name.name.lower()}://{act.db.db_path} user {act.db.user} password '{act.db.password}'" + test_sql = f""" + set list on; + set bail on; + set echo on; + {conn_str}; + select mon$remote_protocol from mon$attachments where mon$attachment_id = current_connection; + """ + tmp_sql.write_text(test_sql) + + with open(tmp_log, 'w') as f_log: + # ISQL-4.x must issue: + # Statement failed, SQLSTATE = 08004 + # Missing database encryption key for your attachment + # -Plugin fbSampleDbCrypt: + # -Crypt key Red not set + # Before fix, ISQL hanged on CONNECT, thus we have to use timeout here! + # + p = subprocess.run( [ act.vars['isql'], + '-q', + '-i', str(tmp_sql) + ], + stdout = f_log, stderr = subprocess.STDOUT, + timeout = 3 + ) + + actual_output += tmp_log.read_text() + + expected_output += f""" + {conn_str}; + Statement failed, SQLSTATE = 08004 + Missing database encryption key for your attachment + -Plugin {ENCRYPTION_PLUGIN}: + -Crypt key {ENCRYPTION_KEY} not set + """ + + except Exception as e: + actual_output += test_sql + '\n' + e.__str__() + finally: + shutil.copy2(kholder_cfg_bak, kholder_cfg_file) + if finish_encryption: + run_encr_decr(act, 'decrypt', MAX_WAITING_ENCR_FINISH, capsys) + + act.expected_stdout = expected_output + act.stdout = actual_output # capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_6120_test.py b/tests/bugs/gh_6120_test.py new file mode 100644 index 00000000..0299fb51 --- /dev/null +++ b/tests/bugs/gh_6120_test.py @@ -0,0 +1,61 @@ +#coding:utf-8 + +""" +ID: issue-6120 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/6120 +TITLE: Support auth_plugin_list dpb/spb item from application to client [CORE5860] +DESCRIPTION: +NOTES: + [22.07.2024] pzotov + Improvement appeared 28.08.2018 18:47, commit: + https://github.com/FirebirdSQL/firebird/commit/dc06a58a1a7923a5954f33c8c131ae3eaf59b907 + + Checked on 6.0.0.396, 5.0.1.1440, 4.0.5.3127, 3.0.12.33765 +""" +import pytest +from firebird.qa import * +from firebird.driver import driver_config, connect, NetProtocol, DatabaseError + +db = db_factory() +act = python_act('db') + +tmp_srp_user = user_factory('db', name='tmp$6120_srp', password='123', plugin = 'Srp') +tmp_leg_user = user_factory('db', name='tmp$6120_leg', password='456', plugin = 'Legacy_UserManager') + +@pytest.mark.version('>=3.0.3') +def test_1(act: Action, tmp_srp_user: User, tmp_leg_user: User, capsys): + + srv_cfg = driver_config.register_server(name = 'srv_cfg_6120', config = '') + + db_cfg_name = f'db_cfg_6120' + + # DatabaseConfig; see PYTHON_HOME/Lib/site-packages/firebird/driver/config.py: + db_cfg_object = driver_config.register_database(name = db_cfg_name) + + db_cfg_object.server.value = srv_cfg.name + db_cfg_object.protocol.value = NetProtocol.INET + db_cfg_object.database.value = str(act.db.db_path) + db_cfg_object.auth_plugin_list.value = ','.join( ['Srp', 'Legacy_Auth'] ) + + for u in (tmp_srp_user, tmp_leg_user): + with connect(db_cfg_name, user = u.name, password = u.password) as con: + cur = con.cursor() + try: + cur.execute('select trim(mon$user) as mon_user, mon$auth_method as auth_way from mon$attachments') + ccol=cur.description + for r in cur: + for i in range(0,len(ccol)): + print( ccol[i][0],':', r[i] ) + except DatabaseError as e: + print(e.__str__()) + + act.expected_stdout = f""" + MON_USER : TMP$6120_SRP + AUTH_WAY : Srp + + MON_USER : TMP$6120_LEG + AUTH_WAY : Legacy_Auth + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() diff --git a/tests/bugs/gh_6267_test.py b/tests/bugs/gh_6267_test.py new file mode 100644 index 00000000..e5fa01c1 --- /dev/null +++ b/tests/bugs/gh_6267_test.py @@ -0,0 +1,43 @@ +#coding:utf-8 + +""" +ID: issue-6267 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/6267 +TITLE: Add transaction info fb_info_tra_snapshot_number [CORE6017] +DESCRIPTION: + Test verifies ability to use appropriate API info and whether value of + returned snapshot_number equals to RDB$GET_CONTEXT('SYSTEM', 'SNAPSHOT_NUMBER'). +NOTES: + [22.07.2024] pzotov + Checked on 6.0.0.396, 5.0.1.1440, 4.0.5.3127 +""" + +import pytest +from firebird.qa import * +from firebird.driver import tpb, Isolation + +db = db_factory() +act = python_act('db') + +CUSTOM_TPB = tpb(isolation = Isolation.SNAPSHOT) + +@pytest.mark.version('>=4.0') +def test_1(act: Action, capsys): + with act.db.connect() as con: + tx1 = con.transaction_manager(CUSTOM_TPB) + tx1.begin() + cur1 = tx1.cursor() + + cur1.execute("select RDB$GET_CONTEXT('SYSTEM', 'SNAPSHOT_NUMBER') from rdb$database") + ctx_sn = int(cur1.fetchone()[0]) + if ctx_sn == tx1.info.snapshot_number: + print('OK') + else: + print(f'MISMATCH: RDB$GET_CONTEXT={ctx_sn}, {tx1.info.snapshot_number=}') + tx1.commit() + + act.expected_stdout = """ + OK + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_6282_test.py b/tests/bugs/gh_6282_test.py new file mode 100644 index 00000000..a38abf20 --- /dev/null +++ b/tests/bugs/gh_6282_test.py @@ -0,0 +1,341 @@ +#coding:utf-8 + +""" +ID: issue-6282 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/6282 +TITLE: Add DPB properties for time zone bind and decfloat configuration [CORE6032] +DESCRIPTION: + Test verifies: + * ability to set appropriate DPB parameters described in the ticket; + * actual result of their change (by performing SQL and compare output with expected); + * surviving of changed parameters after 'slter session reset' (i.e. absence of its influence) +NOTES: + [22.07.2024] pzotov + Improvement appeared 07.04.2019 17:56 (2a9f8fa60b327132373cd7ee3f0a0b52e595f6b1), + but actually this test can run only on build after 29.05.2020 (4.0.0.2011) because of + commit https://github.com/FirebirdSQL/firebird/commit/a9cef6d9aeaabc08d8f104230a38345340edf7a2 + ("Implemented CORE-6320: Replace Util methods to get interface pointer by legacy handle with plain functions") + Attempt to run it on earlier snapshots leads to errors: + firebird.driver.types.DatabaseError: invalid statement handle + or + AttributeError: 'iUtil_v2' object has no attribute 'decode_timestamp_tz' + + [01.09.2024] + On Linux argument of tzfile is shown with prefix ("/usr/share/zoneinfo/"), so we have to remove it: + : + Windows = tzfile('Indian/Cocos') + Linux = tzfile('/usr/share/zoneinfo/Indian/Cocos') + This is done by extracting '_timezone_' property of this instance. + + Checked on 6.0.0.396, 5.0.1.1440, 4.0.5.3127 +""" +import pytest +from firebird.qa import * +from firebird.driver import driver_config, connect, DecfloatRound, DecfloatTraps, NetProtocol, DatabaseError +import datetime + +init_script = """ + set list on; + /****** + Original: ibm.com/developerworks/ru/library/dm-0801chainani/ + See also: functional/datatypes/test_decfloat_round_modes.py: + round-mode 12.341 12.345 12.349 12.355 12.405 -12.345 + --------------------------------------------------------------- + CEILING 12.35 12.35 12.35 12.36 12.41 -12.34 + UP 12.35 12.35 12.35 12.36 12.41 -12.35 + HALF_UP 12.34 12.35 12.35 12.36 12.41 -12.35 + HALF_EVEN 12.34 12.34 12.35 12.36 12.40 -12.34 + HALF_DOWN 12.34 12.34 12.35 12.35 12.40 -12.34 + DOWN 12.34 12.34 12.34 12.35 12.40 -12.34 + FLOOR 12.34 12.34 12.34 12.35 12.40 -12.35 + REROUND 12.34 12.34 12.34 12.36 12.41 -12.34 + *******/ + + recreate view v_test as select 1 id from rdb$database; + commit; + + recreate table test( + v1 decfloat + ,v2 decfloat + ,v3 decfloat + ,v4 decfloat + ,v5 decfloat + ,v6 decfloat + ,vc decfloat + ,vp decfloat + ,vd decfloat + ,vx computed by (vc * vp / vd) + ,vy computed by (vc * vp / vd) + ) + ; + commit; + + insert into test( v1, v2, v3, v4, v5, v6, vc, vp, vd) + values(12.341, 12.345, 12.349, 12.355, 12.405, -12.345, 1608.90, 5.00, 100.00); + commit; + + recreate view v_test as + select + round(v1, 2) r1, round(v2, 2) r2, round(v3, 2) r3, + round(v4, 2) r4, round(v5, 2) r5, round(v6, 2) r6, + round( vx, 2) as rx, + round( -vy, 2) as ry + from test; + commit; +""" + +db = db_factory(init = init_script) +act = python_act('db') + +@pytest.mark.version('>=4.0') +def test_1(act: Action, capsys): + + srv_cfg = driver_config.register_server(name = 'srv_cfg_6282', config = '') + + db_cfg_name = f'db_cfg_6282' + + # DatabaseConfig; see PYTHON_HOME/Lib/site-packages/firebird\driver\config.py: + db_cfg_object = driver_config.register_database(name = db_cfg_name) + + db_cfg_object.server.value = srv_cfg.name + db_cfg_object.protocol.value = NetProtocol.INET + db_cfg_object.database.value = str(act.db.db_path) + + ###################################################### + ### c h e c k s e s s i o n t i m e z o n e ### + ###################################################### + # + SELECTED_TIMEZONE = 'Indian/Cocos' + db_cfg_object.session_time_zone.value = SELECTED_TIMEZONE + with connect(db_cfg_name, user = act.db.user, password = act.db.password) as con: + cur = con.cursor() + cur.execute('select current_timestamp from rdb$database') + for r in cur: + print(r[0].tzinfo._timezone_) + cur.close() + + # The value set through the DPB should survive an `alter session reset` + con.execute_immediate('alter session reset') + con.commit() + + cur = con.cursor() + cur.execute('select current_timestamp from rdb$database') + for r in cur: + # class 'dateutil.zoneinfo.tzfile' + tzfile_nfo = r[0].tzinfo # : Windows = tzfile('Indian/Cocos'); Linux = tzfile('/usr/share/zoneinfo/Indian/Cocos') + # tzfile_arg = tzfile_nfo._filename # : Windows = 'Indian/Cocos'; Linux = '/usr/share/zoneinfo/Indian/Cocos' + print(tzfile_nfo._timezone_) # Windows: 'Indian/Cocos'; Linux: 'Indian/Cocos' + + act.expected_stdout = f""" + {SELECTED_TIMEZONE} + {SELECTED_TIMEZONE} + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() + + # .................................................................................... + + ################################################ + ### c h e c k D e c f l o a t R o u n d ### + ################################################ + # + # doc/sql.extensions/README.data_types.txt: + # CEILING (towards +infinity), + # UP (away from 0), + # HALF_UP (to nearest, if equidistant - up), + # HALF_EVEN (to nearest, if equidistant - ensure last digit in the result to be even), + # HALF_DOWN (to nearest, if equidistant - down), + # DOWN (towards 0), + # FLOOR (towards -infinity), + # REROUND (up if digit to be rounded is 0 or 5, down in other cases). + # + # Examples from functional/datatypes/test_decfloat_round_modes.py: + # + df_round_lst = ( + DecfloatRound.CEILING + ,DecfloatRound.UP + ,DecfloatRound.HALF_UP + ,DecfloatRound.HALF_EVEN + ,DecfloatRound.HALF_DOWN + ,DecfloatRound.DOWN + ,DecfloatRound.FLOOR + ,DecfloatRound.REROUND + ) + for r in df_round_lst: + db_cfg_object.decfloat_round.value = r + print(r.name) + + with connect(db_cfg_name, user = act.db.user, password = act.db.password) as con: + try: + + # The value set through the DPB should survive an `alter session reset` + con.execute_immediate('alter session reset') + con.commit() + + cur = con.cursor() + cur.execute('select v.* from v_test v') + ccol=cur.description + for r in cur: + for i in range(0,len(ccol)): + print( ccol[i][0],':', r[i] ) + except DatabaseError as e: + print(e.__str__()) + + # Return round option to default: + db_cfg_object.decfloat_round.value = DecfloatRound.HALF_UP + + act.expected_stdout = """ + CEILING + R1 : 12.35 + R2 : 12.35 + R3 : 12.35 + R4 : 12.36 + R5 : 12.41 + R6 : -12.34 + RX : 80.45 + RY : -80.44 + UP + R1 : 12.35 + R2 : 12.35 + R3 : 12.35 + R4 : 12.36 + R5 : 12.41 + R6 : -12.35 + RX : 80.45 + RY : -80.45 + HALF_UP + R1 : 12.34 + R2 : 12.35 + R3 : 12.35 + R4 : 12.36 + R5 : 12.41 + R6 : -12.35 + RX : 80.45 + RY : -80.45 + HALF_EVEN + R1 : 12.34 + R2 : 12.34 + R3 : 12.35 + R4 : 12.36 + R5 : 12.40 + R6 : -12.34 + RX : 80.44 + RY : -80.44 + HALF_DOWN + R1 : 12.34 + R2 : 12.34 + R3 : 12.35 + R4 : 12.35 + R5 : 12.40 + R6 : -12.34 + RX : 80.44 + RY : -80.44 + DOWN + R1 : 12.34 + R2 : 12.34 + R3 : 12.34 + R4 : 12.35 + R5 : 12.40 + R6 : -12.34 + RX : 80.44 + RY : -80.44 + FLOOR + R1 : 12.34 + R2 : 12.34 + R3 : 12.34 + R4 : 12.35 + R5 : 12.40 + R6 : -12.35 + RX : 80.44 + RY : -80.45 + REROUND + R1 : 12.34 + R2 : 12.34 + R3 : 12.34 + R4 : 12.36 + R5 : 12.41 + R6 : -12.34 + RX : 80.44 + RY : -80.44 + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() + + # .................................................................................... + + ################################################ + ### c h e c k D e c f l o a t T r a p s ### + ################################################ + # + # doc/sql.extensions/README.data_types.txt: + # SET DECFLOAT TRAPS TO + # Division_by_zero, Inexact, Invalid_operation, Overflow and Underflow + # + + # Examples from functional/datatypes/test_decfloat_exceptions_trapping.py: + # + df_traps_map = { + DecfloatTraps.DIVISION_BY_ZERO : + ( 'select 1/1e-9999' + ,'Decimal float divide by zero.' + ) + ,DecfloatTraps.INEXACT : + ( 'select 1e9999 + 1e9999' + ,'Decimal float inexact result.' + ) + ,DecfloatTraps.INVALID_OPERATION : + ( "select cast('34ffd' as decfloat(16))" + ,'Decimal float invalid operation.' + ) + ,DecfloatTraps.OVERFLOW : + ( 'select 1e9999' + ,'Decimal float overflow.' + ) + ,DecfloatTraps.UNDERFLOW : + ( 'select 1e-9999' + ,'Decimal float underflow.' + ) + } + + expected_out_lst = [] + actual_out_lst = [] + expected_iter = 'EXPECTED error message raised.' + for k,v in df_traps_map.items(): + traps_option = k + execute_sttm = v[0] + ' from rdb$database' + expected_msg = v[1] + db_cfg_object.decfloat_traps.value = [traps_option,] + + # expected_out_lst.append( str(db_cfg_object.decfloat_traps.value[0]) ) # name of option + expected_out_lst.append(traps_option.name) + expected_out_lst.append(expected_iter) + + actual_out_lst.append(traps_option.name) + + with connect(db_cfg_name, user = act.db.user, password = act.db.password) as con: + try: + # The value set through the DPB should survive an `alter session reset` + con.execute_immediate('alter session reset') + con.commit() + + cur = con.cursor() + cur.execute(execute_sttm) + for r in cur: + pass + except DatabaseError as e: + if expected_msg in e.__str__(): + actual_out_lst.append(expected_iter) + else: + actual_out_lst.append('UNEXPECTED ERROR MESSAGE:\n' + e.__str__()) + + + act.expected_stdout = '\n'.join(expected_out_lst) + act.stdout = '\n'.join(actual_out_lst) + assert act.clean_stdout == act.clean_expected_stdout + act.reset() + + # Return traps option to default: + db_cfg_object.decfloat_traps.value = [] + diff --git a/tests/bugs/gh_6416_test.py b/tests/bugs/gh_6416_test.py new file mode 100644 index 00000000..8fe8b864 --- /dev/null +++ b/tests/bugs/gh_6416_test.py @@ -0,0 +1,162 @@ +#coding:utf-8 + +""" +ID: issue-6416 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/6416 +TITLE: Engine cant determine datatype in SQL: Unknown SQL Data type (32752) [CORE6168] +DESCRIPTION: + Test creates table with columns belonging to "new datatypes" family: int128, decfloat and time[stamp] with time zone. + Also, one record is added into this table with values which are valid for numeric types in FB 4.x+ (time zone fields + can remain null or arbitrary). + Further, this DB is copied to another DB using file-level call of shutil.copy2(). + Another DB filename must match to the specified in the databases.conf (alias defined by 'REQUIRED_ALIAS' variable). + Its alias has special value for DataTypeCompatibility parameter. Connection to this DB and query to a table with 'new datatypes' + must return SQLDA with *old* types which are known for FB versions prior 4.x. + + Then we repeat same query to 'initial' test DB and must get SQLDA with actual values for all new columns (known since FB 4.x). +NOTES: + [18.08.2022] pzotov + 1. One need to be sure that firebird.conf does NOT contain DatabaseAccess = None. + 2. Database file for REQUIRED_ALIAS must NOT exist in the QA_root/files/qa/ subdirectory: it will be created here. + Content of databases.conf must be taken from $QA_ROOT/files/qa-databases.conf (one need to replace + it before every test session). + Discussed with pcisar, letters since 30-may-2022 13:48, subject: + "new qa, core_4964_test.py: strange outcome when use... shutil.copy() // comparing to shutil.copy2()" + 3. Value of REQUIRED_ALIAS must be EXACTLY the same as alias specified in the pre-created databases.conf + (for LINUX this equality is case-sensitive, even when aliases are compared!) + + Checked on 6.0.0.438, 5.0.2.1479, 4.0.6.3142. + + [24.05.2025] pzotov + Splitted expected* variables for versions up to 5.x and 6.x+ + This is needed after 11d5d5 ("Fix for #8082 ... user buffers directly (#8145)") by Dmitry Sibiryakov. + Discussed in email 24.05.2025 22:06, subj: "one more consequence of 11d5d5 ..." (since 15.05.2025 17:25). +""" + +import re +from pathlib import Path +import shutil + +import pytest +from firebird.qa import * + +# Pre-defined alias for test DB in the QA_root/files/qa-databases.conf. +# This file (qa-databases.conf) must be copied manually to each testing +# FB home folder, with replacing databases.conf there: +# +REQUIRED_ALIAS = 'tmp_gh_6416_alias' + +init_sql = f''' + set bail on; + recreate table test( + f_sml smallint default -32768 + ,f_int int default -2147483648 + ,f_big bigint default -9223372036854775808 + ,f_128 int128 default -170141183460469231731687303715884105728 + ,f_num numeric(38) default -170141183460469231731687303715884105728 + ,f_dec decfloat default -9.999999999999999999999999999999999E+6144 + ,f_tz time with time zone default '01:02:03 Indian/Cocos' + ,f_tsz timestamp with time zone default '22.09.2023 01:02:03 Indian/Cocos' + ); + insert into test default values; + commit; +''' + +db = db_factory(init = init_sql) + +substitutions = [('^((?!(SQLSTATE|error|Floating-point overflow|sqltype)).)*$', ''), ('[ \t]+', ' ')] +act = python_act('db', substitutions = substitutions) + +@pytest.mark.version('>=4.0') +def test_1(act: Action, capsys): + + + # Scan line-by-line through databases.conf, find line starting with REQUIRED_ALIAS and extract name of file that + # must be created in the $(dir_sampleDb)/qa/ folder. This name will be used further as target database (tmp_db_for_3x_client). + # NOTE: we have to SKIP lines which are commented out, i.e. if they starts with '#': + p_required_alias_ptn = re.compile( '^(?!#)((^|\\s+)' + REQUIRED_ALIAS + ')\\s*=\\s*\\$\\(dir_sampleDb\\)/qa/', re.IGNORECASE ) + fname_in_dbconf = None + + with open(act.home_dir/'databases.conf', 'r') as f: + for line in f: + if p_required_alias_ptn.search(line): + # If databases.conf contains line like this: + # tmp_6416_alias = $(dir_sampleDb)/qa/tmp_gh_6416.fdb + # - then we extract filename: 'tmp_gh_6416.fdb' (see below): + fname_in_dbconf = Path(line.split('=')[1].strip()).name + break + + # if 'fname_in_dbconf' remains undefined here then propably REQUIRED_ALIAS not equals to specified in the databases.conf! + # + assert fname_in_dbconf + + # Full path + filename of database to which we will try to connect: + # + tmp_db_for_3x_client = Path( act.vars['sample_dir'], 'qa', fname_in_dbconf ) + shutil.copy2(act.db.db_path, tmp_db_for_3x_client) + + test_sql = f''' + set bail on; + set list on; + connect '{REQUIRED_ALIAS}' user {act.db.user}; + -- select mon$database_name from mon$database; + set sqlda_display on; + select * + from test; + ''' + + expected_out_5x = f""" + 01: sqltype: 500 SHORT Nullable scale: 0 subtype: 0 len: 2 + 02: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + 03: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + 04: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + 05: sqltype: 580 INT64 Nullable scale: 0 subtype: 1 len: 8 + 06: sqltype: 480 DOUBLE Nullable scale: 0 subtype: 0 len: 8 + 07: sqltype: 560 TIME Nullable scale: 0 subtype: 0 len: 4 + 08: sqltype: 510 TIMESTAMP Nullable scale: 0 subtype: 0 len: 8 + Statement failed, SQLSTATE = 22003 + -SQL error code = -303 + -Floating-point overflow. The exponent of a floating-point operation is greater than the magnitude allowed. + """ + + expected_out_6x = f""" + 01: sqltype: 500 SHORT Nullable scale: 0 subtype: 0 len: 2 + 02: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + 03: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + 04: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + 05: sqltype: 580 INT64 Nullable scale: 0 subtype: 1 len: 8 + 06: sqltype: 480 DOUBLE Nullable scale: 0 subtype: 0 len: 8 + 07: sqltype: 560 TIME Nullable scale: 0 subtype: 0 len: 4 + 08: sqltype: 510 TIMESTAMP Nullable scale: 0 subtype: 0 len: 8 + Statement failed, SQLSTATE = 22003 + Floating-point overflow. The exponent of a floating-point operation is greater than the magnitude allowed. + """ + + act.expected_stdout = expected_out_5x if act.is_version('<6') else expected_out_6x + act.isql(switches = ['-q'], input = test_sql, combine_output = True, credentials = False, connect_db = False) + assert act.clean_stdout == act.clean_expected_stdout + act.reset() + tmp_db_for_3x_client.unlink() + + #------------------------------------------------------------- + + test_sql = f''' + set bail on; + set list on; + set sqlda_display on; + select * + from test; + ''' + act.expected_stdout = """ + 01: sqltype: 500 SHORT Nullable scale: 0 subtype: 0 len: 2 + 02: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + 03: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + 04: sqltype: 32752 INT128 Nullable scale: 0 subtype: 0 len: 16 + 05: sqltype: 32752 INT128 Nullable scale: 0 subtype: 1 len: 16 + 06: sqltype: 32762 DECFLOAT(34) Nullable scale: 0 subtype: 0 len: 16 + 07: sqltype: 32756 TIME WITH TIME ZONE Nullable scale: 0 subtype: 0 len: 8 + 08: sqltype: 32754 TIMESTAMP WITH TIME ZONE Nullable scale: 0 subtype: 0 len: 12 + """ + act.isql(switches = ['-q'], input = test_sql, combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout + act.reset() diff --git a/tests/bugs/gh_6545_test.py b/tests/bugs/gh_6545_test.py new file mode 100644 index 00000000..89bab2b5 --- /dev/null +++ b/tests/bugs/gh_6545_test.py @@ -0,0 +1,689 @@ +#coding:utf-8 + +""" +ID: issue-6545 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/6545 +TITLE: Error writing to TIMESTAMP/TIME WITH TIME ZONE array +DESCRIPTION: + Test generates values which will be inserted into ARRAY columns defined as 'time with time zone' and 'timestamp with time zone'. + We process time zones defined in the FB_HOME/include/firebird/TimeZones.h (except several, see notes below), + and use every time zone value as argument to 'get_timezone()' in datetime.dsatetime / datetime.time calls. + Then we run DML which tries to insert tuple of generated data into appropriate ARRAY columns. This must not raise error. + Finally, we run query to get just inserted data and compare its result with input argument that was used in previous step. +NOTES: + [15.08.2024] pzotov + 1. ### ACHTUNG ### TEST REQUIRES FIREBIRD-DRIVER VERSION 1.10.6+ (date: 15-aug-2024). + See also addition in firebird-driver doc: + https://firebird-driver.readthedocs.io/en/latest/usage-guide.html#working-with-time-timestamp-with-timezone + + 2. Following timezones present in $FB_HOME/include/firebird/TimeZones.h + and in https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + but are absent in pytz.all_timezones list: + America/Ciudad_Juarez + Europe/Kyiv + Pacific/Kanton + We have to SKIP them from handling. + + Checked on 4.0.0.436, 5.0.2.1478, 4.0.6.3142 +""" +import datetime +from firebird.driver import get_timezone +import random + +import pytest +from firebird.qa import * + +init_script = """ + create table test ( + arr_tmtz time with time zone [0:2] + ,arr_tstz timestamp with time zone [0:2] + ); +""" +db = db_factory(init = init_script) + +act = python_act('db') + +@pytest.mark.version('>=4.0') +def test_1(act: Action, capsys): + fb_time_zones = [ + 'Africa/Abidjan' + ,'Africa/Accra' + ,'Africa/Addis_Ababa' + ,'Africa/Algiers' + ,'Africa/Asmara' + ,'Africa/Asmera' + ,'Africa/Bamako' + ,'Africa/Bangui' + ,'Africa/Banjul' + ,'Africa/Bissau' + ,'Africa/Blantyre' + ,'Africa/Brazzaville' + ,'Africa/Bujumbura' + ,'Africa/Cairo' + ,'Africa/Casablanca' + ,'Africa/Ceuta' + ,'Africa/Conakry' + ,'Africa/Dakar' + ,'Africa/Dar_es_Salaam' + ,'Africa/Djibouti' + ,'Africa/Douala' + ,'Africa/El_Aaiun' + ,'Africa/Freetown' + ,'Africa/Gaborone' + ,'Africa/Harare' + ,'Africa/Johannesburg' + ,'Africa/Juba' + ,'Africa/Kampala' + ,'Africa/Khartoum' + ,'Africa/Kigali' + ,'Africa/Kinshasa' + ,'Africa/Lagos' + ,'Africa/Libreville' + ,'Africa/Lome' + ,'Africa/Luanda' + ,'Africa/Lubumbashi' + ,'Africa/Lusaka' + ,'Africa/Malabo' + ,'Africa/Maputo' + ,'Africa/Maseru' + ,'Africa/Mbabane' + ,'Africa/Mogadishu' + ,'Africa/Monrovia' + ,'Africa/Nairobi' + ,'Africa/Ndjamena' + ,'Africa/Niamey' + ,'Africa/Nouakchott' + ,'Africa/Ouagadougou' + ,'Africa/Porto-Novo' + ,'Africa/Sao_Tome' + ,'Africa/Timbuktu' + ,'Africa/Tripoli' + ,'Africa/Tunis' + ,'Africa/Windhoek' + ,'America/Adak' + ,'America/Anchorage' + ,'America/Anguilla' + ,'America/Antigua' + ,'America/Araguaina' + ,'America/Argentina/Buenos_Aires' + ,'America/Argentina/Catamarca' + ,'America/Argentina/ComodRivadavia' + ,'America/Argentina/Cordoba' + ,'America/Argentina/Jujuy' + ,'America/Argentina/La_Rioja' + ,'America/Argentina/Mendoza' + ,'America/Argentina/Rio_Gallegos' + ,'America/Argentina/Salta' + ,'America/Argentina/San_Juan' + ,'America/Argentina/San_Luis' + ,'America/Argentina/Tucuman' + ,'America/Argentina/Ushuaia' + ,'America/Aruba' + ,'America/Asuncion' + ,'America/Atikokan' + ,'America/Atka' + ,'America/Bahia' + ,'America/Bahia_Banderas' + ,'America/Barbados' + ,'America/Belem' + ,'America/Belize' + ,'America/Blanc-Sablon' + ,'America/Boa_Vista' + ,'America/Bogota' + ,'America/Boise' + ,'America/Buenos_Aires' + ,'America/Cambridge_Bay' + ,'America/Campo_Grande' + ,'America/Cancun' + ,'America/Caracas' + ,'America/Catamarca' + ,'America/Cayenne' + ,'America/Cayman' + ,'America/Chicago' + ,'America/Chihuahua' + # -- !! -- ,'America/Ciudad_Juarez' + ,'America/Coral_Harbour' + ,'America/Cordoba' + ,'America/Costa_Rica' + ,'America/Creston' + ,'America/Cuiaba' + ,'America/Curacao' + ,'America/Danmarkshavn' + ,'America/Dawson' + ,'America/Dawson_Creek' + ,'America/Denver' + ,'America/Detroit' + ,'America/Dominica' + ,'America/Edmonton' + ,'America/Eirunepe' + ,'America/El_Salvador' + ,'America/Ensenada' + ,'America/Fort_Nelson' + ,'America/Fort_Wayne' + ,'America/Fortaleza' + ,'America/Glace_Bay' + ,'America/Godthab' + ,'America/Goose_Bay' + ,'America/Grand_Turk' + ,'America/Grenada' + ,'America/Guadeloupe' + ,'America/Guatemala' + ,'America/Guayaquil' + ,'America/Guyana' + ,'America/Halifax' + ,'America/Havana' + ,'America/Hermosillo' + ,'America/Indiana/Indianapolis' + ,'America/Indiana/Knox' + ,'America/Indiana/Marengo' + ,'America/Indiana/Petersburg' + ,'America/Indiana/Tell_City' + ,'America/Indiana/Vevay' + ,'America/Indiana/Vincennes' + ,'America/Indiana/Winamac' + ,'America/Indianapolis' + ,'America/Inuvik' + ,'America/Iqaluit' + ,'America/Jamaica' + ,'America/Jujuy' + ,'America/Juneau' + ,'America/Kentucky/Louisville' + ,'America/Kentucky/Monticello' + ,'America/Knox_IN' + ,'America/Kralendijk' + ,'America/La_Paz' + ,'America/Lima' + ,'America/Los_Angeles' + ,'America/Louisville' + ,'America/Lower_Princes' + ,'America/Maceio' + ,'America/Managua' + ,'America/Manaus' + ,'America/Marigot' + ,'America/Martinique' + ,'America/Matamoros' + ,'America/Mazatlan' + ,'America/Mendoza' + ,'America/Menominee' + ,'America/Merida' + ,'America/Metlakatla' + ,'America/Mexico_City' + ,'America/Miquelon' + ,'America/Moncton' + ,'America/Monterrey' + ,'America/Montevideo' + ,'America/Montreal' + ,'America/Montserrat' + ,'America/Nassau' + ,'America/New_York' + ,'America/Nipigon' + ,'America/Nome' + ,'America/Noronha' + ,'America/North_Dakota/Beulah' + ,'America/North_Dakota/Center' + ,'America/North_Dakota/New_Salem' + ,'America/Nuuk' + ,'America/Ojinaga' + ,'America/Panama' + ,'America/Pangnirtung' + ,'America/Paramaribo' + ,'America/Phoenix' + ,'America/Port-au-Prince' + ,'America/Port_of_Spain' + ,'America/Porto_Acre' + ,'America/Porto_Velho' + ,'America/Puerto_Rico' + ,'America/Punta_Arenas' + ,'America/Rainy_River' + ,'America/Rankin_Inlet' + ,'America/Recife' + ,'America/Regina' + ,'America/Resolute' + ,'America/Rio_Branco' + ,'America/Rosario' + ,'America/Santa_Isabel' + ,'America/Santarem' + ,'America/Santiago' + ,'America/Santo_Domingo' + ,'America/Sao_Paulo' + ,'America/Scoresbysund' + ,'America/Shiprock' + ,'America/Sitka' + ,'America/St_Barthelemy' + ,'America/St_Johns' + ,'America/St_Kitts' + ,'America/St_Lucia' + ,'America/St_Thomas' + ,'America/St_Vincent' + ,'America/Swift_Current' + ,'America/Tegucigalpa' + ,'America/Thule' + ,'America/Thunder_Bay' + ,'America/Tijuana' + ,'America/Toronto' + ,'America/Tortola' + ,'America/Vancouver' + ,'America/Virgin' + ,'America/Whitehorse' + ,'America/Winnipeg' + ,'America/Yakutat' + ,'America/Yellowknife' + ,'Antarctica/Casey' + ,'Antarctica/Davis' + ,'Antarctica/DumontDUrville' + ,'Antarctica/Macquarie' + ,'Antarctica/Mawson' + ,'Antarctica/McMurdo' + ,'Antarctica/Palmer' + ,'Antarctica/Rothera' + ,'Antarctica/South_Pole' + ,'Antarctica/Syowa' + ,'Antarctica/Troll' + ,'Antarctica/Vostok' + ,'Arctic/Longyearbyen' + ,'Asia/Aden' + ,'Asia/Almaty' + ,'Asia/Amman' + ,'Asia/Anadyr' + ,'Asia/Aqtau' + ,'Asia/Aqtobe' + ,'Asia/Ashgabat' + ,'Asia/Ashkhabad' + ,'Asia/Atyrau' + ,'Asia/Baghdad' + ,'Asia/Bahrain' + ,'Asia/Baku' + ,'Asia/Bangkok' + ,'Asia/Barnaul' + ,'Asia/Beirut' + ,'Asia/Bishkek' + ,'Asia/Brunei' + ,'Asia/Calcutta' + ,'Asia/Chita' + ,'Asia/Choibalsan' + ,'Asia/Chongqing' + ,'Asia/Chungking' + ,'Asia/Colombo' + ,'Asia/Dacca' + ,'Asia/Damascus' + ,'Asia/Dhaka' + ,'Asia/Dili' + ,'Asia/Dubai' + ,'Asia/Dushanbe' + ,'Asia/Famagusta' + ,'Asia/Gaza' + ,'Asia/Harbin' + ,'Asia/Hebron' + ,'Asia/Ho_Chi_Minh' + ,'Asia/Hong_Kong' + ,'Asia/Hovd' + ,'Asia/Irkutsk' + ,'Asia/Istanbul' + ,'Asia/Jakarta' + ,'Asia/Jayapura' + ,'Asia/Jerusalem' + ,'Asia/Kabul' + ,'Asia/Kamchatka' + ,'Asia/Karachi' + ,'Asia/Kashgar' + ,'Asia/Kathmandu' + ,'Asia/Katmandu' + ,'Asia/Khandyga' + ,'Asia/Kolkata' + ,'Asia/Krasnoyarsk' + ,'Asia/Kuala_Lumpur' + ,'Asia/Kuching' + ,'Asia/Kuwait' + ,'Asia/Macao' + ,'Asia/Macau' + ,'Asia/Magadan' + ,'Asia/Makassar' + ,'Asia/Manila' + ,'Asia/Muscat' + ,'Asia/Nicosia' + ,'Asia/Novokuznetsk' + ,'Asia/Novosibirsk' + ,'Asia/Omsk' + ,'Asia/Oral' + ,'Asia/Phnom_Penh' + ,'Asia/Pontianak' + ,'Asia/Pyongyang' + ,'Asia/Qatar' + ,'Asia/Qostanay' + ,'Asia/Qyzylorda' + ,'Asia/Rangoon' + ,'Asia/Riyadh' + ,'Asia/Saigon' + ,'Asia/Sakhalin' + ,'Asia/Samarkand' + ,'Asia/Seoul' + ,'Asia/Shanghai' + ,'Asia/Singapore' + ,'Asia/Srednekolymsk' + ,'Asia/Taipei' + ,'Asia/Tashkent' + ,'Asia/Tbilisi' + ,'Asia/Tehran' + ,'Asia/Tel_Aviv' + ,'Asia/Thimbu' + ,'Asia/Thimphu' + ,'Asia/Tokyo' + ,'Asia/Tomsk' + ,'Asia/Ujung_Pandang' + ,'Asia/Ulaanbaatar' + ,'Asia/Ulan_Bator' + ,'Asia/Urumqi' + ,'Asia/Ust-Nera' + ,'Asia/Vientiane' + ,'Asia/Vladivostok' + ,'Asia/Yakutsk' + ,'Asia/Yangon' + ,'Asia/Yekaterinburg' + ,'Asia/Yerevan' + ,'Atlantic/Azores' + ,'Atlantic/Bermuda' + ,'Atlantic/Canary' + ,'Atlantic/Cape_Verde' + ,'Atlantic/Faeroe' + ,'Atlantic/Faroe' + ,'Atlantic/Jan_Mayen' + ,'Atlantic/Madeira' + ,'Atlantic/Reykjavik' + ,'Atlantic/South_Georgia' + ,'Atlantic/St_Helena' + ,'Atlantic/Stanley' + ,'Australia/ACT' + ,'Australia/Adelaide' + ,'Australia/Brisbane' + ,'Australia/Broken_Hill' + ,'Australia/Canberra' + ,'Australia/Currie' + ,'Australia/Darwin' + ,'Australia/Eucla' + ,'Australia/Hobart' + ,'Australia/LHI' + ,'Australia/Lindeman' + ,'Australia/Lord_Howe' + ,'Australia/Melbourne' + ,'Australia/NSW' + ,'Australia/North' + ,'Australia/Perth' + ,'Australia/Queensland' + ,'Australia/South' + ,'Australia/Sydney' + ,'Australia/Tasmania' + ,'Australia/Victoria' + ,'Australia/West' + ,'Australia/Yancowinna' + ,'Brazil/Acre' + ,'Brazil/DeNoronha' + ,'Brazil/East' + ,'Brazil/West' + ,'CET' + ,'CST6CDT' + ,'Canada/Atlantic' + ,'Canada/Central' + ,'Canada/Eastern' + ,'Canada/Mountain' + ,'Canada/Newfoundland' + ,'Canada/Pacific' + ,'Canada/Saskatchewan' + ,'Canada/Yukon' + ,'Chile/Continental' + ,'Chile/EasterIsland' + ,'Cuba' + ,'EET' + ,'EST' + ,'EST5EDT' + ,'Egypt' + ,'Eire' + ,'Etc/GMT' + ,'Etc/GMT+0' + ,'Etc/GMT+1' + ,'Etc/GMT+10' + ,'Etc/GMT+11' + ,'Etc/GMT+12' + ,'Etc/GMT+2' + ,'Etc/GMT+3' + ,'Etc/GMT+4' + ,'Etc/GMT+5' + ,'Etc/GMT+6' + ,'Etc/GMT+7' + ,'Etc/GMT+8' + ,'Etc/GMT+9' + ,'Etc/GMT-0' + ,'Etc/GMT-1' + ,'Etc/GMT-10' + ,'Etc/GMT-11' + ,'Etc/GMT-12' + ,'Etc/GMT-13' + ,'Etc/GMT-14' + ,'Etc/GMT-2' + ,'Etc/GMT-3' + ,'Etc/GMT-4' + ,'Etc/GMT-5' + ,'Etc/GMT-6' + ,'Etc/GMT-7' + ,'Etc/GMT-8' + ,'Etc/GMT-9' + ,'Etc/GMT0' + ,'Etc/Greenwich' + ,'Etc/UCT' + ,'Etc/UTC' + ,'Etc/Universal' + ,'Etc/Zulu' + ,'Europe/Amsterdam' + ,'Europe/Andorra' + ,'Europe/Astrakhan' + ,'Europe/Athens' + ,'Europe/Belfast' + ,'Europe/Belgrade' + ,'Europe/Berlin' + ,'Europe/Bratislava' + ,'Europe/Brussels' + ,'Europe/Bucharest' + ,'Europe/Budapest' + ,'Europe/Busingen' + ,'Europe/Chisinau' + ,'Europe/Copenhagen' + ,'Europe/Dublin' + ,'Europe/Gibraltar' + ,'Europe/Guernsey' + ,'Europe/Helsinki' + ,'Europe/Isle_of_Man' + ,'Europe/Istanbul' + ,'Europe/Jersey' + ,'Europe/Kaliningrad' + ,'Europe/Kiev' + ,'Europe/Kirov' + # -- !! -- ,'Europe/Kyiv' + ,'Europe/Lisbon' + ,'Europe/Ljubljana' + ,'Europe/London' + ,'Europe/Luxembourg' + ,'Europe/Madrid' + ,'Europe/Malta' + ,'Europe/Mariehamn' + ,'Europe/Minsk' + ,'Europe/Monaco' + ,'Europe/Moscow' + ,'Europe/Nicosia' + ,'Europe/Oslo' + ,'Europe/Paris' + ,'Europe/Podgorica' + ,'Europe/Prague' + ,'Europe/Riga' + ,'Europe/Rome' + ,'Europe/Samara' + ,'Europe/San_Marino' + ,'Europe/Sarajevo' + ,'Europe/Saratov' + ,'Europe/Simferopol' + ,'Europe/Skopje' + ,'Europe/Sofia' + ,'Europe/Stockholm' + ,'Europe/Tallinn' + ,'Europe/Tirane' + ,'Europe/Tiraspol' + ,'Europe/Ulyanovsk' + ,'Europe/Uzhgorod' + ,'Europe/Vaduz' + ,'Europe/Vatican' + ,'Europe/Vienna' + ,'Europe/Vilnius' + ,'Europe/Volgograd' + ,'Europe/Warsaw' + ,'Europe/Zagreb' + ,'Europe/Zaporozhye' + ,'Europe/Zurich' + ,'Factory' + ,'GB' + ,'GB-Eire' + ,'GMT+0' + ,'GMT-0' + ,'GMT0' + ,'Greenwich' + ,'HST' + ,'Hongkong' + ,'Iceland' + ,'Indian/Antananarivo' + ,'Indian/Chagos' + ,'Indian/Christmas' + ,'Indian/Cocos' + ,'Indian/Comoro' + ,'Indian/Kerguelen' + ,'Indian/Mahe' + ,'Indian/Maldives' + ,'Indian/Mauritius' + ,'Indian/Mayotte' + ,'Indian/Reunion' + ,'Iran' + ,'Israel' + ,'Jamaica' + ,'Japan' + ,'Kwajalein' + ,'Libya' + ,'MET' + ,'MST' + ,'MST7MDT' + ,'Mexico/BajaNorte' + ,'Mexico/BajaSur' + ,'Mexico/General' + ,'NZ' + ,'NZ-CHAT' + ,'Navajo' + ,'PRC' + ,'PST8PDT' + ,'Pacific/Apia' + ,'Pacific/Auckland' + ,'Pacific/Bougainville' + ,'Pacific/Chatham' + ,'Pacific/Chuuk' + ,'Pacific/Easter' + ,'Pacific/Efate' + ,'Pacific/Enderbury' + ,'Pacific/Fakaofo' + ,'Pacific/Fiji' + ,'Pacific/Funafuti' + ,'Pacific/Galapagos' + ,'Pacific/Gambier' + ,'Pacific/Guadalcanal' + ,'Pacific/Guam' + ,'Pacific/Honolulu' + ,'Pacific/Johnston' + # -- !! -- ,'Pacific/Kanton' + ,'Pacific/Kiritimati' + ,'Pacific/Kosrae' + ,'Pacific/Kwajalein' + ,'Pacific/Majuro' + ,'Pacific/Marquesas' + ,'Pacific/Midway' + ,'Pacific/Nauru' + ,'Pacific/Niue' + ,'Pacific/Norfolk' + ,'Pacific/Noumea' + ,'Pacific/Pago_Pago' + ,'Pacific/Palau' + ,'Pacific/Pitcairn' + ,'Pacific/Pohnpei' + ,'Pacific/Ponape' + ,'Pacific/Port_Moresby' + ,'Pacific/Rarotonga' + ,'Pacific/Saipan' + ,'Pacific/Samoa' + ,'Pacific/Tahiti' + ,'Pacific/Tarawa' + ,'Pacific/Tongatapu' + ,'Pacific/Truk' + ,'Pacific/Wake' + ,'Pacific/Wallis' + ,'Pacific/Yap' + ,'Poland' + ,'Portugal' + ,'ROC' + ,'ROK' + ,'Singapore' + ,'Turkey' + ,'UCT' + ,'US/Alaska' + ,'US/Aleutian' + ,'US/Arizona' + ,'US/Central' + ,'US/East-Indiana' + ,'US/Eastern' + ,'US/Hawaii' + ,'US/Indiana-Starke' + ,'US/Michigan' + ,'US/Mountain' + ,'US/Pacific' + ,'US/Samoa' + ,'UTC' + ,'Universal' + ,'W-SU' + ,'WET' + ,'Zulu' + ] + + problematic_timezones_map = {} + with act.db.connect() as con: + cur = con.cursor() + # random.choice(fb_time_zones) + for tz_name in fb_time_zones: + try: + tz_info = get_timezone(tz_name) + # print(tz_name) + tm_region = ( + datetime.time(11, 22, 33, 561400, get_timezone(tz_name)) + ,datetime.time(12, 23, 34, 672400, get_timezone(tz_name)) + ,datetime.time(13, 24, 35, 783400, get_timezone(tz_name)) + ) + ts_region = ( + datetime.datetime(2020, 10, 20, 11, 22, 33, 561400, get_timezone(tz_name)) + ,datetime.datetime(2021, 11, 21, 12, 23, 34, 672400, get_timezone(tz_name)) + ,datetime.datetime(2022, 12, 22, 13, 24, 35, 783400, get_timezone(tz_name)) + ) + + #------------------------------------------------ + cur.execute("insert into test(arr_tmtz, arr_tstz) values (?, ?) returning arr_tmtz,arr_tstz", ( tm_region, ts_region ) ) + inserted_tmtz_array, inserted_tstz_array = cur.fetchone()[:2] + if set(inserted_tmtz_array) == set(tm_region) and set(inserted_tstz_array) == set(ts_region): + pass + else: + print('MISMATCH detected between input data and stored result:') + for i,x in enumerate(inserted_tmtz_array): + print(i, f'Input element (TIME WITH TIME ZONE): {tm_region[i]}', f'; stored data: {x}') + for i,x in enumerate(inserted_tstz_array): + print(i, f'Input element (TIMESTAMP WITH TIME ZONE): {ts_region[i]}', f'; stored data: {x}') + + cur.execute("delete from test") + except Exception as e: + problematic_timezones_map[tz_name] = e.__str__() + + if problematic_timezones_map: + print('Problems detected with time zone(s):') + for k,v in problematic_timezones_map.items(): + print(k,v) + + act.stdout = capsys.readouterr().out + assert act.clean_stdout == '' + act.reset() diff --git a/tests/bugs/gh_6609_test.py b/tests/bugs/gh_6609_test.py new file mode 100644 index 00000000..2f31f847 --- /dev/null +++ b/tests/bugs/gh_6609_test.py @@ -0,0 +1,114 @@ +#coding:utf-8 + +""" +ID: issue-6609 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/6609 +TITLE: Memory leak at server, when client select computed field, which used COALESCE or CAST [CORE6370] +DESCRIPTION: + Problem can be seen if we run query to mon$memory_usage before and after loop with statement described in the ticket. + It is important that on every iteration of loop cursor will be re-created and closed after statement execution (see 'cur2'). + Ratio between values of mon$memory_usage was about 1.63 (for CS) and 1.26 (for SS) before fix. + After fix these values reduced to ~1.00 (for 3.x and 4.x), but for 5.x+ CS it is about 1.10. + NOTE: for Classic we have to compare mon$memory_usage that corresponds to ATTACHMENT level (see 'MON_QUERY' variable). +NOTES: + [18.05.2024] pzotov + Confirmed bug on 3.0.7.33348 (17-JUL-2020), mon$memo_used ratio for CS: 1.63; SS: 1.26 + Bug was fixed in: dde597e6ae8bbaac45df4f8b38faa9583cd946d4 (27-JUL-2020). + Checked on: + 3.0.7.33350, mon$memo_used ratio for CS: 1.01; SS: 1.00 + 4.0.5.3099, mon$memo_used ratio for CS: 1.01; SS: 1.00 + 5.0.1.1399, mon$memo_used ratio for CS: 1.11; SS: 1.02 + 6.0.0.351, mon$memo_used ratio for CS: 1.11; SS: 1.02 + + [24.12.2024] pzotov + Separated definition of max ratio thresholds according to FB fork ('standard', 'HQbird', 'RedDatabase'). + Requested by Anton Zuev, RedBase. +""" + +import pytest +import platform +from firebird.qa import * + +N_CNT = 30000 + +init_ddl = """ + recreate table tab1 ( + a1 varchar(99), + a2 varchar(199), + a3 computed by (coalesce(a1, '')||'-'||coalesce(a2, '')) + ); +""" + +db = db_factory(init = init_ddl) +act = python_act('db') + +@pytest.mark.version('>=3.0.0') +def test_1(act: Action, capsys): + + mon_memo_beg = 1 + mon_memo_end = 9999999 + with act.db.connect() as con: + + # con.info.server_version output examples: + # Standard: 'WI-V6.3.2.1580 Firebird 5.0 7961de2' + # HQbird: 'WI-V6.3.2.1575 Firebird 5.0 HQbird' + # RedDatabase: 'LI-V6.3.2.0 RedDatabase 5.0 SNAPSHOT.15 ()' + # + fb_vers_txt = con.info.server_version + if 'RedDatabase' in fb_vers_txt: + fb_vers_key = 'red' + elif 'HQbird' in fb_vers_txt: + fb_vers_key = 'hqb' + else: + fb_vers_key = 'std' + + ############################### + ### T H R E S H O L D S ### + ############################### + # Requested by Anton Zuev, RedBase: + max_memo_ratios_map = { + ('std', 'SuperServer') : 1.1 + ,('hqb', 'SuperServer') : 1.1 + ,('red', 'SuperServer') : 1.1 + ,('std', 'Classic') : 1.20 + ,('hqb', 'Classic') : 1.20 + ,('red', 'Classic') : 1.30 + } + + fb_mode = act.get_server_architecture() + MAX_THRESHOLD = max_memo_ratios_map[ fb_vers_key, fb_mode] + + if fb_mode == 'SuperServer': + MON_QUERY = 'select mon$memory_used from mon$memory_usage where mon$stat_group = 0' + else: + MON_QUERY = """ + select m.mon$memory_used + from mon$attachments a + join mon$memory_usage m on a.mon$stat_id = m.mon$stat_id + where a.mon$attachment_id = current_connection and m.mon$stat_group = 1; + """ + + cur = con.cursor() + cur.execute(MON_QUERY) + mon_memo_beg = int(cur.fetchone()[0]) + con.commit() + + for i in range(N_CNT): + cur2 = con.cursor() + cur2.execute(f"select /* iter {i+1} */ t.a3 from tab1 t") + for r in cur2: + pass + cur2.close() + + con.commit() + cur.execute(MON_QUERY) + mon_memo_end = int(cur.fetchone()[0]) + + msg_ok = 'Memory usage: EXPECTED' + if mon_memo_end / mon_memo_beg <= MAX_THRESHOLD: + print(msg_ok) + else: + print(f'Memory usage UNEXPECTED: {mon_memo_end} / {mon_memo_beg} = {mon_memo_end / mon_memo_beg:.2f} - greater than {MAX_THRESHOLD=}') + act.expected_stdout = msg_ok + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_6706_test.py b/tests/bugs/gh_6706_test.py new file mode 100644 index 00000000..103fc041 --- /dev/null +++ b/tests/bugs/gh_6706_test.py @@ -0,0 +1,108 @@ +#coding:utf-8 + +""" +ID: issue-6706 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/6706 +TITLE: Memory leak when running EXECUTE STATEMENT with named parameters [CORE6475] +DESCRIPTION: + We create stored procedure with PARAMS_COUNT input parameters. + Then EXECUTE BLOCK is generated with call of this SP via EXECUTE STATEMENT which applies EXCESS modifier to all arguments. + Value of memory_info().rss is obtained (for appropriate server process), then run execute block MEASURES_COUNT times + and after this - again get memory_info().rss value. + Ratio between current and initial values of memory_info().rss must be less than MAX_RATIO. +NOTES: + [17.08.2024] pzotov + 1. Problem did exist in FB 4.x up to snapshot 4.0.0.2336. + Commit: https://github.com/FirebirdSQL/firebird/commit/4dfb30a45b767994c074bbfcbb8494b8ada19b33 (23-jan-2021, 15:26) + Before this commit ratio for SS was about 5..6 for SS and about 8..9 for CS. + Since 4.0.0.2341 memory consumption was reduced to ~1.6 ... 1.9 + 2. Database must be created with FW = ON otherwise ratio for all snapshots is about 1.5 (and this seems weird). + 3. Test duration is about 35s. + + Checked on 6.0.0.438, 5.0.2.1478, 4.0.6.3142; 4.0.0.2336, 4.0.0.2341. +""" + +import psutil +import pytest +from firebird.qa import * +import time + +########################### +### S E T T I N G S ### +########################### + +# How many input parameters must have procedure: +PARAMS_COUNT = 1000 + +# How many times we call procedures: +MEASURES_COUNT = 1000 + +# Maximal value for ratio between +# new and initial memory_info().rss values: +# +MAX_RATIO = 3 +############# + +db = db_factory(async_write = False) +act = python_act('db') + +#-------------------------------------------------------------------- + +def get_server_pid(con): + with con.cursor() as cur: + cur.execute('select mon$server_pid as p from mon$attachments where mon$attachment_id = current_connection') + fb_pid = int(cur.fetchone()[0]) + return fb_pid + +#-------------------------------------------------------------------- + +@pytest.mark.version('>=4.0.0') +def test_1(act: Action, capsys): + + with act.db.connect() as con: + + sp_ddl = """ + create or alter procedure sp_test( + """ + params_lst = '\n'.join( [ (',' if i else '') +f'p_{i} int' for i in range(PARAMS_COUNT) ] ) + sp_ddl = '\n'.join( ("create or alter procedure sp_test(", params_lst, ") returns(x int) as begin x = 1; suspend; end") ) + con.execute_immediate(sp_ddl) + con.commit() + + server_process = psutil.Process(get_server_pid(con)) + + params_lst = ','.join( [ f':p_{i}' for i in range(PARAMS_COUNT) ] ) + passed_args = ','.join( [ f'excess p_{i} := 1' for i in range(PARAMS_COUNT) ] ) + + srv_memo_rss_init = int(server_process.memory_info().rss / 1024) + srv_memo_vms_init = int(server_process.memory_info().vms / 1024) + + cur = con.cursor() + for k in range(MEASURES_COUNT): + + es_sql = f""" + execute block returns(x int) as + begin + execute statement ('select p.x * {k} from sp_test({params_lst}) p') ({passed_args}) + into x; + suspend; + end + """ + cur.execute(es_sql) + for r in cur: + pass + + srv_memo_rss_curr = int(server_process.memory_info().rss / 1024) + srv_memo_vms_curr = int(server_process.memory_info().vms / 1024) + + memo_ratio = srv_memo_rss_curr / srv_memo_rss_init + + SUCCESS_MSG = 'Ratio between memory values measured before and after loop: acceptable' + if memo_ratio < MAX_RATIO: + print(SUCCESS_MSG) + else: + print( 'Ratio: /* perf_issue_tag */ POOR: %s, more than threshold: %s' % ( '{:.2f}'.format(memo_ratio), '{:.2f}'.format(MAX_RATIO) ) ) + + act.expected_stdout = SUCCESS_MSG + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_6778_test.py b/tests/bugs/gh_6778_test.py index 1b623897..995ccc2f 100644 --- a/tests/bugs/gh_6778_test.py +++ b/tests/bugs/gh_6778_test.py @@ -11,6 +11,11 @@ Checked on: 4.0.0.2448 - works fine. No errors must be during execution of this code. FBTEST: bugs.gh_6778 +NOTES: + [04.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.894; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -123,11 +128,20 @@ PLAN JOIN (C A2 NATURAL, C A1 INDEX (IA1)) """ +fb6_expected_stdout = """ + -- line, column + PLAN JOIN ("C" "PUBLIC"."A1" NATURAL, "C" "PUBLIC"."A2" INDEX ("PUBLIC"."IA2")) + -- line, column + PLAN JOIN ("C" "PUBLIC"."A2" NATURAL, "C" "PUBLIC"."A1" INDEX ("PUBLIC"."IA1")) +""" + act = python_act('db', substitutions=[('-- line(:)?\\s+\\d+,\\s+col(umn)?(:)?\\s+\\d+', '-- line, column')]) @pytest.mark.version('>=4.0') def test_1(act: Action): + test_script = fb5_test_script if act.is_version('>=5') else fb4_test_script - act.expected_stdout = fb5_expected_stdout if act.is_version('>=5') else fb4_expected_stdout - act.isql(switches=['-q'], input=test_script) + expected_stdout = fb4_expected_stdout if act.is_version('<5') else fb5_expected_stdout if act.is_version('<6') else fb6_expected_stdout + act.expected_stdout = expected_stdout + act.isql(switches=['-q'], input=test_script, combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_6782_test.py b/tests/bugs/gh_6782_test.py index 976a40de..b09c0c9a 100644 --- a/tests/bugs/gh_6782_test.py +++ b/tests/bugs/gh_6782_test.py @@ -22,11 +22,16 @@ Test parses trace log and search there lines with names of known procedures/functions and then checks presence of lines with number of fetched records (for selectable procedures) and additional statistics ('fetches/reads/writes/marks'). +FBTEST: bugs.gh_6782 NOTES: [30.06.2022] pzotov - Checked on 3.0.8.33535, 4.0.1.2692, 5.0.0.509. - -FBTEST: bugs.gh_6782 + Checked on 3.0.8.33535, 4.0.1.2692, 5.0.0.509. + [04.07.2025] pzotov + Changed list of patterns to find name of procedures / functions (standalone / packaged): + we have to take in account name of SQL schema and presense of double quotes in 6.x. + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.894; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import locale @@ -142,7 +147,7 @@ act = python_act('db') -expected_stdout_trace = """ +expected_stdout_5x = """ Procedure STANDALONE_SELECTABLE_SP: FOUND line with number of fetched records FOUND line with execution statistics @@ -167,6 +172,32 @@ FOUND line with execution statistics """ +expected_stdout_6x = """ + Procedure "PUBLIC"."STANDALONE_SELECTABLE_SP": + FOUND line with number of fetched records + FOUND line with execution statistics + + Function "PUBLIC"."STANDALONE_FUNC": + FOUND line with execution statistics + + Procedure "PUBLIC"."STANDALONE_NONSELECTED_SP": + FOUND line with execution statistics + + Procedure "PUBLIC"."PG_TEST"."PACKAGED_SELECTABLE_SP": + FOUND line with number of fetched records + FOUND line with execution statistics + + Function "PUBLIC"."PG_TEST"."PACKAGED_FUNC": + FOUND line with execution statistics + + Procedure "PUBLIC"."PG_TEST"."PACKAGED_NONSELECTED_SP": + FOUND line with execution statistics + + Procedure "PUBLIC"."SP_MAIN": + FOUND line with execution statistics +""" + +@pytest.mark.trace @pytest.mark.version('>=3.0.8') def test_1(act: Action, capsys): @@ -188,8 +219,10 @@ def test_1(act: Action, capsys): allowed_patterns = \ ( - r'Procedure\s+(STANDALONE_SELECTABLE_SP:|STANDALONE_NONSELECTED_SP:|PG_TEST.PACKAGED_SELECTABLE_SP:|PG_TEST.PACKAGED_NONSELECTED_SP:|SP_MAIN:)' - ,r'Function\s+(STANDALONE_FUNC:|PG_TEST.PACKAGED_FUNC:)' + r'Procedure\s+("PUBLIC".)?(")?(STANDALONE_SELECTABLE_SP|STANDALONE_NONSELECTED_SP|SP_MAIN)(")?:' + ,r'Function\s+("PUBLIC".)?(")?STANDALONE_FUNC(")?:' + ,r'Procedure\s+("PUBLIC".)?(")?PG_TEST(")?.(")?(PACKAGED_SELECTABLE_SP|PACKAGED_NONSELECTED_SP)(")?:' + ,r'Function\s+("PUBLIC".)?(")?PG_TEST(")?.(")?(PACKAGED_FUNC)(")?:' ,r'\d+\s+record(s|\(s\))?\s+fetched' ,r'\s+\d+\s+ms(,)?' ) @@ -198,7 +231,7 @@ def test_1(act: Action, capsys): for line in act.trace_log: if line.strip(): - #print('>'+line.strip()+'<', act.match_any(line, allowed_patterns)) + # print('>'+line.strip()+'<', act.match_any(line, allowed_patterns)) if act.match_any(line, allowed_patterns): if ' ms' in line and 'fetch' in line: print('FOUND line with execution statistics') @@ -207,6 +240,6 @@ def test_1(act: Action, capsys): else: print(line) - act.expected_stdout = expected_stdout_trace + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_6797_test.py b/tests/bugs/gh_6797_test.py index 1b357a44..a21ee3bc 100644 --- a/tests/bugs/gh_6797_test.py +++ b/tests/bugs/gh_6797_test.py @@ -13,6 +13,9 @@ Added 'SQLSTATE' in substitutions: runtime error must not be filtered out by '?!(...)' pattern ("negative lookahead assertion", see https://docs.python.org/3/library/re.html#regular-expression-syntax). Added 'combine_output = True' in order to see SQLSTATE if any error occurs. + [04.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' and variables - to be substituted in expected_* on FB 6.x + Checked on 6.0.0.894; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -33,13 +36,14 @@ act = isql_act('db', test_script, substitutions=[('^((?!(SQLSTATE|sqltype)).)*$', ''), ('[ \t]+', ' ')]) -expected_stdout = """ - 01: sqltype: 448 VARYING scale: 0 subtype: 0 len: 1 charset: 1 OCTETS - 02: sqltype: 520 BLOB scale: 0 subtype: 0 len: 8 -""" - @pytest.mark.version('>=4.0') def test_1(act: Action): + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'SYSTEM.' + expected_stdout = f""" + 01: sqltype: 448 VARYING scale: 0 subtype: 0 len: 1 charset: 1 {SQL_SCHEMA_PREFIX}OCTETS + 02: sqltype: 520 BLOB scale: 0 subtype: 0 len: 8 + """ act.expected_stdout = expected_stdout act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_6798_test.py b/tests/bugs/gh_6798_test.py index 1f51e488..daa17550 100644 --- a/tests/bugs/gh_6798_test.py +++ b/tests/bugs/gh_6798_test.py @@ -99,6 +99,7 @@ -Integer overflow. The result of an integer operation caused the most significant bit of the result to carry. """ +@pytest.mark.intl @pytest.mark.version('>=5.0') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/gh_6812_test.py b/tests/bugs/gh_6812_test.py index ec761db5..dab140f6 100644 --- a/tests/bugs/gh_6812_test.py +++ b/tests/bugs/gh_6812_test.py @@ -11,6 +11,9 @@ Added 'SQLSTATE' in substitutions: runtime error must not be filtered out by '?!(...)' pattern ("negative lookahead assertion", see https://docs.python.org/3/library/re.html#regular-expression-syntax). Added 'combine_output = True' in order to see SQLSTATE if any error occurs. + [04.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' and variables - to be substituted in expected_* on FB 6.x + Checked on 6.0.0.894; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -26,15 +29,15 @@ act = isql_act('db', test_script, substitutions=[('^((?!(SQLSTATE|sqltype|enc)).)*$', ''), ('[ \t]+', ' ')]) -expected_stdout = """ - 01: sqltype: 520 BLOB scale: 0 subtype: 1 len: 8 charset: 2 ASCII - : name: HEX_ENCODE alias: enc_01 - 01: sqltype: 520 BLOB scale: 0 subtype: 1 len: 8 charset: 2 ASCII - : name: BASE64_ENCODE alias: enc_02 -""" - @pytest.mark.version('>=4.0') def test_1(act: Action): + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'SYSTEM.' + expected_stdout = f""" + 01: sqltype: 520 BLOB scale: 0 subtype: 1 len: 8 charset: 2 {SQL_SCHEMA_PREFIX}ASCII + : name: HEX_ENCODE alias: enc_01 + 01: sqltype: 520 BLOB scale: 0 subtype: 1 len: 8 charset: 2 {SQL_SCHEMA_PREFIX}ASCII + : name: BASE64_ENCODE alias: enc_02 + """ act.expected_stdout = expected_stdout act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_6816_test.py b/tests/bugs/gh_6816_test.py index 1ce903ed..1103cac7 100644 --- a/tests/bugs/gh_6816_test.py +++ b/tests/bugs/gh_6816_test.py @@ -11,6 +11,9 @@ Added 'SQLSTATE' in substitutions: runtime error must not be filtered out by '?!(...)' pattern ("negative lookahead assertion", see https://docs.python.org/3/library/re.html#regular-expression-syntax). Added 'combine_output = True' in order to see SQLSTATE if any error occurs. + [04.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' and variables - to be substituted in expected_* on FB 6.x + Checked on 6.0.0.894; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -29,15 +32,16 @@ act = isql_act('db', test_script, substitutions=[('^((?!(SQLSTATE|sqltype)).)*$', ''), ('[ \t]+', ' ')]) -expected_stdout = """ - 01: sqltype: 448 VARYING scale: 0 subtype: 0 len: 10 charset: 2 ASCII - 02: sqltype: 448 VARYING scale: 0 subtype: 0 len: 8 charset: 2 ASCII - 01: sqltype: 448 VARYING scale: 0 subtype: 0 len: 3 charset: 1 OCTETS - 02: sqltype: 448 VARYING scale: 0 subtype: 0 len: 2 charset: 1 OCTETS -""" - @pytest.mark.version('>=4.0') def test_1(act: Action): + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'SYSTEM.' + expected_stdout = f""" + 01: sqltype: 448 VARYING scale: 0 subtype: 0 len: 10 charset: 2 {SQL_SCHEMA_PREFIX}ASCII + 02: sqltype: 448 VARYING scale: 0 subtype: 0 len: 8 charset: 2 {SQL_SCHEMA_PREFIX}ASCII + 01: sqltype: 448 VARYING scale: 0 subtype: 0 len: 3 charset: 1 {SQL_SCHEMA_PREFIX}OCTETS + 02: sqltype: 448 VARYING scale: 0 subtype: 0 len: 2 charset: 1 {SQL_SCHEMA_PREFIX}OCTETS + """ act.expected_stdout = expected_stdout act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_6825_test.py b/tests/bugs/gh_6825_test.py index 118f041a..52307272 100644 --- a/tests/bugs/gh_6825_test.py +++ b/tests/bugs/gh_6825_test.py @@ -6,6 +6,11 @@ TITLE: Correct error message for DROP VIEW DESCRIPTION: FBTEST: bugs.gh_6825 +NOTES: + [04.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -27,7 +32,7 @@ act = isql_act('db', test_script, substitutions=[('(-)?Effective user is.*', '')]) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 28000 unsuccessful metadata update -DROP VIEW V1 failed @@ -35,8 +40,16 @@ -Effective user is TMP$GH_6825 """ +expected_stdout_6x = """ + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -DROP VIEW "PUBLIC"."V1" failed + -no permission for DROP access to VIEW "PUBLIC"."V1" +""" + @pytest.mark.version('>=3.0.8') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_6869_test.py b/tests/bugs/gh_6869_test.py index 11fc54e9..1860a4a0 100644 --- a/tests/bugs/gh_6869_test.py +++ b/tests/bugs/gh_6869_test.py @@ -6,8 +6,12 @@ TITLE: Domain CHECK-expression can be ignored when we DROP objects that are involved in it NOTES: [25.02.2023] pzotov - Confirmed bug on 5.0.0.520. - Checked on 5.0.0.959 - all OK. + Confirmed bug on 5.0.0.520. + Checked on 5.0.0.959 - all OK. + [04.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.876; 5.0.3.1668. """ import pytest @@ -43,7 +47,7 @@ act = isql_act('db', test_script) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = 42000 unsuccessful metadata update -cannot delete @@ -57,8 +61,21 @@ -there are 1 dependencies """ +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -cannot delete + -COLUMN "PUBLIC"."TEST"."I" + -there are 1 dependencies + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -cannot delete + -COLUMN "PUBLIC"."TEST"."I" + -there are 1 dependencies +""" + @pytest.mark.version('>=5.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_6872_test.py b/tests/bugs/gh_6872_test.py index 7fd93024..9175fa2c 100644 --- a/tests/bugs/gh_6872_test.py +++ b/tests/bugs/gh_6872_test.py @@ -85,6 +85,7 @@ def median(lst): Duration ratio: acceptable. """ +@pytest.mark.intl @pytest.mark.version('>=4.0.1') def test_1(act: Action, capsys): diff --git a/tests/bugs/gh_6873_test.py b/tests/bugs/gh_6873_test.py index 2a089dac..9ee4a331 100644 --- a/tests/bugs/gh_6873_test.py +++ b/tests/bugs/gh_6873_test.py @@ -6,6 +6,11 @@ TITLE: SIMILAR TO does not use index when pattern starts with non-wildcard character (in contrary to LIKE) DESCRIPTION: FBTEST: bugs.gh_6873 +NOTES: + [04.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.876; 5.0.3.1668. """ import pytest @@ -102,9 +107,10 @@ """ -act = isql_act('db', test_script) +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) -expected_stdout = """ +expected_stdout_5x = """ PLAN (TEST INDEX (TEST_X1_ASC)) ID 2 ID 4 @@ -166,8 +172,54 @@ ID 2 """ +expected_stdout_6x = """ + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_X1_ASC")) + ID 2 + ID 4 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_X1_ASC")) + ID 4 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_X1_ASC")) + ID 2 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_Y1_DEC")) + ID 2 + ID 4 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_Y1_DEC")) + ID 4 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_Y1_DEC")) + ID 2 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_X2_ASC")) + ID 2 + ID 4 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_X2_ASC")) + ID 4 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_X2_ASC")) + ID 2 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_Y2_DEC")) + ID 2 + ID 4 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_Y2_DEC")) + ID 4 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_Y2_DEC")) + ID 2 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_X3_ASC")) + ID 2 + ID 4 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_X3_ASC")) + ID 4 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_X3_ASC")) + ID 2 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_Y3_DEC")) + ID 2 + ID 4 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_Y3_DEC")) + ID 4 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_Y3_DEC")) + ID 2 +""" + @pytest.mark.version('>=5.0') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_6886_test.py b/tests/bugs/gh_6886_test.py index 3ddc5f8c..292251bc 100644 --- a/tests/bugs/gh_6886_test.py +++ b/tests/bugs/gh_6886_test.py @@ -78,6 +78,7 @@ Records affected: 0 """ +@pytest.mark.es_eds @pytest.mark.version('>=4.0.1') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/gh_6903_test.py b/tests/bugs/gh_6903_test.py index f1f0cda9..72c06864 100644 --- a/tests/bugs/gh_6903_test.py +++ b/tests/bugs/gh_6903_test.py @@ -6,6 +6,11 @@ TITLE: Unable to create ICU-based collation with locale keywords DESCRIPTION: FBTEST: bugs.gh_6903 +NOTES: + [04.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -46,15 +51,23 @@ act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) -expected_stderr = """ +expected_stdout_5x = """ Statement failed, SQLSTATE = HY000 unsuccessful metadata update -CREATE COLLATION UNICODE_BAD_01 failed -Invalid collation attributes """ +expected_stdout_6x = """ + Statement failed, SQLSTATE = HY000 + unsuccessful metadata update + -CREATE COLLATION "PUBLIC"."UNICODE_BAD_01" failed + -Invalid collation attributes +""" + @pytest.mark.version('>=4.0.1') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_6910_test.py b/tests/bugs/gh_6910_test.py index 9bfbbab6..7c3ecaa8 100644 --- a/tests/bugs/gh_6910_test.py +++ b/tests/bugs/gh_6910_test.py @@ -14,6 +14,10 @@ * state of 'set exec_path_display blr' command will not change after reconnect. ( https://github.com/FirebirdSQL/firebird/commit/32c3cf573bf36f576b6116983786107df5a2cb33 ) FBTEST: bugs.gh_6910 +NOTES: + [15.05.2025] pzotov + Splitted expected_out for versions up to 5.x and 6.x+ (they become differ since 6.0.0.776). + Checked on 6.0.0.778 """ import pytest @@ -39,9 +43,9 @@ end^ """ -act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) +act = isql_act('db', test_script) # , substitutions=[('[ \t]+', ' ')]) -expected_stdout = """ +expected_5x = """ Execution path (BLR): 0 blr_version5, 1 blr_begin, @@ -82,8 +86,31 @@ 30 blr_eoc """ +expected_6x = """ + Execution path (BLR): + 0 blr_version5, + 1 blr_begin, + 2 blr_begin, + 3 blr_label, 0, + 5 blr_begin, + 6 blr_end, + 7 blr_end, + 8 blr_end, + 9 blr_eoc + Execution path (BLR): + 0 blr_version5, + 1 blr_begin, + 2 blr_begin, + 3 blr_label, 0, + 5 blr_begin, + 6 blr_end, + 7 blr_end, + 8 blr_end, + 9 blr_eoc +""" + @pytest.mark.version('>=4.0.1') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = expected_5x if act.is_version('<6') else expected_6x + act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_6915_cs_cz_test.py b/tests/bugs/gh_6915_cs_cz_test.py new file mode 100644 index 00000000..c46137e2 --- /dev/null +++ b/tests/bugs/gh_6915_cs_cz_test.py @@ -0,0 +1,284 @@ +#coding:utf-8 + +""" +ID: issue-6915-cs-cz +ISSUE: https://github.com/FirebirdSQL/firebird/issues/6915 +TITLE: Performance effect of applying 'DISABLE-COMPRESSIONS=1' in UNICODE collation for LOCALE=cs_CZ +DESCRIPTION: + Original discussion: + https://sourceforge.net/p/firebird/mailman/firebird-devel/thread/9361c612-d720-eb76-d412-7101518ca60d%40ibphoenix.cz/ + Test verifies only PERFORMANCE issues referred to in the ticket #6915. Correctness of ORDER BY is not checked. + A pre-build database is used for check, see: files/gh_6915_cs_cz.zip (it was created in FB 4.x with date ~aug-2021). + SQL script that was used to fulfill test DB see in the end of this file. + We decompress .fbk, restore from it and check that for every testing queries number of indexed reads will not + exceed threshold, see 'MAX_IDX_READS_THRESHOLD' (con.info.get_table_access_stats() is used for that). + After improvement this threshold could be set to 1. + Only columns with attribute 'DISABLE-COMPRESSIONS=1' are checked. +NOTES: + [23.12.2024] pzotov + + It seems that commit in 4.x (2af9ded1a696a43f5b0bea39a88610287e3ab06c; 04-aug-2021 17:58) had no effect: + performance in 4.x remains poort for queries from this test up to recent snapshots (dec-2024). + + Commit in 5.x (cfc09f75a3dea099f54c09808e39fe778457f441; 04-aug-2021 20:25) really SOLVED problem: adding + attribute 'DISABLE-COMPRESSIONS=1' causes reducing indexed reads to 0 or 1 for all queries. + + There was commit in 5.x: 171cb7eebc365e301a7384eff96c0e3e069c95cc (date: 17-mar-2022 22:38) - which had + further improvement for 'DISABLE-COMPRESSIONS=0' (i.e. when compression is Enabled). Snapshots of FB 5.x + before that commit (i.e. up to 5.0.0.425) had poor performance for 'DISABLE-COMPRESSIONS=0', and after + this commit (since 5.0.0.426) performance became equal to 'DISABLE-COMPRESSIONS=1'. + Because of that, this test verifies performance of only ONE case: 'DISABLE-COMPRESSIONS=1', by comparing + of indexed reads for each query with threshold, see MAX_IDX_READS_THRESHOLD. + Before improvement related to 'DISABLE-COMPRESSIONS=1', indexed reads were huge for almost all check queries. + This is outcome for 5.0.0.126 (31.07.2021): + where f_ci_compr_disabled >= 'C' order by f_ci_compr_disabled rows 1 ==> idx reads: 352944 + where f_ci_compr_disabled >= 'Z' order by f_ci_compr_disabled rows 1 ==> idx reads: 1000000 + where f_ci_compr_disabled like 'C%' order by f_ci_compr_disabled rows 1 ==> idx reads: 352945 + where f_ci_compr_disabled like 'Z%' order by f_ci_compr_disabled rows 1 ==> idx reads: 1000000 + where f_ci_compr_disabled similar to 'C%' order by f_ci_compr_disabled rows 1 ==> idx reads: 352945 + where f_ci_compr_disabled similar to 'Z%' order by f_ci_compr_disabled rows 1 ==> idx reads: 1000000 + where f_ci_compr_disabled starting with 'C' order by f_ci_compr_disabled rows 1 ==> idx reads: 352945 + where f_ci_compr_disabled starting with 'Z' order by f_ci_compr_disabled rows 1 ==> idx reads: 1000000 + where f_cs_compr_disabled >= 'C' order by f_cs_compr_disabled rows 1 ==> idx reads: 1 + where f_cs_compr_disabled >= 'Z' order by f_cs_compr_disabled rows 1 ==> idx reads: 0 + where f_cs_compr_disabled like 'C%' order by f_cs_compr_disabled rows 1 ==> idx reads: 352945 + where f_cs_compr_disabled like 'Z%' order by f_cs_compr_disabled rows 1 ==> idx reads: 1000000 + where f_cs_compr_disabled similar to 'C%' order by f_cs_compr_disabled rows 1 ==> idx reads: 352945 + where f_cs_compr_disabled similar to 'Z%' order by f_cs_compr_disabled rows 1 ==> idx reads: 1000000 + where f_cs_compr_disabled starting with 'C' order by f_cs_compr_disabled rows 1 ==> idx reads: 352945 + where f_cs_compr_disabled starting with 'Z' order by f_cs_compr_disabled rows 1 ==> idx reads: 1000000 + + Confirmed poor performance on 5.0.0.126 (31.07.2021): all check queries have huge indexed reads, + regardless on 'DISABLE-COMPRESSIONS=1' attribute (i.e. it had no effect on performance), + execution time was 5...15 seconds for each query. + Checked on 5.0.0.129 (05.08.2021 04:25) -- all OK, indexed reads for all queries are 0 or 1. + Checked on 6.0.0.553, 5.0.2.1580. +""" + +from pathlib import Path +import zipfile +import locale +import pytest +from firebird.qa import * +from firebird.driver import connect + +db = db_factory(charset = 'utf8') +act = python_act('db', substitutions=[('[ \t]+', ' ')]) + +tmp_fbk = temp_file('gh_6915.tmp.fbk') +tmp_fdb = temp_file('gh_6915.tmp.fdb') + +MAX_IDX_READS_THRESHOLD = 1 +EXPECTED_MSG = f'Expected. All queries have indexed reads no more than {MAX_IDX_READS_THRESHOLD=}' + +test_sql = """ + with + d as ( + select '0' as disabled_compression from rdb$database + -- union all + -- select '1' as disabled_compression from rdb$database + ) + , + c as ( + select 'ci' as case_attribute from rdb$database union all + select 'cs' from rdb$database + ) + ,o as ( + select '>=' as search_op from rdb$database union all + select 'starting with' from rdb$database union all + select 'like' from rdb$database union all + select 'similar to' from rdb$database + ) + ,e as ( + select 'C' as letter from rdb$database union all + select 'Z' from rdb$database + ) + ,f as ( + select + d.*, c.*, o.*, e.* + ,'select 1 from test where f_' || c.case_attribute || '_compr_' || iif(d.disabled_compression = '0', 'disabled', 'enabled') + || ' ' || trim(o.search_op) || ' ' + || '''' + || e.letter + || trim( iif( upper(trim(o.search_op)) in ('>=', upper('starting with')), '', '%') ) + || '''' + || ' order by f_' || c.case_attribute || '_compr_' || iif(d.disabled_compression = '0', 'disabled', 'enabled') + || ' rows 1' + as query_txt + from d + cross join c + cross join o + cross join e + ) + select + --case_attribute + --,search_op + --,letter + max(iif(disabled_compression = 0, query_txt, null)) as q_compr_disabled + --max(iif(disabled_compression = 1, query_txt, null)) as q_compr_enabled + from f + group by + case_attribute + ,search_op + ,letter + ; +""" + +@pytest.mark.intl +@pytest.mark.version('>=5.0.0') +def test_1(act: Action, tmp_fbk: Path, tmp_fdb: Path, capsys): + + zipped_fbk_file = zipfile.Path(act.files_dir / 'gh_6915_cs_cz.zip', at = 'gh_6915_cs_cz.fbk') + tmp_fbk.write_bytes(zipped_fbk_file.read_bytes()) + + act.gbak(switches = ['-rep', str(tmp_fbk), str(tmp_fdb)], combine_output = True, io_enc = locale.getpreferredencoding()) + assert '' == act.stdout + act.reset() + reads_map = {} + with connect(str(tmp_fdb), user = act.db.user, password = act.db.password, charset = 'utf8') as con: + cur = con.cursor() + cur2 = con.cursor() + + cur.execute("select rdb$relation_id from rdb$relations where rdb$relation_name = upper('test')") + src_relation_id = cur.fetchone()[0] + + cur.execute(test_sql) + for r in cur: + idx_reads = -1 + for x_table in con.info.get_table_access_stats(): + if x_table.table_id == src_relation_id: + idx_reads = - (x_table.indexed if x_table.indexed else 0) + + cur2.execute(r[0]) + cur2.fetchall() + + for x_table in con.info.get_table_access_stats(): + if x_table.table_id == src_relation_id: + idx_reads += (x_table.indexed if x_table.indexed else 0) + + reads_map[ r[0] ] = idx_reads + + + if max(reads_map.values()) <= MAX_IDX_READS_THRESHOLD: + print(EXPECTED_MSG) + else: + print(f'UNEXPECTED: at least one query has values of indexed reads greater than {MAX_IDX_READS_THRESHOLD=}') + for check_qry, idx_reads in reads_map.items(): + if idx_reads > MAX_IDX_READS_THRESHOLD: + print(f'{check_qry=}, {idx_reads=}') + + act.expected_stdout = f""" + {EXPECTED_MSG} + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + +# End of test. + +################################ +# SQL with initial DDL and data: +################################ +# create database 'localhost:r:\temp\tmp4test.fdb' default character set utf8; +# +# create collation u_ci_compr_disabled +# for utf8 +# from unicode +# case insensitive +# 'LOCALE=cs_CZ;DISABLE-COMPRESSIONS=1' +# ; +# +# create collation u_cs_compr_disabled +# for utf8 +# from unicode +# case sensitive +# 'LOCALE=cs_CZ;DISABLE-COMPRESSIONS=1' +# ; +# +# create collation u_ci_compr_enabled +# for utf8 +# from unicode +# case insensitive +# 'LOCALE=cs_CZ;DISABLE-COMPRESSIONS=0' +# ; +# +# create collation u_cs_compr_enabled +# for utf8 +# from unicode +# case sensitive +# 'LOCALE=cs_CZ;DISABLE-COMPRESSIONS=0' +# ; +# +# create table test ( +# f_cs_compr_disabled varchar(10) collate u_cs_compr_disabled +# ,f_ci_compr_disabled varchar(10) collate u_ci_compr_disabled +# ,f_cs_compr_enabled varchar(10) collate u_cs_compr_enabled +# ,f_ci_compr_enabled varchar(10) collate u_ci_compr_enabled +# ); +# +# set term ^; +# create or alter procedure getstr(aorderid bigint) returns (aresult char(10)) +# as +# declare base36chars char(36); +# declare mresult varchar(10); +# declare id bigint; +# declare i int; +# begin +# base36chars = upper('0123456789abcdefghijklmnopqrstuvwxyz'); +# mresult = ''; +# aresult = mresult; +# id = aorderid; +# while (id > 0) do +# begin +# i = mod(id, 36); +# id = id / 36; +# mresult = mresult || substring(base36chars from i + 1 for 1); +# end +# aresult = left(mresult || '0000000', 7); +# suspend; +# end +# ^ +# +# -- Generate test string data +# -- 000000, 100000...900000...A00000...Z00000, +# -- 010000, 110000...910000...A10000...Z10000, +# -- ... +# +# execute block +# as +# declare rowscount int = 100000; +# --declare rowscount int = 1000; +# declare i int = 0; +# declare c int = 0; +# declare str varchar(10); +# begin +# while (c < rowscount) do +# begin +# select aresult from getstr(:i) into :str; +# -- skip y, z +# if ( left(str, 1) not in ( upper('y'), upper('z') ) ) then +# begin +# insert into test( +# f_cs_compr_disabled +# ,f_ci_compr_disabled +# ,f_cs_compr_enabled +# ,f_ci_compr_enabled +# ) values ( +# :str +# ,:str +# ,:str +# ,:str +# ); +# c = c + 1; +# end +# i = i + 1; +# end +# end +# ^ +# set term ;^ +# commit; +# +# create index test_cs_compr_disabled on test (f_cs_compr_disabled); +# create index test_ci_compr_disabled on test (f_ci_compr_disabled); +# create index test_cs_compr_enabled on test (f_cs_compr_enabled); +# create index test_ci_compr_enabled on test (f_ci_compr_enabled); +# commit; diff --git a/tests/bugs/gh_6915_hu_hu_test.py b/tests/bugs/gh_6915_hu_hu_test.py new file mode 100644 index 00000000..52553b1b --- /dev/null +++ b/tests/bugs/gh_6915_hu_hu_test.py @@ -0,0 +1,313 @@ +#coding:utf-8 + +""" +ID: issue-6915-hu-hu +ISSUE: https://github.com/FirebirdSQL/firebird/issues/6915 +TITLE: Performance effect of applying 'DISABLE-COMPRESSIONS=1' in UNICODE collation for LOCALE=hu_HU +DESCRIPTION: + Original discussion: + https://sourceforge.net/p/firebird/mailman/firebird-devel/thread/9361c612-d720-eb76-d412-7101518ca60d%40ibphoenix.cz/ + Test verifies only PERFORMANCE issues referred to in the ticket #6915. Correctness of ORDER BY is not checked. + A pre-build database is used for check, see: files/gh_6915_hu_hu.zip (it was created in FB 4.x with date 31-jul-2021). + Hungarian alphabet uses several digraphs and one trigraph: cs,dz,dzs,gy,ly,ny,sz,ty,zs. + Test table has uniform distribution for following characters: c,d,g,l,n,s,t,z,cs,dz,dzs,gy,ly,ny,sz,ty,zs + (they all have been randomly selected in the loop of ~ 1E6 iterations). + SQL script that was used to fulfill test DB see in the end of this file. + + We decompress .fbk, restore from it and check that for every testing queries number of indexed reads will not + exceed threshold, see 'MAX_IDX_READS_THRESHOLD' (con.info.get_table_access_stats() is used for that). + After improvement this threshold could be set to 1. + Only columns with attribute 'DISABLE-COMPRESSIONS=1' are checked. +NOTES: + [24.12.2024] pzotov + + It seems that commit in 4.x (2af9ded1a696a43f5b0bea39a88610287e3ab06c; 04-aug-2021 17:58) had no effect: + performance in 4.x remains poort for queries from this test up to recent snapshots (dec-2024). + + Commit in 5.x (cfc09f75a3dea099f54c09808e39fe778457f441; 04-aug-2021 20:25; 5.0.0.129) really SOLVED problem: + adding attribute 'DISABLE-COMPRESSIONS=1' causes reducing indexed reads to 0 or 1 for all queries. + + There was commit in 5.x: 171cb7eebc365e301a7384eff96c0e3e069c95cc (date: 17-mar-2022 22:38) - which had + further improvement for 'DISABLE-COMPRESSIONS=0' (i.e. when compression is Enabled). Snapshots of FB 5.x + before that commit (i.e. up to 5.0.0.425) had poor performance for 'DISABLE-COMPRESSIONS=0', and after + this commit (since 5.0.0.426) performance became equal to 'DISABLE-COMPRESSIONS=1'. + Because of that, this test verifies performance of only ONE case: 'DISABLE-COMPRESSIONS=1', by comparing + of indexed reads for each query with threshold, see MAX_IDX_READS_THRESHOLD. + Before improvement related to 'DISABLE-COMPRESSIONS=1', indexed reads were huge for almost all check queries. + This is outcome for 5.0.0.126 (31.07.2021): + where f_ci_compr_disabled >= 'cs' order by f_ci_compr_disabled rows 1 ==> idx_reads=122087 + where f_ci_compr_disabled >= 'd' order by f_ci_compr_disabled rows 1 ==> idx_reads=182562 + where f_ci_compr_disabled >= 'dz' order by f_ci_compr_disabled rows 1 ==> idx_reads=243178 + where f_ci_compr_disabled >= 'dzs' order by f_ci_compr_disabled rows 1 ==> idx_reads=60339 + ... + (and similarpoor results for 'starting with', 'like' and 'similar to') + + Confirmed poor performance on 5.0.0.126 (31.07.2021): all check queries have huge indexed reads, + regardless on 'DISABLE-COMPRESSIONS=1' attribute (i.e. it had no effect on performance), + execution time was 5...15 seconds for each query. + Checked on 5.0.0.129 (05.08.2021 04:25) -- all OK, indexed reads for all queries are 0 or 1. + Checked on 6.0.0.553, 5.0.2.1580. +""" + +from pathlib import Path +import zipfile +import locale +import pytest +from firebird.qa import * +from firebird.driver import connect + +db = db_factory(charset = 'utf8') +act = python_act('db', substitutions=[('[ \t]+', ' ')]) + +tmp_fbk = temp_file('gh_6915_hu_hu.tmp.fbk') +tmp_fdb = temp_file('gh_6915_hu_hu.tmp.fdb') + +MAX_IDX_READS_THRESHOLD = 1 +EXPECTED_MSG = f'Expected. All queries have indexed reads no more than {MAX_IDX_READS_THRESHOLD=}' + +test_sql = """ + with + d as ( + select '0' as disabled_compression from rdb$database + -- union all + -- select '1' as disabled_compression from rdb$database + ) + , + c as ( + select 'ci' as case_attribute from rdb$database union all + select 'cs' from rdb$database + ) + ,o as ( + select '>=' as search_op from rdb$database union all + select 'starting with' from rdb$database union all + select 'like' from rdb$database union all + select 'similar to' from rdb$database + ) + ,e as ( + -- c,d,g,l,n,s,t,z,cs,dz,dzs,gy,ly,ny,sz,ty,zs + select p.item as letter + from list_to_rows('c,d,g,l,n,s,t,z,cs,dz,dzs,gy,ly,ny,sz,ty,zs') p + ) + ,f as ( + select + d.*, c.*, o.*, e.* + ,'select 1 from test where f_' || c.case_attribute || '_compr_' || iif(d.disabled_compression = '0', 'disabled', 'enabled') + || ' ' || trim(o.search_op) || ' ' + || '''' + || e.letter + || trim( iif( upper(trim(o.search_op)) in ('>=', upper('starting with')), '', '%') ) + || '''' + || ' order by f_' || c.case_attribute || '_compr_' || iif(d.disabled_compression = '0', 'disabled', 'enabled') + || ' rows 1' + as query_txt + from d + cross join c + cross join o + cross join e + ) + select + --case_attribute + --,search_op + --,letter + max(iif(disabled_compression = 0, query_txt, null)) as q_compr_disabled + --max(iif(disabled_compression = 1, query_txt, null)) as q_compr_enabled + from f + group by + case_attribute + ,search_op + ,letter + ; +""" + +@pytest.mark.intl +@pytest.mark.version('>=5.0.0') +def test_1(act: Action, tmp_fbk: Path, tmp_fdb: Path, capsys): + + zipped_fbk_file = zipfile.Path(act.files_dir / 'gh_6915_hu_hu.zip', at = 'gh_6915_hu_hu.fbk') + tmp_fbk.write_bytes(zipped_fbk_file.read_bytes()) + + act.gbak(switches = ['-rep', str(tmp_fbk), str(tmp_fdb)], combine_output = True, io_enc = locale.getpreferredencoding()) + assert '' == act.stdout + act.reset() + reads_map = {} + with connect(str(tmp_fdb), user = act.db.user, password = act.db.password, charset = 'utf8') as con: + cur = con.cursor() + cur2 = con.cursor() + + cur.execute("select 1 from rdb$database r left join rdb$procedures p on p.rdb$procedure_name = upper('LIST_TO_ROWS')") + required_sp_exists = cur.fetchone()[0] + assert required_sp_exists == 1, 'Missed required procedure. Check restored database.' + + cur.execute("select rdb$relation_id from rdb$relations where rdb$relation_name = upper('test')") + src_relation_id = cur.fetchone()[0] + + cur.execute(test_sql) + for r in cur: + idx_reads = -1 + for x_table in con.info.get_table_access_stats(): + if x_table.table_id == src_relation_id: + idx_reads = - (x_table.indexed if x_table.indexed else 0) + + cur2.execute(r[0]) + cur2.fetchall() + + for x_table in con.info.get_table_access_stats(): + if x_table.table_id == src_relation_id: + idx_reads += (x_table.indexed if x_table.indexed else 0) + + reads_map[ r[0] ] = idx_reads + + + if max(reads_map.values()) <= MAX_IDX_READS_THRESHOLD: + print(EXPECTED_MSG) + else: + print(f'UNEXPECTED: at least one query has values of indexed reads greater than {MAX_IDX_READS_THRESHOLD=}') + for check_qry, idx_reads in reads_map.items(): + if idx_reads > MAX_IDX_READS_THRESHOLD: + print(f'{check_qry=}, {idx_reads=}') + + act.expected_stdout = f""" + {EXPECTED_MSG} + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + +# End of test. + +################################ +# SQL with initial DDL and data: +################################ + +# set names utf8; +# -- https://github.com/FirebirdSQL/firebird/issues/6915#issue-958497709 +# -- contractions in Hungarian lang: cs, dz, dzs, gy, ly, ny, sz, ty, zs +# create database 'localhost:r:\temp\tmp4test.fdb' default character set utf8; +# create collation u_ci_compr_disabled +# for utf8 +# from unicode +# case insensitive +# 'LOCALE=hu_HU;DISABLE-COMPRESSIONS=1' +# ; +# +# create collation u_cs_compr_disabled +# for utf8 +# from unicode +# case sensitive +# 'LOCALE=hu_HU;DISABLE-COMPRESSIONS=1' +# ; +# create collation u_ci_compr_enabled +# for utf8 +# from unicode +# case insensitive +# 'LOCALE=hu_HU;DISABLE-COMPRESSIONS=0' +# ; +# +# create collation u_cs_compr_enabled +# for utf8 +# from unicode +# case sensitive +# 'LOCALE=hu_HU;DISABLE-COMPRESSIONS=0' +# ; +# create table test ( +# f_cs_compr_disabled varchar(10) collate u_cs_compr_disabled +# ,f_ci_compr_disabled varchar(10) collate u_ci_compr_disabled +# ,f_cs_compr_enabled varchar(10) collate u_cs_compr_enabled +# ,f_ci_compr_enabled varchar(10) collate u_ci_compr_enabled +# ); +# create global temporary table gtt ( +# id smallint generated by default as identity constraint gtt_pk primary key +# ,txt varchar(10) +# ) on commit preserve rows +# ; +# +# set term ^; +# create or alter procedure list_to_rows ( +# A_LST blob sub_type 1 segment size 80, +# A_DEL char(1) = ',') +# returns ( +# LINE integer, +# EOF integer, +# ITEM varchar(8190)) +# AS +# declare pos_ int; +# declare noffset int = 1; +# declare beg int; +# declare buf varchar(8190); +# begin +# -- Splits blob to lines by single char delimiter. +# -- adapted from here: +# -- http://www.sql.ru/forum/actualthread.aspx?bid=2&tid=607154&pg=2#6686267 +# if (a_lst is null) then exit; +# line=0; +# +# while (0=0) do begin +# buf = substring(a_lst from noffset for 30100); +# pos_ = 1; beg = 1; +# while (pos_ <= char_length(buf) and pos_ <= 30000) do begin +# if (substring(buf from pos_ for 1) = :a_del) then begin +# if (pos_ > beg) then +# item = substring(buf from beg for pos_ - beg); +# else +# item = ''; --null; +# suspend; +# line=line+1; +# beg = pos_ + 1; +# end +# pos_ = pos_ + 1; +# end +# if (noffset + pos_ - 2 = char_length(a_lst)) then leave; +# noffset = noffset + beg - 1; +# if (noffset > char_length(a_lst)) then leave; +# end +# +# if (pos_ > beg) then begin +# item = substring(buf from beg for pos_ - beg); +# eof=-1; +# end +# else begin +# item = ''; +# eof=-1; +# end +# suspend; +# +# end +# ^ +# execute block +# as +# declare rowscount int = 1000000; +# --declare rowscount int = 100; +# declare id_min smallint; +# declare id_max smallint; +# declare id_rnd smallint; +# declare i int = 0; +# declare c varchar(10); +# begin +# insert into gtt(txt) select p.item from list_to_rows('c,d,g,l,n,s,t,z,cs,dz,dzs,gy,ly,ny,sz,ty,zs', ',') p; +# select min(id), max(id) from gtt into id_min, id_max; +# while (i < rowscount) do +# begin +# id_rnd = cast( -0.5 + rand() * (0.5 + id_max - id_min) as int ); +# id_rnd = minvalue( maxvalue(id_min,id_rnd), id_max); +# select g.txt from gtt g where g.id = :id_rnd into c; +# insert into test( +# f_cs_compr_disabled +# ,f_ci_compr_disabled +# ,f_cs_compr_enabled +# ,f_ci_compr_enabled +# ) values ( +# :c +# ,:c +# ,:c +# ,:c +# ); +# i = i + 1; +# end +# end +# ^ +# set term ;^ +# commit; +# create index test_cs_compr_disabled on test (f_cs_compr_disabled); +# create index test_ci_compr_disabled on test (f_ci_compr_disabled); +# create index test_cs_compr_enabled on test (f_cs_compr_enabled); +# create index test_ci_compr_enabled on test (f_ci_compr_enabled); +# commit; diff --git a/tests/bugs/gh_6915_test.py b/tests/bugs/gh_6915_test.py index dda69e2e..2754e794 100644 --- a/tests/bugs/gh_6915_test.py +++ b/tests/bugs/gh_6915_test.py @@ -5,9 +5,16 @@ ISSUE: 6915 TITLE: Allow attribute DISABLE-COMPRESSIONS in UNICODE collations DESCRIPTION: - Only ability to use 'DISABLE-COMPRESSION' in attributes list is checked here. - Performance comparison with and without this attribute will be checked in separate test. -FBTEST: bugs.gh_6915 + Original discussion: + https://sourceforge.net/p/firebird/mailman/firebird-devel/thread/9361c612-d720-eb76-d412-7101518ca60d%40ibphoenix.cz/ + + Only ability to use 'DISABLE-COMPRESSION' in attributes list is checked here. + Performance comparison with and without this attribute will be checked in separate test. +NOTES: + [24.12.2024] pzotov + Several tests have been added in order to check PERFORMANCE affect of 'DISABLE-COMPRESSIONS=1': + * bugs/gh_6915_cs_cz_test.py + * bugs/gh_6915_hu_hu_test.py """ import pytest @@ -47,6 +54,7 @@ act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) +@pytest.mark.intl @pytest.mark.version('>=4.0.1') def test_1(act: Action): act.execute() diff --git a/tests/bugs/gh_6947_test.py b/tests/bugs/gh_6947_test.py index de7e70ae..34d55274 100644 --- a/tests/bugs/gh_6947_test.py +++ b/tests/bugs/gh_6947_test.py @@ -28,6 +28,13 @@ [11.03.2023] pzotov Marked as SKIPPED because covered by core_6048_test. Probably will be deleted soon. + + [18.01.2025] pzotov + Resultset of cursor that executes using instance of selectable PreparedStatement must be stored + in some variable in order to have ability close it EXPLICITLY (before PS will be freed). + Otherwise access violation raises during Python GC and pytest hangs at final point (does not return control to OS). + This occurs at least for: Python 3.11.2 / pytest: 7.4.4 / firebird.driver: 1.10.6 / Firebird.Qa: 0.19.3 + The reason of that was explained by Vlad, 26.10.24 17:42 ("oddities when use instances of selective statements"). """ import os import datetime as py_dt @@ -118,38 +125,57 @@ def test_1(act: Action, capsys): cur = con.cursor() cu2 = con.cursor() - ps = cur.prepare('select mon$crypt_page from mon$database') - - while encryption_started: - t2=py_dt.datetime.now() - d1=t2-t1 - if d1.seconds*1000 + d1.microseconds//1000 > MAX_WAITING_ENCR_FINISH: - print(f'TIMEOUT EXPIRATION: encryption took {d1.seconds*1000 + d1.microseconds//1000} ms which exceeds limit = {MAX_WAITING_ENCR_FINISH} ms.') - break - - cur.execute(ps) - p = cur.fetchone()[0] - cu2.callproc('sp_tmp', [ p, ] ) - con.commit() - - if p > 0: - encrypted_pages_set.add(p) - if len(encrypted_pages_set) >= MIN_DISTINCT_ENCRYPTED_PAGES: - # We got enough data from mon$database to conclude that encryption is in PROGRESS. + ps, rs = None, None + try: + ps = cur.prepare('select mon$crypt_page from mon$database') + while encryption_started: + t2=py_dt.datetime.now() + d1=t2-t1 + if d1.seconds*1000 + d1.microseconds//1000 > MAX_WAITING_ENCR_FINISH: + print(f'TIMEOUT EXPIRATION: encryption took {d1.seconds*1000 + d1.microseconds//1000} ms which exceeds limit = {MAX_WAITING_ENCR_FINISH} ms.') break - # Possible output: - # Database not encrypted - # Database encrypted, crypt thread not complete - act.isql(switches=['-q'], input = 'show database;', combine_output = True) - if 'Database encrypted' in act.stdout: - if 'not complete' in act.stdout: - pass - else: - encryption_finished = True - break - act.reset() + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + rs = cur.execute(ps) + for r in rs: + p = r[0] + + cu2.callproc('sp_tmp', [ p, ] ) + con.commit() + + if p > 0: + encrypted_pages_set.add(p) + if len(encrypted_pages_set) >= MIN_DISTINCT_ENCRYPTED_PAGES: + # We got enough data from mon$database to conclude that encryption is in PROGRESS. + break + + # Possible output: + # Database not encrypted + # Database encrypted, crypt thread not complete + act.isql(switches=['-q'], input = 'show database;', combine_output = True) + if 'Database encrypted' in act.stdout: + if 'not complete' in act.stdout: + pass + else: + encryption_finished = True + break + act.reset() + except DatabaseError as e: + print( e.__str__() ) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() + + if encryption_started: if len(encrypted_pages_set) >= MIN_DISTINCT_ENCRYPTED_PAGES: print(expected_stdout) diff --git a/tests/bugs/gh_6954_test.py b/tests/bugs/gh_6954_test.py new file mode 100644 index 00000000..614c0bd2 --- /dev/null +++ b/tests/bugs/gh_6954_test.py @@ -0,0 +1,37 @@ +#coding:utf-8 + +""" +ID: issue-6954 +ISSUE: 6954 +TITLE: Add fb_info_protocol_version information request to Attachment::getInfo(). +DESCRIPTION: + We attempt to obtain DbInfoCode.PROTOCOL_VERSION and print only the fact that we could do that + (instead of its concrete value which, of course can change). +NOTES: + Improvement was committed: + * in FB 4.x: 15.09.2021 18:25, cb2d8dfb (4.0.1.2602) + * in FB 5.x: 09.09.2021 17:27, 18d59a5e (5.0.0.196) + Before these snapshots attempt to obtain protocol version caused error: + ====== + raise InterfaceError("An error response was received") + firebird.driver.types.InterfaceError: An error response was received + ====== + + Checked on 6.0.0.396, 5.0.1.1440, 4.0.53127. +""" +import pytest +from firebird.qa import * +from firebird.driver import DbInfoCode + +db = db_factory() +act = python_act('db') #, substitutions=[('[ \t]+', ' ')]) + +@pytest.mark.version('>=4.0.1') +def test_1(act: Action, capsys): + + with act.db.connect() as con: + print( con.info.get_info(DbInfoCode.PROTOCOL_VERSION) > 0 ) + + act.expected_stdout = 'True' + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_6963_test.py b/tests/bugs/gh_6963_test.py index 6d1a6bff..5a18ed4a 100644 --- a/tests/bugs/gh_6963_test.py +++ b/tests/bugs/gh_6963_test.py @@ -6,6 +6,11 @@ TITLE: grant references not working DESCRIPTION: FBTEST: bugs.gh_6963 +NOTES: + [04.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.876; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -83,31 +88,42 @@ act = isql_act('db', test_script) -expected_stdout = """ +expected_stdout_5x = """ + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER TABLE TMAIN_A failed + -no permission for ALTER access to TABLE TMAIN_A + -Effective user is TMP$GH6963 + Statement failed, SQLSTATE = 28000 + unsuccessful metadata update + -ALTER TABLE TMAIN_A failed + -no permission for ALTER access to TABLE TMAIN_A + -Effective user is TMP$GH6963 RDB$RELATION_NAME TDETL_A_WITHOUT_CASC RDB$RELATION_NAME TDETL_A_WITH_CASC RDB$RELATION_NAME TMAIN_A Records affected: 3 """ -expected_stderr = """ +expected_stdout_6x = """ Statement failed, SQLSTATE = 28000 unsuccessful metadata update - -ALTER TABLE TMAIN_A failed - -no permission for ALTER access to TABLE TMAIN_A + -ALTER TABLE "PUBLIC"."TMAIN_A" failed + -no permission for ALTER access to TABLE "PUBLIC"."TMAIN_A" -Effective user is TMP$GH6963 - Statement failed, SQLSTATE = 28000 unsuccessful metadata update - -ALTER TABLE TMAIN_A failed - -no permission for ALTER access to TABLE TMAIN_A + -ALTER TABLE "PUBLIC"."TMAIN_A" failed + -no permission for ALTER access to TABLE "PUBLIC"."TMAIN_A" -Effective user is TMP$GH6963 + RDB$RELATION_NAME TDETL_A_WITHOUT_CASC + RDB$RELATION_NAME TDETL_A_WITH_CASC + RDB$RELATION_NAME TMAIN_A + Records affected: 3 """ @pytest.mark.version('>=4.0.1') def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_6976_test.py b/tests/bugs/gh_6976_test.py index 7f762e9c..c28543e6 100644 --- a/tests/bugs/gh_6976_test.py +++ b/tests/bugs/gh_6976_test.py @@ -21,7 +21,6 @@ NOTES: [20.07.2022] pzotov - Bug reproduced on 4.0.1.2606 but only when it is tested just after 5.0.0.591. In that case firebird.log will have following messages: @@ -48,7 +47,6 @@ [localized message can be here]: "operation completed successfully" NB: time interval between 01:25:17 and 01:27:07 is exactly 110 seconds. - On recent FB 4.x and 5.x median duration for delivering error to client must be about 80 ms. On 4.0.1.2606 this was about 640 ms (in case when it was tested without previous 5.x test). @@ -56,12 +54,14 @@ Recent FB build show that time to wait exception with expected gdscode = 335544344 must be less than 300 ms but it depends on ServerMode (for Classic it is about 2x more than for Super). Median value will be compared with THRESHOLD_FOR_MAKE_CONNECT_MS variable.. - Each attempt to connect top broken DB must bring stack with TWO gdscodes: isc_io_error = 335544344; isc_io_read_err = 335544736; - Checked on Windows: 3.0.8.33535 (SS/CS), 4.0.1.2692 (SS/CS), 5.0.0.730 + + [26.02.2025] pzotov + Increased valueTHRESHOLD_FOR_MAKE_CONNECT_MS for servermode = 'Super': on poor hardware + previous value was not enough. Problem appeared on 6.0.0.655, Windows 10, build 19045.3086. """ import os @@ -150,7 +150,7 @@ def try_cuted_off_db(act_source, act_broken, db_page_size, db_pages_cnt, cut_off @pytest.mark.platform('Windows') def test_1(act_source: Action, act_broken: Action, capsys): - THRESHOLD_FOR_MAKE_CONNECT_MS = 250 if 'classic' in act_source.vars['server-arch'].lower() else 100 + THRESHOLD_FOR_MAKE_CONNECT_MS = 250 if 'classic' in act_source.vars['server-arch'].lower() else 150 with act_source.db.connect() as con: db_page_size = con.info.page_size diff --git a/tests/bugs/gh_6992_test.py b/tests/bugs/gh_6992_test.py index eacfcb38..a0ce22c0 100644 --- a/tests/bugs/gh_6992_test.py +++ b/tests/bugs/gh_6992_test.py @@ -5,16 +5,18 @@ ISSUE: 6992 TITLE: Transform OUTER joins into INNER ones if the WHERE condition violates the outer join rules NOTES: - [17.02.2023] pzotov - Initial implementation. Additional tests will be added further. + [04.07.2025] pzotov + Re-implemented: queries and comments/explanations to be displayed in expected_out (using f-notation). + Output is organized to be more suitable for reading and search for mismatches (see 'qry_map' dict). + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.892; 5.0.3.1668; 4.0.6.3214. """ import pytest from firebird.qa import * -db = db_factory() - -test_script = """ +init_script = """ set bail on; recreate table tmain( id int primary key using index tmain_pk @@ -52,7 +54,11 @@ set statistics index tmain_pk; set statistics index tdetl_a_fk; set statistics index tdetl_b_fk; + commit; +""" +db = db_factory(init = init_script) +test_script = """ set planonly; set explain on; @@ -92,50 +98,196 @@ ; """ -act = isql_act('db', test_script) - -expected_stdout = """ - Select Expression - -> Filter - -> Nested Loop Join (outer) - -> Table "TMAIN" as "M1" Full Scan - -> Filter - -> Table "TDETL_A" as "D1" Access By ID - -> Bitmap - -> Index "TDETL_A_FK" Range Scan (full match) - - Select Expression - -> Nested Loop Join (inner) - -> Filter - -> Table "TMAIN" as "M2" Access By ID - -> Bitmap - -> Index "TMAIN_PK" Unique Scan - -> Filter - -> Table "TDETL_A" as "D2" Access By ID - -> Bitmap - -> Index "TDETL_A_FK" Range Scan (full match) - - Select Expression - -> Filter - -> Nested Loop Join (outer) - -> Table "TMAIN" as "M3" Full Scan - -> Filter - -> Table "TDETL_A" as "D3" Access By ID - -> Bitmap - -> Index "TDETL_A_FK" Range Scan (full match) - - Select Expression - -> Filter - -> Nested Loop Join (outer) - -> Table "TMAIN" as "M4" Full Scan - -> Filter - -> Table "TDETL_B" as "D4" Access By ID - -> Bitmap - -> Index "TDETL_B_FK" Range Scan (full match) -""" +qry_map = { + 1000 : + ( + """ + select * + from tmain m1 + left join tdetl_a d1 on m1.id = d1.pid + where d1.pid is null + """ + , + """ + Must NOT be transformed because we make here ANTI-JOIN. + Outer join is the only way to get proper result here. + """ + ) + , + 2000 : + ( + """ + select * + from tmain m2 + left join tdetl_a d2 on m2.id = d2.pid + where d2.pid = 0 + """ + , + """ + This MUST be transformed to INNER join because WHERE expression effectively will skip nulls. + See also issue in the ticket: + "regular comparisons that ignore NULLs by their nature, will cause the LEFT->INNER transformation" + """ + ) + + , + 3000 : + ( + """ + select * + from tmain m3 + left join tdetl_a d3 on m3.id = d3.pid + where d3.pid is not null + """ + , + """ + This must NOT be transformed, see ticket: + "checks for NULL, e.g. WHERE T2.ID IS NOT NULL ..., would not transform LEFT into INNER" + """ + ) + , + 4000 : + ( + """ + select * + from tmain m4 + left join tdetl_b d4 on m4.id = d4.pid + where d4.pid is not null + + """ + , + """ + This must NOT be transformed, reason is the same: + "checks for NULL, e.g. WHERE T2.ID IS NOT NULL ..., would not transform LEFT into INNER" + NB: the fact that column tdetl_b.pid is declared as NOT NULL is ignored here. + This limitation seems redunant here. + """ + + ) +} + +act = python_act('db') + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- @pytest.mark.version('>=5.0') -def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute(combine_output = True) +def test_1(act: Action, capsys): + with act.db.connect() as con: + cur = con.cursor() + for qry_idx, qry_data in qry_map.items(): + test_sql, qry_comment = qry_data[:2] + ps, rs = None, None + try: + cur = con.cursor() + ps = cur.prepare(test_sql) + + print(qry_comment) + # Print explained plan with padding eash line by dots in order to see indentations: + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + except DatabaseError as e: + print(e.__str__()) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() + + expected_out_5x = f""" + {qry_map.get(1000)[1]} + Select Expression + ....-> Filter + ........-> Nested Loop Join (outer) + ............-> Table "TMAIN" as "M1" Full Scan + ............-> Filter + ................-> Table "TDETL_A" as "D1" Access By ID + ....................-> Bitmap + ........................-> Index "TDETL_A_FK" Range Scan (full match) + + {qry_map.get(2000)[1]} + Select Expression + ....-> Nested Loop Join (inner) + ........-> Filter + ............-> Table "TMAIN" as "M2" Access By ID + ................-> Bitmap + ....................-> Index "TMAIN_PK" Unique Scan + ........-> Filter + ............-> Table "TDETL_A" as "D2" Access By ID + ................-> Bitmap + ....................-> Index "TDETL_A_FK" Range Scan (full match) + + {qry_map.get(3000)[1]} + Select Expression + ....-> Filter + ........-> Nested Loop Join (outer) + ............-> Table "TMAIN" as "M3" Full Scan + ............-> Filter + ................-> Table "TDETL_A" as "D3" Access By ID + ....................-> Bitmap + ........................-> Index "TDETL_A_FK" Range Scan (full match) + + {qry_map.get(4000)[1]} + Select Expression + ....-> Filter + ........-> Nested Loop Join (outer) + ............-> Table "TMAIN" as "M4" Full Scan + ............-> Filter + ................-> Table "TDETL_B" as "D4" Access By ID + ....................-> Bitmap + ........................-> Index "TDETL_B_FK" Range Scan (full match) + """ + + expected_out_6x = f""" + + {qry_map.get(1000)[1]} + Select Expression + ....-> Filter + ........-> Nested Loop Join (outer) + ............-> Table "PUBLIC"."TMAIN" as "M1" Full Scan + ............-> Filter + ................-> Table "PUBLIC"."TDETL_A" as "D1" Access By ID + ....................-> Bitmap + ........................-> Index "PUBLIC"."TDETL_A_FK" Range Scan (full match) + + {qry_map.get(2000)[1]} + Select Expression + ....-> Nested Loop Join (inner) + ........-> Filter + ............-> Table "PUBLIC"."TMAIN" as "M2" Access By ID + ................-> Bitmap + ....................-> Index "PUBLIC"."TMAIN_PK" Unique Scan + ........-> Filter + ............-> Table "PUBLIC"."TDETL_A" as "D2" Access By ID + ................-> Bitmap + ....................-> Index "PUBLIC"."TDETL_A_FK" Range Scan (full match) + + {qry_map.get(3000)[1]} + Select Expression + ....-> Filter + ........-> Nested Loop Join (outer) + ............-> Table "PUBLIC"."TMAIN" as "M3" Full Scan + ............-> Filter + ................-> Table "PUBLIC"."TDETL_A" as "D3" Access By ID + ....................-> Bitmap + ........................-> Index "PUBLIC"."TDETL_A_FK" Range Scan (full match) + + {qry_map.get(4000)[1]} + Select Expression + ....-> Filter + ........-> Nested Loop Join (outer) + ............-> Table "PUBLIC"."TMAIN" as "M4" Full Scan + ............-> Filter + ................-> Table "PUBLIC"."TDETL_B" as "D4" Access By ID + ....................-> Bitmap + ........................-> Index "PUBLIC"."TDETL_B_FK" Range Scan (full match) + """ + + act.expected_stdout = expected_out_5x if act.is_version('<6') else expected_out_6x + act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7045_test.py b/tests/bugs/gh_7045_test.py index f41ee0e5..858448b9 100644 --- a/tests/bugs/gh_7045_test.py +++ b/tests/bugs/gh_7045_test.py @@ -25,6 +25,7 @@ act = python_act('db') +@pytest.mark.intl @pytest.mark.version('>=5.0') def test_1(act: Action, capsys): with act.db.connect(charset='iso8859_1') as con: diff --git a/tests/bugs/gh_7046_test.py b/tests/bugs/gh_7046_test.py index 38cacb77..9067540a 100644 --- a/tests/bugs/gh_7046_test.py +++ b/tests/bugs/gh_7046_test.py @@ -23,7 +23,6 @@ "new qa, core_4964_test.py: strange outcome when use... shutil.copy() // comparing to shutil.copy2()" 3. Value of REQUIRED_ALIAS must be EXACTLY the same as alias specified in the pre-created databases.conf (for LINUX this equality is case-sensitive, even when aliases are compared!) - Checked on 5.0.0.958 [26.06.2023] pzotov @@ -31,8 +30,11 @@ https://github.com/FirebirdSQL/firebird/commit/15b0b297dcde81cc5e1c38cbd4ea761e27f442bd Added check for this ability. Also, comment text now is non-ascii (decided to use parts of 'lorem ipsum' encoded in armenian and georgian) - Checked on 5.0.0.1087 + + [04.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.894; 5.0.3.1668; 4.0.6.3214. """ import os @@ -58,6 +60,7 @@ tmp_file = temp_file('tmp_gh_7046-ddl.sql') fn_meta_log = temp_file('tmp_gh_7046-meta.log') +@pytest.mark.intl @pytest.mark.version('>=5.0') def test_1(act: Action, tmp_file: Path, fn_meta_log: Path, capsys): @@ -203,10 +206,12 @@ def test_1(act: Action, tmp_file: Path, fn_meta_log: Path, capsys): elif 'SQLSTATE' in line: print('UNEXPECTED ERROR: ',line) + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'PUBLIC.' act.expected_stdout = u""" CREATE MAPPING %(MAPPING_NAME)s USING PLUGIN CREATE OR ALTER GLOBAL MAPPING %(MAPPING_NAME)s USING PLUGIN - COMMENT ON VIEW V_MAP_INFO IS '%(VIEW_COMMENT)s'; + COMMENT ON VIEW %(SQL_SCHEMA_PREFIX)sV_MAP_INFO IS '%(VIEW_COMMENT)s'; COMMENT ON MAPPING TRUSTED_AUTH_7046 IS '%(MAPPING_COMMENT)s'; COMMENT ON GLOBAL MAPPING TRUSTED_AUTH_7046 IS '%(MAPPING_COMMENT)s'; """ % locals() diff --git a/tests/bugs/gh_7050_test.py b/tests/bugs/gh_7050_test.py index 3239c428..4eb1a99a 100644 --- a/tests/bugs/gh_7050_test.py +++ b/tests/bugs/gh_7050_test.py @@ -9,6 +9,10 @@ Test based on example provided in doc/README.monitoring_tables Probably much useful test will be implemened later (with join mon$memory_usage etc). Checked on 6.0.0.213, 5.0.1.1307. + + [04.07.2025] pzotov + Added 'SQL_SCHEMA_*_PREFIX' variables to be substituted in expected_* on FB 6.x + Checked on 6.0.0.894; 5.0.3.1668. """ import pytest @@ -111,102 +115,104 @@ act = isql_act('db', test_script, substitutions = subs) -expected_stdout = """ - N_FACTORIAL 120 - MON_STTM_ID - MON_CSTM_ID - MON_STAT_ID - MON_STATE 1 - MON_SQL_TEXT_BLOB_ID - select n_factorial from fact_static_psql(5) - MON_EXPLAINED_PLAN_BLOB_ID - Select Expression - -> Procedure "FACT_STATIC_PSQL" Scan - Records affected: 1 - - - MON_CSTM_ID - MON_SQL_TEXT_BLOB_ID - select n_factorial from fact_static_psql(5) - MON_EXPLAINED_PLAN_BLOB_ID - Select Expression - -> Procedure "FACT_STATIC_PSQL" Scan - MON_OBJ_NAME - MON_OBJ_TYPE - MON_PKG_NAME - MON_STAT_ID - - MON_CSTM_ID - MON_SQL_TEXT_BLOB_ID - MON_EXPLAINED_PLAN_BLOB_ID - Select Expression - -> Singularity Check - -> Procedure "FACT_STATIC_PSQL" Scan - Select Expression - -> Singularity Check - -> Table "MON$DATABASE" Full Scan - MON_OBJ_NAME FACT_STATIC_PSQL - MON_OBJ_TYPE 5 - MON_PKG_NAME - MON_STAT_ID - Records affected: 2 - - - MON_STTM_ID - MON_CALL_ID 192 - MON_CALLER_ID - MON_STAT_ID - MON_CSTM_ID - MON_OBJ_NAME FACT_STATIC_PSQL - MON_OBJ_TYPE 5 - MON_SRC_ROW 8 - MON_SRC_COL 17 - - MON_STTM_ID - MON_CALL_ID 193 - MON_CALLER_ID 192 - MON_STAT_ID - MON_CSTM_ID - MON_OBJ_NAME FACT_STATIC_PSQL - MON_OBJ_TYPE 5 - MON_SRC_ROW 8 - MON_SRC_COL 17 - - MON_STTM_ID - MON_CALL_ID 194 - MON_CALLER_ID 193 - MON_STAT_ID - MON_CSTM_ID - MON_OBJ_NAME FACT_STATIC_PSQL - MON_OBJ_TYPE 5 - MON_SRC_ROW 8 - MON_SRC_COL 17 - - MON_STTM_ID - MON_CALL_ID 195 - MON_CALLER_ID 194 - MON_STAT_ID - MON_CSTM_ID - MON_OBJ_NAME FACT_STATIC_PSQL - MON_OBJ_TYPE 5 - MON_SRC_ROW 8 - MON_SRC_COL 17 - - MON_STTM_ID - MON_CALL_ID 196 - MON_CALLER_ID 195 - MON_STAT_ID - MON_CSTM_ID - MON_OBJ_NAME FACT_STATIC_PSQL - MON_OBJ_TYPE 5 - MON_SRC_ROW 15 - MON_SRC_COL 13 - Records affected: 5 - -""" - @pytest.mark.version('>=5.0') def test_1(act: Action): + + SQL_SCHEMA_PUBLIC_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + SQL_SCHEMA_SYSTEM_PREFIX = '' if act.is_version('<6') else '"SYSTEM".' + expected_stdout = f""" + N_FACTORIAL 120 + MON_STTM_ID + MON_CSTM_ID + MON_STAT_ID + MON_STATE 1 + MON_SQL_TEXT_BLOB_ID + select n_factorial from fact_static_psql(5) + MON_EXPLAINED_PLAN_BLOB_ID + Select Expression + -> Procedure {SQL_SCHEMA_PUBLIC_PREFIX}"FACT_STATIC_PSQL" Scan + Records affected: 1 + + + MON_CSTM_ID + MON_SQL_TEXT_BLOB_ID + select n_factorial from fact_static_psql(5) + MON_EXPLAINED_PLAN_BLOB_ID + Select Expression + -> Procedure {SQL_SCHEMA_PUBLIC_PREFIX}"FACT_STATIC_PSQL" Scan + MON_OBJ_NAME + MON_OBJ_TYPE + MON_PKG_NAME + MON_STAT_ID + + MON_CSTM_ID + MON_SQL_TEXT_BLOB_ID + MON_EXPLAINED_PLAN_BLOB_ID + Select Expression + -> Singularity Check + -> Procedure {SQL_SCHEMA_PUBLIC_PREFIX}"FACT_STATIC_PSQL" Scan + Select Expression + -> Singularity Check + -> Table {SQL_SCHEMA_SYSTEM_PREFIX}"MON$DATABASE" Full Scan + MON_OBJ_NAME FACT_STATIC_PSQL + MON_OBJ_TYPE 5 + MON_PKG_NAME + MON_STAT_ID + Records affected: 2 + + + MON_STTM_ID + MON_CALL_ID 192 + MON_CALLER_ID + MON_STAT_ID + MON_CSTM_ID + MON_OBJ_NAME FACT_STATIC_PSQL + MON_OBJ_TYPE 5 + MON_SRC_ROW 8 + MON_SRC_COL 17 + + MON_STTM_ID + MON_CALL_ID 193 + MON_CALLER_ID 192 + MON_STAT_ID + MON_CSTM_ID + MON_OBJ_NAME FACT_STATIC_PSQL + MON_OBJ_TYPE 5 + MON_SRC_ROW 8 + MON_SRC_COL 17 + + MON_STTM_ID + MON_CALL_ID 194 + MON_CALLER_ID 193 + MON_STAT_ID + MON_CSTM_ID + MON_OBJ_NAME FACT_STATIC_PSQL + MON_OBJ_TYPE 5 + MON_SRC_ROW 8 + MON_SRC_COL 17 + + MON_STTM_ID + MON_CALL_ID 195 + MON_CALLER_ID 194 + MON_STAT_ID + MON_CSTM_ID + MON_OBJ_NAME FACT_STATIC_PSQL + MON_OBJ_TYPE 5 + MON_SRC_ROW 8 + MON_SRC_COL 17 + + MON_STTM_ID + MON_CALL_ID 196 + MON_CALLER_ID 195 + MON_STAT_ID + MON_CSTM_ID + MON_OBJ_NAME FACT_STATIC_PSQL + MON_OBJ_TYPE 5 + MON_SRC_ROW 15 + MON_SRC_COL 13 + Records affected: 5 + """ + act.expected_stdout = expected_stdout act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7056_test.py b/tests/bugs/gh_7056_test.py new file mode 100644 index 00000000..f4af1a79 --- /dev/null +++ b/tests/bugs/gh_7056_test.py @@ -0,0 +1,90 @@ +#coding:utf-8 + +""" +ID: issue-8168 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8168 +TITLE: Fetching from a scrollable cursor may overwrite user-specified buffer and corrupt memory +DESCRIPTION: + Engine did overwrite the user-specified buffer with four more bytes than expected that could corrupt the caller memory. + Discussed between dimitr, pcisar and pzotov, see letters of 29-30 NOV 2021, + subj: "firebird-driver & scrollable cursors // misc. tests, requested by dimitr" +NOTES: + [18.07.2024] pzotov + 1. ### ACHTUNG ### + Old snapshots (before 5.0.0.890-aa847a7) must be checked with usage "--disable-db-cache" command switch for pytest! + Otherwise one may FALSE failure (bugcheck) with: + "internal Firebird consistency check (decompression overran buffer (179), file: sqz.cpp line: 293)" + + 2. Initial problem related to cursor operation was found in 5.0.0.316 and result depends on CONNECTION PROTOCOL(!): + 1.1. For REMOTE protocol cur.fetch_first() raised "firebird.driver.types.DatabaseError: feature is not supported" + 1.2. For LOCAL protocol Python crashed with console output: + ============== + Current thread 0x00004ad0 (most recent call first): + File "C:/FBTESTING/qa/firebird-qa/tests/bugs/gh_7056_test.py", line 74 in test_1 + File "C:/Python3x/Lib/site-packages/_pytest/python.py", line 194 in pytest_pyfunc_call + File "C:/Python3x/Lib/site-packages/pluggy/_callers.py", line 102 in _multicall + ... + File "C:/Python3x/Lib/site-packages/_pytest/config/__init__.py", line 198 in console_main + File "C:/Python3x/Scripts/pytest.exe/__main__.py", line 7 in + File "", line 88 in _run_code + File "", line 198 in _run_module_as_main + ============== + + Problem with "firebird.driver.types.DatabaseError: feature is not supported" has been fixed in 5.0.0.320, commit: + 5a5a2992f78c1af9408091a0bd3fff50e0bc5d6a (26-nov-2021 09:11) + "Better boundary checks, code unification, removed end-of-stream errors when fetching past EOF / beyond BOF (as per SQL spec)" + Problem with Python crash did exist up to 5.0.0.325 (30-nov-2021) and has been fixed in 5.0.0.326-fd6bf8d (01-dec-2021 08:44) + 3. Problem appeared only for column with width = 32765 characters, thus DB charset must be single-byte, e.g. win1251 etc. + Otherwise (with default charset = 'utf8') this test will fail with: + "SQLSTATE = 54000 / ... or string truncation / -Implementation limit exceeded" + 4. Custom driver-config object must be used for DPB because TWO protocols are checked here: LOCAL and REMOTE. + + Checked on 6.0.0.396, 5.0.1.1440 +""" +import pytest +from firebird.qa import * +from firebird.driver import driver_config, connect, NetProtocol, DatabaseError +import re + +N_WIDTH = 32765 + +init_script = f""" + create table ts(id int primary key, s varchar({N_WIDTH})); + insert into ts(id,s) values( 1, lpad('', {N_WIDTH}, 'A') ); + commit; +""" +db = db_factory(init=init_script, charset = 'win1251') +act = python_act('db', substitutions=[('[ \t]+', ' ')]) + +def strip_white(value): + value = re.sub('(?m)^\\s+', '', value) + return re.sub('(?m)\\s+$', '', value) + +@pytest.mark.scroll_cur +@pytest.mark.version('>=5.0.0') +def test_1(act: Action, capsys): + srv_cfg = driver_config.register_server(name = 'test_srv_gh_7056', config = '') + actual_out = expected_out = '' + for protocol_name in ('local', 'remote'): + + db_cfg_name = f'tmp_7056_{protocol_name}' + db_cfg_object = driver_config.register_database(name = db_cfg_name) + db_cfg_object.server.value = srv_cfg.name + db_cfg_object.protocol.value = None if protocol_name == 'local' else NetProtocol.INET + db_cfg_object.database.value = str(act.db.db_path) + + success_msg = f'Protocol: {protocol_name} - COMPLETED.' + expected_out += success_msg + '\n' + + with connect(db_cfg_name, user = act.db.user, password = act.db.password) as con: + try: + cur = con.cursor() + cur.open('select id, s from ts order by id') + cur.fetch_first() + print(success_msg) + except DatabaseError as e: + print(e.__str__()) + + actual_out += capsys.readouterr().out + '\n' + + assert expected_out != '' and strip_white(actual_out) == strip_white(expected_out) diff --git a/tests/bugs/gh_7057_test.py b/tests/bugs/gh_7057_test.py new file mode 100644 index 00000000..80f1600b --- /dev/null +++ b/tests/bugs/gh_7057_test.py @@ -0,0 +1,152 @@ +#coding:utf-8 + +""" +ID: issue-7057 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/7057 +TITLE: Client-side positioned updates work wrongly with scrollable cursors +DESCRIPTION: + Fetching from a scrollable cursor may overwrite user-specified buffer and corrupt memory. + Engine did overwrite the user-specified buffer with four more bytes than expected that could corrupt the caller memory. + Discussed between dimitr, pcisar and pzotov, see letters of 29-30 NOV 2021, + subj: "firebird-driver & scrollable cursors // misc. tests, requested by dimitr" +NOTES: + [29.07.2024] pzotov + 1. ### ACHTUNG ### + Old snapshots (before 5.0.0.890-aa847a7) must be checked with usage "--disable-db-cache" command switch for pytest! + Otherwise one may FALSE failure (bugcheck) with: + "internal Firebird consistency check (decompression overran buffer (179), file: sqz.cpp line: 293)" + 2. Test caused crash of server on snapshots before 6.0.0.401-a7d10a4. + Problem related to MaxStatementCacheSize which default value > 0 + (explained by dimitr, letter 19-JUL-2024 12:52). + + It seems that bug was fixed in: + FB 5.x: https://github.com/FirebirdSQL/firebird/commit/08dc25f8c45342a73c786bc60571c8a5f2c8c6e3 (27.07.2024 14:55) + FB 6.x: https://github.com/FirebirdSQL/firebird/commit/a7d10a40147d326e56540498b50e40b2da0e5850 (29.07.2024 03:53) + ("Fix #8185 - SIGSEGV with WHERE CURRENT OF statement with statement cache turned on.") + + 3. Attempt to run this test on FB 4.0.5.3127 (10-JUL-2024) raises: + "E firebird.driver.types.DatabaseError: feature is not supported" + (scollable cursors are not supported in network protocol in FB-4.x) + + Checked on 6.0.0.401-a7d10a4, 5.0.1.1453-62ee5f1. +""" +import pytest +from firebird.qa import * +from firebird.driver import driver_config, connect, NetProtocol, DatabaseError +import re + +db = db_factory() +act = python_act('db', substitutions=[('[ \t]+', ' ')]) + +#------------------------------------------------------ +def print_row(row, cur = None): + if row: + print(f"{row[0]}") + if cur and (cur.is_bof() or cur.is_eof()): + print('### STRANGE BOF/EOR WHILE SOME DATA CAN BE SEEN ###') + else: + msg = '*** NO_DATA***' + if cur: + msg += ' BOF=%r EOF=%r' % ( cur.is_bof(), cur.is_eof() ) + print(msg) +#------------------------------------------------------ + + +@pytest.mark.scroll_cur +@pytest.mark.version('>=5.0.1') +def test_1(act: Action, capsys): + with act.db.connect() as con: + con.execute_immediate('recreate table ts(id int)') + con.commit() + con.execute_immediate('insert into ts (id) select row_number() over() from rdb$types rows 10') + con.commit() + + cur = con.cursor() + cur.open('select id from ts for update') + cur.set_cursor_name('X') + + for row in cur: + print_row(row) + + cur.fetch_first() + print('Updating first record') + con.execute_immediate('update ts set id = -id where current of X') + con.commit() + + cur = con.cursor() + cur.open('select id from ts for update') + cur.set_cursor_name('X') + + for row in cur: + print_row(row) + + cur.fetch_last() + print('Updating last record') + con.execute_immediate('update ts set id = -id where current of X') + con.commit() + + cur = con.cursor() + cur.open('select id from ts for update') + cur.set_cursor_name('X') + + for row in cur: + print_row(row) + + cur.fetch_absolute(5) + print('Updating 5th record') + con.execute_immediate('update ts set id = -id where current of X') + con.commit() + + cur = con.cursor() + cur.open('select id from ts') + + for row in cur: + print_row(row) + + act.stdout = capsys.readouterr().out + act.expected_stdout = """ + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + Updating first record + -1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + Updating last record + -1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + -10 + Updating 5th record + -1 + 2 + 3 + 4 + -5 + 6 + 7 + 8 + 9 + -10 + """ + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7062_test.py b/tests/bugs/gh_7062_test.py index 1be3dcfb..ca29275e 100644 --- a/tests/bugs/gh_7062_test.py +++ b/tests/bugs/gh_7062_test.py @@ -6,6 +6,10 @@ TITLE: Creation of expression index does not release its statement correctly DESCRIPTION: FBTEST: bugs.gh_7062 +NOTES: + [04.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.894; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -36,15 +40,17 @@ act = isql_act('db', test_script) -expected_stderr = """ - Statement failed, SQLSTATE = 22012 - Expression evaluation error for index "***unknown***" on table "TEST_A" - -arithmetic exception, numeric overflow, or string truncation - -Integer divide by zero. The code attempted to divide an integer value by an integer divisor of zero. -""" - @pytest.mark.version('>=4.0.1') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + expected_stdout = f""" + Statement failed, SQLSTATE = 22012 + Expression evaluation error for index "***unknown***" on table {SQL_SCHEMA_PREFIX}"TEST_A" + -arithmetic exception, numeric overflow, or string truncation + -Integer divide by zero. The code attempted to divide an integer value by an integer divisor of zero. + """ + + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7092_test.py b/tests/bugs/gh_7092_test.py index d89ee1ff..6e505e2b 100644 --- a/tests/bugs/gh_7092_test.py +++ b/tests/bugs/gh_7092_test.py @@ -13,15 +13,20 @@ We do these measures times for each SP, and each result is added to the list which, in turn, is the source for median evaluation. Finally, we get ratio between minimal and maximal medians (see 'median_ratio') - On Windows 8.1 usually this ratio is about 7 (before fix it was more than 100). + This ratio is about: + * Windows: 0.6 ... 0.7 + * Linux: 0.5 ... 0.6 + Before fix it was more than 10. Test is considered as passed if median_ratio less than threshold . NOTES: + Number of iterations for loops differ: we have to perform 'sp_empty_loop' at least 1E6 times + in order to get valuable difference between CPU user time counters and use it as denomitator. + Procedure 'sp_ctime_loop' must be called for 10x times LESS than 'sp_empty_loop'. + Confirmed problem on: - 4.0.1.2699 (01-jan-2022): median ratio was 109 ... 110 (3.40 vs 0.03) - 5.0.0.362 (01-jan-2022): median ratio was 111 ... 113 (3.51 vs 0.03) + 5.0.0.362, 4.0.1.2699 (bith snapshots have date 01-jan-2022) Checked on 6.0.0.195, 5.0.0.1305, 4.0.5.3049. - Scope of median ratio values: 4.33 ... 7.00 """ import psutil @@ -43,12 +48,13 @@ def median(lst): N_MEASURES = 15 # How many iterations must be done: -N_COUNT_PER_MEASURE = 100000 +N_COUNT_TIME_LOOP = 100000 +N_COUNT_EMPTY_LOOP = 1000000 # Maximal value for ratio between maximal and minimal medians # -MAX_RATIO = 15 -############## +MAX_RATIO = 1.5 +############### init_script = \ f''' @@ -60,12 +66,12 @@ def median(lst): begin while (n < a_limit) do begin - n = n + 1; d = current_time; + n = n + 1; end end ^ - create procedure sp_dummy_loop(a_limit int) + create procedure sp_empty_loop(a_limit int) as declare n int = 1; begin @@ -79,7 +85,7 @@ def median(lst): ^ ''' -db = db_factory(init = init_script) +db = db_factory(init = init_script, charset = 'win1251') act = python_act('db') expected_stdout = """ @@ -96,24 +102,25 @@ def test_1(act: Action, capsys): sp_time = {} for i in range(0, N_MEASURES): - for sp_name in ('sp_ctime_loop', 'sp_dummy_loop'): + for sp_name in ('sp_ctime_loop', 'sp_empty_loop'): + n_count = N_COUNT_TIME_LOOP if sp_name == 'sp_ctime_loop' else N_COUNT_EMPTY_LOOP fb_info_init = psutil.Process(fb_pid).cpu_times() - cur.callproc( sp_name, (N_COUNT_PER_MEASURE,) ) + cur.callproc( sp_name, (n_count,) ) fb_info_curr = psutil.Process(fb_pid).cpu_times() sp_time[ sp_name, i ] = max(fb_info_curr.user - fb_info_init.user, 0.000001) sp_ctime_median = median([v for k,v in sp_time.items() if k[0] == 'sp_ctime_loop']) - sp_dummy_median = median([v for k,v in sp_time.items() if k[0] == 'sp_dummy_loop']) + sp_dummy_median = median([v for k,v in sp_time.items() if k[0] == 'sp_empty_loop']) #---------------------------------- median_ratio = sp_ctime_median / sp_dummy_median print( 'Medians ratio: ' + ('acceptable' if median_ratio <= MAX_RATIO else '/* perf_issue_tag */ POOR: %s, more than threshold: %s' % ( '{:9g}'.format(median_ratio), '{:9g}'.format(MAX_RATIO) ) ) ) if median_ratio > MAX_RATIO: - print('CPU times for each of {N_MEASURES} measures:') + print(f'CPU times for each of {N_MEASURES} measures:') for k,v in sp_time.items(): print(k,':::',v) - print(f'Median cpu time for {N_MEASURES} measures using loops for {N_COUNT_PER_MEASURE} iterations in each SP call:') + print(f'Median cpu time for {N_MEASURES} measures:') print('sp_ctime_median:',sp_ctime_median) print('sp_dummy_median:',sp_dummy_median) print('median_ratio:',median_ratio) diff --git a/tests/bugs/gh_7093_test.py b/tests/bugs/gh_7093_test.py index ba22066b..261d2b97 100644 --- a/tests/bugs/gh_7093_test.py +++ b/tests/bugs/gh_7093_test.py @@ -277,6 +277,7 @@ Records affected: 0 """ +@pytest.mark.intl @pytest.mark.version('>=5.0.0') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/gh_7094_test.py b/tests/bugs/gh_7094_test.py index f1f64620..164cba38 100644 --- a/tests/bugs/gh_7094_test.py +++ b/tests/bugs/gh_7094_test.py @@ -10,6 +10,11 @@ Confirmed problem on 5.0.0.425. Checked on 5.0.0.1163, 4.0.4.2978. Test fails on 3.0.12 with 'invalid collation attribute', thus min_version was set to 4.0.2. + + [04.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.876; 5.0.3.1668. """ import pytest @@ -46,32 +51,56 @@ select t1.* from t1 where c1 > 'c' plan (t1 natural) order by c1, c2; """ -act = isql_act('db', test_script) - -expected_stdout = """ - PLAN SORT (T1 INDEX (T1_C1_C2_DESC)) - C1 d - C2 d - C1 e - C2 e - C1 f - C2 f - C1 ch - C2 ch - - PLAN SORT (T1 NATURAL) - C1 d - C2 d - C1 e - C2 e - C1 f - C2 f - C1 ch - C2 ch -""" +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) +@pytest.mark.intl @pytest.mark.version('>=4.0.2') def test_1(act: Action): - act.expected_stdout = expected_stdout + + expected_stdout_5x = """ + PLAN SORT (T1 INDEX (T1_C1_C2_DESC)) + C1 d + C2 d + C1 e + C2 e + C1 f + C2 f + C1 ch + C2 ch + + PLAN SORT (T1 NATURAL) + C1 d + C2 d + C1 e + C2 e + C1 f + C2 f + C1 ch + C2 ch + """ + + expected_stdout_6x = """ + PLAN SORT ("PUBLIC"."T1" INDEX ("PUBLIC"."T1_C1_C2_DESC")) + C1 d + C2 d + C1 e + C2 e + C1 f + C2 f + C1 ch + C2 ch + PLAN SORT ("PUBLIC"."T1" NATURAL) + C1 d + C2 d + C1 e + C2 e + C1 f + C2 f + C1 ch + C2 ch + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7106_test.py b/tests/bugs/gh_7106_test.py index fa86fe54..6f079473 100644 --- a/tests/bugs/gh_7106_test.py +++ b/tests/bugs/gh_7106_test.py @@ -26,6 +26,17 @@ Explanation by Alex with example: letter 06.03.2023 18:58 Confirmed problem on 5.0.0.376 (with user name 'test-user') Checked on 5.0.0.379, 5.0.0.970, 4.0.3.2904, 3.0.11.33665 -- all OK. + + [31.12.2024] pzotov + User names with delimiting character must be enclosed in double quotes since 6.0.0.570 otherwise we get + "SQLSTATE = 08006 / Error occurred during login, please check server firebird.log for details" and firebird.log + will contain: + Authentication error cannot attach to password database + Error in isc_compile_request() API call when working + with legacy security database table PLG$USERS is not defined + + Parsing problem appeared on 6.0.0.0.570 after d6ad19aa07deeaac8107a25a9243c5699a3c4ea1 + ("Refactor ISQL creating FrontendParser class"). """ import os @@ -161,23 +172,23 @@ def test_1(act: Action, capsys): for use_connect_sttm in (True,False,): - for u in CHECKED_NAMES: + for u_name in CHECKED_NAMES: isql_connect_sttm = \ '\n'.join( ( 'set bail on;', 'set heading off;', - f"connect 'localhost:{REQUIRED_ALIAS}' user {u} password '123';" if use_connect_sttm else "", + f"""connect 'localhost:{REQUIRED_ALIAS}' user "{u_name}" password '123';""" if use_connect_sttm else "", 'select mon$user from mon$attachments a where a.mon$attachment_id = current_connection;' ) ) - act.expected_stdout = u + act.expected_stdout = u_name if use_connect_sttm: # Try to make connection using isql CONNECT operator: act.isql(switches = ['-q'], input = isql_connect_sttm, connect_db=False, credentials = False, combine_output = True, io_enc = locale.getpreferredencoding()) else: # try to make connection via command-line argument "-user ...": - act.isql(switches = ['-q', '-user', u, '-pas', '123', f'localhost:{REQUIRED_ALIAS}'], input = isql_connect_sttm, connect_db=False, credentials = False, combine_output = True, io_enc = locale.getpreferredencoding()) - assert act.clean_stdout == act.clean_expected_stdout, f'User "{u}" could not make connection using isql {"" if use_connect_sttm else " -user ... -pas ..."} and script:\n{isql_connect_sttm}' + act.isql(switches = ['-q', '-user', f'"{u_name}"', '-pas', '123', f'localhost:{REQUIRED_ALIAS}'], input = isql_connect_sttm, connect_db=False, credentials = False, combine_output = True, io_enc = locale.getpreferredencoding()) + assert act.clean_stdout == act.clean_expected_stdout, f'User "{u_name}" could not make connection using isql {"" if use_connect_sttm else " -user ... -pas ..."} and script:\n{isql_connect_sttm}' act.reset() act.gfix(switches = [ '-shut', 'full', '-force', '0', f'localhost:{REQUIRED_ALIAS}', '-user', act.db.user, '-pas', act.db.password ], credentials = False, combine_output = True, io_enc = locale.getpreferredencoding()) diff --git a/tests/bugs/gh_7118_test.py b/tests/bugs/gh_7118_test.py index e0a222bc..01c0fa92 100644 --- a/tests/bugs/gh_7118_test.py +++ b/tests/bugs/gh_7118_test.py @@ -6,7 +6,16 @@ TITLE: Chained JOIN .. USING across the same column names may be optimized badly NOTES: [01.03.2023] pzotov + Commit related to this test: + https://github.com/FirebirdSQL/firebird/commit/1b192404d43a15d403b5ff92760bc5df9d3c89c3 + (13.09.2022 19:17, "More complete solution for #3357 and #7118") + One more test that attempts to verify this commit: bugs/gh_7398_test.py Checked on 3.0.11.33665, 4.0.3.2904, 5.0.0.964 + + [04.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.863; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -69,17 +78,23 @@ act = isql_act('db', test_script) -"" - -expected_stdout = """ - PLAN JOIN (T1 INDEX (T1_COL), T2 INDEX (RDB$PRIMARY2), T3 INDEX (RDB$PRIMARY3)) - PLAN JOIN (T1 INDEX (T1_COL), T2 INDEX (RDB$PRIMARY2), T3 INDEX (RDB$PRIMARY3)) - PLAN JOIN (T1 INDEX (T1_COL), T2 INDEX (RDB$PRIMARY2), T3 INDEX (RDB$PRIMARY3)) - PLAN JOIN (T1 INDEX (T1_COL), T2 INDEX (RDB$PRIMARY2), T3 INDEX (RDB$PRIMARY3)) -""" - @pytest.mark.version('>=3.0.9') def test_1(act: Action): - act.expected_stdout = expected_stdout + + expected_stdout_5x = """ + PLAN JOIN (T1 INDEX (T1_COL), T2 INDEX (RDB$PRIMARY2), T3 INDEX (RDB$PRIMARY3)) + PLAN JOIN (T1 INDEX (T1_COL), T2 INDEX (RDB$PRIMARY2), T3 INDEX (RDB$PRIMARY3)) + PLAN JOIN (T1 INDEX (T1_COL), T2 INDEX (RDB$PRIMARY2), T3 INDEX (RDB$PRIMARY3)) + PLAN JOIN (T1 INDEX (T1_COL), T2 INDEX (RDB$PRIMARY2), T3 INDEX (RDB$PRIMARY3)) + """ + + expected_stdout_6x = """ + PLAN JOIN ("T1" INDEX ("PUBLIC"."T1_COL"), "T2" INDEX ("PUBLIC"."RDB$PRIMARY2"), "T3" INDEX ("PUBLIC"."RDB$PRIMARY3")) + PLAN JOIN ("T1" INDEX ("PUBLIC"."T1_COL"), "T2" INDEX ("PUBLIC"."RDB$PRIMARY2"), "T3" INDEX ("PUBLIC"."RDB$PRIMARY3")) + PLAN JOIN ("T1" INDEX ("PUBLIC"."T1_COL"), "T2" INDEX ("PUBLIC"."RDB$PRIMARY2"), "T3" INDEX ("PUBLIC"."RDB$PRIMARY3")) + PLAN JOIN ("T1" INDEX ("PUBLIC"."T1_COL"), "T2" INDEX ("PUBLIC"."RDB$PRIMARY2"), "T3" INDEX ("PUBLIC"."RDB$PRIMARY3")) + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7123_test.py b/tests/bugs/gh_7123_test.py index 9ca4163f..0d3f98d2 100644 --- a/tests/bugs/gh_7123_test.py +++ b/tests/bugs/gh_7123_test.py @@ -13,6 +13,9 @@ Added 'SQLSTATE' in substitutions: runtime error must not be filtered out by '?!(...)' pattern ("negative lookahead assertion", see https://docs.python.org/3/library/re.html#regular-expression-syntax). Added 'combine_output = True' in order to see SQLSTATE if any error occurs. + [04.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.894; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -33,17 +36,19 @@ db = db_factory(init = init_script) act = python_act('db', substitutions = [('^((?!SQLSTATE|ID1(A|B|C|D)|ID2(A|B|C|D)).)*$', '')] ) -expected_stdout = """ - CREATE TABLE TEST (ID1A INTEGER GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT 111) NOT NULL, - ID1B INTEGER GENERATED ALWAYS AS IDENTITY (START WITH -222 INCREMENT 222) NOT NULL, - ID1C INTEGER GENERATED ALWAYS AS IDENTITY (START WITH -333) NOT NULL, - ID2A INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH 1 INCREMENT 1111) NOT NULL, - ID2B INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH -2222 INCREMENT 2222) NOT NULL, - ID2C INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH -3333) NOT NULL); -""" @pytest.mark.version('>=4.0.2') def test_1(act: Action): - # meta = act.extract_meta() + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'PUBLIC.' + expected_stdout = f""" + CREATE TABLE {SQL_SCHEMA_PREFIX}TEST (ID1A INTEGER GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT 111) NOT NULL, + ID1B INTEGER GENERATED ALWAYS AS IDENTITY (START WITH -222 INCREMENT 222) NOT NULL, + ID1C INTEGER GENERATED ALWAYS AS IDENTITY (START WITH -333) NOT NULL, + ID2A INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH 1 INCREMENT 1111) NOT NULL, + ID2B INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH -2222 INCREMENT 2222) NOT NULL, + ID2C INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH -3333) NOT NULL); + """ + act.expected_stdout = expected_stdout act.isql(switches=['-x'], charset='utf8', combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7128_test.py b/tests/bugs/gh_7128_test.py new file mode 100644 index 00000000..a49263ad --- /dev/null +++ b/tests/bugs/gh_7128_test.py @@ -0,0 +1,87 @@ +#coding:utf-8 +""" +ID: issue-7128 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/7128 +TITLE: Incorrect error message with isc_sql_interprete() +DESCRIPTION: +NOTES: + [28.03.2024] pzotov + Bug caused crash of FB up to 5.0.0.890 (10-jan-2023). + Since 5.0.0.905 (11-jan-2023) following error raises: + Invalid resultset interface + -901 + 335545049 + + [03.09.2024] pzotov + 1. Warning is issued: + $PYTHON_HOME/Lib/site-packages/firebird/driver/interfaces.py:710: FirebirdWarning: Invalid resultset interface + self._check() + It was decided to suppress warning by using 'warnings' package. + + 2. Result for snapshots with date = 09-feb-2022: + 3.0.9.33560: + Exception ignored in: + Traceback (most recent call last): + File "$PYTHON_HOME/Lib/site-packages/firebird/driver/core.py", line 3047, in __del__ + File "$PYTHON_HOME/Lib/site-packages/firebird/driver/core.py", line 3788, in close + File "$PYTHON_HOME/Lib/site-packages/firebird/driver/core.py", line 3655, in _clear + File "$PYTHON_HOME/Lib/site-packages/firebird/driver/interfaces.py", line 709, in close + OSError: exception: access violation writing 0x0000000000000024 + 4.0.1.2175: passed. + 5.0.0.393: crashed, + > raise self.__report(DatabaseError, self.status.get_errors()) + E firebird.driver.types.DatabaseError: Error writing data to the connection. + E -send_packet/send + 3. Version 3.0.13.33793 raises: + > raise self.__report(DatabaseError, self.status.get_errors()) + E firebird.driver.types.DatabaseError: Invalid resultset interface + (and this exceprion is not catched for some reason). + + Checked on 6.0.0.447, 5.0.2.1487, 4.0.6.3142 +""" + +import pytest +from firebird.qa import * +from firebird.driver import tpb, Isolation, TraLockResolution, TraAccessMode, DatabaseError, FirebirdWarning +import time +import warnings + +db = db_factory() +act = python_act('db') + +@pytest.mark.version('>=4.0') +def test_1(act: Action, capsys): + + tpb_isol_set = (Isolation.SERIALIZABLE, Isolation.SNAPSHOT, Isolation.READ_COMMITTED_READ_CONSISTENCY, Isolation.READ_COMMITTED_RECORD_VERSION, Isolation.READ_COMMITTED_NO_RECORD_VERSION) + + with act.db.connect() as con: + for x_isol in tpb_isol_set: + custom_tpb = tpb(isolation = x_isol, lock_timeout = 0) + tx = con.transaction_manager(custom_tpb) + cur = tx.cursor() + tx.begin() + with warnings.catch_warnings(): + warnings.filterwarnings('ignore') + try: + print(x_isol.name) + cur.execute('select 0 from rdb$types rows 2') + cur.fetchone() + tx._cursors = [] + tx.commit() + cur.fetchone() + except DatabaseError as e: + print(e.__str__()) + print(e.sqlcode) + for g in e.gds_codes: + print(g) + finally: + cur.close() + + act.expected_stdout = f""" + {x_isol.name} + Invalid resultset interface + -901 + 335545049 + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7137_test.py b/tests/bugs/gh_7137_test.py index 9d5747dd..a7cb03d4 100644 --- a/tests/bugs/gh_7137_test.py +++ b/tests/bugs/gh_7137_test.py @@ -6,8 +6,12 @@ TITLE: Optimizer regression: bad plan (HASH instead of JOIN) is chosen for some inner joins NOTES: [26.04.2022] pzotov - Confirmed bug (ineffective execution plan) on 3.0.9.33560 (09.02.2022). - Checked on 6.0.0.336, 5.0.1.1383, 4.0.5.3086, 3.0.10.33569 (24.02.2022) - all fine. + Confirmed bug (ineffective execution plan) on 3.0.9.33560 (09.02.2022). + Checked on 6.0.0.336, 5.0.1.1383, 4.0.5.3086, 3.0.10.33569 (24.02.2022) - all fine. + [04.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.863; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -88,7 +92,7 @@ def test_1(act: Action, capsys): with cur.prepare(q) as ps: print( '\n'.join([replace_leading(s) for s in ps.detailed_plan .split('\n')]) ) - expected_stdout = """ + expected_stdout_5x = """ Select Expression ....-> Nested Loop Join (inner) ........-> Filter @@ -104,7 +108,24 @@ def test_1(act: Action, capsys): ................-> Bitmap ....................-> Index "TEST_C_FK" Range Scan (full match) """ - - act.expected_stdout = expected_stdout + + expected_stdout_6x = """ + Select Expression + ....-> Nested Loop Join (inner) + ........-> Filter + ............-> Table "PUBLIC"."TEST_B" as "B" Access By ID + ................-> Bitmap + ....................-> Index "PUBLIC"."TEST_B_PK" Unique Scan + ........-> Filter + ............-> Table "PUBLIC"."TEST_A" as "A" Access By ID + ................-> Bitmap + ....................-> Index "PUBLIC"."TEST_A_NAME" Range Scan (full match) + ........-> Filter + ............-> Table "PUBLIC"."TEST_C" as "C" Access By ID + ................-> Bitmap + ....................-> Index "PUBLIC"."TEST_C_FK" Range Scan (full match) + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7139_test.py b/tests/bugs/gh_7139_test.py index 40d5fa78..f77564f0 100644 --- a/tests/bugs/gh_7139_test.py +++ b/tests/bugs/gh_7139_test.py @@ -65,6 +65,7 @@ def get_external_trace_id(act: Action, a_what_to_check, a_ext_trace_session_name #------------------------------------------------------------------------- +@pytest.mark.trace @pytest.mark.version('>=4.0.2') def test_1(act: Action, tmp_trace_cfg: Path, tmp_trace_log: Path, capsys): diff --git a/tests/bugs/gh_7140_test.py b/tests/bugs/gh_7140_test.py index 552db972..c0296dc8 100644 --- a/tests/bugs/gh_7140_test.py +++ b/tests/bugs/gh_7140_test.py @@ -52,6 +52,7 @@ C1 """ +@pytest.mark.intl @pytest.mark.version('>=4.0.2') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/gh_7141_test.py b/tests/bugs/gh_7141_test.py new file mode 100644 index 00000000..4ac956ab --- /dev/null +++ b/tests/bugs/gh_7141_test.py @@ -0,0 +1,141 @@ +#coding:utf-8 + +""" +ID: issue-7141 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/7141 +TITLE: Services manager breaks long lines into 1023 bytes portions when using isc_info_svc_line in Service::query() +DESCRIPTION: + Test invokes fbsvcmgr utility with requirement to start sepaarate trace session which will have long name. + This name is stored in variable LONG_NAME_OF_TRACE_SESSION and its maximal len currently is 16254. + Then we try to find this name in two ways: + * using act.connect_server().trace.sessions.items(); + * using fbsvcmgr action_trace_list. + Both way must return info which contains without line breaking (this is what was fixed). +NOTES: + Confirmed bug on 4.0.1.2735, 3.0.10.33570: name of session did contain LF characters in its middle points. + Confirmed problem on 5.0.0.418 - but only for console FB utility (fbsvcmgr), and NOT for usage firebird-QA framework + (it causes BUGCHECK "decompression overran buffer (179), file: sqz.cpp line: 293" on test teardown phase). + Checked on 6.0.0.363, 5.0.1.1408, 4.0.5.3103, 3.0.12.33744 +""" + +import pytest +import platform +import re +from firebird.qa import * +from pathlib import Path +import subprocess +import time + +#db = db_factory(async_write = False) +db = db_factory() +act = python_act('db') + +tmp_trace_cfg = temp_file('test_trace_7141.cfg') +tmp_trace_log = temp_file('test_trace_7141.log') + +MAX_WAIT_FOR_TRACE_STOP = 10 +TRC_SESSION_NAME_PREFIX = 'gh_7141_' +TRC_SESSION_NAME_MAX_LEN = 16254 + +# 65000 100000 --> FileNotFoundError: [WinError 206] The filename or extension is too long // in localized form! +# 32000 --> AssertionError: Could not find trace session to be stopped in {act.connect_server().trace.sessions.items()=} // None +LONG_NAME_OF_TRACE_SESSION = (TRC_SESSION_NAME_PREFIX * 10000000)[:TRC_SESSION_NAME_MAX_LEN] +EXPECTED_MSG1 = 'Success: found trace session name in act.connect_server().trace.sessions.items()' +EXPECTED_MSG2 = 'Success: found trace session name in the result of fbsvcmgr action_trace_list' + +@pytest.mark.trace +@pytest.mark.version('>=3.0.10') +def test_1(act: Action, tmp_trace_cfg: Path, tmp_trace_log: Path, capsys): + + trace_txt = f""" + database=%[\\\\/]{act.db.db_path.name} + {{ + enabled = true + log_initfini = false + }} + """ + + tmp_trace_cfg.write_text(trace_txt) + trace_session_id = -1 + trace_session_nm = '' + + with tmp_trace_log.open('w') as f_log: + # EXPLICIT call of FB utility 'fbsvcmgr': + p = subprocess.Popen( [ act.vars['fbsvcmgr'], + 'localhost:service_mgr', + 'user', act.db.user, + 'password', act.db.password, + 'action_trace_start', + 'trc_name', LONG_NAME_OF_TRACE_SESSION, + 'trc_cfg', tmp_trace_cfg + ], + stdout = f_log, stderr = subprocess.STDOUT + ) + time.sleep(1.1) + + q1 = subprocess.run( [ act.vars['fbsvcmgr'], + 'localhost:service_mgr', + 'user', act.db.user, + 'password', act.db.password, + 'action_trace_list', + ], + stdout = f_log, stderr = subprocess.STDOUT + ) + + assert q1.returncode == 0 + + with act.connect_server() as srv: + # K = 1 + # V = TraceSession(id=1, user='SYSDBA', timestamp=..., name=, flags=['active', ' trace']) + for k,v in srv.trace.sessions.items(): + if v.flags[0] == 'active' and v.name.startswith(TRC_SESSION_NAME_PREFIX): + trace_session_id = v.id + trace_session_nm = v.name + + assert trace_session_id > 0, f'Could not find trace session to be stopped in {act.connect_server().trace.sessions.items()=}' + + q2 = subprocess.run( [ act.vars['fbsvcmgr'], + 'localhost:service_mgr', + 'user', act.db.user, + 'password', act.db.password, + 'action_trace_stop', + 'trc_id', str(trace_session_id) + ], + stdout = f_log, stderr = subprocess.STDOUT, + timeout = MAX_WAIT_FOR_TRACE_STOP + ) + + time.sleep(1.1) + if not p.poll(): + p.terminate() + assert q2.returncode == 0 + + if trace_session_nm == LONG_NAME_OF_TRACE_SESSION: + print(EXPECTED_MSG1) + else: + print('UNEXPECTED. COULD NOT FIND trace session name in in act.connect_server().trace.sessions.items()') + + p_prefix_in_list = re.compile(f'name(:)?\\s+{TRC_SESSION_NAME_PREFIX}', re.IGNORECASE) + + found_in_trc_list = False + with tmp_trace_log.open('r') as f_log: + for line in f_log: + #if p_prefix_in_list.search(line): + if LONG_NAME_OF_TRACE_SESSION in line: + found_in_trc_list = True + print(EXPECTED_MSG2) + break + + if not found_in_trc_list: + print('Check result of fbsvcmgr action_trace_list:') + with tmp_trace_log.open('r') as f: + trace_lines = [ x for x in f.read().splitlines() if x.split() ] + for i, x in enumerate(trace_lines): + print(f'line {i}, length = {len(x.rstrip())}: >' + x.rstrip() + '<') + + act.expected_stdout = f""" + {EXPECTED_MSG1} + {EXPECTED_MSG2} + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7164_test.py b/tests/bugs/gh_7164_test.py index f53fb10c..5f0303fe 100644 --- a/tests/bugs/gh_7164_test.py +++ b/tests/bugs/gh_7164_test.py @@ -7,14 +7,17 @@ DESCRIPTION: NOTES: [28.02.2023] pzotov - ::: NB ::: - Currently improvement relates only to the case when data sources have no appropriate indices. - Otherwise "old" way is used: server attempts to make nested loopss, but finally it checks - whether hash join will be cheaper. And, if yes, then it applies hash join, but it is applied - to each joined stream, so execution plan will look as "nested" (multi-way) hashes. - Thanks to dimitr for explanation (letter 28.02.2023 10:52). - - Checked on 5.0.0.961 - all OK. + ::: NB ::: + Currently improvement relates only to the case when data sources have no appropriate indices. + Otherwise "old" way is used: server attempts to make nested loopss, but finally it checks + whether hash join will be cheaper. And, if yes, then it applies hash join, but it is applied + to each joined stream, so execution plan will look as "nested" (multi-way) hashes. + Thanks to dimitr for explanation (letter 28.02.2023 10:52). + Checked on 5.0.0.961 - all OK. + [04.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.863; 5.0.3.1668. """ import pytest @@ -23,12 +26,6 @@ db = db_factory() act = python_act('db') -expected_stdout = """ - PLAN HASH (T1 NATURAL, T2 NATURAL, T3 NATURAL) - PLAN HASH (A NATURAL, B NATURAL, C NATURAL) - PLAN HASH (HASH (V1 A NATURAL, V1 B NATURAL, V1 C NATURAL), HASH (V2 A NATURAL, V2 B NATURAL, V2 C NATURAL), HASH (V3 A NATURAL, V3 B NATURAL, V3 C NATURAL)) - PLAN HASH (HASH (U1 A NATURAL, U1 B NATURAL, U1 C NATURAL), HASH (U2 A NATURAL, U2 B NATURAL, U2 C NATURAL), HASH (U3 A NATURAL, U3 B NATURAL, U3 C NATURAL)) -""" @pytest.mark.version('>=5.0') def test_1(act: Action): test_sql = """ @@ -59,6 +56,21 @@ def test_1(act: Action): ; """ - act.expected_stdout = expected_stdout + + expected_stdout_5x = """ + PLAN HASH (T1 NATURAL, T2 NATURAL, T3 NATURAL) + PLAN HASH (A NATURAL, B NATURAL, C NATURAL) + PLAN HASH (HASH (V1 A NATURAL, V1 B NATURAL, V1 C NATURAL), HASH (V2 A NATURAL, V2 B NATURAL, V2 C NATURAL), HASH (V3 A NATURAL, V3 B NATURAL, V3 C NATURAL)) + PLAN HASH (HASH (U1 A NATURAL, U1 B NATURAL, U1 C NATURAL), HASH (U2 A NATURAL, U2 B NATURAL, U2 C NATURAL), HASH (U3 A NATURAL, U3 B NATURAL, U3 C NATURAL)) + """ + + expected_stdout_6x = """ + PLAN HASH ("T1" NATURAL, "T2" NATURAL, "T3" NATURAL) + PLAN HASH ("A" NATURAL, "B" NATURAL, "C" NATURAL) + PLAN HASH (HASH ("V1" "A" NATURAL, "V1" "B" NATURAL, "V1" "C" NATURAL), HASH ("V2" "A" NATURAL, "V2" "B" NATURAL, "V2" "C" NATURAL), HASH ("V3" "A" NATURAL, "V3" "B" NATURAL, "V3" "C" NATURAL)) + PLAN HASH (HASH ("U1" "A" NATURAL, "U1" "B" NATURAL, "U1" "C" NATURAL), HASH ("U2" "A" NATURAL, "U2" "B" NATURAL, "U2" "C" NATURAL), HASH ("U3" "A" NATURAL, "U3" "B" NATURAL, "U3" "C" NATURAL)) + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.isql(switches=['-q'], input = test_sql, combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7165_test.py b/tests/bugs/gh_7165_test.py index 6f403737..2624576a 100644 --- a/tests/bugs/gh_7165_test.py +++ b/tests/bugs/gh_7165_test.py @@ -53,6 +53,7 @@ Missing security context for TEST.FDB """ +@pytest.mark.trace @pytest.mark.version('>=5.0') @pytest.mark.platform('Windows') def test_1(act: Action, capsys): diff --git a/tests/bugs/gh_7167_test.py b/tests/bugs/gh_7167_test.py index 4c50e11e..e364142d 100644 --- a/tests/bugs/gh_7167_test.py +++ b/tests/bugs/gh_7167_test.py @@ -7,11 +7,13 @@ DESCRIPTION: NOTES: [28.02.2023] pzotov - Confirmed bug on 4.0.1.2692 Windows and Linux. - NB: on Linux we have to write SQL script into file with encoding = cp1251 - and run it as script, otherwise issue not reproduced. - - Checked on Windows and Linux, builds 5.0.0.961, 4.0.3.2903 - all OK. + Confirmed bug on 4.0.1.2692 Windows and Linux. + NB: on Linux we have to write SQL script into file with encoding = cp1251 + and run it as script, otherwise issue not reproduced. + Checked on Windows and Linux, builds 5.0.0.961, 4.0.3.2903 - all OK. + [04.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.894; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -22,11 +24,7 @@ db = db_factory(charset = 'win1251') act = python_act('db', substitutions = [('After line \\d+ in file .*', '')]) tmp_sql = temp_file('tmp_gh_7167.tmp.sql') -expected_stdout = """ - Statement failed, SQLSTATE = 23000 - violation of PRIMARY or UNIQUE KEY constraint "уни" on table "абв" - -Problematic key value is ("аб" = 'аб', "вг" = 'аб', "де" = 'аб') -""" + @pytest.mark.version('>=4.0.2') def test_1(act: Action, tmp_sql: Path): test_sql = """ @@ -40,6 +38,14 @@ def test_1(act: Action, tmp_sql: Path): insert into "абв" values ('аб','аб','аб'); """ tmp_sql.write_bytes(test_sql.encode('cp1251')) + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + expected_stdout = f""" + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "уни" on table {SQL_SCHEMA_PREFIX}"абв" + -Problematic key value is ("аб" = 'аб', "вг" = 'аб', "де" = 'аб') + """ + act.expected_stdout = expected_stdout act.isql(switches=['-q'], charset = 'win1251', input_file = tmp_sql, combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7188_test.py b/tests/bugs/gh_7188_test.py new file mode 100644 index 00000000..cf17268a --- /dev/null +++ b/tests/bugs/gh_7188_test.py @@ -0,0 +1,85 @@ +#coding:utf-8 + +""" +ID: issue-7188 +ISSUE: 7188 +TITLE: Memory leak in GDS32.DLL(FBClient.DLL), when multi-database transaction has done +DESCRIPTION: + Test obtains PID of currently running Python script which serves as CLIENT for server. + Then we get values of RSS and VMS by invoking client_process.memory_info() and store them. + After that, we use DistributedTransactionManager instance for making two connections to different + databases and run times Tx start and commit (in loop). + Finally, we get again RSS and VMS values for currently running Python process, and compare them + with initially stored ones. + Ratio between appropriate final and initial values of RSS and VMS must not exceed thresholds + defined by RSS_MAX_DIFF and VMS_MAX_DIFF. +NOTES: + [21.07.2024] pzotov + Confirmed memory leak in the CLIENT process (i.e. python.exe) when running test on: + * 5.0.0.511 (09-jun-2022): + memo_rss_list=[46988, 54192] diff = 1.15332 + memo_vms_list=[37048, 44228] diff = 1.19380 + * 4.0.2.2776 (10.06.2022): + memo_rss_list=[47248, 54456] diff = 1.15255 + memo_vms_list=[37240, 44424] diff = 1.19291 + Aftef fix: + * 5.0.1.514: + memo_rss_list=[47164, 47184] diff = 1.000424 + memo_vms_list=[37200, 37200] no_diff + * 4.0.2.2779: + memo_rss_list=[47256, 47272] diff = 1.0003386 + memo_vms_list=[37172, 37172] no_diff + Checked on 6.0.0.396, 5.0.1.1440, 4.0.53127. + Thanks to Vlad for suggestions about test implementation. +""" +import os +import pytest +from firebird.qa import * +from firebird.driver import DistributedTransactionManager, tpb, Isolation +import psutil + +db1 = db_factory(filename='core_7188_a.fdb') +db2 = db_factory(filename='core_7188_b.fdb') + +tmp_user1 = user_factory('db1', name='tmp$7188_1', password='123') +tmp_user2 = user_factory('db2', name='tmp$7188_2', password='456') + +act1 = python_act('db1') +act2 = python_act('db2') + +LOOP_COUNT = 10000 +CUSTOM_TPB = tpb(isolation = Isolation.READ_COMMITTED) +RSS_MAX_DIFF = 1.003 +VMS_MAX_DIFF = 1.001 +PASSED_MSG = 'OK' + +@pytest.mark.version('>=4.0.2') +def test_1(act1: Action, act2: Action, tmp_user1: User, tmp_user2: User, capsys): + dt_list = [] + memo_rss_list = [] + memo_vms_list = [] + client_process = psutil.Process( os.getpid() ) + + memo_rss_list.append(int(client_process.memory_info().rss / 1024)) + memo_vms_list.append(int(client_process.memory_info().vms / 1024)) + with act1.db.connect(user = tmp_user1.name, password = tmp_user1.password) as con1, \ + act2.db.connect(user = tmp_user2.name, password = tmp_user2.password) as con2: + + for i in range(LOOP_COUNT): + dt = DistributedTransactionManager([con1, con2]) + dt.begin(tpb = CUSTOM_TPB) + dt.commit() + + memo_rss_list.append(int(client_process.memory_info().rss / 1024)) + memo_vms_list.append(int(client_process.memory_info().vms / 1024)) + + if memo_rss_list[1] / memo_rss_list[0] < RSS_MAX_DIFF and memo_vms_list[1] / memo_vms_list[0] < VMS_MAX_DIFF: + print(PASSED_MSG) + else: + print('client_process.memory_info(): ratio of RSS and/or VMS values exceeds threshold.') + print(f'{memo_rss_list=}, ratio: {memo_rss_list[1] / memo_rss_list[0]:.3f}, {RSS_MAX_DIFF=:.3f}') + print(f'{memo_vms_list=}, ratio: {memo_vms_list[1] / memo_vms_list[0]:.3f}, {VMS_MAX_DIFF=:.3f}') + + act1.expected_stdout = PASSED_MSG + act1.stdout = capsys.readouterr().out + assert act1.clean_stdout == act1.clean_expected_stdout diff --git a/tests/bugs/gh_7200_test.py b/tests/bugs/gh_7200_test.py index f0de731c..570982d8 100644 --- a/tests/bugs/gh_7200_test.py +++ b/tests/bugs/gh_7200_test.py @@ -1,203 +1,145 @@ -#coding:utf-8 - -""" -ID: issue-7200 -ISSUE: https://github.com/FirebirdSQL/firebird/issues/7200 -TITLE: DROP DATABASE lead FB to hang if it is issued while DB encrypting/decrypting is in progress -DESCRIPTION: - Test creates database that will be droppped MANUALLY (i.e. by this test itself, not by fixture). - This database will contain table with wide indexed column and add some data to it, and its FW will be set to ON. - Volume of data must be big enough so that the encryption thread will not complete instantly. - - Then 'ALTER DATABASE ENCRYPT...' is issued by ISQL which is launched ASYNCHRONOUSLY, and we start - loop with query: 'select mon$crypt_state from mon$database'. - As far as query will return column mon$crypt_state = 3 ("is encrypting") - we break from loop and try to DROP database. - Attempt to drop database during incompleted (running) encryption must raise exception: - lock time-out on wait transaction - -object is in use - Test verifies that this exception actually raises (i.e. this is EXPECTED behaviour). - - ::: NB ::: 03-mar-2023. - We have to run second ISQL for DROP DATABASE (using 'act_tmp.isql(...)' for that). - Attempt to use drop_database() of class Connection behaves strange on Classic: it does not return exception 'obj in use' - and silently allows code to continue. The reason currently is unknown. To be discussed with pcisar/alex et al. - -NOTES: - [03.03.2023] pzotov - 0. On SuperServer FB 4.0.2.2772 hanged. On Classic another problem did exist: DROP DATABASE could start only after encryption - completed (i.e. until value MON$CRYPT_STATE will not changed from 3 to 1). - 1. Settings for encryption are taken from act.files_dir/'test_config.ini' file. - 2. We have to avoid usage of act_tmp.db.drop_database() because it suppresses any occurring exception. - 3. Confirmed problem on 4.0.2.2772 SS (02-jun-2022), 5.0.0.236 SS (30-sep-2021) - test hangs. - ::: NB ::: - FB 5.x seems to be escaped this problem much earlier than FB 4.x. Build 5.0.0.240 (01-oct-2021) altready NOT hangs. - Checked on 5.0.0.961 SS, 4.0.3.2903 SS - all fine. - - [07.12.2023] pzotov - Increased number of inserted rows (from 100'000 to 200'000) and indexed column width (from 700 to 800). - Otherwise test could fail because encryption thread completes too fast (encountered under Linux). - Loop that checks for appearance of encryption state = 3 must have delay much less than one second (changed it from 1 to 0.1). -""" - -import datetime as py_dt -from pathlib import Path -import subprocess -import time -from datetime import datetime as dt - -import pytest -from firebird.qa import * -from firebird.driver import DatabaseError, tpb, Isolation, TraLockResolution, DatabaseError - -FLD_LEN = 800 -N_ROWS = 200000 -MAX_WAIT_FOR_ENCRYPTION_START_MS = 30000 - -# Value in mon$crypt_state for "Database is currently encrypting" -IS_ENCRYPTING_STATE = 3 - -db = db_factory(page_size = 16384) -tmp_fdb = db_factory(filename = 'tmp_gh_7200.tmp.fdb') -tmp_sql = temp_file(filename = 'tmp_gh_7200.tmp.sql') -tmp_log = temp_file(filename = 'tmp_gh_7200.tmp.log') - -act = python_act('db', substitutions=[('[ \t]+', ' ')]) -act_tmp = python_act('tmp_fdb', substitutions=[ ('[ \t]+', ' '), ('-object .* is in use', '-object is in use'), ('(After|(-)?At) line \\d+.*', '') ]) - - -@pytest.mark.encryption -@pytest.mark.version('>=4.0.2') -def test_1(act: Action, act_tmp: Action, tmp_sql: Path, tmp_log: Path, capsys): - - init_sql = f""" - recreate table test(s varchar({FLD_LEN})); - commit; - set term ^; - execute block as - declare n int = {N_ROWS}; - begin - while (n>0) do - begin - insert into test(s) values(lpad('', {FLD_LEN}, uuid_to_char(gen_uuid()))); - n = n - 1; - end - end - ^ - -- for debug, trace must be started with log_proc = true: - create procedure sp_debug (a_point varchar(50)) as - begin - -- nop -- - end - ^ - set term ;^ - commit; - create index test_s on test(s); - """ - act_tmp.isql(switches=['-q'], input = init_sql, combine_output = True) - assert act_tmp.clean_stdout == '' - act_tmp.reset() - - ############################################# - ### c h a n g e F W t o O N ### - ############################################# - act_tmp.db.set_sync_write() - - - # QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings - # from act.files_dir/'test_config.ini': - enc_settings = QA_GLOBALS['encryption'] - - encryption_plugin = enc_settings['encryption_plugin'] # fbSampleDbCrypt - encryption_holder = enc_settings['encryption_holder'] # fbSampleKeyHolder - encryption_key = enc_settings['encryption_key'] # Red - - sttm = f'alter database encrypt with "{encryption_plugin}" key "{encryption_key}";' - tmp_sql.write_bytes(sttm.encode('utf-8')) - - with tmp_log.open('w') as f_log: - - p = subprocess.Popen( [ act_tmp.vars['isql'], - '-q', - '-user', act_tmp.db.user, - '-password', act_tmp.db.password, - act_tmp.db.dsn, - '-i', tmp_sql - ], - stdout = f_log, stderr = subprocess.STDOUT - ) - - encryption_started = False - with act_tmp.db.connect() as con_watcher: - - custom_tpb = tpb(isolation = Isolation.SNAPSHOT, lock_timeout = -1) - tx_watcher = con_watcher.transaction_manager(custom_tpb) - cur_watcher = tx_watcher.cursor() - - # 0 = non-encrypted; 1 = encrypted; 2 = is DEcrypting; 3 - is Encrypting - ps = cur_watcher.prepare('select mon$crypt_state from mon$database') - - i = 0 - da = dt.now() - while True: - cur_watcher.execute(ps) - for r in cur_watcher: - db_crypt_state = r[0] - - tx_watcher.commit() - db = dt.now() - diff_ms = (db-da).seconds*1000 + (db-da).microseconds//1000 - if db_crypt_state == IS_ENCRYPTING_STATE: - encryption_started = True - cur_watcher.call_procedure('sp_debug', ('encryption_started',)) - break - elif diff_ms > MAX_WAIT_FOR_ENCRYPTION_START_MS: - break - - time.sleep(0.1) - - ps.free() - - assert encryption_started, f'Could not find start of encryption process for {MAX_WAIT_FOR_ENCRYPTION_START_MS} ms.' - - #----------------------------------------------------------------- - - drop_db_when_running_encryption_sql = f""" - set list on; - select mon$crypt_state from mon$database; - commit; - set echo on; - DROP DATABASE; - set echo off; - select lower(rdb$get_context('SYSTEM', 'DB_NAME')) as db_name from rdb$database; - """ - tmp_sql.write_text(drop_db_when_running_encryption_sql) - - drop_db_expected_stdout = f""" - MON$CRYPT_STATE {IS_ENCRYPTING_STATE} - DROP DATABASE; - Statement failed, SQLSTATE = 42000 - unsuccessful metadata update - -object is in use - set echo off; - DB_NAME {str(act_tmp.db.db_path).lower()} - """ - - act_tmp.expected_stdout = drop_db_expected_stdout - - # Get current state of encryption (again, just for additional check) - # and attempt to DROP database: - ############################### - act_tmp.isql(switches=['-q', '-n'], input_file = tmp_sql, combine_output=True) - - # If following assert fails then act_tmp.db.db_path was unexpectedly removed from disk: - assert act_tmp.clean_stdout == act_tmp.clean_expected_stdout - act_tmp.reset() - - #< with tmp_log.open('w') as f_log - - #with tmp_log.open('r') as f: - # print(f.read()) - # - #act.expected_stdout = '' - #act.stdout = capsys.readouterr().out - #assert act.clean_stdout == act.clean_expected_stdout + +#coding:utf-8 + +""" +ID: issue-7200 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/7200 +TITLE: DROP DATABASE lead FB to hang if it is issued while DB encrypting/decrypting is in progress +DESCRIPTION: + Test does exactly what is described in the ticket: creates DB, does not add anydata to it, runs encryption + ('alter database encrypt ...' using fbSampleDbCrypt plugin) and *immediately* attempts to drop this DB. + Only encryption is checked (i.e. no decrypt). + + The key issue: no delay must be added between encryption command and drop DB attempt. + Problem can be reproduced on snapshot 4.0.1.2692 (at least on Windows): FB hangs. + After this bug was fixed, client received exception: + SQLSTATE = 42000 / unsuccessful metadata update / -object DATABASE is in use + - and terminates itself (without need to do this forcibly via subprocess terminate() call). + + But it must be remembered that encryption command works in asynchronous mode, so it can be the case when + DROP database starts to execute *before* encryption thread, as it was noted by Alex: + https://github.com/FirebirdSQL/firebird/issues/7200#issuecomment-1147672310 + This means that client can get NO exception at all and database will be 'silently' dropped eventually. + Because of this, we must *not* wait for exception delivering to client and check its presense. + Rather, we must only to ensure that client can make some FURTHER actions after DROP command, e.g. + it can try to create ANOTHER database. + This caused us to use TWO databases for this test: one for purpose to check ability to DROP it and second + to ensure that client does not hang and can do something after finish with first DB (see 'act1' and 'act2'). + Both databases are created and dropped 'manually', i.e. w/o fixture. + + Summary, the test should check two things: + 1) self-termination of client process for reasonable time (see setting MAX_WAIT_FOR_ISQL_TERMINATE); + 2) ability of client to create another DB after the one that was dropped. + Code related to these requirements operates with 'EXPECTED_MSG_1' and 'EXPECTED_MSG_2' variables: we check in + stdout presense of the text which they store. + +NOTES: + [03.03.2023] pzotov + 1. Settings for encryption are taken from act.files_dir/test_config.ini file. + 2. We have to avoid usage of act_tmp.db.drop_database() because it suppresses any occurring exception. + + Checked on Linux: 6.0.0.660; 5.0.3.1628; 4.0.6.3190 (SS and CS). + Checked on Windows: 6.0.0.658; 5.0.3.1624; 4.0.6.3189 (SS). +""" + +from pathlib import Path +import subprocess +import time + +import pytest +from firebird.qa import * +from firebird.driver import DatabaseError + +######################### +### S E T T I N G S ### +######################### + +MAX_WAIT_FOR_ISQL_TERMINATE = 5 + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings from $QA_ROOT/test_config.ini: +enc_settings = QA_GLOBALS['encryption'] + +encryption_plugin = enc_settings['encryption_plugin'] # fbSampleDbCrypt +encryption_holder = enc_settings['encryption_holder'] # fbSampleKeyHolder +encryption_key = enc_settings['encryption_key'] # Red + +EXPECTED_MSG_1 = f'EXPECTED: ISQL process has terminated for less than {MAX_WAIT_FOR_ISQL_TERMINATE} second(s).' +EXPECTED_MSG_2 = "EXPECTED: script could continue after 'DROP DATABASE'" + +db1 = db_factory(filename = 'tmp_gh_7200.tmp.fdb', do_not_create = True, do_not_drop = True) +db2 = db_factory(filename = 'tmp_gh_7200.tmp2.fdb', do_not_create = True, do_not_drop = True) + +act1 = python_act('db1', substitutions = [('[ \t]+', ' '), ('^((?!(EXPECTED:|ISQL_LOG:)).)*$', '')]) +act2 = python_act('db2') + +tmp_run_encrypt_sql = temp_file(filename = 'tmp_gh_7200-run-encr.sql') +tmp_run_encrypt_log = temp_file(filename = 'tmp_gh_7200-run-encr.log') + +@pytest.mark.encryption +@pytest.mark.version('>=4.0.1') +def test_1(act1: Action, act2: Action, tmp_run_encrypt_sql: Path, tmp_run_encrypt_log: Path, capsys): + + + act1.db.db_path.unlink(missing_ok = True) + act2.db.db_path.unlink(missing_ok = True) + + sttm = f""" + create database '{act1.db.dsn}'; + alter database encrypt with "{encryption_plugin}" key "{encryption_key}"; + drop database; + rollback; + create database '{act2.db.dsn}'; + set headinf off; + select iif( mon$database_name containing '{act2.db.db_path}' + ,q'[{EXPECTED_MSG_2}]' + ,'UNEXPECTED value of mon$database_name = ' || mon$database_name + ) as result_for_check from mon$database + ; + """ + tmp_run_encrypt_sql.write_bytes(sttm.encode('utf-8')) + + with tmp_run_encrypt_log.open('w') as f_log: + p_isql_encr = subprocess.Popen( [ act1.vars['isql'], + '-q', + '-user', act1.db.user, + '-password', act1.db.password, + '-i', tmp_run_encrypt_sql + ], + stdout = f_log, stderr = subprocess.STDOUT + ) + + time.sleep(1) + if p_isql_encr: + try: + p_isql_encr.wait(MAX_WAIT_FOR_ISQL_TERMINATE) + print(EXPECTED_MSG_1) + with tmp_run_encrypt_log.open('r') as f_log: + isql_log = f_log.read() + if EXPECTED_MSG_2 in isql_log: + print(EXPECTED_MSG_2) + else: + # Statement failed, SQLSTATE = 42000 + # unsuccessful metadata update + # -object DATABASE is in use + for line in isql_log.splitlines(): + if line.split(): + print(f'ISQL_LOG: {line}') + + except subprocess.TimeoutExpired: + p_isql_encr.terminate() + print(f'UNEXPECTED: ISQL process WAS NOT completed in {MAX_WAIT_FOR_ISQL_TERMINATE=} second(s) and was forcibly terminated.') + + try: + act1.db.db_path.unlink(missing_ok = True) + except PermissionError as e: + print(f'UNEXPECTED: Could not remove file {act1.db.db_path}') + print(f'UNEXPECTED: {e.__class__=}, {e.errno=}') + + act2.db.db_path.unlink(missing_ok = True) + + act1.expected_stdout = f""" + {EXPECTED_MSG_1} + {EXPECTED_MSG_2} + """ + act1.stdout = capsys.readouterr().out + assert act1.clean_stdout == act1.clean_expected_stdout diff --git a/tests/bugs/gh_7208_test.py b/tests/bugs/gh_7208_test.py index b91a8377..534ff933 100644 --- a/tests/bugs/gh_7208_test.py +++ b/tests/bugs/gh_7208_test.py @@ -271,6 +271,7 @@ 0 records fetched """ +@pytest.mark.trace @pytest.mark.version('>=4.0.2') def test_1(act: Action, capsys): diff --git a/tests/bugs/gh_7220_test.py b/tests/bugs/gh_7220_test.py index d94e2f3c..be677667 100644 --- a/tests/bugs/gh_7220_test.py +++ b/tests/bugs/gh_7220_test.py @@ -7,8 +7,12 @@ DESCRIPTION: NOTES: [23.02.2023] pzotov - Confirmed bug on 5.0.0.520 (but 'drop table' will not fail only if it is executed in the same connect as DDL). - Checked on 5.0.0.958 - all fine. + Confirmed bug on 5.0.0.520 (but 'drop table' will not fail only if it is executed in the same connect as DDL). + Checked on 5.0.0.958 - all fine. + [04.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.863; 5.0.3.1668. """ import pytest from firebird.qa import * @@ -53,34 +57,55 @@ drop domain domain1; """ -expected_out = """ - RDB$DEPENDENT_NAME PKG - RDB$DEPENDED_ON_NAME TABLE1 +@pytest.mark.version('>=5.0') +def test_1(act: Action): - RDB$DEPENDENT_NAME PKG - RDB$DEPENDED_ON_NAME DOMAIN1 + expected_stdout_5x = """ + RDB$DEPENDENT_NAME PKG + RDB$DEPENDED_ON_NAME TABLE1 - RDB$DEPENDENT_NAME WAIT_EVENT - RDB$DEPENDED_ON_NAME TABLE1 - Records affected: 3 + RDB$DEPENDENT_NAME PKG + RDB$DEPENDED_ON_NAME DOMAIN1 - drop table table1; - Statement failed, SQLSTATE = 42000 - unsuccessful metadata update - -cannot delete - -COLUMN TABLE1.N - -there are 1 dependencies + RDB$DEPENDENT_NAME WAIT_EVENT + RDB$DEPENDED_ON_NAME TABLE1 + Records affected: 3 - drop domain domain1; - Statement failed, SQLSTATE = 42000 - unsuccessful metadata update - -DROP DOMAIN DOMAIN1 failed - -Domain DOMAIN1 is used in procedure PKG.PROC2 (parameter name I) and cannot be dropped -""" + drop table table1; + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -cannot delete + -COLUMN TABLE1.N + -there are 1 dependencies -@pytest.mark.version('>=5.0') -def test_1(act: Action): + drop domain domain1; + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -DROP DOMAIN DOMAIN1 failed + -Domain DOMAIN1 is used in procedure PKG.PROC2 (parameter name I) and cannot be dropped + """ + + expected_stdout_6x = """ + RDB$DEPENDENT_NAME PKG + RDB$DEPENDED_ON_NAME TABLE1 + RDB$DEPENDENT_NAME PKG + RDB$DEPENDED_ON_NAME DOMAIN1 + RDB$DEPENDENT_NAME WAIT_EVENT + RDB$DEPENDED_ON_NAME TABLE1 + Records affected: 3 + drop table table1; + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -cannot delete + -COLUMN "PUBLIC"."TABLE1"."N" + -there are 1 dependencies + drop domain domain1; + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -DROP DOMAIN "PUBLIC"."DOMAIN1" failed + -Domain "PUBLIC"."DOMAIN1" is used in procedure "PUBLIC"."PKG"."PROC2" (parameter name "I") and cannot be dropped + """ - act.expected_stdout = expected_out + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.isql(switches=['-q'], combine_output = True, input = test_sql) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7255_test.py b/tests/bugs/gh_7255_test.py new file mode 100644 index 00000000..8ebea200 --- /dev/null +++ b/tests/bugs/gh_7255_test.py @@ -0,0 +1,144 @@ +#coding:utf-8 + +""" +ID: issue-7255 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/7255 +TITLE: READ COMMITTED READ CONSISTENCY mode is broken in Classic / SuperClassic on Linux (the newly created users are not visible to execute statement) +DESCRIPTION: + Issue can be reproduced only if firebird.conf contains ReadConsistency = 1. + Problem will NOT appear if this parameter is set to 0 and we start transactions as 'READ COMMITTED READ CONSISTENCY'. + Because of that, it was decided to make copy of firebird.conf, read its content and remote any line with 'ReadConsistency'. + Then we add new line with ReadConsistency = 1 and OVERWRITE firebird.conf. + After this, we run ISQL with script similar to provided in the ticket. + Output must display name of new user and TIL of his transaction ('read committed read consistency') that is seen in procedure sp_main. + Finally, we revert changes in firebir.conf using its original copy. + All these actions are enclosed in the try/except/finally block. +NOTES: + ### ACHTUNG ### + Test tries temporary to change content of firebird.conf. In any outcome, this content will be restored at final point. + + Confirmed bug on 5.0.0.599, Classic. LINUX only. + Checked on 5.0.0.1397, 4.0.5.3098. + + Command switch '--disable-db-cache' must be used if this test is running under 5.0.0.999 after some fresh FB with same ODS was tested. + Otherwise "internal Firebird consistency check (decompression overran buffer (179), file: sqz.cpp line: 293)" will raise. + Example: + /opt/distr/venv/bin/pytest --disable-db-cache -vv --tb=long --server qa_rundaily_FB50 tests/bugs/gh_7255_test.py +""" +import shutil +import pytest +import locale +import re +import time +import platform +from pathlib import Path +from firebird.qa import * + +db = db_factory(async_write = True) +act = python_act('db', substitutions = [('[ \t]+', ' ')]) + +fbcfg_bak = temp_file('firebird.conf') +p_read_consist_param = re.compile('ReadConsistency\\s*=\\s*(0|1)', re.IGNORECASE) +TMP_USR_NAME = 'tmp$7255' + +@pytest.mark.skipif(platform.system() == 'Windows', reason='Reproduced on Linux only.') +@pytest.mark.version('>=4.0.3') +def test_1(act: Action, fbcfg_bak: Path, capsys): + + if act.vars['server-arch'].lower() != 'classic': + pytest.skip('Can be reproduced only for Servermode = Classic.') + + fbcfg_file = act.vars['home-dir'] / 'firebird.conf' + shutil.copy2(fbcfg_file, fbcfg_bak) + + try: + fbcfg_ini = fbcfg_file.read_text(encoding='utf-8').splitlines() + fbcfg_new = [] + for x in fbcfg_ini: + if p_read_consist_param.search(x): + pass + else: + fbcfg_new.append(x) + + fbcfg_new.append('ReadConsistency = 1') + fbcfg_file.write_text('\n'.join(fbcfg_new), encoding='utf-8' ) + + test_sql = f""" + set list on; + commit; + SET KEEP_TRAN_PARAMS ON; + SET TRANSACTION READ COMMITTED READ CONSISTENCY; + + create or alter user {TMP_USR_NAME} password '123'; + commit; + + set term ^; + create or alter procedure sp_main( + a_usr varchar(31), a_pwd varchar(31) + ) returns( + who varchar(50) + ,til varchar(50) + ) as + begin + for + execute statement + q'#select #' + || q'# a.mon$user as who #' + || q'# ,decode( #' + || q'# t.mon$isolation_mode #' + || q'# ,0, 'snapshot table stability' #' + || q'# ,1, 'concurrency (snapshot)' #' + || q'# ,2, 'read committed record version' #' + || q'# ,3, 'read committed no record version' #' + || q'# ,4, 'read committed read consistency' #' + || q'# ) as til #' + || q'#from mon$attachments a #' + || q'#join mon$transactions t on a.mon$attachment_id = t.mon$attachment_id #' + || q'#where a.mon$attachment_id = current_connection and t.mon$state = 1 #' + as user a_usr password a_pwd + into who, til + do + suspend; + end + ^ + set term ;^ + commit; + + grant execute on procedure sp_main to {TMP_USR_NAME}; + commit; + + -- wait 10 seconds and it will work + + set term ^; + execute block returns( + who varchar(50) + ,til varchar(50) + ) as + begin + for + execute statement ('select who, til from sp_main(:u, :p)') ( u := '{TMP_USR_NAME}', p := '123' ) + into who, til + do + suspend; + end + ^ + set term ;^ + commit; + + drop user {TMP_USR_NAME}; + commit; + """ + + act.isql(switches = ['-q'], input = test_sql, combine_output = True, io_enc = locale.getpreferredencoding()) + + except OSError as e: + print(e) + finally: + shutil.copy2(fbcfg_bak, act.vars['home-dir'] / 'firebird.conf') + + act.expected_stdout = f""" + WHO {TMP_USR_NAME.upper()} + TIL read committed read consistency + """ + # act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7256_test.py b/tests/bugs/gh_7256_test.py index 7638980d..66a89061 100644 --- a/tests/bugs/gh_7256_test.py +++ b/tests/bugs/gh_7256_test.py @@ -4,8 +4,7 @@ ID: issue-7256 ISSUE: 7256 TITLE: Inconsistent conversion of non-TEXT blobs in BLOB_APPEND -NOTES: - [22.02.2023] pzotov +DESCRIPTION: 1. Test makes TWO subsequent connections: 1) to write blobs in the table using charset UTF8 2) to READ that table using charset NONE. @@ -19,21 +18,23 @@ 3. Statement 'select blob_append() from test' currently does not show literal. This is a bug and must/will be fixed. After that, test will be adjusted. - +NOTES: + [22.02.2023] pzotov Thanks to Vlad for suggestions. Discussed 20-21 feb 2023. Checked on 5.0.0.958. - - [03.03.2023] pzotov + [03.03.2023] pzotov Added 'set blob all' because result of blob_append(null, null) must be visible as literal ''. Added substitution for suppressing 'Nullable' flags in the SQLDA output: it is sufficient for this test to check only datatypes of result. Discussed with Vlad, letters 02-mar-2023 16:01 and 03-mar-2023 14:43. - - Checked on 5.0.0.967, 4.0.3.2904 (intermediate build 03-mar-2023 12:33) + Checked on 5.0.0.967, 4.0.3.2904 (intermediate build 03-mar-2023 12:33) [14.12.2023] pzotov Added 'SQLSTATE' in substitutions: runtime error must not be filtered out by '?!(...)' pattern ("negative lookahead assertion", see https://docs.python.org/3/library/re.html#regular-expression-syntax). Added 'combine_output = True' in order to see SQLSTATE if any error occurs. + [04.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.894; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -108,30 +109,33 @@ act = isql_act('db', test_script, substitutions = [('^((?!SQLSTATE|sqltype:|BLOB_RESULT).)*$', ''), ('BLOB Nullable', 'BLOB'), ('[ \t]+', ' ')]) -expected_stdout = """ - 01: sqltype: 520 BLOB scale: 0 subtype: 1 len: 8 charset: 1 OCTETS - : name: BLOB_APPEND alias: BLOB_RESULT_1 +@pytest.mark.version('>=4.0.3') +def test_1(act: Action): - 01: sqltype: 520 BLOB scale: 0 subtype: 0 len: 8 - : name: BLOB_APPEND alias: BLOB_RESULT_2 + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'SYSTEM.' + expected_stdout = f""" + 01: sqltype: 520 BLOB scale: 0 subtype: 1 len: 8 charset: 1 {SQL_SCHEMA_PREFIX}OCTETS + : name: BLOB_APPEND alias: BLOB_RESULT_1 - 01: sqltype: 520 BLOB scale: 0 subtype: 1 len: 8 charset: 21 ISO8859_1 - : name: BLOB_APPEND alias: BLOB_RESULT_3A + 01: sqltype: 520 BLOB scale: 0 subtype: 0 len: 8 + : name: BLOB_APPEND alias: BLOB_RESULT_2 - 01: sqltype: 520 BLOB scale: 0 subtype: 1 len: 8 charset: 4 UTF8 - : name: BLOB_APPEND alias: BLOB_RESULT_3B + 01: sqltype: 520 BLOB scale: 0 subtype: 1 len: 8 charset: 21 {SQL_SCHEMA_PREFIX}ISO8859_1 + : name: BLOB_APPEND alias: BLOB_RESULT_3A - 01: sqltype: 520 BLOB scale: 0 subtype: 1 len: 8 charset: 2 ASCII - : name: BLOB_APPEND alias: BLOB_RESULT_4 + 01: sqltype: 520 BLOB scale: 0 subtype: 1 len: 8 charset: 4 {SQL_SCHEMA_PREFIX}UTF8 + : name: BLOB_APPEND alias: BLOB_RESULT_3B - 01: sqltype: 520 BLOB Nullable scale: 0 subtype: 0 len: 8 - : name: BLOB_APPEND alias: BLOB_RESULT_5 + 01: sqltype: 520 BLOB scale: 0 subtype: 1 len: 8 charset: 2 {SQL_SCHEMA_PREFIX}ASCII + : name: BLOB_APPEND alias: BLOB_RESULT_4 - BLOB_RESULT_5 -""" + 01: sqltype: 520 BLOB Nullable scale: 0 subtype: 0 len: 8 + : name: BLOB_APPEND alias: BLOB_RESULT_5 + + BLOB_RESULT_5 + """ -@pytest.mark.version('>=4.0.3') -def test_1(act: Action): act.expected_stdout = expected_stdout act.execute(combine_output = True, charset = 'None' ) assert act.clean_stdout == act.clean_expected_stdout + \ No newline at end of file diff --git a/tests/bugs/gh_7257_misc_datatypes_test.py b/tests/bugs/gh_7257_misc_datatypes_test.py new file mode 100644 index 00000000..235e8a8c --- /dev/null +++ b/tests/bugs/gh_7257_misc_datatypes_test.py @@ -0,0 +1,172 @@ +#coding:utf-8 + +""" +ID: issue-7257 +ISSUE: 7257 +TITLE: Support for partial indices +DESCRIPTION: + Additional test to check misc datatypes in partial indices. +NOTES: + [18.01.2025] pzotov + Resultset of cursor that executes using instance of selectable PreparedStatement must be stored + in some variable in order to have ability close it EXPLICITLY (before PS will be freed). + Otherwise access violation raises during Python GC and pytest hangs at final point (does not return control to OS). + This occurs at least for: Python 3.11.2 / pytest: 7.4.4 / firebird.driver: 1.10.6 / Firebird.Qa: 0.19.3 + The reason of that was explained by Vlad, 26.10.24 17:42 ("oddities when use instances of selective statements"). + Checked on 6.0.0.409, 5.0.1.1469 + [04.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.863; 5.0.3.1668. +""" + +import pytest +from firebird.qa import * +from firebird.driver import DatabaseError +from decimal import Decimal + +db = db_factory() +act = python_act('db', substitutions = [('[ \t]+', ' '), ]) + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- + +def run_ddl_dml(act, capsys, dtype, v_chk, v_max, use_rand = True): + + with act.db.connect() as con: + cur = con.cursor() + idx_partial_name = f'test_f01_{dtype.split()[0]}_partial'.upper() + idx_common_name = f'test_f02_{dtype.split()[0]}_common'.upper() + if dtype =='date': + v_chk = 'current_date' + insert_sttm = f"insert into test(id, f01) select row_number()over(), iif(mod(row_number()over(), 100) = 0, {v_chk}, dateadd(rand()*1000 day to date '01.01.2000')) from rdb$types,rdb$types rows 1000" + elif dtype == 'time with time zone': + v_chk = "time '11:11:11.111 Indian/Cocos'" + insert_sttm = f"insert into test(id, f01) select row_number()over(), iif(mod(row_number()over(), 100) = 0, {v_chk}, time '11:11:11.111 Pacific/Fiji' ) from rdb$types,rdb$types rows 1000" + elif dtype == 'varchar(80) character set utf8': + idx_partial_name = f'test_f01_utf8_partial'.upper() + idx_common_name = f'test_f02_utf8_common'.upper() + v_chk = "'Sporvognsskinneskidtskraberkonduktørbuksebæltespændeemblempoleringsmiddelshylde€'" + v_max = "'Minoritetsladningsbærerdiffusjonskoeffisientmålingsapparatur'" + insert_sttm = f"insert into test(id, f01) select row_number()over(),iif(mod(row_number()over(), 100) = 0, {v_chk}, {v_max}) from rdb$types,rdb$types rows 1000" + elif dtype == 'varbinary(16)': + idx_partial_name = f'test_f01_vbin_partial'.upper() + idx_common_name = f'test_f02_vbin_common'.upper() + v_chk = "x'0A'" + v_max = "gen_uuid()" + insert_sttm = f"insert into test(id, f01) select row_number()over(),iif(mod(row_number()over(), 100) = 0, {v_chk}, {v_max}) from rdb$types,rdb$types rows 1000" + elif dtype == 'boolean': + v_chk = "false" + v_max = "true" + insert_sttm = f"insert into test(id, f01) select row_number()over(),iif(mod(row_number()over(), 100) = 0, {v_chk}, {v_max}) from rdb$types,rdb$types rows 1000" + else: + insert_sttm = f"insert into test(id, f01) select row_number()over(), iif(mod(row_number()over(), 100) = 0, cast({v_chk} as {dtype}), cast(rand()*{v_max} as {dtype})) from rdb$types,rdb$types rows 1000" + ddl = f""" + recreate table test(id int primary key, f01 {dtype}, f02 {dtype}) ^ + {insert_sttm} ^ + update test set f02 = f01 ^ + create index {idx_partial_name} on test computed by (f01) where f01 = {v_chk} ^ + create index {idx_common_name} on test(f02) ^ + set statistics index {idx_partial_name} ^ + set statistics index {idx_common_name} ^ + """ + + dml = f""" + select count(*) from test where f01 = {v_chk} ^ + select count(*) from test where f02 = {v_chk} ^ + """ + + for x in [p for p in ddl.split('^') if p.strip()]: + if x.startswith('--'): + pass + else: + con.execute_immediate(x) + con.commit() + + for x in [p for p in dml.split('^') if p.strip()]: + ps, rs = None, None + try: + ps = cur.prepare(x) + for s in ps.detailed_plan.split('\n'): + print( replace_leading(s) ) + + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + rs = cur.execute(ps) + cur_cols = cur.description + for r in rs: + for i in range(0,len(cur_cols)): + print( cur_cols[i][0], ':', r[i] ) + con.commit() + except DatabaseError as e: + print(e.__str__()) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() + + expected_stdout_5x = f""" + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Table "TEST" Access By ID + ................-> Bitmap + ....................-> Index "{idx_partial_name}" Full Scan + COUNT : 10 + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Table "TEST" Access By ID + ................-> Bitmap + ....................-> Index "{idx_common_name}" Range Scan (full match) + COUNT : 10 + """ + + expected_stdout_6x = f""" + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Table "PUBLIC"."TEST" Access By ID + ................-> Bitmap + ....................-> Index "PUBLIC"."{idx_partial_name}" Full Scan + COUNT : 10 + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Table "PUBLIC"."TEST" Access By ID + ................-> Bitmap + ....................-> Index "PUBLIC"."{idx_common_name}" Range Scan (full match) + COUNT : 10 + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() + +#----------------------------------------------------------- + +@pytest.mark.version('>=5.0') +def test_1(act: Action, capsys): + + run_ddl_dml(act, capsys, 'smallint', -32768, 32767, True) + run_ddl_dml(act, capsys, 'bigint', -9223372036854775808, 9223372036854775807, True) + run_ddl_dml(act, capsys, 'double precision', -2.2250738585072014e-308, 1.7976931348623158e+308, True) + run_ddl_dml(act, capsys, 'int128', Decimal(-170141183460469231731687303715884105728), Decimal(170141183460469231731687303715884105727), True) + run_ddl_dml(act, capsys, 'decfloat', 0, Decimal(1.7976931348623158e+308), True) + run_ddl_dml(act, capsys, 'date', None, None) + run_ddl_dml(act, capsys, 'time with time zone', None, None) + run_ddl_dml(act, capsys, 'varchar(80) character set utf8', None, None) + run_ddl_dml(act, capsys, 'varbinary(16)', None, None) + run_ddl_dml(act, capsys, 'boolean', None, None) diff --git a/tests/bugs/gh_7257_test.py b/tests/bugs/gh_7257_test.py index 11b110d4..9443cab9 100644 --- a/tests/bugs/gh_7257_test.py +++ b/tests/bugs/gh_7257_test.py @@ -5,11 +5,16 @@ ISSUE: 7257 TITLE: Support for partial indices NOTES: - Initial discussion: https://github.com/FirebirdSQL/firebird/issues/3750 - Checked on 5.0.0.957 (intermediate build). - NB. Currently this test contains only trivial cases for check. - More complex examples, including misc datatypes (non-ascii, decfloat and int128), - will be added later. + [18.01.2025] pzotov + Initial discussion: https://github.com/FirebirdSQL/firebird/issues/3750 + Checked on 5.0.0.957 (intermediate build). + NB. Currently this test contains only trivial cases for check. + More complex examples, including misc datatypes (non-ascii, decfloat and int128), + will be added later. + [04.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.863; 5.0.3.1668. """ import pytest @@ -102,37 +107,58 @@ """ -act = isql_act('db', test_script) - -expected_stdout = """ - PLAN (TEST NATURAL) - COUNT 4 - - PLAN (TEST NATURAL) - COUNT 3 - - PLAN (TEST INDEX (TEST_F01)) - COUNT 196 - - PLAN (TEST INDEX (TEST_F02)) - COUNT 195 - - - PLAN (TEST ORDER TEST_COMPUTED_ASC) - COUNT 190 - - PLAN (TEST ORDER TEST_COMPUTED_DEC) - COUNT 190 - - PLAN (TEST INDEX (TEST_COMPUTED_ASC)) - COUNT 190 - - PLAN (TEST INDEX (TEST_COMPUTED_DEC)) - COUNT 190 -""" +substitutions = [('[ \t]+', ' ')] +act = isql_act('db', test_script, substitutions = substitutions) @pytest.mark.version('>=5.0') def test_1(act: Action): - act.expected_stdout = expected_stdout + + expected_stdout_5x = """ + PLAN (TEST NATURAL) + COUNT 4 + + PLAN (TEST NATURAL) + COUNT 3 + + PLAN (TEST INDEX (TEST_F01)) + COUNT 196 + + PLAN (TEST INDEX (TEST_F02)) + COUNT 195 + + + PLAN (TEST ORDER TEST_COMPUTED_ASC) + COUNT 190 + + PLAN (TEST ORDER TEST_COMPUTED_DEC) + COUNT 190 + + PLAN (TEST INDEX (TEST_COMPUTED_ASC)) + COUNT 190 + + PLAN (TEST INDEX (TEST_COMPUTED_DEC)) + COUNT 190 + """ + + expected_stdout_6x = """ + PLAN ("PUBLIC"."TEST" NATURAL) + COUNT 4 + PLAN ("PUBLIC"."TEST" NATURAL) + COUNT 3 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_F01")) + COUNT 196 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_F02")) + COUNT 195 + PLAN ("PUBLIC"."TEST" ORDER "PUBLIC"."TEST_COMPUTED_ASC") + COUNT 190 + PLAN ("PUBLIC"."TEST" ORDER "PUBLIC"."TEST_COMPUTED_DEC") + COUNT 190 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_COMPUTED_ASC")) + COUNT 190 + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_COMPUTED_DEC")) + COUNT 190 + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7269_test.py b/tests/bugs/gh_7269_test.py new file mode 100644 index 00000000..c1aee6b0 --- /dev/null +++ b/tests/bugs/gh_7269_test.py @@ -0,0 +1,88 @@ +#coding:utf-8 + +""" +ID: issue-7269 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/7269 +TITLE: Database restore must make every effort on activating deferred indexes +DESCRIPTION: + Test uses unrecoverable .fbk that was provided in the ticket and tries to restore it using '-verbose' option. + After restore finish, we check its log. It must contain SEVERAL errors related to indices (PK and two FK), + and also it must have messages about FINAL point of restore (regardless error that follows after this): + gbak:finishing, closing, and going home + gbak:adjusting the ONLINE and FORCED WRITES flags +NOTES: + [02.11.2024] pzotov + Checked on 5.0.2.1551, 6.0.0.415. + [04.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.863; 5.0.3.1668. +""" +import subprocess +from pathlib import Path +import zipfile +import locale +import re +import pytest +from firebird.qa import * +from firebird.driver import SrvRestoreFlag + +db = db_factory() +act = python_act('db') +tmp_fbk = temp_file('gh_7269.tmp.fbk') +tmp_fdb = temp_file('gh_7269.tmp.fdb') + +@pytest.mark.version('>=5.0.2') +def test_1(act: Action, tmp_fbk: Path, tmp_fdb: Path, capsys): + zipped_fbk_file = zipfile.Path(act.files_dir / 'gh_7269.zip', at = 'gh-7269-unrecoverable.fbk') + tmp_fbk.write_bytes(zipped_fbk_file.read_bytes()) + + allowed_patterns = \ + ( + r'gbak:(\s+)?ERROR(:)?' + ,r'gbak:(\s+)?finishing, closing, and going home' + ,r'gbak:(\s+)?adjusting the ONLINE and FORCED WRITES flags' + ) + allowed_patterns = [ re.compile(p, re.IGNORECASE) for p in allowed_patterns ] + + act.gbak(switches = ['-rep', '-v', str(tmp_fbk), str(tmp_fdb)], combine_output = True, io_enc = locale.getpreferredencoding()) + + for line in act.stdout.splitlines(): + if act.match_any(line.strip(), allowed_patterns): + print(line) + + expected_stdout_5x = """ + gbak: ERROR:violation of PRIMARY or UNIQUE KEY constraint "PK_A3" on table "A3" + gbak: ERROR: Problematic key value is ("ID" = 9) + gbak: ERROR:violation of PRIMARY or UNIQUE KEY constraint "PK_A1" on table "A1" + gbak: ERROR: Problematic key value is ("ID" = 5) + gbak: ERROR:Cannot create foreign key constraint FK_A1. Partner index does not exist or is inactive. + gbak: ERROR:violation of FOREIGN KEY constraint "FK_A2" on table "B2" + gbak: ERROR: Foreign key reference target does not exist + gbak: ERROR: Problematic key value is ("A2_ID" = 5) + gbak: ERROR:Cannot create foreign key constraint FK_A3. Partner index does not exist or is inactive. + gbak:finishing, closing, and going home + gbak:adjusting the ONLINE and FORCED WRITES flags + gbak: ERROR:Database is not online due to failure to activate one or more indices. + gbak: ERROR: Run gfix -online to bring database online without active indices. + """ + + expected_stdout_6x = """ + gbak: ERROR:violation of PRIMARY or UNIQUE KEY constraint "PK_A3" on table "PUBLIC"."A3" + gbak: ERROR: Problematic key value is ("ID" = 9) + gbak: ERROR:violation of PRIMARY or UNIQUE KEY constraint "PK_A1" on table "PUBLIC"."A1" + gbak: ERROR: Problematic key value is ("ID" = 5) + gbak: ERROR:Cannot create foreign key constraint "FK_A1". Partner index does not exist or is inactive. + gbak: ERROR:violation of FOREIGN KEY constraint "FK_A2" on table "PUBLIC"."B2" + gbak: ERROR: Foreign key reference target does not exist + gbak: ERROR: Problematic key value is ("A2_ID" = 5) + gbak: ERROR:Cannot create foreign key constraint "FK_A3". Partner index does not exist or is inactive. + gbak:finishing, closing, and going home + gbak:adjusting the ONLINE and FORCED WRITES flags + gbak: ERROR:Database is not online due to failure to activate one or more indices. + gbak: ERROR: Run gfix -online to bring database online without active indices. + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7288_test.py b/tests/bugs/gh_7288_test.py index 18a89dfd..d4546021 100644 --- a/tests/bugs/gh_7288_test.py +++ b/tests/bugs/gh_7288_test.py @@ -9,16 +9,23 @@ [20.02.2023] pzotov Confirmed crahses on 5.0.0.698 Checked on 5.0.0.733 -- all fine. + + [24.06.2025] pzotov + Fixed wrong value of charset that was used to connect: "utf-8". This caused crash of isql in recent 6.x. + https://github.com/FirebirdSQL/firebird/commit/5b41342b169e0d79d63b8d2fdbc033061323fa1b + Thanks to Vlad for solved problem. """ import pytest from firebird.qa import * from pathlib import Path db = db_factory(do_not_create=True, do_not_drop = True) -act = python_act('db') -db_tmp = temp_file('gh_7288.tmp.fdb') # db_factory(filename='tmp_core_7288.fdb', do_not_create=True, do_not_drop = True) -#tmp_file = temp_file('gh_7288.tmp.sql') +substitutions = [('[ \t]+', ' ')] + +act = python_act('db', substitutions = substitutions) + +db_tmp = temp_file('gh_7288.tmp.fdb') @pytest.mark.version('>=3.0.11') def test_1(act: Action, db_tmp: Path): @@ -39,6 +46,6 @@ def test_1(act: Action, db_tmp: Path): drop database; """ - expected_stdout = "COUNT 1" - act.isql(switches=['-q'], input = chk_sql, charset='utf-8', io_enc='utf-8', connect_db = False, credentials = False, combine_output = True) + expected_stdout = "COUNT 1" + act.isql(switches=['-q'], input = chk_sql, charset='utf8', io_enc='utf8', connect_db = False, credentials = False, combine_output = True) assert act.clean_stdout == expected_stdout diff --git a/tests/bugs/gh_7304_test.py b/tests/bugs/gh_7304_test.py new file mode 100644 index 00000000..4d61ce8f --- /dev/null +++ b/tests/bugs/gh_7304_test.py @@ -0,0 +1,293 @@ +#coding:utf-8 + +""" +ID: issue-7304 +ISSUE: 7304 +TITLE: Events in system attachments (like garbage collector) are not traced +DESCRIPTION: + Test changes sweep interval to some low value (see SWEEP_GAP) and runs TX_COUNT transactions which + lead difference between OST and OIT to exceed given sweep interval. These transactions are performed + by ISQL which is launched as child process. SQL script uses table with record that is locked at the + beginning of script and execute block with loop of TX_COUNT statements which insert new records. + After this loop finish, we make ISQL to hang by forcing it to update first record (see LOCKED_ROW). + Then we change DB state to full shutdown and wait until ISQL will be terminated. + At this point database has sweep gap that is enough to run auto sweep at first connection to DB. + Finally, we bring DB online and start trace with log_sweep = true and log_transactions = true. + Doing connection and wait about 2..3 seconds cause auto sweep to be started and completed. + This must be reflected in the trace. + + If ServerMode = 'Super' and ParallelWorkers >= 2 and MaxParallelWorkers >= ParallelWorkers + then trace log will contain folloing five lines related to worker(s) activity: + (...) START_TRANSACTION + (ATT_..., , NONE, ) --------------------- [ 1 ] + (TRA_..., READ_COMMITTED | REC_VERSION | WAIT | READ_ONLY) + (...) COMMIT_TRANSACTION + (ATT_..., , NONE, ) --------------------- [ 2 ] + + This is the only difference that can be observed for snapshots before and after fix + (i.e. BEFORE fix trace had no such lines but all other data about sweep *did* present). + Test checks that trace log contains TWO lines with '', see above [ 1 ] and [ 2 ]. + +JIRA: CORE-2668 +FBTEST: bugs.core_2668 +NOTES: + [07.11.2024] pzotov + Confirmed absense of lines marked as "" in the trace log for snapshot 5.0.0.731 (15.09.2022). + Checked on 5.0.0.733 (16.09.2022); 5.0.2.1553, 6.0.0.515 + + [18.01.2025] pzotov + ### CRITICAL ISSUE ### PROBABLY MUST BE APPLIED TO ALL TESTS WITH SIMILAR BEHAVOUR ### + + Resultset of cursor that executes using instance of selectable PreparedStatement must be stored + in some variable in order to have ability close it EXPLICITLY (before PS will be freed). + Otherwise access violation raises. + This occurs at least for: Python 3.11.2 / pytest: 7.4.4 / firebird.driver: 1.10.6 / Firebird.Qa: 0.19.3 + The reason of that was explained by Vlad, letter 26.10.24 17:42 + (subject: "oddities when use instances of selective statements"): + * line 'cur1.execute(ps1)' creates a new cursor but looses reference on it; + * but this cursor is linked with instance of ps1 which *has* reference on that cursor; + * call 'ps1.free()' delete this anonimous cursor but Python runtime + (or - maybe - code that makes connection cleanup) does not know about it + and tries to delete this anon cursor AGAIN when code finishes 'with' block. + This attempt causes AV. + + [02.03.2025] pzotov + Active trace session must present before DB state will be changed on online: call to srv.database.bring_online() + causes sweep itself, no need to establish one more connection. This change must fix unstable results on Linux. + Checked again on 5.0.0.731 (15.09.2022) and 5.0.0.733 (16.09.2022) - Windows; 6.0.0.656 (Linux) - Linux. +""" + +import time +import subprocess +from datetime import datetime as dt +import re +from pathlib import Path +from difflib import unified_diff +from firebird.driver import DatabaseError, tpb, Isolation, DbWriteMode, ShutdownMode, ShutdownMethod + +import pytest +from firebird.qa import * + +db = db_factory() +act = python_act('db', substitutions = [('\\(ATT_\\d+', '(ATT_N')]) + +################ +### SETTINGS ### +################ +SWEEP_GAP = 100 +TX_COUNT = 150 +#TX_COUNT = 5000 +LOCKED_ROW = -1 +MAX_WAIT_FOR_ISQL_PID_APPEARS_MS = 5000 +WATCH_FOR_PTN = re.compile( r'\(ATT_\d+,\s+,\s+NONE,\s+\)', re.IGNORECASE) +################ + +tmp_sql = temp_file('tmp_2668.sql') +tmp_log = temp_file('tmp_2668.log') + +@pytest.mark.trace +@pytest.mark.version('>=5.0.0') +def test_1(act: Action, tmp_sql: Path, tmp_log: Path, capsys): + + if act.vars['server-arch'] != 'SuperServer': + pytest.skip("Applies only to SuperServer") + + with act.db.connect() as con: + cur = con.cursor() + sql = """ + select + cast(max(iif(g.rdb$config_name = 'ParallelWorkers', g.rdb$config_value, null)) as int) as cfg_par_workers + ,cast(max(iif(g.rdb$config_name = 'MaxParallelWorkers', g.rdb$config_value, null)) as int) as cfg_max_par_workers + from rdb$database + left join rdb$config g on g.rdb$config_name in ('ParallelWorkers', 'MaxParallelWorkers') + """ + cur.execute(sql) + cfg_par_workers, cfg_max_par_workers = cur.fetchone() + + assert cfg_par_workers >=2 and cfg_max_par_workers >= cfg_par_workers, "Server must be configured for parallel work. Check values of ParallelWorkers and MaxParallelWorkers" + + test_script = f""" + set echo on; + set bail on; + connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; + + recreate table test(id int primary key, s varchar(2000) unique); + insert into test(id) values({LOCKED_ROW}); + insert into test(id, s) select row_number()over(), lpad('', 2000, uuid_to_char(gen_uuid())) from rdb$types rows {TX_COUNT}; + commit; + + set transaction read committed WAIT; + + update test set id = id where id = {LOCKED_ROW}; + set term ^; + execute block as + declare n int = {TX_COUNT}; + declare v_role varchar(31); + begin + while (n > 0) do + begin + in autonomous transaction do + delete from test where id = :n; + n = n - 1; + --insert into test(id) values(:n) + -- returning :n-1 into n; + end + + v_role = left(replace( uuid_to_char(gen_uuid()), '-', ''), 31); + + begin + execute statement ('update test /* ' || ascii_char(65) || ' */ set id = id where id = ?') ({LOCKED_ROW}) + on external + 'localhost:' || rdb$get_context('SYSTEM', 'DB_NAME') + as user '{act.db.user}' password '{act.db.password}' role v_role + with autonomous transaction; + when any do + begin + end + end + + end + ^ + set term ;^ + set heading off; + select '-- shutdown me now --' from rdb$database; + """ + + tmp_sql.write_text(test_script) + with act.connect_server() as srv: + ############################## + ### reduce SWEEEP interval ### + ############################## + srv.database.set_sweep_interval(database = act.db.db_path, interval = SWEEP_GAP) + srv.database.set_write_mode(database = act.db.db_path, mode = DbWriteMode.SYNC) + + with open(tmp_log,'w') as f_log: + p_work_sql = subprocess.Popen([act.vars['isql'], '-q', '-i', str(tmp_sql)], stdout = f_log, stderr = subprocess.STDOUT) + + chk_mon_sql = """ + select 1 + from mon$attachments a + join mon$statements s + using (mon$attachment_id) + where + a.mon$attachment_id <> current_connection + and cast(s.mon$sql_text as varchar(8192)) containing '/* A */' + """ + + found_in_mon_tables = False + with act.db.connect() as con_watcher: + + custom_tpb = tpb(isolation = Isolation.SNAPSHOT, lock_timeout = -1) + tx_watcher = con_watcher.transaction_manager(custom_tpb) + cur_watcher = tx_watcher.cursor() + + ps = cur_watcher.prepare(chk_mon_sql) + + i = 0 + da = dt.now() + while True: + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute(ps) in order to close it + # *explicitly* otherwise AV can occur when Python collects garbage. + # Explained by hvlad, email 26.10.24 17:42 + rs = cur_watcher.execute(ps) + mon_result = -1 + for r in cur_watcher: + mon_result = r[0] + rs.close() # <<< EXPLICIT CLOSE RESULT OF CURSOR. + + tx_watcher.commit() + db = dt.now() + diff_ms = (db-da).seconds*1000 + (db-da).microseconds//1000 + if mon_result == 1: + found_in_mon_tables = True + break + elif diff_ms > MAX_WAIT_FOR_ISQL_PID_APPEARS_MS: + break + + time.sleep(0.1) + + ps.free() + + assert found_in_mon_tables, f'Could not find attachment in mon$ tables for {MAX_WAIT_FOR_ISQL_PID_APPEARS_MS} ms.' + + try: + ############################################# + ### f u l l s h u t d o w n D B ### + ############################################# + srv.database.shutdown(database=act.db.db_path, mode=ShutdownMode.FULL, + method=ShutdownMethod.FORCED, timeout=0) + finally: + p_work_sql.terminate() + # < with open(tmp_log,'w') as f_log + + trace_options = \ + [ + 'time_threshold = 0' + ,'log_initfini = false' + ,'log_connections = true' + ,'log_transactions = true' + ,'log_errors = true' + ,'log_sweep = true' + ] + + act.trace_log.clear() + + #with act.trace(db_events = trace_options, encoding='utf8', encoding_errors='utf8'), \ + # act.connect_server() as srv: + # # ################################ + # # This will cause AUTOSWEEP start: + # # ################################ + # srv.database.bring_online(database=act.db.db_path) + + fblog_1 = act.get_firebird_log() + with act.trace(db_events = trace_options, encoding='utf8', encoding_errors='utf8'): + # ################################ + # This will cause AUTOSWEEP start: + # ################################ + #srv.database.bring_online(database=act.db.db_path) + act.gfix(switches=['-online', act.db.dsn]) + + time.sleep(1) # Allow content of firebird log be fully flushed on disk. + fblog_2 = act.get_firebird_log() + + num_found = 0 + out_lst = [] + for line in act.trace_log: + if WATCH_FOR_PTN.search(line): + out_lst.append( WATCH_FOR_PTN.search(line).group() ) + num_found += 1 + + if num_found == 2: + print( '\n'.join( out_lst ) ) + else: + print(f'ERROR: pattern "{WATCH_FOR_PTN}" was not found in any line of trace.') + print('\nCheck trace log:') + for line in act.trace_log: + if (s := line.strip() ): + print(s) + + + # Sweep is started by SWEEPER + # Database "C:\TEMP\PYTEST\TEST_10\TEST.FDB" + # OIT 8, OAT 167, OST 167, Next 168 + # Sweep is finished + # Database "C:\TEMP\PYTEST\TEST_10\TEST.FDB" + # 2 workers, time 0.047 sec + # OIT 168, OAT 169, OST 169, Next 170 + + fb_log_diff_patterns = [r'Sweep(\s+is)?\s+(started|finished)', r'Database\s+', r'OIT\s+\d+,\s+OAT\s+\d+,\s+OST\s+\d+,\s+Next\s+\d+', r'\d+ worker(s)?,\s+time'] + fb_log_diff_patterns = [re.compile(s, re.IGNORECASE) for s in fb_log_diff_patterns] + print('\nCheck diff in firebird.log:') + for line in unified_diff(fblog_1, fblog_2): + if line.startswith('+'): + if act.match_any(line, fb_log_diff_patterns): + print(line.split('+')[-1]) + + + act.expected_stdout = """ + (ATT_N, , NONE, ) + (ATT_N, , NONE, ) + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7331_test.py b/tests/bugs/gh_7331_test.py index 55dd971e..00fc33b3 100644 --- a/tests/bugs/gh_7331_test.py +++ b/tests/bugs/gh_7331_test.py @@ -6,13 +6,16 @@ TITLE: Cost-based choice between nested loop join and hash join NOTES: [20.02.2023] pzotov - Confirmed difference between snapshots before and after commit - https://github.com/FirebirdSQL/firebird/commit/99c9f63f874d74beb53d338c97c033fe7c8d71a9 - Checked on 5.0.0.763 (plan did not use hash join); 5.0.0.957 (plan uses HJ). - + Confirmed difference between snapshots before and after commit + https://github.com/FirebirdSQL/firebird/commit/99c9f63f874d74beb53d338c97c033fe7c8d71a9 + Checked on 5.0.0.763 (plan did not use hash join); 5.0.0.957 (plan uses HJ). [12.09.2023] pzotov - Adjusted plan for query #2 after letter from dimitr, 11-sep-2023 20:23. - Checked on 5.0.0.1204 + Adjusted plan for query #2 after letter from dimitr, 11-sep-2023 20:23. + Checked on 5.0.0.1204 + [04.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.909; 5.0.3.1668. """ import pytest @@ -112,13 +115,19 @@ act = isql_act('db', test_script) -expected_stdout = """ - PLAN HASH (LINEITEM INDEX (LINEITEM_SHIPDATE), ORDERS NATURAL) - PLAN HASH (JOIN (CUSTOMER NATURAL, ORDERS INDEX (ORDERS_CUSTKEY_FK)), LINEITEM INDEX (LINEITEM_SHIPDATE)) -""" - @pytest.mark.version('>=5.0') def test_1(act: Action): - act.expected_stdout = expected_stdout + + expected_stdout_5x = """ + PLAN HASH (LINEITEM INDEX (LINEITEM_SHIPDATE), ORDERS NATURAL) + PLAN HASH (JOIN (CUSTOMER NATURAL, ORDERS INDEX (ORDERS_CUSTKEY_FK)), LINEITEM INDEX (LINEITEM_SHIPDATE)) + """ + + expected_stdout_6x = """ + PLAN HASH ("PUBLIC"."LINEITEM" INDEX ("PUBLIC"."LINEITEM_SHIPDATE"), "PUBLIC"."ORDERS" NATURAL) + PLAN HASH (JOIN ("PUBLIC"."CUSTOMER" NATURAL, "PUBLIC"."ORDERS" INDEX ("PUBLIC"."ORDERS_CUSTKEY_FK")), "PUBLIC"."LINEITEM" INDEX ("PUBLIC"."LINEITEM_SHIPDATE")) + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7350_test.py b/tests/bugs/gh_7350_test.py index e474e673..d09b126b 100644 --- a/tests/bugs/gh_7350_test.py +++ b/tests/bugs/gh_7350_test.py @@ -13,29 +13,21 @@ All kinds of transaction isolation levels are involved: TABLE STABILITY; SNAPSHOT; RC READ_CONSISTENCY; RC REC_VER and RC NO_REC_VER. NOTES: [22.11.2023] pzotov - 1. Parameter ReadConsistency in firebird.conf must be set to 0, i.e. NOT-default value. - 2. TIL = "RC NO_record_version" can be used to check feature since gh-7811 was fixed (20.11.2023 in master) - 3. Only persistent table is checked. For session-level GTT (using two transactions for single attach) - it is unable to run 'select ... with lock' or 'update/delete ... skip locked': exception raises with message - "SQLSTATE = HY000 / Cannot select temporary table ... WITH LOCK" - Discussed with dimitr and hvlad, 13.09.2023 - 4. Added WAIT mode to be checked after fixed https://github.com/FirebirdSQL/firebird/issues/7700 - ( https://github.com/FirebirdSQL/firebird/commit/5b14baa37b6ee214cd8ccc21f2e99dce119fe60e ) - NOTE: before fix gh-7700, following statement hanged: - set transaction read committed record_version WAIT; - select id from test order by id with lock skip locked - 5. Here we check only 'SELECT ... WITH LOCK' behavour. Results of UPDATE and DELETE are checked in gh_7810_test.py - - [25.11.2023] pzotov - Writing code requires more care since 6.0.0.150: ISQL does not allow to specify THE SAME terminator twice, - i.e. - set term @; select 1 from rdb$database @ set term @; - will not compile ("Unexpected end of command" raises). - - Checked on 6.0.0.137 (SS/CS), 5.0.0.1274 (SS/CS). - - [27.11.2023] pzotov - Removed branched for 5.x vs 6.x, code now the same for 5.x and 6.x - Checked on 5.0.0.1280 (intermediate build 27.11.2023, after backporting, commit dc2d85c17b41fb6c378bffc0896338c4f8856998). + 1. Parameter ReadConsistency in firebird.conf must be set to 0, i.e. NOT-default value. + 2. TIL = "RC NO_record_version" can be used to check feature since gh-7811 was fixed (20.11.2023 in master) + 3. Only persistent table is checked. For session-level GTT (using two transactions for single attach) + it is unable to run 'select ... with lock' or 'update/delete ... skip locked': exception raises with message + "SQLSTATE = HY000 / Cannot select temporary table ... WITH LOCK" + Discussed with dimitr and hvlad, 13.09.2023 + 4. Added WAIT mode to be checked after fixed https://github.com/FirebirdSQL/firebird/issues/7700 + ( https://github.com/FirebirdSQL/firebird/commit/5b14baa37b6ee214cd8ccc21f2e99dce119fe60e ) + NOTE: before fix gh-7700, following statement hanged: + set transaction read committed record_version WAIT; + select id from test order by id with lock skip locked + 5. Here we check only 'SELECT ... WITH LOCK' behavour. Results of UPDATE and DELETE are checked in gh_7810_test.py + [05.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' and variables to be substituted in expected_* on FB 6.x + Checked on 6.0.0.909; 5.0.3.1668. """ import pytest @@ -44,410 +36,17 @@ import time db = db_factory() -substitutions = [ ('transaction number is \\d+', 'transaction number is'), - ("-At procedure 'SP_GET_UNLOCKED_ROWS_LOCAL' line.*", "-At procedure 'SP_GET_UNLOCKED_ROWS_LOCAL'"), - ("-At procedure 'SP_GET_UNLOCKED_ROWS_REMOTE' line.*", "-At procedure 'SP_GET_UNLOCKED_ROWS_REMOTE'"), +substitutions = [ (r'line(:)?\s+\d+.*', ''), + ('transaction number is \\d+', 'transaction number is'), + #("-At procedure 'SP_GET_UNLOCKED_ROWS_LOCAL' line.*", "-At procedure 'SP_GET_UNLOCKED_ROWS_LOCAL'"), + #("-At procedure 'SP_GET_UNLOCKED_ROWS_REMOTE' line.*", "-At procedure 'SP_GET_UNLOCKED_ROWS_REMOTE'"), ('Data source : Firebird::localhost:.*', 'Data source : Firebird::localhost:') ] act = python_act('db', substitutions = substitutions) CHECK_SQL = 'select id from test order by id with lock skip locked' -expected_stdout = f""" - QUERY_TYPE = DSQL, TIL = SERIALIZABLE, ACCESS = READ, WAIT = NO_WAIT: - lock conflict on no wait transaction - -Acquire lock for relation (TEST) failed - -901 - 335544345 - 335544382 - QUERY_TYPE = DSQL, TIL = SNAPSHOT, ACCESS = READ, WAIT = NO_WAIT: - attempted update during read-only transaction - -817 - 335544361 - QUERY_TYPE = DSQL, TIL = SNAPSHOT, ACCESS = READ, WAIT = WAIT: - attempted update during read-only transaction - -817 - 335544361 - QUERY_TYPE = DSQL, TIL = READ_COMMITTED_READ_CONSISTENCY, ACCESS = READ, WAIT = NO_WAIT: - attempted update during read-only transaction - -817 - 335544361 - QUERY_TYPE = DSQL, TIL = READ_COMMITTED_READ_CONSISTENCY, ACCESS = READ, WAIT = WAIT: - attempted update during read-only transaction - -817 - 335544361 - QUERY_TYPE = DSQL, TIL = READ_COMMITTED_RECORD_VERSION, ACCESS = READ, WAIT = NO_WAIT: - attempted update during read-only transaction - -817 - 335544361 - QUERY_TYPE = DSQL, TIL = READ_COMMITTED_RECORD_VERSION, ACCESS = READ, WAIT = WAIT: - attempted update during read-only transaction - -817 - 335544361 - QUERY_TYPE = DSQL, TIL = READ_COMMITTED_NO_RECORD_VERSION, ACCESS = READ, WAIT = NO_WAIT: - attempted update during read-only transaction - -817 - 335544361 - QUERY_TYPE = DSQL, TIL = READ_COMMITTED_NO_RECORD_VERSION, ACCESS = READ, WAIT = WAIT: - attempted update during read-only transaction - -817 - 335544361 - QUERY_TYPE = DSQL, TIL = SERIALIZABLE, ACCESS = WRITE, WAIT = NO_WAIT: - lock conflict on no wait transaction - -Acquire lock for relation (TEST) failed - -901 - 335544345 - 335544382 - QUERY_TYPE = DSQL, TIL = SNAPSHOT, ACCESS = WRITE, WAIT = NO_WAIT: - ID=2 - ID=3 - ID=4 - ID=6 - ID=7 - ID=8 - ID=10 - QUERY_TYPE = DSQL, TIL = SNAPSHOT, ACCESS = WRITE, WAIT = WAIT: - ID=2 - ID=3 - ID=4 - ID=6 - ID=7 - ID=8 - ID=10 - QUERY_TYPE = DSQL, TIL = READ_COMMITTED_READ_CONSISTENCY, ACCESS = WRITE, WAIT = NO_WAIT: - ID=2 - ID=3 - ID=4 - ID=6 - ID=7 - ID=8 - ID=10 - QUERY_TYPE = DSQL, TIL = READ_COMMITTED_READ_CONSISTENCY, ACCESS = WRITE, WAIT = WAIT: - ID=2 - ID=3 - ID=4 - ID=6 - ID=7 - ID=8 - ID=10 - QUERY_TYPE = DSQL, TIL = READ_COMMITTED_RECORD_VERSION, ACCESS = WRITE, WAIT = NO_WAIT: - ID=2 - ID=3 - ID=4 - ID=6 - ID=7 - ID=8 - ID=10 - QUERY_TYPE = DSQL, TIL = READ_COMMITTED_RECORD_VERSION, ACCESS = WRITE, WAIT = WAIT: - ID=2 - ID=3 - ID=4 - ID=6 - ID=7 - ID=8 - ID=10 - QUERY_TYPE = DSQL, TIL = READ_COMMITTED_NO_RECORD_VERSION, ACCESS = WRITE, WAIT = NO_WAIT: - ID=2 - ID=3 - ID=4 - ID=6 - ID=7 - ID=8 - ID=10 - QUERY_TYPE = DSQL, TIL = READ_COMMITTED_NO_RECORD_VERSION, ACCESS = WRITE, WAIT = WAIT: - ID=2 - ID=3 - ID=4 - ID=6 - ID=7 - ID=8 - ID=10 - QUERY_TYPE = PSQL_LOCAL, TIL = SERIALIZABLE, ACCESS = READ, WAIT = NO_WAIT: - lock conflict on no wait transaction - -Acquire lock for relation (TEST) failed - -At procedure 'SP_GET_UNLOCKED_ROWS_LOCAL' - -901 - 335544345 - 335544382 - 335544842 - QUERY_TYPE = PSQL_LOCAL, TIL = SNAPSHOT, ACCESS = READ, WAIT = NO_WAIT: - attempted update during read-only transaction - -At procedure 'SP_GET_UNLOCKED_ROWS_LOCAL' - -817 - 335544361 - 335544842 - QUERY_TYPE = PSQL_LOCAL, TIL = SNAPSHOT, ACCESS = READ, WAIT = WAIT: - attempted update during read-only transaction - -At procedure 'SP_GET_UNLOCKED_ROWS_LOCAL' - -817 - 335544361 - 335544842 - QUERY_TYPE = PSQL_LOCAL, TIL = READ_COMMITTED_READ_CONSISTENCY, ACCESS = READ, WAIT = NO_WAIT: - attempted update during read-only transaction - -At procedure 'SP_GET_UNLOCKED_ROWS_LOCAL' - -817 - 335544361 - 335544842 - QUERY_TYPE = PSQL_LOCAL, TIL = READ_COMMITTED_READ_CONSISTENCY, ACCESS = READ, WAIT = WAIT: - attempted update during read-only transaction - -At procedure 'SP_GET_UNLOCKED_ROWS_LOCAL' - -817 - 335544361 - 335544842 - QUERY_TYPE = PSQL_LOCAL, TIL = READ_COMMITTED_RECORD_VERSION, ACCESS = READ, WAIT = NO_WAIT: - attempted update during read-only transaction - -At procedure 'SP_GET_UNLOCKED_ROWS_LOCAL' - -817 - 335544361 - 335544842 - QUERY_TYPE = PSQL_LOCAL, TIL = READ_COMMITTED_RECORD_VERSION, ACCESS = READ, WAIT = WAIT: - attempted update during read-only transaction - -At procedure 'SP_GET_UNLOCKED_ROWS_LOCAL' - -817 - 335544361 - 335544842 - QUERY_TYPE = PSQL_LOCAL, TIL = READ_COMMITTED_NO_RECORD_VERSION, ACCESS = READ, WAIT = NO_WAIT: - attempted update during read-only transaction - -At procedure 'SP_GET_UNLOCKED_ROWS_LOCAL' - -817 - 335544361 - 335544842 - QUERY_TYPE = PSQL_LOCAL, TIL = READ_COMMITTED_NO_RECORD_VERSION, ACCESS = READ, WAIT = WAIT: - attempted update during read-only transaction - -At procedure 'SP_GET_UNLOCKED_ROWS_LOCAL' - -817 - 335544361 - 335544842 - QUERY_TYPE = PSQL_LOCAL, TIL = SERIALIZABLE, ACCESS = WRITE, WAIT = NO_WAIT: - lock conflict on no wait transaction - -Acquire lock for relation (TEST) failed - -At procedure 'SP_GET_UNLOCKED_ROWS_LOCAL' - -901 - 335544345 - 335544382 - 335544842 - QUERY_TYPE = PSQL_LOCAL, TIL = SNAPSHOT, ACCESS = WRITE, WAIT = NO_WAIT: - ID=2 - ID=3 - ID=4 - ID=6 - ID=7 - ID=8 - ID=10 - QUERY_TYPE = PSQL_LOCAL, TIL = SNAPSHOT, ACCESS = WRITE, WAIT = WAIT: - ID=2 - ID=3 - ID=4 - ID=6 - ID=7 - ID=8 - ID=10 - QUERY_TYPE = PSQL_LOCAL, TIL = READ_COMMITTED_READ_CONSISTENCY, ACCESS = WRITE, WAIT = NO_WAIT: - ID=2 - ID=3 - ID=4 - ID=6 - ID=7 - ID=8 - ID=10 - QUERY_TYPE = PSQL_LOCAL, TIL = READ_COMMITTED_READ_CONSISTENCY, ACCESS = WRITE, WAIT = WAIT: - ID=2 - ID=3 - ID=4 - ID=6 - ID=7 - ID=8 - ID=10 - QUERY_TYPE = PSQL_LOCAL, TIL = READ_COMMITTED_RECORD_VERSION, ACCESS = WRITE, WAIT = NO_WAIT: - ID=2 - ID=3 - ID=4 - ID=6 - ID=7 - ID=8 - ID=10 - QUERY_TYPE = PSQL_LOCAL, TIL = READ_COMMITTED_RECORD_VERSION, ACCESS = WRITE, WAIT = WAIT: - ID=2 - ID=3 - ID=4 - ID=6 - ID=7 - ID=8 - ID=10 - QUERY_TYPE = PSQL_LOCAL, TIL = READ_COMMITTED_NO_RECORD_VERSION, ACCESS = WRITE, WAIT = NO_WAIT: - ID=2 - ID=3 - ID=4 - ID=6 - ID=7 - ID=8 - ID=10 - QUERY_TYPE = PSQL_LOCAL, TIL = READ_COMMITTED_NO_RECORD_VERSION, ACCESS = WRITE, WAIT = WAIT: - ID=2 - ID=3 - ID=4 - ID=6 - ID=7 - ID=8 - ID=10 - QUERY_TYPE = PSQL_REMOTE, TIL = SERIALIZABLE, ACCESS = READ, WAIT = NO_WAIT: - Execute statement error at isc_dsql_fetch : - 335544345 : lock conflict on no wait transaction - 335544382 : Acquire lock for relation (TEST) failed - Statement : select id from test order by id with lock skip locked - Data source : Firebird::localhost: - -At procedure 'SP_GET_UNLOCKED_ROWS_REMOTE' - -901 - 335544926 - 335544842 - QUERY_TYPE = PSQL_REMOTE, TIL = SNAPSHOT, ACCESS = READ, WAIT = NO_WAIT: - Execute statement error at isc_dsql_fetch : - 335544361 : attempted update during read-only transaction - Statement : select id from test order by id with lock skip locked - Data source : Firebird::localhost: - -At procedure 'SP_GET_UNLOCKED_ROWS_REMOTE' - -901 - 335544926 - 335544842 - QUERY_TYPE = PSQL_REMOTE, TIL = SNAPSHOT, ACCESS = READ, WAIT = WAIT: - Execute statement error at isc_dsql_fetch : - 335544361 : attempted update during read-only transaction - Statement : select id from test order by id with lock skip locked - Data source : Firebird::localhost: - -At procedure 'SP_GET_UNLOCKED_ROWS_REMOTE' - -901 - 335544926 - 335544842 - QUERY_TYPE = PSQL_REMOTE, TIL = READ_COMMITTED_READ_CONSISTENCY, ACCESS = READ, WAIT = NO_WAIT: - Execute statement error at isc_dsql_fetch : - 335544361 : attempted update during read-only transaction - Statement : select id from test order by id with lock skip locked - Data source : Firebird::localhost: - -At procedure 'SP_GET_UNLOCKED_ROWS_REMOTE' - -901 - 335544926 - 335544842 - QUERY_TYPE = PSQL_REMOTE, TIL = READ_COMMITTED_READ_CONSISTENCY, ACCESS = READ, WAIT = WAIT: - Execute statement error at isc_dsql_fetch : - 335544361 : attempted update during read-only transaction - Statement : select id from test order by id with lock skip locked - Data source : Firebird::localhost: - -At procedure 'SP_GET_UNLOCKED_ROWS_REMOTE' - -901 - 335544926 - 335544842 - QUERY_TYPE = PSQL_REMOTE, TIL = READ_COMMITTED_RECORD_VERSION, ACCESS = READ, WAIT = NO_WAIT: - Execute statement error at isc_dsql_fetch : - 335544361 : attempted update during read-only transaction - Statement : select id from test order by id with lock skip locked - Data source : Firebird::localhost: - -At procedure 'SP_GET_UNLOCKED_ROWS_REMOTE' - -901 - 335544926 - 335544842 - QUERY_TYPE = PSQL_REMOTE, TIL = READ_COMMITTED_RECORD_VERSION, ACCESS = READ, WAIT = WAIT: - Execute statement error at isc_dsql_fetch : - 335544361 : attempted update during read-only transaction - Statement : select id from test order by id with lock skip locked - Data source : Firebird::localhost: - -At procedure 'SP_GET_UNLOCKED_ROWS_REMOTE' - -901 - 335544926 - 335544842 - QUERY_TYPE = PSQL_REMOTE, TIL = READ_COMMITTED_NO_RECORD_VERSION, ACCESS = READ, WAIT = NO_WAIT: - Execute statement error at isc_dsql_fetch : - 335544361 : attempted update during read-only transaction - Statement : select id from test order by id with lock skip locked - Data source : Firebird::localhost: - -At procedure 'SP_GET_UNLOCKED_ROWS_REMOTE' - -901 - 335544926 - 335544842 - QUERY_TYPE = PSQL_REMOTE, TIL = READ_COMMITTED_NO_RECORD_VERSION, ACCESS = READ, WAIT = WAIT: - Execute statement error at isc_dsql_fetch : - 335544361 : attempted update during read-only transaction - Statement : select id from test order by id with lock skip locked - Data source : Firebird::localhost: - -At procedure 'SP_GET_UNLOCKED_ROWS_REMOTE' - -901 - 335544926 - 335544842 - QUERY_TYPE = PSQL_REMOTE, TIL = SERIALIZABLE, ACCESS = WRITE, WAIT = NO_WAIT: - Execute statement error at isc_dsql_fetch : - 335544345 : lock conflict on no wait transaction - 335544382 : Acquire lock for relation (TEST) failed - Statement : select id from test order by id with lock skip locked - Data source : Firebird::localhost: - -At procedure 'SP_GET_UNLOCKED_ROWS_REMOTE' - -901 - 335544926 - 335544842 - QUERY_TYPE = PSQL_REMOTE, TIL = SNAPSHOT, ACCESS = WRITE, WAIT = NO_WAIT: - ID=2 - ID=3 - ID=4 - ID=6 - ID=7 - ID=8 - ID=10 - QUERY_TYPE = PSQL_REMOTE, TIL = SNAPSHOT, ACCESS = WRITE, WAIT = WAIT: - ID=2 - ID=3 - ID=4 - ID=6 - ID=7 - ID=8 - ID=10 - QUERY_TYPE = PSQL_REMOTE, TIL = READ_COMMITTED_READ_CONSISTENCY, ACCESS = WRITE, WAIT = NO_WAIT: - ID=2 - ID=3 - ID=4 - ID=6 - ID=7 - ID=8 - ID=10 - QUERY_TYPE = PSQL_REMOTE, TIL = READ_COMMITTED_READ_CONSISTENCY, ACCESS = WRITE, WAIT = WAIT: - ID=2 - ID=3 - ID=4 - ID=6 - ID=7 - ID=8 - ID=10 - QUERY_TYPE = PSQL_REMOTE, TIL = READ_COMMITTED_RECORD_VERSION, ACCESS = WRITE, WAIT = NO_WAIT: - ID=2 - ID=3 - ID=4 - ID=6 - ID=7 - ID=8 - ID=10 - QUERY_TYPE = PSQL_REMOTE, TIL = READ_COMMITTED_RECORD_VERSION, ACCESS = WRITE, WAIT = WAIT: - ID=2 - ID=3 - ID=4 - ID=6 - ID=7 - ID=8 - ID=10 - QUERY_TYPE = PSQL_REMOTE, TIL = READ_COMMITTED_NO_RECORD_VERSION, ACCESS = WRITE, WAIT = NO_WAIT: - ID=2 - ID=3 - ID=4 - ID=6 - ID=7 - ID=8 - ID=10 - QUERY_TYPE = PSQL_REMOTE, TIL = READ_COMMITTED_NO_RECORD_VERSION, ACCESS = WRITE, WAIT = WAIT: - ID=2 - ID=3 - ID=4 - ID=6 - ID=7 - ID=8 - ID=10 -""" - +@pytest.mark.es_eds @pytest.mark.version('>=5.0') def test_1(act: Action, capsys): @@ -538,6 +137,406 @@ def test_1(act: Action, capsys): finally: tx_free_seeker.rollback() + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + TABLE_TEST_NAME = 'TEST' if act.is_version('<6') else f'{SQL_SCHEMA_PREFIX}"TEST"' + SP_LOCAL_NAME = "'SP_GET_UNLOCKED_ROWS_LOCAL'" if act.is_version('<6') else f'{SQL_SCHEMA_PREFIX}"SP_GET_UNLOCKED_ROWS_LOCAL"' + SP_REMOTE_NAME = "'SP_GET_UNLOCKED_ROWS_REMOTE'" if act.is_version('<6') else f'{SQL_SCHEMA_PREFIX}"SP_GET_UNLOCKED_ROWS_REMOTE"' + + expected_stdout = f""" + QUERY_TYPE = DSQL, TIL = SERIALIZABLE, ACCESS = READ, WAIT = NO_WAIT: + lock conflict on no wait transaction + -Acquire lock for relation ({TABLE_TEST_NAME}) failed + -901 + 335544345 + 335544382 + QUERY_TYPE = DSQL, TIL = SNAPSHOT, ACCESS = READ, WAIT = NO_WAIT: + attempted update during read-only transaction + -817 + 335544361 + QUERY_TYPE = DSQL, TIL = SNAPSHOT, ACCESS = READ, WAIT = WAIT: + attempted update during read-only transaction + -817 + 335544361 + QUERY_TYPE = DSQL, TIL = READ_COMMITTED_READ_CONSISTENCY, ACCESS = READ, WAIT = NO_WAIT: + attempted update during read-only transaction + -817 + 335544361 + QUERY_TYPE = DSQL, TIL = READ_COMMITTED_READ_CONSISTENCY, ACCESS = READ, WAIT = WAIT: + attempted update during read-only transaction + -817 + 335544361 + QUERY_TYPE = DSQL, TIL = READ_COMMITTED_RECORD_VERSION, ACCESS = READ, WAIT = NO_WAIT: + attempted update during read-only transaction + -817 + 335544361 + QUERY_TYPE = DSQL, TIL = READ_COMMITTED_RECORD_VERSION, ACCESS = READ, WAIT = WAIT: + attempted update during read-only transaction + -817 + 335544361 + QUERY_TYPE = DSQL, TIL = READ_COMMITTED_NO_RECORD_VERSION, ACCESS = READ, WAIT = NO_WAIT: + attempted update during read-only transaction + -817 + 335544361 + QUERY_TYPE = DSQL, TIL = READ_COMMITTED_NO_RECORD_VERSION, ACCESS = READ, WAIT = WAIT: + attempted update during read-only transaction + -817 + 335544361 + QUERY_TYPE = DSQL, TIL = SERIALIZABLE, ACCESS = WRITE, WAIT = NO_WAIT: + lock conflict on no wait transaction + -Acquire lock for relation ({TABLE_TEST_NAME}) failed + -901 + 335544345 + 335544382 + QUERY_TYPE = DSQL, TIL = SNAPSHOT, ACCESS = WRITE, WAIT = NO_WAIT: + ID=2 + ID=3 + ID=4 + ID=6 + ID=7 + ID=8 + ID=10 + QUERY_TYPE = DSQL, TIL = SNAPSHOT, ACCESS = WRITE, WAIT = WAIT: + ID=2 + ID=3 + ID=4 + ID=6 + ID=7 + ID=8 + ID=10 + QUERY_TYPE = DSQL, TIL = READ_COMMITTED_READ_CONSISTENCY, ACCESS = WRITE, WAIT = NO_WAIT: + ID=2 + ID=3 + ID=4 + ID=6 + ID=7 + ID=8 + ID=10 + QUERY_TYPE = DSQL, TIL = READ_COMMITTED_READ_CONSISTENCY, ACCESS = WRITE, WAIT = WAIT: + ID=2 + ID=3 + ID=4 + ID=6 + ID=7 + ID=8 + ID=10 + QUERY_TYPE = DSQL, TIL = READ_COMMITTED_RECORD_VERSION, ACCESS = WRITE, WAIT = NO_WAIT: + ID=2 + ID=3 + ID=4 + ID=6 + ID=7 + ID=8 + ID=10 + QUERY_TYPE = DSQL, TIL = READ_COMMITTED_RECORD_VERSION, ACCESS = WRITE, WAIT = WAIT: + ID=2 + ID=3 + ID=4 + ID=6 + ID=7 + ID=8 + ID=10 + QUERY_TYPE = DSQL, TIL = READ_COMMITTED_NO_RECORD_VERSION, ACCESS = WRITE, WAIT = NO_WAIT: + ID=2 + ID=3 + ID=4 + ID=6 + ID=7 + ID=8 + ID=10 + QUERY_TYPE = DSQL, TIL = READ_COMMITTED_NO_RECORD_VERSION, ACCESS = WRITE, WAIT = WAIT: + ID=2 + ID=3 + ID=4 + ID=6 + ID=7 + ID=8 + ID=10 + QUERY_TYPE = PSQL_LOCAL, TIL = SERIALIZABLE, ACCESS = READ, WAIT = NO_WAIT: + lock conflict on no wait transaction + -Acquire lock for relation ({TABLE_TEST_NAME}) failed + -At procedure {SP_LOCAL_NAME} + -901 + 335544345 + 335544382 + 335544842 + QUERY_TYPE = PSQL_LOCAL, TIL = SNAPSHOT, ACCESS = READ, WAIT = NO_WAIT: + attempted update during read-only transaction + -At procedure {SP_LOCAL_NAME} + -817 + 335544361 + 335544842 + QUERY_TYPE = PSQL_LOCAL, TIL = SNAPSHOT, ACCESS = READ, WAIT = WAIT: + attempted update during read-only transaction + -At procedure {SP_LOCAL_NAME} + -817 + 335544361 + 335544842 + QUERY_TYPE = PSQL_LOCAL, TIL = READ_COMMITTED_READ_CONSISTENCY, ACCESS = READ, WAIT = NO_WAIT: + attempted update during read-only transaction + -At procedure {SP_LOCAL_NAME} + -817 + 335544361 + 335544842 + QUERY_TYPE = PSQL_LOCAL, TIL = READ_COMMITTED_READ_CONSISTENCY, ACCESS = READ, WAIT = WAIT: + attempted update during read-only transaction + -At procedure {SP_LOCAL_NAME} + -817 + 335544361 + 335544842 + QUERY_TYPE = PSQL_LOCAL, TIL = READ_COMMITTED_RECORD_VERSION, ACCESS = READ, WAIT = NO_WAIT: + attempted update during read-only transaction + -At procedure {SP_LOCAL_NAME} + -817 + 335544361 + 335544842 + QUERY_TYPE = PSQL_LOCAL, TIL = READ_COMMITTED_RECORD_VERSION, ACCESS = READ, WAIT = WAIT: + attempted update during read-only transaction + -At procedure {SP_LOCAL_NAME} + -817 + 335544361 + 335544842 + QUERY_TYPE = PSQL_LOCAL, TIL = READ_COMMITTED_NO_RECORD_VERSION, ACCESS = READ, WAIT = NO_WAIT: + attempted update during read-only transaction + -At procedure {SP_LOCAL_NAME} + -817 + 335544361 + 335544842 + QUERY_TYPE = PSQL_LOCAL, TIL = READ_COMMITTED_NO_RECORD_VERSION, ACCESS = READ, WAIT = WAIT: + attempted update during read-only transaction + -At procedure {SP_LOCAL_NAME} + -817 + 335544361 + 335544842 + QUERY_TYPE = PSQL_LOCAL, TIL = SERIALIZABLE, ACCESS = WRITE, WAIT = NO_WAIT: + lock conflict on no wait transaction + -Acquire lock for relation ({TABLE_TEST_NAME}) failed + -At procedure {SP_LOCAL_NAME} + -901 + 335544345 + 335544382 + 335544842 + QUERY_TYPE = PSQL_LOCAL, TIL = SNAPSHOT, ACCESS = WRITE, WAIT = NO_WAIT: + ID=2 + ID=3 + ID=4 + ID=6 + ID=7 + ID=8 + ID=10 + QUERY_TYPE = PSQL_LOCAL, TIL = SNAPSHOT, ACCESS = WRITE, WAIT = WAIT: + ID=2 + ID=3 + ID=4 + ID=6 + ID=7 + ID=8 + ID=10 + QUERY_TYPE = PSQL_LOCAL, TIL = READ_COMMITTED_READ_CONSISTENCY, ACCESS = WRITE, WAIT = NO_WAIT: + ID=2 + ID=3 + ID=4 + ID=6 + ID=7 + ID=8 + ID=10 + QUERY_TYPE = PSQL_LOCAL, TIL = READ_COMMITTED_READ_CONSISTENCY, ACCESS = WRITE, WAIT = WAIT: + ID=2 + ID=3 + ID=4 + ID=6 + ID=7 + ID=8 + ID=10 + QUERY_TYPE = PSQL_LOCAL, TIL = READ_COMMITTED_RECORD_VERSION, ACCESS = WRITE, WAIT = NO_WAIT: + ID=2 + ID=3 + ID=4 + ID=6 + ID=7 + ID=8 + ID=10 + QUERY_TYPE = PSQL_LOCAL, TIL = READ_COMMITTED_RECORD_VERSION, ACCESS = WRITE, WAIT = WAIT: + ID=2 + ID=3 + ID=4 + ID=6 + ID=7 + ID=8 + ID=10 + QUERY_TYPE = PSQL_LOCAL, TIL = READ_COMMITTED_NO_RECORD_VERSION, ACCESS = WRITE, WAIT = NO_WAIT: + ID=2 + ID=3 + ID=4 + ID=6 + ID=7 + ID=8 + ID=10 + QUERY_TYPE = PSQL_LOCAL, TIL = READ_COMMITTED_NO_RECORD_VERSION, ACCESS = WRITE, WAIT = WAIT: + ID=2 + ID=3 + ID=4 + ID=6 + ID=7 + ID=8 + ID=10 + QUERY_TYPE = PSQL_REMOTE, TIL = SERIALIZABLE, ACCESS = READ, WAIT = NO_WAIT: + Execute statement error at isc_dsql_fetch : + 335544345 : lock conflict on no wait transaction + 335544382 : Acquire lock for relation ({TABLE_TEST_NAME}) failed + Statement : select id from test order by id with lock skip locked + Data source : Firebird::localhost: + -At procedure {SP_REMOTE_NAME} + -901 + 335544926 + 335544842 + QUERY_TYPE = PSQL_REMOTE, TIL = SNAPSHOT, ACCESS = READ, WAIT = NO_WAIT: + Execute statement error at isc_dsql_fetch : + 335544361 : attempted update during read-only transaction + Statement : select id from test order by id with lock skip locked + Data source : Firebird::localhost: + -At procedure {SP_REMOTE_NAME} + -901 + 335544926 + 335544842 + QUERY_TYPE = PSQL_REMOTE, TIL = SNAPSHOT, ACCESS = READ, WAIT = WAIT: + Execute statement error at isc_dsql_fetch : + 335544361 : attempted update during read-only transaction + Statement : select id from test order by id with lock skip locked + Data source : Firebird::localhost: + -At procedure {SP_REMOTE_NAME} + -901 + 335544926 + 335544842 + QUERY_TYPE = PSQL_REMOTE, TIL = READ_COMMITTED_READ_CONSISTENCY, ACCESS = READ, WAIT = NO_WAIT: + Execute statement error at isc_dsql_fetch : + 335544361 : attempted update during read-only transaction + Statement : select id from test order by id with lock skip locked + Data source : Firebird::localhost: + -At procedure {SP_REMOTE_NAME} + -901 + 335544926 + 335544842 + QUERY_TYPE = PSQL_REMOTE, TIL = READ_COMMITTED_READ_CONSISTENCY, ACCESS = READ, WAIT = WAIT: + Execute statement error at isc_dsql_fetch : + 335544361 : attempted update during read-only transaction + Statement : select id from test order by id with lock skip locked + Data source : Firebird::localhost: + -At procedure {SP_REMOTE_NAME} + -901 + 335544926 + 335544842 + QUERY_TYPE = PSQL_REMOTE, TIL = READ_COMMITTED_RECORD_VERSION, ACCESS = READ, WAIT = NO_WAIT: + Execute statement error at isc_dsql_fetch : + 335544361 : attempted update during read-only transaction + Statement : select id from test order by id with lock skip locked + Data source : Firebird::localhost: + -At procedure {SP_REMOTE_NAME} + -901 + 335544926 + 335544842 + QUERY_TYPE = PSQL_REMOTE, TIL = READ_COMMITTED_RECORD_VERSION, ACCESS = READ, WAIT = WAIT: + Execute statement error at isc_dsql_fetch : + 335544361 : attempted update during read-only transaction + Statement : select id from test order by id with lock skip locked + Data source : Firebird::localhost: + -At procedure {SP_REMOTE_NAME} + -901 + 335544926 + 335544842 + QUERY_TYPE = PSQL_REMOTE, TIL = READ_COMMITTED_NO_RECORD_VERSION, ACCESS = READ, WAIT = NO_WAIT: + Execute statement error at isc_dsql_fetch : + 335544361 : attempted update during read-only transaction + Statement : select id from test order by id with lock skip locked + Data source : Firebird::localhost: + -At procedure {SP_REMOTE_NAME} + -901 + 335544926 + 335544842 + QUERY_TYPE = PSQL_REMOTE, TIL = READ_COMMITTED_NO_RECORD_VERSION, ACCESS = READ, WAIT = WAIT: + Execute statement error at isc_dsql_fetch : + 335544361 : attempted update during read-only transaction + Statement : select id from test order by id with lock skip locked + Data source : Firebird::localhost: + -At procedure {SP_REMOTE_NAME} + -901 + 335544926 + 335544842 + QUERY_TYPE = PSQL_REMOTE, TIL = SERIALIZABLE, ACCESS = WRITE, WAIT = NO_WAIT: + Execute statement error at isc_dsql_fetch : + 335544345 : lock conflict on no wait transaction + 335544382 : Acquire lock for relation ({TABLE_TEST_NAME}) failed + Statement : select id from test order by id with lock skip locked + Data source : Firebird::localhost: + -At procedure {SP_REMOTE_NAME} + -901 + 335544926 + 335544842 + QUERY_TYPE = PSQL_REMOTE, TIL = SNAPSHOT, ACCESS = WRITE, WAIT = NO_WAIT: + ID=2 + ID=3 + ID=4 + ID=6 + ID=7 + ID=8 + ID=10 + QUERY_TYPE = PSQL_REMOTE, TIL = SNAPSHOT, ACCESS = WRITE, WAIT = WAIT: + ID=2 + ID=3 + ID=4 + ID=6 + ID=7 + ID=8 + ID=10 + QUERY_TYPE = PSQL_REMOTE, TIL = READ_COMMITTED_READ_CONSISTENCY, ACCESS = WRITE, WAIT = NO_WAIT: + ID=2 + ID=3 + ID=4 + ID=6 + ID=7 + ID=8 + ID=10 + QUERY_TYPE = PSQL_REMOTE, TIL = READ_COMMITTED_READ_CONSISTENCY, ACCESS = WRITE, WAIT = WAIT: + ID=2 + ID=3 + ID=4 + ID=6 + ID=7 + ID=8 + ID=10 + QUERY_TYPE = PSQL_REMOTE, TIL = READ_COMMITTED_RECORD_VERSION, ACCESS = WRITE, WAIT = NO_WAIT: + ID=2 + ID=3 + ID=4 + ID=6 + ID=7 + ID=8 + ID=10 + QUERY_TYPE = PSQL_REMOTE, TIL = READ_COMMITTED_RECORD_VERSION, ACCESS = WRITE, WAIT = WAIT: + ID=2 + ID=3 + ID=4 + ID=6 + ID=7 + ID=8 + ID=10 + QUERY_TYPE = PSQL_REMOTE, TIL = READ_COMMITTED_NO_RECORD_VERSION, ACCESS = WRITE, WAIT = NO_WAIT: + ID=2 + ID=3 + ID=4 + ID=6 + ID=7 + ID=8 + ID=10 + QUERY_TYPE = PSQL_REMOTE, TIL = READ_COMMITTED_NO_RECORD_VERSION, ACCESS = WRITE, WAIT = WAIT: + ID=2 + ID=3 + ID=4 + ID=6 + ID=7 + ID=8 + ID=10 + """ + act.expected_stdout = expected_stdout act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7388_test.py b/tests/bugs/gh_7388_test.py index 7fa40747..89735d7e 100644 --- a/tests/bugs/gh_7388_test.py +++ b/tests/bugs/gh_7388_test.py @@ -13,11 +13,20 @@ [20.01.2024] pzotov Confirmed problem on 5.0.0.871. Checked on 6.0.0.218, 5.0.1.1318. + + [18.01.2025] pzotov + Resultset of cursor that executes using instance of selectable PreparedStatement must be stored + in some variable in order to have ability close it EXPLICITLY (before PS will be freed). + Otherwise access violation raises during Python GC and pytest hangs at final point (does not return control to OS). + This occurs at least for: Python 3.11.2 / pytest: 7.4.4 / firebird.driver: 1.10.6 / Firebird.Qa: 0.19.3 + The reason of that was explained by Vlad, 26.10.24 17:42 ("oddities when use instances of selective statements"). """ -import pytest from pathlib import Path + +import pytest from firebird.qa import * +from firebird.driver import DatabaseError init_sql = """ create view v1 @@ -37,7 +46,7 @@ #---------------------------------------------------------- -def replace_leading(source, char="#"): +def replace_leading(source, char="."): stripped = source.lstrip() return char * (len(source) - len(stripped)) + stripped @@ -78,12 +87,21 @@ def test_1(act: Action, capsys): result_map = {} for qry_txt in q_map.keys(): - with cur.prepare(qry_txt) as ps: + ps, rs = None, None + try: + ps = cur.prepare(qry_txt) q_map[qry_txt] = ps.detailed_plan for tab_nm,tab_id in t_map.items(): tabstat1 = [ p for p in con.info.get_table_access_stats() if p.table_id == tab_id ] - cur.execute(qry_txt) - for r in cur: + + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + rs = cur.execute(ps) + for r in rs: pass tabstat2 = [ p for p in con.info.get_table_access_stats() if p.table_id == tab_id ] @@ -98,6 +116,15 @@ def test_1(act: Action, capsys): idx -= (tabstat1[0].indexed if tabstat1[0].indexed else 0) result_map[qry_txt, tab_nm] = (seq, idx) + except DatabaseError as e: + print( e.__str__() ) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() + ''' print('q_map.items():') for k,v in q_map.items(): @@ -133,7 +160,8 @@ def test_1(act: Action, capsys): print(qry_txt) print('-' * 22) print('Plan:') - print( '\n'.join([replace_leading(s) for s in q_map[qry_txt].split('\n')]) ) # explained plan, with preserving indents by replacing leading spaces with '#' + # Show explained plan, with preserving indents by replacing leading spaces with '.': + print( '\n'.join([replace_leading(s) for s in q_map[qry_txt].split('\n')]) ) print('') else: print(SUCCESS_MSG) diff --git a/tests/bugs/gh_7398_test.py b/tests/bugs/gh_7398_test.py new file mode 100644 index 00000000..218a8d93 --- /dev/null +++ b/tests/bugs/gh_7398_test.py @@ -0,0 +1,158 @@ +#coding:utf-8 + +""" +ID: issue-7398 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/7398 +TITLE: Worst plan sort created to execute an indexed tables +DESCRIPTION: +NOTES: + [29.09.2024] pzotov + 1. Ineffective execution plan was up to 4.0.3.2840. + Since 4.0.3.2843 plan changed and is the same for all subsequent FB-4.x snapshots. + Commit: https://github.com/FirebirdSQL/firebird/commit/1b192404d43a15d403b5ff92760bc5df9d3c89c3 + (13.09.2022 19:17, "More complete solution for #3357 and #7118") + + 2. Database provided in the ticket has too big size (~335 Mb). + Test uses much smaller DB that was created on basis of original one by + extraction of small portions of data from tables PCP_TIN_REC_MAT and INV_ETQ_MAT. + These tables in original DB have 114115 and 1351211 rows. + In DB that is used here these tables have 15000 and 30000 rows corresp. + NOT all constraints are used in the test DB. Particularly, following DDL were abandoned: + ALTER TABLE PCP_TIN_REC ADD CONSTRAINT FK_PCP_TIN_REC_EMP FOREIGN KEY (ID_EMP) REFERENCES SYS_EMP (ID_EMP); + ALTER TABLE PCP_TIN_REC ADD CONSTRAINT FK_PCP_TIN_REC_OP FOREIGN KEY (ID_OP) REFERENCES PCP_OP (ID_OP); + ALTER TABLE PCP_TIN_REC_MAT ADD CONSTRAINT FK_PCP_TIN_REC_MAT_MAT FOREIGN KEY (ID_MAT) REFERENCES INV_MAT (ID_MAT); + Test database have been backed up using 4.0.3.2840 and compressed to .zip file. + 3. Because of missed valuable part of source data, I'm not sure that this test verifies exactly ticket issue. + But in any case, using this test one may see difference in execution plan that is produced in 4.0.3.2840 and 4.0.3.2843. + And such difference also can be seen on original DB (although plans there differ from those which are in test DB). + + Checked on 6.0.0.471, 5.0.2.1519, 4.0.6.3157. + [05.07.2025] pzotov + Added substitution to suppress all except sqltype and fields name from SQLDA output. + Checked on 6.0.0.892; 5.0.3.1668. +""" + +import locale +import re +import zipfile +from pathlib import Path +from firebird.driver import SrvRestoreFlag +import time + +import pytest +from firebird.qa import * + +db = db_factory() + +act = python_act('db') + +check_sql = """ + select r.id_op, r.id_rec, sum(m.q_mat * cus.cus_med) + from pcp_tin_rec r + join pcp_tin_rec_mat m on r.id_rec = m.id_rec + join inv_etq_mat cus on cus.id_mat = m.id_mat and cus.anomes = r.am_bai + join inv_etq_nat nat on nat.id_nat = cus.id_nat + where + nat.cml_stat = 1 and r.id_op = 216262 + group by r.id_op, r.id_rec +""" + +fbk_file = temp_file('gh_7398.tmp.fbk') + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- + +expected_out_4x = """ + Select Expression + ....-> Aggregate + ........-> Sort (record length: 148, key length: 16) + ............-> Nested Loop Join (inner) + ................-> Filter + ....................-> Table "PCP_TIN_REC" as "R" Full Scan + ................-> Filter + ....................-> Table "PCP_TIN_REC_MAT" as "M" Access By ID + ........................-> Bitmap + ............................-> Index "FK_PCP_TIN_REC_MAT_REC" Range Scan (full match) + ................-> Filter + ....................-> Table "INV_ETQ_MAT" as "CUS" Access By ID + ........................-> Bitmap + ............................-> Index "IDX_INV_ETQ_MAT_ANOMES" Range Scan (full match) + ................-> Filter + ....................-> Table "INV_ETQ_NAT" as "NAT" Access By ID + ........................-> Bitmap + ............................-> Index "PK_INV_ETQ_NAT" Unique Scan +""" +expected_out_5x = """ + Select Expression + ....-> Aggregate + ........-> Sort (record length: 148, key length: 16) + ............-> Filter + ................-> Hash Join (inner) + ....................-> Nested Loop Join (inner) + ........................-> Filter + ............................-> Table "PCP_TIN_REC" as "R" Full Scan + ........................-> Filter + ............................-> Table "PCP_TIN_REC_MAT" as "M" Access By ID + ................................-> Bitmap + ....................................-> Index "FK_PCP_TIN_REC_MAT_REC" Range Scan (full match) + ........................-> Filter + ............................-> Table "INV_ETQ_MAT" as "CUS" Access By ID + ................................-> Bitmap + ....................................-> Index "IDX_INV_ETQ_MAT_ANOMES" Range Scan (full match) + ....................-> Record Buffer (record length: 33) + ........................-> Filter + ............................-> Table "INV_ETQ_NAT" as "NAT" Access By ID + ................................-> Bitmap + ....................................-> Index "IDX_INV_ETQ_NAT_CML_STAT" Range Scan (full match) +""" +expected_out_6x = """ + Select Expression + ....-> Aggregate + ........-> Sort (record length: 148, key length: 16) + ............-> Filter + ................-> Hash Join (inner) (keys: 1, total key length: 4) + ....................-> Nested Loop Join (inner) + ........................-> Filter + ............................-> Table "PUBLIC"."PCP_TIN_REC" as "R" Full Scan + ........................-> Filter + ............................-> Table "PUBLIC"."PCP_TIN_REC_MAT" as "M" Access By ID + ................................-> Bitmap + ....................................-> Index "PUBLIC"."FK_PCP_TIN_REC_MAT_REC" Range Scan (full match) + ........................-> Filter + ............................-> Table "PUBLIC"."INV_ETQ_MAT" as "CUS" Access By ID + ................................-> Bitmap + ....................................-> Index "PUBLIC"."IDX_INV_ETQ_MAT_ANOMES" Range Scan (full match) + ....................-> Record Buffer (record length: 33) + ........................-> Filter + ............................-> Table "PUBLIC"."INV_ETQ_NAT" as "NAT" Access By ID + ................................-> Bitmap + ....................................-> Index "PUBLIC"."IDX_INV_ETQ_NAT_CML_STAT" Range Scan (full match) +""" + +@pytest.mark.version('>=4.0') +def test_1(act: Action, fbk_file: Path, capsys): + zipped_fbk_file = zipfile.Path(act.files_dir / 'gh_7398.zip', at = 'gh_7398.fbk') + fbk_file.write_bytes(zipped_fbk_file.read_bytes()) + + with act.connect_server(encoding=locale.getpreferredencoding()) as srv: + srv.database.restore(database = act.db.db_path, backup = fbk_file, flags = SrvRestoreFlag.REPLACE) + restore_log = srv.readlines() + assert restore_log == [] + + + with act.db.connect() as con: + chk_sql = 'select 1 from test order by id' + cur = con.cursor() + ps = cur.prepare(check_sql) + # Print explained plan with padding eash line by dots in order to see indentations: + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + + act.expected_stdout = expected_out_4x if act.is_version('<5') else expected_out_5x if act.is_version('<6') else expected_out_6x + + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7413_test.py b/tests/bugs/gh_7413_test.py index 994f21f9..b18b924a 100644 --- a/tests/bugs/gh_7413_test.py +++ b/tests/bugs/gh_7413_test.py @@ -6,8 +6,12 @@ TITLE: Regression: bad plan in FB 3.0.9+ (correct in FB 3.0.8) NOTES: [01.03.2023] pzotov - Test database was created beforehand, fulfilled with data provided in the ticket, backed up and compressed. - Checked on 3.0.11.33665, 4.0.3.2904, 5.0.0.964 + Test database was created beforehand, fulfilled with data provided in the ticket, backed up and compressed. + Checked on 3.0.11.33665, 4.0.3.2904, 5.0.0.964 + [05.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.909; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -18,16 +22,8 @@ db = db_factory() -act = python_act('db') - -expected_stdout = """ - PLAN JOIN (T3 INDEX (XAK2T3), T1 INDEX (R_542), T2 INDEX (XPKT2)) - ID_X 1 - ID_T1 6026229 - TOTAL 30.0000 - INVOICE_NO 1683998 - CREATED_AT 2022-11-28 -""" +substitutions = [('[ \t]+', ' ')] +act = python_act('db', substitutions = substitutions) fbk_file = temp_file('gh_7413.tmp.fbk') @@ -54,7 +50,25 @@ def test_1(act: Action, fbk_file: Path, capsys): where t3.invoice_no = 1683998; """ - act.expected_stdout = expected_stdout + expected_stdout_5x = """ + PLAN JOIN (T3 INDEX (XAK2T3), T1 INDEX (R_542), T2 INDEX (XPKT2)) + ID_X 1 + ID_T1 6026229 + TOTAL 30.0000 + INVOICE_NO 1683998 + CREATED_AT 2022-11-28 + """ + + expected_stdout_6x = """ + PLAN JOIN ("PUBLIC"."T3" INDEX ("PUBLIC"."XAK2T3"), "PUBLIC"."T1" INDEX ("PUBLIC"."R_542"), "PUBLIC"."T2" INDEX ("PUBLIC"."XPKT2")) + ID_X 1 + ID_T1 6026229 + TOTAL 30.0000 + INVOICE_NO 1683998 + CREATED_AT 2022-11-28 + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.isql(switches=[], input = script, combine_output=True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7426_test.py b/tests/bugs/gh_7426_test.py index e073a9eb..f4a9d0ae 100644 --- a/tests/bugs/gh_7426_test.py +++ b/tests/bugs/gh_7426_test.py @@ -17,12 +17,15 @@ No errors must present in the trace log. NOTES: [07-sep-2023] pzotov - ::: NB ::: - 1. It must be noted that the term 'COMPILE' means parsing of BLR code into an execution tree, i.e. this action - occurs when unit code is loaded into metadata cache. - 2. Currently there is no way to specify in the trace what EXACT type of DDL trigger fired. It is shown as "AFTER DDL". - - Checked on 5.0.0.1190. + 1. The term 'COMPILE' means parsing of BLR code into an execution tree, i.e. this action + occurs when unit code is loaded into metadata cache. + 2. Currently there is no way to specify in the trace what EXACT type of DDL trigger fired. + It is shown as "AFTER DDL". + Checked on 5.0.0.1190. + [05.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.909; 5.0.3.1668. """ import locale import re @@ -38,9 +41,10 @@ 'log_trigger_compile = true', ] -allowed_patterns = [ ' ERROR AT ', 'Trigger TRG_ANY_DDL_STATEMENT_', ] +allowed_patterns = [ ' ERROR AT ', 'Trigger ("PUBLIC".)?(")?TRG_ANY_DDL_STATEMENT_(BEFORE|AFTER)(")?', ] allowed_patterns = [ re.compile(r, re.IGNORECASE) for r in allowed_patterns] +@pytest.mark.trace @pytest.mark.version('>=5.0') def test_1(act: Action, capsys): @@ -63,7 +67,7 @@ def test_1(act: Action, capsys): rdb$set_context('USER_SESSION', 'SKIP_DDL_TRG', '1'); end ^ - create or alter trigger trg_any_ddl_statement_alter active after any ddl statement as + create or alter trigger trg_any_ddl_statement_after active after any ddl statement as begin if (rdb$get_context('USER_SESSION', 'SKIP_DDL_TRG') is null) then execute statement @@ -134,11 +138,16 @@ def test_1(act: Action, capsys): if p.search(line): print(line.strip()) - expected_stdout = f""" + expected_stdout_5x = f""" Trigger TRG_ANY_DDL_STATEMENT_BEFORE (BEFORE DDL): - Trigger TRG_ANY_DDL_STATEMENT_ALTER (AFTER DDL): + Trigger TRG_ANY_DDL_STATEMENT_AFTER (AFTER DDL): """ - act.expected_stdout = expected_stdout + expected_stdout_6x = f""" + Trigger "PUBLIC"."TRG_ANY_DDL_STATEMENT_BEFORE" (BEFORE DDL): + Trigger "PUBLIC"."TRG_ANY_DDL_STATEMENT_AFTER" (AFTER DDL): + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7461_test.py b/tests/bugs/gh_7461_test.py index 8aea90a8..c3de3e85 100644 --- a/tests/bugs/gh_7461_test.py +++ b/tests/bugs/gh_7461_test.py @@ -6,36 +6,33 @@ TITLE: Differences in field metadata descriptions between Firebird 2.5 and Firebird 4 NOTES: [21.01.2024] pzotov + NB: original title of ticket was changed. Commits refer to pull reuest rather than to this ticket number: + FB 4.x (16.11.2023): https://github.com/FirebirdSQL/firebird/commit/2886cd78991209842ee7e3065bde83ab75571af4 + FB 5.x (17.11.2023): https://github.com/FirebirdSQL/firebird/commit/1ed7f81f168b643a29357fce2e1f49156e9f5a1f + FB 6.x (17.11.2023): https://github.com/FirebirdSQL/firebird/commit/ab6aced05723dc1b2e6bb96bfdaa86cb3090daf2 + Before fix: + 01: sqltype: 580 INT64 Nullable scale: -2 subtype: 1 len: 8 + : name: alias: SALARY --- + : table: owner: + 02: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + : name: alias: EMP_NO --- + : table: owner: --- - NB: original title of ticket was changed. Commits refer to pull reuest rather than to this ticket number: - FB 4.x (16.11.2023): https://github.com/FirebirdSQL/firebird/commit/2886cd78991209842ee7e3065bde83ab75571af4 - FB 5.x (17.11.2023): https://github.com/FirebirdSQL/firebird/commit/1ed7f81f168b643a29357fce2e1f49156e9f5a1f - FB 6.x (17.11.2023): https://github.com/FirebirdSQL/firebird/commit/ab6aced05723dc1b2e6bb96bfdaa86cb3090daf2 - Actual text of commits: - FB 4.x: - Merge pull request #7848 from medi6/4.0.2 - Differencies in queries results between Firebird 2.5 and Firebird 4 - FB 5.x and 6.x: - correction metaData + After fix: + 01: sqltype: 580 INT64 Nullable scale: -2 subtype: 1 len: 8 + : name: MAX alias: SALARY +++ + : table: owner: + 02: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + : name: EMP_NO alias: EMP_NO +++ + : table: SALARY_HISTORY owner: SYSDBA +++ - Before fix: - 01: sqltype: 580 INT64 Nullable scale: -2 subtype: 1 len: 8 - : name: alias: SALARY --- - : table: owner: - 02: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 - : name: alias: EMP_NO --- - : table: owner: --- + Confirmed bug on 4.0.4.3016, 5.0.0.1268 + Checked on 4.0.4.3021 (build date: 17-nov-2023), 5.0.0.1271 (build date: 18-nov-2023); 6.0.0.219 - After fix: - 01: sqltype: 580 INT64 Nullable scale: -2 subtype: 1 len: 8 - : name: MAX alias: SALARY +++ - : table: owner: - 02: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 - : name: EMP_NO alias: EMP_NO +++ - : table: SALARY_HISTORY owner: SYSDBA +++ - - Confirmed bug on 4.0.4.3016, 5.0.0.1268 - Checked on 4.0.4.3021 (build date: 17-nov-2023), 5.0.0.1271 (build date: 18-nov-2023); 6.0.0.219 + [05.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.909; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -66,13 +63,19 @@ @pytest.mark.version('>=4.0.5') def test_1(act: Action): - expected_stdout = f""" + expected_stdout_5x = f""" : name: MAX alias: SALARY : table: owner: : name: EMP_NO alias: EMP_NO : table: SALARY_HISTORY owner: {act.db.user} """ + expected_stdout_6x = f""" + : name: MAX alias: SALARY + : table: schema: owner: + : name: EMP_NO alias: EMP_NO + : table: SALARY_HISTORY schema: PUBLIC owner: SYSDBA + """ - act.expected_stdout = expected_stdout + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7466_plans_tracking_test.py b/tests/bugs/gh_7466_plans_tracking_test.py index 46d9a830..b59283f8 100644 --- a/tests/bugs/gh_7466_plans_tracking_test.py +++ b/tests/bugs/gh_7466_plans_tracking_test.py @@ -11,41 +11,44 @@ We check here that compiling of this SP leads to appearing in the trace execution plans for every statement. NOTES: [18-aug-2023] pzotov - 1. It must be noted that the term 'COMPILE' means parsing of BLR code into an execution tree, i.e. this action - occurs when unit code is loaded into metadata cache. - 2. All subqueries like EXISTS() or IN() (and their "not" form) will be displayed in the trace as "separate" block - followed by block with "Select expression" or "Cursor". This seems not readable but currently it is so. - 3. Number of lines and columns for 'Subquery' will be shown only when this subquery is used as part PSQL statement - (i.e. not as part of SQL query) -- see 'decode()' below: - ======= - Sub-query (line 37, column 26) - -> Singularity Check - ... - ======= - 4. Plans, of course, can be changed in the future, so this test must be adjusted if this will occur. - - Thanks to dimitr for explanations. - Discussed with dimitr, letters 18.08.2023. - - Checked on 5.0.0.1164 + 1. It must be noted that the term 'COMPILE' means parsing of BLR code into an execution tree, i.e. this action + occurs when unit code is loaded into metadata cache. + 2. All subqueries like EXISTS() or IN() (and their "not" form) will be displayed in the trace as "separate" block + followed by block with "Select expression" or "Cursor". This seems not readable but currently it is so. + 3. Number of lines and columns for 'Subquery' will be shown only when this subquery is used as part PSQL statement + (i.e. not as part of SQL query) -- see 'decode()' below: + ======= + Sub-query (line 37, column 26) + -> Singularity Check + ... + ======= + 4. Plans, of course, can be changed in the future, so this test must be adjusted if this will occur. + + Thanks to dimitr for explanations. + Discussed with dimitr, letters 18.08.2023. + Checked on 5.0.0.1164 [08-sep-2023] pzotov - 1. Changed plan output: it is desirable to show indentations but they are 'swallowed' when act.clean_stdout is displayed. - Because of that, explained plan lines are 'padded' with dot character to their original length. - 2. Adjusted execution plan for one of queries to actual: one need to replace "Range Scan" with "List Scan" if we have - subquery with IN-list which refers to some columns from outer query. - See: https://github.com/FirebirdSQL/firebird/commit/5df6668c7bf5a4b27e15f687f8c6cc40e260ced8 - (Allow computable but non-invariant lists to be used for index lookup) - - Checked on 5.0.0.1200 - + 1. Changed plan output: it is desirable to show indentations but they are 'swallowed' when act.clean_stdout is displayed. + Because of that, explained plan lines are 'padded' with dot character to their original length. + 2. Adjusted execution plan for one of queries to actual: one need to replace "Range Scan" with "List Scan" if we have + subquery with IN-list which refers to some columns from outer query. + See: https://github.com/FirebirdSQL/firebird/commit/5df6668c7bf5a4b27e15f687f8c6cc40e260ced8 + (Allow computable but non-invariant lists to be used for index lookup) + Checked on 5.0.0.1200 [19-dec-2023] pzotov - Removed 'rand()' in order to have predictable values in table column. Use mod() instead. - Unstable outcomes started since 6.0.0.180 (18.12.2023). - It seems that following commits caused this: - https://github.com/FirebirdSQL/firebird/commit/ae427762d5a3e740b69c7239acb9e2383bc9ca83 // 5.x - https://github.com/FirebirdSQL/firebird/commit/f647dfd757de3c4065ef2b875c95d19311bb9691 // 6.x - + Removed 'rand()' in order to have predictable values in table column. Use mod() instead. + Unstable outcomes started since 6.0.0.180 (18.12.2023). + It seems that following commits caused this: + https://github.com/FirebirdSQL/firebird/commit/ae427762d5a3e740b69c7239acb9e2383bc9ca83 // 5.x + https://github.com/FirebirdSQL/firebird/commit/f647dfd757de3c4065ef2b875c95d19311bb9691 // 6.x + [04-feb-2025] + Adjusted execution plan for EXISTS() part of recursive query: "List Scan" was replaced with "Range Scan" for + "and m0a.x in (dx.y, dx.z)". This change caused by commit 0cc77c89 ("Fix #8109: Plan/Performance regression ...") + [05.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.909; 5.0.3.1668. """ import locale import re @@ -73,6 +76,7 @@ def replace_leading(source, char="."): stripped = source.lstrip() return char * (len(source) - len(stripped)) + stripped +@pytest.mark.trace @pytest.mark.version('>=5.0') def test_1(act: Action, capsys): @@ -135,8 +139,10 @@ def test_1(act: Action, capsys): -- https://github.com/FirebirdSQL/firebird/commit/5df6668c7bf5a4b27e15f687f8c6cc40e260ced8 -- (Allow computable but non-invariant lists to be used for index lookup) -- See also: tests/functional/tabloid/test_e260ced8.py - -- Here "Index "TMAIN_X" List Scan (full match)" will be! - -- Old: "Index "TMAIN_X" Range Scan (full match)" + -- ######################################################################################## + -- NOTE! + -- Before 03-feb-2025 "Index "TMAIN_X" List Scan (full match)" was here, but since 0cc77c89 + -- bitmap_Or for two scans will be performed ("Index "TMAIN_X" Range Scan (full match)"). and m0a.x in (dx.y, dx.z) -- ### ATTENTION ### ) ) @@ -188,7 +194,7 @@ def test_1(act: Action, capsys): if start_show and line.rstrip(): print( replace_leading(line,'.') ) - expected_stdout = f""" + expected_stdout_5x = """ Sub-query (invariant) ....-> Filter ........-> Aggregate @@ -216,8 +222,11 @@ def test_1(act: Action, capsys): Sub-query ....-> Filter ........-> Table "TMAIN" as "R M0A" Access By ID - ............-> Bitmap - ................-> Index "TMAIN_X" List Scan (full match) + ............-> Bitmap Or + ................-> Bitmap + ....................-> Index "TMAIN_X" Range Scan (full match) + ................-> Bitmap + ....................-> Index "TMAIN_X" Range Scan (full match) Select Expression(line, column) ....-> Singularity Check ........-> Aggregate @@ -274,6 +283,95 @@ def test_1(act: Action, capsys): ......0 ms """ - act.expected_stdout = expected_stdout + expected_stdout_6x = """ + Sub-query (invariant) + ....-> Filter + ........-> Aggregate + ............-> Table "PUBLIC"."TDETL" as "K" "DY" Access By ID + ................-> Index "PUBLIC"."TDETL_FK" Full Scan + Cursor "K"(line, column) + ....-> Filter (preliminary) + ........-> Nested Loop Join (inner) + ............-> Table "PUBLIC"."TMAIN" as "K" "M4" Full Scan + ............-> Filter + ................-> Table "PUBLIC"."TDETL" as "K" "D4" "DX" Access By ID + ....................-> Bitmap And + ........................-> Bitmap And + ............................-> Bitmap + ................................-> Index "PUBLIC"."TDETL_Z" Range Scan (lower bound: 1/1) + ............................-> Bitmap + ................................-> Index "PUBLIC"."TDETL_Y" Range Scan (upper bound: 1/1) + ........................-> Bitmap + ............................-> Index "PUBLIC"."TDETL_FK" Range Scan (full match) + Sub-query + ....-> Filter + ........-> Table "PUBLIC"."TMAIN" as "M0" Access By ID + ............-> Bitmap + ................-> Index "PUBLIC"."TMAIN_X" Range Scan (lower bound: 1/1) + Sub-query + ....-> Filter + ........-> Table "PUBLIC"."TMAIN" as "R" "M0A" Access By ID + ............-> Bitmap Or + ................-> Bitmap + ....................-> Index "PUBLIC"."TMAIN_X" Range Scan (full match) + ................-> Bitmap + ....................-> Index "PUBLIC"."TMAIN_X" Range Scan (full match) + Select Expression(line, column) + ....-> Singularity Check + ........-> Aggregate + ............-> Filter + ................-> Recursion + ....................-> Filter + ........................-> Table "PUBLIC"."TDETL" as "R" "D0" Access By ID + ............................-> Bitmap + ................................-> Index "PUBLIC"."TDETL_FK" Range Scan (full match) + ....................-> Filter + ........................-> Table "PUBLIC"."TDETL" as "R" "DX" Access By ID + ............................-> Bitmap + ................................-> Index "PUBLIC"."TDETL_FK" Range Scan (full match) + Sub-query(line, column) + ....-> Singularity Check + ........-> Aggregate + ............-> Table "PUBLIC"."TMAIN" as "M1A" Full Scan + Sub-query(line, column) + ....-> Singularity Check + ........-> Aggregate + ............-> Table "PUBLIC"."TMAIN" as "M1B" Access By ID + ................-> Index "PUBLIC"."TMAIN_X" Full Scan + Sub-query(line, column) + ....-> Singularity Check + ........-> Aggregate + ............-> Table "PUBLIC"."TDETL" as "D1B" Access By ID + ................-> Index "PUBLIC"."TDETL_FK" Full Scan + Sub-query(line, column) + ....-> Singularity Check + ........-> Aggregate + ............-> Table "PUBLIC"."TDETL" as "D1C" Full Scan + Select Expression(line, column) + ....-> Aggregate + ........-> Nested Loop Join (inner) + ............-> Filter + ................-> Table "PUBLIC"."TMAIN" as "M2" Access By ID + ....................-> Index "PUBLIC"."TMAIN_PK" Full Scan + ........................-> Bitmap + ............................-> Index "PUBLIC"."TMAIN_X" Range Scan (lower bound: 1/1) + ............-> Filter + ................-> Table "PUBLIC"."TDETL" as "D" Access By ID + ....................-> Bitmap + ........................-> Index "PUBLIC"."TDETL_PK" Unique Scan + Sub-query + ....-> Filter + ........-> Table "PUBLIC"."TDETL" as "D" Access By ID + ............-> Bitmap + ................-> Index "PUBLIC"."TDETL_FK" Range Scan (full match) + Select Expression(line, column) + ....-> Filter + ........-> Table "PUBLIC"."TMAIN" as "M3" Access By ID + ............-> Bitmap + ................-> Index "PUBLIC"."TMAIN_X" Range Scan (lower bound: 1/1) + ......0 ms + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7466_test.py b/tests/bugs/gh_7466_test.py index 641a788d..d0510eea 100644 --- a/tests/bugs/gh_7466_test.py +++ b/tests/bugs/gh_7466_test.py @@ -14,36 +14,39 @@ No errors must present in the trace log. All created units must be specified in blocks related to compilation. NOTES: [17-aug-2023] pzotov - ::: NB ::: - 0. This test DOES NOT check tracking of plans for queries inside those PSQL modules (i.e. strarting ticket issue, - see: https://github.com/FirebirdSQL/firebird/pull/7466#issue-1564439735 ). - SEPARATE TEST WILL BE IMPLEMENTED FOR THAT. - 1. It must be noted that the term 'COMPILE' means parsing of BLR code into an execution tree, i.e. this action - occurs when unit code is loaded into metadata cache. - 2. Procedures and functions are loaded into metadata cache immediatelly when they are created. - 3. Triggers are loaded into metadata cache in 'deferred' way, when something occurs that causes trigger to fire. - So, DML trigger will fire when we do (for example) INSERT, DB_level trigger - when we do some action on DB level - (e.g. connect/disconnect), and similar to DDL trigger. - 4. Currently there is no way to specify in the trace what EXACT type of DDL trigger fired. It is shown as "AFTER DDL". - 5. Lot of system-related triggers are displayed in the trace log during creating user-defined units: - Trigger RDB$TRIGGER_26 FOR RDB$RELATION_CONSTRAINTS - Trigger RDB$TRIGGER_18 FOR RDB$INDEX_SEGMENTS (BEFORE UPDATE) - Trigger RDB$TRIGGER_8 FOR RDB$USER_PRIVILEGES (BEFORE DELETE) - etc. Test ignores them and takes in account only triggers that have been creates by "our" SQL script. - 6. User-defined DDL trigger will be loaded into metadata cache MULTIPLE times (three in this test: for create view, - its altering and its dropping - although there is no re-connect between these actions). This is conisdered as bug, - see: https://github.com/FirebirdSQL/firebird/pull/7426 (currently it is not yet fixed). - - Checked on 5.0.0.1164. - Thanks to dimitr for explanations. - Discussed with dimitr, letters 17.08.2023. + ::: NB ::: + 0. This test DOES NOT check tracking of plans for queries inside those PSQL modules (i.e. strarting ticket issue, + see: https://github.com/FirebirdSQL/firebird/pull/7466#issue-1564439735 ). + SEPARATE TEST WILL BE IMPLEMENTED FOR THAT. + 1. The term 'COMPILE' means parsing of BLR code into an execution tree, i.e. this action + occurs when unit code is loaded into metadata cache. + 2. Procedures and functions are loaded into metadata cache immediatelly when they are created. + 3. Triggers are loaded into metadata cache in 'deferred' way, when something occurs that causes trigger to fire. + So, DML trigger will fire when we do (for example) INSERT, DB_level trigger - when we do some action on DB level + (e.g. connect/disconnect), and similar to DDL trigger. + 4. Currently there is no way to specify in the trace what EXACT type of DDL trigger fired. It is shown as "AFTER DDL". + 5. Lot of system-related triggers are displayed in the trace log during creating user-defined units: + Trigger RDB$TRIGGER_26 FOR RDB$RELATION_CONSTRAINTS + Trigger RDB$TRIGGER_18 FOR RDB$INDEX_SEGMENTS (BEFORE UPDATE) + Trigger RDB$TRIGGER_8 FOR RDB$USER_PRIVILEGES (BEFORE DELETE) + etc. Test ignores them and takes in account only triggers that have been creates by "our" SQL script. + 6. User-defined DDL trigger will be loaded into metadata cache MULTIPLE times (three in this test: for create view, + its altering and its dropping - although there is no re-connect between these actions). This is conisdered as bug, + see: https://github.com/FirebirdSQL/firebird/pull/7426 (currently it is not yet fixed). + + Checked on 5.0.0.1164. + Thanks to dimitr for explanations. + Discussed with dimitr, letters 17.08.2023. [06-sep-2023] pzotov - Changed expected output: DDL trigger is loaded into metadata cache only once, so we have to check only SINGLE - occurence of "Trigger TRG_DDL (AFTER DDL)" event. - See also: https://github.com/FirebirdSQL/firebird/commit/00c2d10102468d5494b413c0de295079f62a27ec - - Checkec on 5.0.0.1190 + Changed expected output: DDL trigger is loaded into metadata cache only once, so we have to check only SINGLE + occurence of "Trigger TRG_DDL (AFTER DDL)" event. + See also: https://github.com/FirebirdSQL/firebird/commit/00c2d10102468d5494b413c0de295079f62a27ec + Checked on 5.0.0.1190 + [05.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.909; 5.0.3.1668. """ import locale import re @@ -61,9 +64,15 @@ 'log_trigger_compile = true', ] -allowed_patterns = [ ' ERROR AT ', 'Trigger TRG_', 'Procedure (SP_TEST|PG_TEST.PG_SP_WORKER)', 'Function (FN_TEST|PG_TEST.PG_FN_WORKER)' ] +allowed_patterns = [ + ' ERROR AT ', + 'Trigger ("PUBLIC".)?(")?TRG_', + 'Procedure ("PUBLIC".)?(")?(SP_TEST|PG_TEST(")?.(")?PG_SP_WORKER(")?)', + 'Function ("PUBLIC".)?(")?(FN_TEST|PG_TEST(")?.(")?PG_FN_WORKER)' +] allowed_patterns = [ re.compile(r, re.IGNORECASE) for r in allowed_patterns] +@pytest.mark.trace @pytest.mark.version('>=5.0') def test_1(act: Action, capsys): @@ -266,7 +275,7 @@ def test_1(act: Action, capsys): if p.search(line): print(line.strip()) - expected_stdout = f""" + expected_stdout_5x = f""" Procedure SP_TEST: Procedure PG_TEST.PG_SP_WORKER: Function FN_TEST: @@ -276,6 +285,16 @@ def test_1(act: Action, capsys): Trigger TRG_DDL (AFTER DDL): """ - act.expected_stdout = expected_stdout + expected_stdout_6x = f""" + Procedure "PUBLIC"."SP_TEST": + Procedure "PUBLIC"."PG_TEST"."PG_SP_WORKER": + Function "PUBLIC"."FN_TEST": + Function "PUBLIC"."PG_TEST"."PG_FN_WORKER": + Trigger "PUBLIC"."TRG_DB_CONN" (ON CONNECT): + Trigger "PUBLIC"."TRG_TEST_BIU" FOR "PUBLIC"."TEST" (BEFORE INSERT): + Trigger "PUBLIC"."TRG_DDL" (AFTER DDL): + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7499_test.py b/tests/bugs/gh_7499_test.py index 94583494..88d44f33 100644 --- a/tests/bugs/gh_7499_test.py +++ b/tests/bugs/gh_7499_test.py @@ -11,9 +11,12 @@ Restore from this .fbk must finish with WARNING and display name of that index. NOTES: [30.03.2023] pzotov - Unfortunately, I could not find DDL that leads not only to warning (on FB builds after fix) - but also to "gbak: ERROR" if we try to restore from this .fbk on FB builds *before* this - problem was fixed: 'gbak -rep' on all major FB just silently finished w/o any message. + Unfortunately, I could not find DDL that leads not only to warning (on FB builds after fix) + but also to "gbak: ERROR" if we try to restore from this .fbk on FB builds *before* this + problem was fixed: 'gbak -rep' on all major FB just silently finished w/o any message. + [04.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' and variables - to be substituted in expected_* on FB 6.x + Checked on 6.0.0.909; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -28,12 +31,6 @@ act = python_act('db') -expected_stdout = """ - gbak: WARNING:index T2_FLD cannot be used in the specified plan - gbak:finishing, closing, and going home - gbak:adjusting the ONLINE and FORCED WRITES flags -""" - fbk_file = temp_file('gh_7499.tmp.fbk') @pytest.mark.version('>=3.0.11') @@ -56,6 +53,14 @@ def test_1(act: Action, fbk_file: Path, capsys): if act.match_any(line.strip(), allowed_patterns): print(line) + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + INDEX_NAME = 'T2_FLD' if act.is_version('<6') else f'{SQL_SCHEMA_PREFIX}"T2_FLD"' + expected_stdout = f""" + gbak: WARNING:index {INDEX_NAME} cannot be used in the specified plan + gbak:finishing, closing, and going home + gbak:adjusting the ONLINE and FORCED WRITES flags + """ act.expected_stdout = expected_stdout act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7506_test.py b/tests/bugs/gh_7506_test.py index b33fd999..537d2de5 100644 --- a/tests/bugs/gh_7506_test.py +++ b/tests/bugs/gh_7506_test.py @@ -9,24 +9,31 @@ 2) user; 1) role. NOTES: - Checked on 5.0.0.1030. + [15.05.2025] pzotov + Additional subs for suppress excessive lines from 'show grants' output: remain only rows that contain prefix 'TMP_GH_7506_'. + Replaced expected-out text: use f-syntax with reference to user/role names provided by action instance instead of hardcoding them. + Checked on 6.0.0.778; 5.0.3.1649. Initial check was 24-apr-2023 on 5.0.0.1030. + [04.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' and variables - to be substituted in expected_* on FB 6.x + Checked on 6.0.0.909; 5.0.3.1668. """ import pytest from firebird.qa import * -tmp_user_boss = user_factory('db', name='tmp$gh_7506_john', password='123', plugin = 'Srp') -tmp_user_mngr = user_factory('db', name='tmp$gh_7506_mike', password='456', plugin = 'Srp') +tmp_user_boss = user_factory('db', name='tmp_gh_7506_john', password='123', plugin = 'Srp') +tmp_user_mngr = user_factory('db', name='tmp_gh_7506_mike', password='456', plugin = 'Srp') -tmp_role_boss = role_factory('db', name='tmp$gh_7506_boss') -tmp_role_mngr = role_factory('db', name='tmp$gh_7506_mngr') +tmp_role_boss = role_factory('db', name='tmp_gh_7506_boss') +tmp_role_mngr = role_factory('db', name='tmp_gh_7506_mngr') db = db_factory() -act = python_act('db', substitutions=[('[ \t]+', ' ')]) +act = python_act('db', substitutions = [ ('[ \t]+', ' '), ('^((?!TMP_GH_7506_).)*$', '') ] ) @pytest.mark.version('>=5.0') def test_1(act: Action, tmp_user_boss: User, tmp_user_mngr: User, tmp_role_boss: Role, tmp_role_mngr: Role, capsys): + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'PUBLIC.' test_user_sql = f""" recreate table test(id int primary key, f01 int, f02 int, f03 int, f04 int, f05 int, f06 int); recreate view v_test as select * from test; @@ -50,13 +57,11 @@ def test_1(act: Action, tmp_user_boss: User, tmp_user_mngr: User, tmp_role_boss: """ act.expected_stdout = f""" - /* Grant permissions for this database */ - GRANT DELETE, INSERT, UPDATE (F01, F02, F03) ON TEST TO USER {tmp_user_boss.name.upper()} - GRANT SELECT, UPDATE (F04, F05, F06) ON TEST TO USER {tmp_user_boss.name.upper()} WITH GRANT OPTION - GRANT SELECT ON TEST TO USER {tmp_user_mngr.name.upper()} GRANTED BY {tmp_user_boss.name.upper()} - GRANT UPDATE (F01, F03) ON TEST TO USER {tmp_user_mngr.name.upper()} - GRANT UPDATE (F04, F05, F06) ON TEST TO USER {tmp_user_mngr.name.upper()} GRANTED BY {tmp_user_boss.name.upper()} - GRANT ALL ON TEST TO VIEW V_TEST + GRANT DELETE, INSERT, UPDATE (F01, F02, F03) ON {SQL_SCHEMA_PREFIX}TEST TO USER {tmp_user_boss.name.upper()} + GRANT SELECT, UPDATE (F04, F05, F06) ON {SQL_SCHEMA_PREFIX}TEST TO USER {tmp_user_boss.name.upper()} WITH GRANT OPTION + GRANT SELECT ON {SQL_SCHEMA_PREFIX}TEST TO USER {tmp_user_mngr.name.upper()} GRANTED BY {tmp_user_boss.name.upper()} + GRANT UPDATE (F01, F03) ON {SQL_SCHEMA_PREFIX}TEST TO USER {tmp_user_mngr.name.upper()} + GRANT UPDATE (F04, F05, F06) ON {SQL_SCHEMA_PREFIX}TEST TO USER {tmp_user_mngr.name.upper()} GRANTED BY {tmp_user_boss.name.upper()} """ act.isql(input = test_user_sql, combine_output = True) assert act.clean_stdout == act.clean_expected_stdout @@ -90,13 +95,12 @@ def test_1(act: Action, tmp_user_boss: User, tmp_user_mngr: User, tmp_role_boss: """ act.expected_stdout = f""" - /* Grant permissions for this database */ - GRANT DELETE, INSERT, UPDATE (F01, F02, F03) ON TEST TO ROLE TMP$GH_7506_BOSS - GRANT SELECT, UPDATE (F04, F05, F06) ON TEST TO ROLE TMP$GH_7506_BOSS WITH GRANT OPTION - GRANT SELECT ON TEST TO ROLE TMP$GH_7506_MNGR GRANTED BY TMP$GH_7506_JOHN - GRANT UPDATE (F01, F03) ON TEST TO ROLE TMP$GH_7506_MNGR - GRANT UPDATE (F04, F05, F06) ON TEST TO ROLE TMP$GH_7506_MNGR GRANTED BY TMP$GH_7506_JOHN - GRANT TMP$GH_7506_BOSS TO TMP$GH_7506_JOHN + GRANT DELETE, INSERT, UPDATE (F01, F02, F03) ON {SQL_SCHEMA_PREFIX}TEST TO ROLE {tmp_role_boss.name} + GRANT SELECT, UPDATE (F04, F05, F06) ON {SQL_SCHEMA_PREFIX}TEST TO ROLE {tmp_role_boss.name} WITH GRANT OPTION + GRANT SELECT ON {SQL_SCHEMA_PREFIX}TEST TO ROLE {tmp_role_mngr.name} GRANTED BY {tmp_user_boss.name} + GRANT UPDATE (F01, F03) ON {SQL_SCHEMA_PREFIX}TEST TO ROLE {tmp_role_mngr.name} + GRANT UPDATE (F04, F05, F06) ON {SQL_SCHEMA_PREFIX}TEST TO ROLE {tmp_role_mngr.name} GRANTED BY {tmp_user_boss.name} + GRANT {tmp_role_boss.name} TO {tmp_user_boss.name} """ act.isql(input = test_role_sql, combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7517_test.py b/tests/bugs/gh_7517_test.py index 44d9b0b4..8dd28dd6 100644 --- a/tests/bugs/gh_7517_test.py +++ b/tests/bugs/gh_7517_test.py @@ -6,9 +6,12 @@ TITLE: Successful compiling of procedure with wrong PLAN(s) used by some of its statement(s) NOTES: [29.03.2023] pzotov - Code for reproducing was provided by dimitr, letter 29.03.2023 09:46. - Confirmed bug on 3.0.11.33665. - Cheched on 5.0.0.978; 4.0.3.2913; 3.0.11.33666 - all fine. + Code for reproducing was provided by dimitr, letter 29.03.2023 09:46. + Confirmed bug on 3.0.11.33665. + Checked on 5.0.0.978; 4.0.3.2913; 3.0.11.33666 - all fine. + [04.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' and variables - to be substituted in expected_* on FB 6.x + Checked on 6.0.0.909; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -33,15 +36,17 @@ from t1; """ -expected_stdout = """ - Statement failed, SQLSTATE = 42000 - index T2_FLD cannot be used in the specified plan -""" - act = isql_act('db', test_script) @pytest.mark.version('>=3.0.11') def test_1(act: Action): + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + INDEX_NAME = 'T2_FLD' if act.is_version('<6') else f'{SQL_SCHEMA_PREFIX}"T2_FLD"' + expected_stdout = f""" + Statement failed, SQLSTATE = 42000 + index {INDEX_NAME} cannot be used in the specified plan + """ act.expected_stdout = expected_stdout act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7558_test.py b/tests/bugs/gh_7558_test.py index 219e1376..ca790c37 100644 --- a/tests/bugs/gh_7558_test.py +++ b/tests/bugs/gh_7558_test.py @@ -35,6 +35,7 @@ act = python_act('db', substitutions = substitutions) act_non_existing_database = python_act('db_non_existing_database') +@pytest.mark.trace @pytest.mark.version('>=4.0.3') def test_1(act: Action, act_non_existing_database: Action, tmp_user: User, tmp_role: Role, tmp_usr2: User, capsys): diff --git a/tests/bugs/gh_7568_test.py b/tests/bugs/gh_7568_test.py index 62c3ce1b..c8d67987 100644 --- a/tests/bugs/gh_7568_test.py +++ b/tests/bugs/gh_7568_test.py @@ -6,9 +6,13 @@ TITLE: Equivalence of boolean condition in partial index NOTES: [03.02.2024] pzotov - Test is based on https://github.com/FirebirdSQL/firebird/pull/7987 - Confirmed problem on 6.0.0.244. - Checked on 6.0.0.247. + Test is based on https://github.com/FirebirdSQL/firebird/pull/7987 + Confirmed problem on 6.0.0.244. + Checked on 6.0.0.247. + [04.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.863. """ import pytest @@ -69,38 +73,42 @@ act = isql_act('db', test_script) -expected_stdout = """ - PLAN (TEST INDEX (TEST_IDX_OFFER_ASC)) - - PLAN (TEST INDEX (TEST_IDX_OFFER_ASC)) - - PLAN (TEST INDEX (TEST_IDX_OFFER_ASC)) - - PLAN (TEST INDEX (TEST_IDX_OFFER_ASC)) - - PLAN (TEST INDEX (TEST_IDX_OFFER_ASC)) - - PLAN (TEST INDEX (TEST_IDX_OFFER_ASC)) - - PLAN (TEST INDEX (TEST_IDX_OFFER_ASC)) - - PLAN (TEST INDEX (TEST_IDX_OFFER_DEC)) - - PLAN (TEST INDEX (TEST_IDX_OFFER_DEC)) - - PLAN (TEST INDEX (TEST_IDX_OFFER_DEC)) - - PLAN (TEST INDEX (TEST_IDX_OFFER_DEC)) - - PLAN (TEST INDEX (TEST_IDX_OFFER_DEC)) - - PLAN (TEST INDEX (TEST_IDX_OFFER_DEC)) - - PLAN (TEST INDEX (TEST_IDX_OFFER_DEC)) -""" - @pytest.mark.version('>=6.0') def test_1(act: Action): - act.expected_stdout = expected_stdout + + expected_stdout_5x = """ + PLAN (TEST INDEX (TEST_IDX_OFFER_ASC)) + PLAN (TEST INDEX (TEST_IDX_OFFER_ASC)) + PLAN (TEST INDEX (TEST_IDX_OFFER_ASC)) + PLAN (TEST INDEX (TEST_IDX_OFFER_ASC)) + PLAN (TEST INDEX (TEST_IDX_OFFER_ASC)) + PLAN (TEST INDEX (TEST_IDX_OFFER_ASC)) + PLAN (TEST INDEX (TEST_IDX_OFFER_ASC)) + PLAN (TEST INDEX (TEST_IDX_OFFER_DEC)) + PLAN (TEST INDEX (TEST_IDX_OFFER_DEC)) + PLAN (TEST INDEX (TEST_IDX_OFFER_DEC)) + PLAN (TEST INDEX (TEST_IDX_OFFER_DEC)) + PLAN (TEST INDEX (TEST_IDX_OFFER_DEC)) + PLAN (TEST INDEX (TEST_IDX_OFFER_DEC)) + PLAN (TEST INDEX (TEST_IDX_OFFER_DEC)) + """ + expected_stdout_6x = """ + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_IDX_OFFER_ASC")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_IDX_OFFER_ASC")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_IDX_OFFER_ASC")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_IDX_OFFER_ASC")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_IDX_OFFER_ASC")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_IDX_OFFER_ASC")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_IDX_OFFER_ASC")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_IDX_OFFER_DEC")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_IDX_OFFER_DEC")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_IDX_OFFER_DEC")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_IDX_OFFER_DEC")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_IDX_OFFER_DEC")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_IDX_OFFER_DEC")) + PLAN ("PUBLIC"."TEST" INDEX ("PUBLIC"."TEST_IDX_OFFER_DEC")) + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7604_test.py b/tests/bugs/gh_7604_test.py index c2dfb82c..9661b776 100644 --- a/tests/bugs/gh_7604_test.py +++ b/tests/bugs/gh_7604_test.py @@ -12,6 +12,9 @@ Added 'SQLSTATE' in substitutions: runtime error must not be filtered out by '?!(...)' pattern ("negative lookahead assertion", see https://docs.python.org/3/library/re.html#regular-expression-syntax). Added 'combine_output = True' in order to see SQLSTATE if any error occurs. + [04.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' and variables - to be substituted in expected_* on FB 6.x + Checked on 6.0.0.909; 5.0.3.1668; 4.0.6.3214. """ import locale import pytest @@ -42,8 +45,9 @@ def test_1(act: Action): select sp_test_func() as runtotal from rdb$database rows 0; """ - expected_stdout = """ - 01: sqltype: 520 BLOB Nullable scale: 0 subtype: 1 len: 8 charset: 52 WIN1251 + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'SYSTEM.' + expected_stdout = f""" + 01: sqltype: 520 BLOB Nullable scale: 0 subtype: 1 len: 8 charset: 52 {SQL_SCHEMA_PREFIX}WIN1251 """ act.expected_stdout = expected_stdout diff --git a/tests/bugs/gh_7610_test.py b/tests/bugs/gh_7610_test.py index ee5c466a..a2bdb530 100644 --- a/tests/bugs/gh_7610_test.py +++ b/tests/bugs/gh_7610_test.py @@ -14,15 +14,18 @@ Query 'select * from test' must fail with 'no permission for SELECT' error. NOTES: [03.06.2023] pzotov - BOTH problems (ability to query table and random numbers in rdb$system_privileges) could be reproduced only in OLD - snapshots, not in recent ones! - In FB 4.x last snapshot where *both* problems present is 4.0.0.2571 (20-aug-2021). In 4.0.0.2573 only problem with - random number in rdb$ exists, but user can no longer query table. - In 4.0.3.2948 (01-jun-2023) content of rdb$ is 0000000000000000. + BOTH problems (ability to query table and random numbers in rdb$system_privileges) could be reproduced only in OLD + snapshots, not in recent ones! + In FB 4.x last snapshot where *both* problems present is 4.0.0.2571 (20-aug-2021). In 4.0.0.2573 only problem with + random number in rdb$ exists, but user can no longer query table. + In 4.0.3.2948 (01-jun-2023) content of rdb$ is 0000000000000000. - In FB 5.x situation is similar: last snapshot with *both* problems is 5.0.0.1000 (02-apr-2023), and since 5.0.0.1001 - one may see only problem with numbers in rdb$, but they look 'constant': 3400000000000000, and this is so up to 5.0.0.1063. - Since 5.0.0.1065 (01-jun-2023) content of rdb$ is 0000000000000000. + In FB 5.x situation is similar: last snapshot with *both* problems is 5.0.0.1000 (02-apr-2023), and since 5.0.0.1001 + one may see only problem with numbers in rdb$, but they look 'constant': 3400000000000000, and this is so up to 5.0.0.1063. + Since 5.0.0.1065 (01-jun-2023) content of rdb$ is 0000000000000000. + [04.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' and variables - to be substituted in expected_* on FB 6.x + Checked on 6.0.0.909; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -81,6 +84,8 @@ def test_1(act: Action, fbk_file: Path, tmp_user: User, tmp_role: Role, capsys): select * from test; """ + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + TABLE_TEST_NAME = 'TEST' if act.is_version('<6') else f'{SQL_SCHEMA_PREFIX}"TEST"' expected_stdout = f""" MON$USER {tmp_user.name.upper()} MON$ROLE {tmp_role.name.upper()} @@ -89,7 +94,7 @@ def test_1(act: Action, fbk_file: Path, tmp_user: User, tmp_role: Role, capsys): RDB$ROLE_NAME {tmp_role.name.upper()} RDB$SYSTEM_PRIVILEGES 0000000000000000 Statement failed, SQLSTATE = 28000 - no permission for SELECT access to TABLE TEST + no permission for SELECT access to TABLE {TABLE_TEST_NAME} -Effective user is {tmp_user.name.upper()} """ diff --git a/tests/bugs/gh_7647_test.py b/tests/bugs/gh_7647_test.py index 064948ef..07d243ec 100644 --- a/tests/bugs/gh_7647_test.py +++ b/tests/bugs/gh_7647_test.py @@ -12,6 +12,11 @@ "ValueError: Incorrect ARRAY field value." raises on firebird.driver: 1.10.1 / firebird-qa-0.19.2 Added restriction for major versions that are allowed to be checked: currently only 3.x and 4.x. See also: https://github.com/FirebirdSQL/firebird/issues/7862 + + [21.07.2024] pzotov + Problem with 6.x has been fixed in 6.0.0.346, commit date 07.05.2024: + https://github.com/FirebirdSQL/firebird/commit/17b007d14f8ccc6cfba0d63a3b2f21622ced20d0 + Removed upper limit restriction for major version. """ import pytest @@ -25,7 +30,7 @@ act = python_act('db') -@pytest.mark.version('>=3.0.12,<5.0') +@pytest.mark.version('>=3.0.12') def test_1(act: Action, capsys): with act.db.connect() as con: cur = con.cursor() diff --git a/tests/bugs/gh_7652_test.py b/tests/bugs/gh_7652_test.py new file mode 100644 index 00000000..973e665f --- /dev/null +++ b/tests/bugs/gh_7652_test.py @@ -0,0 +1,130 @@ +#coding:utf-8 + +""" +ID: issue-7652 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/7652 +TITLE: Make the profiler store aggregated requests by default, with option for detailed store +DESCRIPTION: + Test executes two times script with loop which does call to trivial stored procedure. + First time we launch profiler without any parameters except session name, then - with 'DETAILED_REQUESTS'. + In both cases (after finish profiling) we run SQL against tables plg$prof_requests and plg$prof_psql_stats + and obtain TWO aggregated values: count number of records with request_id = 0 and request_id <> 0. + For case when we call profiler WITHOUT any parameters (except session name): + * before this ticket was implemented (for builds with date <= 27-JUN-2023) data in both mentioned tables + did not contain records with request_id = 0 (i.e. all rows had request_id > 0); + * after implementation ( https://github.com/FirebirdSQL/firebird/commit/00bb8e4581b66b624de47bfcde6b248e163ec6c1 ) + all rows in tables plg$prof_requests and plg$prof_psql_stats have request_id > 0. + For case when we call profiler WITH 'DETAILED_REQUESTS': + * builds with date <= 27-JUN-2023 could not be used, exception raised: + Statement failed, SQLSTATE = 42000 + validation error for variable ATTACHMENT_ID, value "*** null ***" + -At function 'RDB$PROFILER.START_SESSION' + * after implementation all rows in tables plg$prof_requests and plg$prof_psql_stats have request_id > 0. +NOTES: + Compared 5.0.0.1087 (26-JUN-2023) vs 5.0.0.1088 (27-JUN-2023) + Checked on 6.0.0.395 +""" +import pytest +from firebird.qa import * +import locale +import re + +db = db_factory() +act = python_act('db', substitutions=[('[ \t]+', ' ')]) + +def strip_white(value): + value = re.sub('(?m)^\\s+', '', value) + return re.sub('(?m)\\s+$', '', value) + +@pytest.mark.version('>=5.0') +def test_1(act: Action, capsys): + + actual_out = '' + test_sql = f""" + set bail on; + set list on; + set term ^; + create or alter procedure sp_rand_pair(a_i smallint) returns(r double precision, s varchar(36)) as + begin + if ( mod(a_i, 2) = 0 ) then + begin + r = -1; + s = ''; + end + else + begin + r = rand(); + s = uuid_to_char(gen_uuid()); + end + suspend; + end + ^ + set term ;^ + commit; + + --set echo on; + %(debug_message_sttm)s; + %(profiler_start_sttm)s; + --set echo off; + + set term ^; + execute block as + declare i int = 3; + declare r double precision; + declare s varchar(36); + begin + while (i > 0) do + begin + select r,s from sp_rand_pair( :i ) into r,s; + i = i - 1; + end + end + ^ + set term ;^ + execute procedure rdb$profiler.finish_session(true); + commit; + + set transaction read committed; + + select + iif( sum( iif(request_id = 0, 1, 0) ) > 0, 'NON_ZERO', 'ZERO' ) as requests_cnt_zero_request_id + ,iif( sum( iif(request_id = 0, 0, 1) ) > 0, 'NON_ZERO', 'ZERO' ) as requests_cnt_non_zero_req_id + from plg$prof_requests; + + select + iif( sum( iif(request_id = 0, 1, 0) ) > 0, 'NON_ZERO', 'ZERO' ) as psql_stats_cnt_zero_request_id + ,iif( sum( iif(request_id = 0, 0, 1) ) > 0, 'NON_ZERO', 'ZERO' ) as psql_stats_cnt_non_zero_req_id + from plg$prof_psql_stats; + """ + + + debug_message_sttm = "select 'DETAILED_REQUESTS: OFF' as msg from rdb$database" + profiler_start_sttm = "select sign(rdb$profiler.start_session('prof_ssn_no_details')) from rdb$database" + act.isql(input = test_sql % locals(), combine_output = True) + actual_out += act.clean_stdout + '\n' + act.reset() + + #------------------------------------------------------------------------------------------------------- + + debug_message_sttm = "select 'DETAILED_REQUESTS: ON' as msg from rdb$database" + profiler_start_sttm = "select sign(rdb$profiler.start_session('prof_ssn_with_details', null, null, null, 'DETAILED_REQUESTS')) from rdb$database" + act.isql(input = test_sql % locals(), combine_output = True) + actual_out += act.clean_stdout + '\n' + act.reset() + + expected_out = f""" + MSG DETAILED_REQUESTS: OFF + SIGN 1 + REQUESTS_CNT_ZERO_REQUEST_ID NON_ZERO + REQUESTS_CNT_NON_ZERO_REQ_ID ZERO + PSQL_STATS_CNT_ZERO_REQUEST_ID NON_ZERO + PSQL_STATS_CNT_NON_ZERO_REQ_ID ZERO + + MSG DETAILED_REQUESTS: ON + SIGN 1 + REQUESTS_CNT_ZERO_REQUEST_ID NON_ZERO + REQUESTS_CNT_NON_ZERO_REQ_ID NON_ZERO + PSQL_STATS_CNT_ZERO_REQUEST_ID NON_ZERO + PSQL_STATS_CNT_NON_ZERO_REQ_ID NON_ZERO + """ + assert strip_white(actual_out) == strip_white(expected_out) diff --git a/tests/bugs/gh_7670_test.py b/tests/bugs/gh_7670_test.py index 536887c3..803cfc29 100644 --- a/tests/bugs/gh_7670_test.py +++ b/tests/bugs/gh_7670_test.py @@ -6,8 +6,12 @@ TITLE: Cursor name can duplicate parameter and variable names in procedures and functions DESCRIPTION: NOTES: - Confirmed bug on 4.0.3.2957, 5.0.0.1100: all statements from this test did not issue error. - Checked on 4.0.3.2966, 5.0.0.1121: all OK. + Confirmed bug on 4.0.3.2957, 5.0.0.1100: all statements from this test did not issue error. + Checked on 4.0.3.2966, 5.0.0.1121: all OK. + [04.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.909; 5.0.3.1668. """ import pytest @@ -83,36 +87,65 @@ act = isql_act('db', test_script) -expected_stdout = """ - Statement failed, SQLSTATE = 42000 - CREATE FUNCTION F1 failed - -Dynamic SQL Error - -SQL error code = -637 - -duplicate specification of A_NAME_IN_STANDALONE_FUNC - not supported - Statement failed, SQLSTATE = 42000 - CREATE PROCEDURE P1 failed - -Dynamic SQL Error - -SQL error code = -637 - -duplicate specification of A_NAME_IN_STANDALONE_PROC - not supported - Statement failed, SQLSTATE = 42000 - CREATE PROCEDURE P2 failed - -Dynamic SQL Error - -SQL error code = -637 - -duplicate specification of O_NAME_IN_STANDALONE_PROC - not supported - Statement failed, SQLSTATE = 42000 - RECREATE PACKAGE BODY PG1 failed - -Dynamic SQL Error - -SQL error code = -637 - -duplicate specification of A_NAME_IN_PACKAGED_FUNC - not supported - Statement failed, SQLSTATE = 42000 - RECREATE PACKAGE BODY PG2 failed - -Dynamic SQL Error - -SQL error code = -637 - -duplicate specification of A_NAME_IN_PACKAGED_PROC - not supported -""" - @pytest.mark.version('>=4.0.2') def test_1(act: Action): - act.expected_stdout = expected_stdout + + expected_stdout_5x = """ + Statement failed, SQLSTATE = 42000 + CREATE FUNCTION F1 failed + -Dynamic SQL Error + -SQL error code = -637 + -duplicate specification of A_NAME_IN_STANDALONE_FUNC - not supported + Statement failed, SQLSTATE = 42000 + CREATE PROCEDURE P1 failed + -Dynamic SQL Error + -SQL error code = -637 + -duplicate specification of A_NAME_IN_STANDALONE_PROC - not supported + Statement failed, SQLSTATE = 42000 + CREATE PROCEDURE P2 failed + -Dynamic SQL Error + -SQL error code = -637 + -duplicate specification of O_NAME_IN_STANDALONE_PROC - not supported + Statement failed, SQLSTATE = 42000 + RECREATE PACKAGE BODY PG1 failed + -Dynamic SQL Error + -SQL error code = -637 + -duplicate specification of A_NAME_IN_PACKAGED_FUNC - not supported + Statement failed, SQLSTATE = 42000 + RECREATE PACKAGE BODY PG2 failed + -Dynamic SQL Error + -SQL error code = -637 + -duplicate specification of A_NAME_IN_PACKAGED_PROC - not supported + """ + + expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + CREATE FUNCTION "PUBLIC"."F1" failed + -Dynamic SQL Error + -SQL error code = -637 + -duplicate specification of "A_NAME_IN_STANDALONE_FUNC" - not supported + Statement failed, SQLSTATE = 42000 + CREATE PROCEDURE "PUBLIC"."P1" failed + -Dynamic SQL Error + -SQL error code = -637 + -duplicate specification of "A_NAME_IN_STANDALONE_PROC" - not supported + Statement failed, SQLSTATE = 42000 + CREATE PROCEDURE "PUBLIC"."P2" failed + -Dynamic SQL Error + -SQL error code = -637 + -duplicate specification of "O_NAME_IN_STANDALONE_PROC" - not supported + Statement failed, SQLSTATE = 42000 + RECREATE PACKAGE BODY "PUBLIC"."PG1" failed + -Dynamic SQL Error + -SQL error code = -637 + -duplicate specification of "A_NAME_IN_PACKAGED_FUNC" - not supported + Statement failed, SQLSTATE = 42000 + RECREATE PACKAGE BODY "PUBLIC"."PG2" failed + -Dynamic SQL Error + -SQL error code = -637 + -duplicate specification of "A_NAME_IN_PACKAGED_PROC" - not supported + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7675_test.py b/tests/bugs/gh_7675_test.py index 6abca3e0..b52439ec 100644 --- a/tests/bugs/gh_7675_test.py +++ b/tests/bugs/gh_7675_test.py @@ -8,9 +8,16 @@ Test only ckecks ability to use RDB$SQL package as it is described in the doc. More complex tests will be implemented later. NOTES: - [02.10.2023] pzotov - Checked on 6.0.0.65. + [17.11.2024] pzotov + Removed output of concrete data for checked query. + It is enough only to display content of SQLDA (lines with 'sqltype:' and 'name:') and SQLSTATE (if some error occurs). + Checked on 6.0.0.532. + [04.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.909. """ +import os import pytest from firebird.qa import * @@ -35,123 +42,85 @@ db = db_factory(init = init_sql) test_script = """ - set list on; - select * - from rdb$sql.explain( - q'{ - select m2.id, count(*) - from tmain m2 - join tdetl d using(id) - where m2.x > 0 - group by 1 - }' - ) p - order by p.plan_line; + set sqlda_display on; + select p.* + from rdb$sql.explain('select 1 from rdb$database') as p + rows 0 + ; """ -act = isql_act('db', test_script) - -expected_stdout = """ - PLAN_LINE 1 - RECORD_SOURCE_ID 7 - PARENT_RECORD_SOURCE_ID - LEVEL 0 - OBJECT_TYPE - PACKAGE_NAME - OBJECT_NAME - ALIAS - CARDINALITY - RECORD_LENGTH - KEY_LENGTH - ACCESS_PATH 0:4 - Select Expression - PLAN_LINE 2 - RECORD_SOURCE_ID 6 - PARENT_RECORD_SOURCE_ID 7 - LEVEL 1 - OBJECT_TYPE - PACKAGE_NAME - OBJECT_NAME - ALIAS - CARDINALITY 0.1000000000000000 - RECORD_LENGTH - KEY_LENGTH - ACCESS_PATH 0:5 - -> Aggregate - PLAN_LINE 3 - RECORD_SOURCE_ID 5 - PARENT_RECORD_SOURCE_ID 6 - LEVEL 2 - OBJECT_TYPE - PACKAGE_NAME - OBJECT_NAME - ALIAS - CARDINALITY 100.0000000000000 - RECORD_LENGTH - KEY_LENGTH - ACCESS_PATH 0:6 - -> Nested Loop Join (inner) - PLAN_LINE 4 - RECORD_SOURCE_ID 2 - PARENT_RECORD_SOURCE_ID 5 - LEVEL 3 - OBJECT_TYPE - PACKAGE_NAME - OBJECT_NAME - ALIAS - CARDINALITY 100.0000000000000 - RECORD_LENGTH - KEY_LENGTH - ACCESS_PATH 0:7 - -> Filter - PLAN_LINE 5 - RECORD_SOURCE_ID 1 - PARENT_RECORD_SOURCE_ID 2 - LEVEL 4 - OBJECT_TYPE 0 - PACKAGE_NAME - OBJECT_NAME TMAIN - ALIAS M2 - CARDINALITY 100.0000000000000 - RECORD_LENGTH - KEY_LENGTH - ACCESS_PATH 0:8 - -> Table "TMAIN" as "M2" Access By ID - -> Index "TMAIN_PK" Full Scan - -> Bitmap - -> Index "TMAIN_X" Range Scan (lower bound: 1/1) - PLAN_LINE 6 - RECORD_SOURCE_ID 4 - PARENT_RECORD_SOURCE_ID 5 - LEVEL 3 - OBJECT_TYPE - PACKAGE_NAME - OBJECT_NAME - ALIAS - CARDINALITY 1.000000000000000 - RECORD_LENGTH - KEY_LENGTH - ACCESS_PATH 0:9 - -> Filter - PLAN_LINE 7 - RECORD_SOURCE_ID 3 - PARENT_RECORD_SOURCE_ID 4 - LEVEL 4 - OBJECT_TYPE 0 - PACKAGE_NAME - OBJECT_NAME TDETL - ALIAS D - CARDINALITY 0.9999999999999999 - RECORD_LENGTH - KEY_LENGTH - ACCESS_PATH 0:a - -> Table "TDETL" as "D" Access By ID - -> Bitmap - -> Index "TDETL_PK" Unique Scan -""" +act = isql_act('db', test_script, substitutions=[('^((?!SQLSTATE|sqltype:|name:).)*$',''),('[ \t]+',' ')]) @pytest.mark.version('>=6.0') def test_1(act: Action): - act.expected_stdout = expected_stdout + + expected_stdout_5x = """ + 01: sqltype: 496 LONG scale: 0 subtype: 0 len: 4 + : name: PLAN_LINE alias: PLAN_LINE + + 02: sqltype: 580 INT64 scale: 0 subtype: 0 len: 8 + : name: RECORD_SOURCE_ID alias: RECORD_SOURCE_ID + + 03: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: PARENT_RECORD_SOURCE_ID alias: PARENT_RECORD_SOURCE_ID + + 04: sqltype: 496 LONG scale: 0 subtype: 0 len: 4 + : name: LEVEL alias: LEVEL + + 05: sqltype: 500 SHORT Nullable scale: 0 subtype: 0 len: 2 + : name: OBJECT_TYPE alias: OBJECT_TYPE + + 06: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 252 charset: 4 UTF8 + : name: PACKAGE_NAME alias: PACKAGE_NAME + + 07: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 252 charset: 4 UTF8 + : name: OBJECT_NAME alias: OBJECT_NAME + + 08: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 1020 charset: 4 UTF8 + : name: ALIAS alias: ALIAS + + 09: sqltype: 480 DOUBLE Nullable scale: 0 subtype: 0 len: 8 + : name: CARDINALITY alias: CARDINALITY + + 10: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + : name: RECORD_LENGTH alias: RECORD_LENGTH + + 11: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + : name: KEY_LENGTH alias: KEY_LENGTH + + 12: sqltype: 520 BLOB scale: 0 subtype: 1 len: 8 charset: 4 UTF8 + : name: ACCESS_PATH alias: ACCESS_PATH + """ + + expected_stdout_6x = """ + 01: sqltype: 496 LONG scale: 0 subtype: 0 len: 4 + : name: PLAN_LINE alias: PLAN_LINE + 02: sqltype: 580 INT64 scale: 0 subtype: 0 len: 8 + : name: RECORD_SOURCE_ID alias: RECORD_SOURCE_ID + 03: sqltype: 580 INT64 Nullable scale: 0 subtype: 0 len: 8 + : name: PARENT_RECORD_SOURCE_ID alias: PARENT_RECORD_SOURCE_ID + 04: sqltype: 496 LONG scale: 0 subtype: 0 len: 4 + : name: LEVEL alias: LEVEL + 05: sqltype: 500 SHORT Nullable scale: 0 subtype: 0 len: 2 + : name: OBJECT_TYPE alias: OBJECT_TYPE + 06: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 252 charset: 4 SYSTEM.UTF8 + : name: SCHEMA_NAME alias: SCHEMA_NAME + 07: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 252 charset: 4 SYSTEM.UTF8 + : name: PACKAGE_NAME alias: PACKAGE_NAME + 08: sqltype: 452 TEXT Nullable scale: 0 subtype: 0 len: 252 charset: 4 SYSTEM.UTF8 + : name: OBJECT_NAME alias: OBJECT_NAME + 09: sqltype: 448 VARYING Nullable scale: 0 subtype: 0 len: 1020 charset: 4 SYSTEM.UTF8 + : name: ALIAS alias: ALIAS + 10: sqltype: 480 DOUBLE Nullable scale: 0 subtype: 0 len: 8 + : name: CARDINALITY alias: CARDINALITY + 11: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + : name: RECORD_LENGTH alias: RECORD_LENGTH + 12: sqltype: 496 LONG Nullable scale: 0 subtype: 0 len: 4 + : name: KEY_LENGTH alias: KEY_LENGTH + 13: sqltype: 520 BLOB scale: 0 subtype: 1 len: 8 charset: 4 SYSTEM.UTF8 + : name: ACCESS_PATH alias: ACCESS_PATH + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7676_test.py b/tests/bugs/gh_7676_test.py new file mode 100644 index 00000000..4b186032 --- /dev/null +++ b/tests/bugs/gh_7676_test.py @@ -0,0 +1,43 @@ +#coding:utf-8 + +""" +ID: issue-7676 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/7676 +TITLE: Invalid message when violating unique constraint ("Attempt to evaluate index expression recursively") +DESCRIPTION: +NOTES: + [28.05.2024] pzotov + Confirmed bug on 5.0.0.1111: got SQLSTATE = HY000 / Attempt to evaluate index expression recursively + Checked on 5.0.0.1121. + [04.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' and variables - to be substituted in expected_* on FB 6.x + Checked on 6.0.0.909; 5.0.3.1668. +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + create table test(x int); + create unique index test_unq on test computed by (x); + commit; + insert into test values(1); + insert into test values(1); +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +@pytest.mark.version('>=5.0.0') +def test_1(act: Action): + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + expected_stdout = f""" + Statement failed, SQLSTATE = 23000 + attempt to store duplicate value (visible to active transactions) in unique index {SQL_SCHEMA_PREFIX}"TEST_UNQ" + -Problematic key value is ( = 1) + """ + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7698_test.py b/tests/bugs/gh_7698_test.py index 65e73943..b63e894e 100644 --- a/tests/bugs/gh_7698_test.py +++ b/tests/bugs/gh_7698_test.py @@ -6,8 +6,12 @@ TITLE: The legacy plan with window functions is broken DESCRIPTION: NOTES: - Confirmed bug on 5.0.0.1149 - Checked on 5.0.0.1155 -- all OK. + Confirmed bug on 5.0.0.1149 + Checked on 5.0.0.1155 -- all OK. + [05.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.909; 5.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest @@ -23,12 +27,16 @@ act = isql_act('db', test_script) -expected_stdout = """ - PLAN (RDB$RELATIONS NATURAL) -""" - @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout + + expected_stdout_5x = """ + PLAN (RDB$RELATIONS NATURAL) + """ + expected_stdout_6x = """ + PLAN ("SYSTEM"."RDB$RELATIONS" NATURAL) + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7727_test.py b/tests/bugs/gh_7727_test.py index a75379a3..d0196ad1 100644 --- a/tests/bugs/gh_7727_test.py +++ b/tests/bugs/gh_7727_test.py @@ -7,8 +7,12 @@ DESCRIPTION: NOTES: [31.08.2023] pzotov - Confirmed problem on 5.0.0.1177, 4.0.4.2979 - Checked on 5.0.0.1183, 4.0.4.2983 (intermediate snapshots). + Confirmed problem on 5.0.0.1177, 4.0.4.2979 + Checked on 5.0.0.1183, 4.0.4.2983 (intermediate snapshots). + [05.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.909; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -41,13 +45,18 @@ act = isql_act('db', test_script) -expected_stdout = f""" - PLAN JOIN (TA NATURAL, TB INDEX (PK_TEST_B)) - PLAN JOIN (TA NATURAL, TB INDEX (PK_TEST_B)) -""" - @pytest.mark.version('>=4.0.4') def test_1(act: Action): - act.expected_stdout = expected_stdout + + expected_stdout_5x = f""" + PLAN JOIN (TA NATURAL, TB INDEX (PK_TEST_B)) + PLAN JOIN (TA NATURAL, TB INDEX (PK_TEST_B)) + """ + expected_stdout_6x = f""" + PLAN JOIN ("TA" NATURAL, "TB" INDEX ("PUBLIC"."PK_TEST_B")) + PLAN JOIN ("TA" NATURAL, "TB" INDEX ("PUBLIC"."PK_TEST_B")) + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.execute(combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7730_test.py b/tests/bugs/gh_7730_test.py index 6ca076ac..e602aa08 100644 --- a/tests/bugs/gh_7730_test.py +++ b/tests/bugs/gh_7730_test.py @@ -13,6 +13,9 @@ Added 'SQLSTATE' in substitutions: runtime error must not be filtered out by '?!(...)' pattern ("negative lookahead assertion", see https://docs.python.org/3/library/re.html#regular-expression-syntax). Added 'combine_output = True' in order to see SQLSTATE if any error occurs. + [05.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.909; 5.0.3.1668; 4.0.6.3214. """ import locale @@ -30,12 +33,14 @@ select timestamp '{CHK_TIMESTAMP}' from rdb$database; """ -expected_stdout = f""" - 01: sqltype: 448 VARYING scale: 0 subtype: 0 len: 128 charset: 0 NONE -""" @pytest.mark.version('>=4.0.4') def test_1(act: Action): + + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else 'SYSTEM.' + expected_stdout = f""" + 01: sqltype: 448 VARYING scale: 0 subtype: 0 len: 128 charset: 0 {SQL_SCHEMA_PREFIX}NONE + """ act.expected_stdout = expected_stdout act.isql(switches=['-q'], input = test_sql, combine_output = True, io_enc = locale.getpreferredencoding()) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7744_test.py b/tests/bugs/gh_7744_test.py index 8074b095..752dd75c 100644 --- a/tests/bugs/gh_7744_test.py +++ b/tests/bugs/gh_7744_test.py @@ -14,8 +14,9 @@ After this we return SQL SECURITY attribute to 'DEFINER' and repeat the same. This attempt must complete with success. Finally, we DROP SQL SECURITY. This must again cause permission error for call of every PSQL units. NOTES: - [12.11.2023] pzotov - Checked on 6.0.0.122 + [05.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.909; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -32,41 +33,6 @@ @pytest.mark.version('>=6.0') def test_1(act: Action, u_senior: User, u_junior: User): - expected_stdout = f""" - RES_1 4.641588833612778892410076350919446 - RES_2 5.848035476425732131013574720275845 - RES_3 6.694329500821695218826593246399307 - - Statement failed, SQLSTATE = 28000 - no permission for SELECT access to TABLE TEST - -Effective user is {u_junior.name} - - Statement failed, SQLSTATE = 28000 - no permission for SELECT access to TABLE TEST - -Effective user is {u_junior.name} - - Statement failed, SQLSTATE = 28000 - no permission for SELECT access to TABLE TEST - -Effective user is {u_junior.name} - - RES_7 8.879040017426007084292689552528769 - RES_8 9.283177667225557784820152701838891 - RES_9 9.654893846056297578599327844350667 - - Statement failed, SQLSTATE = 28000 - no permission for SELECT access to TABLE TEST - -Effective user is {u_junior.name} - - Statement failed, SQLSTATE = 28000 - no permission for SELECT access to TABLE TEST - -Effective user is {u_junior.name} - - Statement failed, SQLSTATE = 28000 - no permission for SELECT access to TABLE TEST - -Effective user is {u_junior.name} - """ - - act.expected_stdout = expected_stdout test_script = f""" set list on; connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; @@ -194,5 +160,43 @@ def test_1(act: Action, u_senior: User, u_junior: User): rollback; """ + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + TABLE_TEST_NAME = 'TEST' if act.is_version('<6') else f'{SQL_SCHEMA_PREFIX}"TEST"' + + expected_stdout = f""" + RES_1 4.641588833612778892410076350919446 + RES_2 5.848035476425732131013574720275845 + RES_3 6.694329500821695218826593246399307 + + Statement failed, SQLSTATE = 28000 + no permission for SELECT access to TABLE {TABLE_TEST_NAME} + -Effective user is {u_junior.name} + + Statement failed, SQLSTATE = 28000 + no permission for SELECT access to TABLE {TABLE_TEST_NAME} + -Effective user is {u_junior.name} + + Statement failed, SQLSTATE = 28000 + no permission for SELECT access to TABLE {TABLE_TEST_NAME} + -Effective user is {u_junior.name} + + RES_7 8.879040017426007084292689552528769 + RES_8 9.283177667225557784820152701838891 + RES_9 9.654893846056297578599327844350667 + + Statement failed, SQLSTATE = 28000 + no permission for SELECT access to TABLE {TABLE_TEST_NAME} + -Effective user is {u_junior.name} + + Statement failed, SQLSTATE = 28000 + no permission for SELECT access to TABLE {TABLE_TEST_NAME} + -Effective user is {u_junior.name} + + Statement failed, SQLSTATE = 28000 + no permission for SELECT access to TABLE {TABLE_TEST_NAME} + -Effective user is {u_junior.name} + """ + + act.expected_stdout = expected_stdout act.isql(switches=['-q'], input = test_script, connect_db = False, combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7748_test.py b/tests/bugs/gh_7748_test.py index 1b84aec7..5a8963a3 100644 --- a/tests/bugs/gh_7748_test.py +++ b/tests/bugs/gh_7748_test.py @@ -13,6 +13,7 @@ NOTES: [25.11.2023] pzotov Checked on 6.0.0.150. + See also: gh_8249_test.py """ import pytest diff --git a/tests/bugs/gh_7749_test.py b/tests/bugs/gh_7749_test.py index 5f4ad0fe..974e3c68 100644 --- a/tests/bugs/gh_7749_test.py +++ b/tests/bugs/gh_7749_test.py @@ -207,59 +207,58 @@ def test_1(act: Action, capsys): ^ """ - isql_show_expected_stdout = """ - DM_VC_DEFAULT_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET UTF8 COLLATE UTF8 Nullable - DM_VC_DEFAULT_CSET_IMPLICIT_COLL VARCHAR(10) CHARACTER SET UTF8 COLLATE UNICODE_CI Nullable - DM_VC_NONDEF_CSET_IMPLICIT_COLL VARCHAR(10) CHARACTER SET WIN1251 Nullable - DM_VC_NONDEF_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET WIN1251 COLLATE PXW_CYRL Nullable - VC_DEFAULT_CSET_IMPLICIT_COLL VARCHAR(10) CHARACTER SET UTF8 COLLATE UNICODE_CI Nullable - VC_DEFAULT_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET UTF8 COLLATE UTF8 Nullable - VC_NONDEF_CSET_IMPLICIT_COLL VARCHAR(10) CHARACTER SET WIN1251 Nullable - VC_NONDEF_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET WIN1251 COLLATE PXW_CYRL Nullable - NC_FIXED_CHAR_IMPLICIT_COLL CHAR(10) CHARACTER SET ISO8859_1 Nullable - NC_FIXED_CHAR_EXPLICIT_COLL CHAR(10) CHARACTER SET ISO8859_1 COLLATE FR_FR Nullable - BT_DEFAULT_CSET_IMPLICIT_COLL BLOB segment 80, subtype TEXT CHARACTER SET UTF8 COLLATE UNICODE_CI Nullable - BT_DEFAULT_CSET_EXPLICIT_COLL BLOB segment 80, subtype TEXT CHARACTER SET UTF8 COLLATE UTF8 Nullable - BT_NONDEF_CSET_IMPLICIT_COLL BLOB segment 80, subtype TEXT CHARACTER SET WIN1251 Nullable - BT_NONDEF_CSET_EXPLICIT_COLL BLOB segment 80, subtype TEXT CHARACTER SET WIN1251 COLLATE PXW_CYRL Nullable + isql_show_expected_stdout_6x = """ + PUBLIC.DM_VC_DEFAULT_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8 Nullable + PUBLIC.DM_VC_DEFAULT_CSET_IMPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UNICODE_CI Nullable + PUBLIC.DM_VC_NONDEF_CSET_IMPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.WIN1251 Nullable + PUBLIC.DM_VC_NONDEF_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL Nullable + VC_DEFAULT_CSET_IMPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UNICODE_CI Nullable + VC_DEFAULT_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8 Nullable + VC_NONDEF_CSET_IMPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.WIN1251 Nullable + VC_NONDEF_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL Nullable + NC_FIXED_CHAR_IMPLICIT_COLL CHAR(10) CHARACTER SET SYSTEM.ISO8859_1 Nullable + NC_FIXED_CHAR_EXPLICIT_COLL CHAR(10) CHARACTER SET SYSTEM.ISO8859_1 COLLATE SYSTEM.FR_FR Nullable + BT_DEFAULT_CSET_IMPLICIT_COLL BLOB segment 80, subtype TEXT CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UNICODE_CI Nullable + BT_DEFAULT_CSET_EXPLICIT_COLL BLOB segment 80, subtype TEXT CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8 Nullable + BT_NONDEF_CSET_IMPLICIT_COLL BLOB segment 80, subtype TEXT CHARACTER SET SYSTEM.WIN1251 Nullable + BT_NONDEF_CSET_EXPLICIT_COLL BLOB segment 80, subtype TEXT CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL Nullable BLOB_BINARY BLOB segment 80, subtype BINARY Nullable - A_VC_DEFAULT_CSET_IMPLICIT_COLL INPUT VARCHAR(10) CHARACTER SET UTF8 COLLATE UNICODE_CI - A_VC_DEFAULT_CSET_EXPLICIT_COLL INPUT VARCHAR(10) CHARACTER SET UTF8 COLLATE UTF8 - A_VC_NONDEF_CSET_IMPLICIT_COLL INPUT VARCHAR(10) CHARACTER SET WIN1251 - A_VC_NONDEF_CSET_EXPLICIT_COLL INPUT VARCHAR(10) CHARACTER SET WIN1251 COLLATE PXW_CYRL - A_NC_FIXED_CHAR_IMPLICIT_COLL INPUT CHAR(10) CHARACTER SET ISO8859_1 - A_NC_FIXED_CHAR_EXPLICIT_COLL INPUT CHAR(10) CHARACTER SET ISO8859_1 COLLATE FR_FR - A_BT_DEFAULT_CSET_IMPLICIT_COLL INPUT BLOB CHARACTER SET UTF8 COLLATE UNICODE_CI - A_BT_DEFAULT_CSET_EXPLICIT_COLL INPUT BLOB CHARACTER SET UTF8 COLLATE UTF8 - A_BT_NONDEF_CSET_IMPLICIT_COLL INPUT BLOB CHARACTER SET WIN1251 - A_BT_NONDEF_CSET_EXPLICIT_COLL INPUT BLOB CHARACTER SET WIN1251 COLLATE PXW_CYRL - A_BLOB_BINARY INPUT BLOB CHARACTER SET NONE - O_VC_DEFAULT_CSET_IMPLICIT_COLL OUTPUT VARCHAR(10) CHARACTER SET UTF8 COLLATE UNICODE_CI - O_VC_DEFAULT_CSET_EXPLICIT_COLL OUTPUT VARCHAR(10) CHARACTER SET UTF8 COLLATE UTF8 - O_VC_NONDEF_CSET_IMPLICIT_COLL OUTPUT VARCHAR(10) CHARACTER SET WIN1251 - O_VC_NONDEF_CSET_EXPLICIT_COLL OUTPUT VARCHAR(10) CHARACTER SET WIN1251 COLLATE PXW_CYRL - O_NC_FIXED_CHAR_IMPLICIT_COLL OUTPUT CHAR(10) CHARACTER SET ISO8859_1 - O_NC_FIXED_CHAR_EXPLICIT_COLL OUTPUT CHAR(10) CHARACTER SET ISO8859_1 COLLATE FR_FR - O_BT_DEFAULT_CSET_IMPLICIT_COLL OUTPUT BLOB CHARACTER SET UTF8 COLLATE UNICODE_CI - O_BT_DEFAULT_CSET_EXPLICIT_COLL OUTPUT BLOB CHARACTER SET UTF8 COLLATE UTF8 - O_BT_NONDEF_CSET_IMPLICIT_COLL OUTPUT BLOB CHARACTER SET WIN1251 - O_BT_NONDEF_CSET_EXPLICIT_COLL OUTPUT BLOB CHARACTER SET WIN1251 COLLATE PXW_CYRL - O_BLOB_BINARY OUTPUT BLOB CHARACTER SET NONE - OUTPUT (DM_VC_DEFAULT_CSET_EXPLICIT_COLL) VARCHAR(10) CHARACTER SET UTF8 COLLATE UNICODE_CI_AI - A_VC_DEFAULT_CSET_IMPLICIT_COLL INPUT VARCHAR(10) CHARACTER SET UTF8 COLLATE UNICODE_CI - A_VC_DEFAULT_CSET_EXPLICIT_COLL INPUT VARCHAR(10) CHARACTER SET UTF8 COLLATE UTF8 - A_VC_NONDEF_CSET_IMPLICIT_COLL INPUT VARCHAR(10) CHARACTER SET WIN1251 - A_VC_NONDEF_CSET_EXPLICIT_COLL INPUT VARCHAR(10) CHARACTER SET WIN1251 COLLATE PXW_CYRL - A_NC_FIXED_CHAR_IMPLICIT_COLL INPUT CHAR(10) CHARACTER SET ISO8859_1 - A_NC_FIXED_CHAR_EXPLICIT_COLL INPUT CHAR(10) CHARACTER SET ISO8859_1 COLLATE FR_FR - A_BT_DEFAULT_CSET_IMPLICIT_COLL INPUT BLOB CHARACTER SET UTF8 COLLATE UNICODE_CI - A_BT_DEFAULT_CSET_EXPLICIT_COLL INPUT BLOB CHARACTER SET UTF8 COLLATE UTF8 - A_BT_NONDEF_CSET_IMPLICIT_COLL INPUT BLOB CHARACTER SET WIN1251 - A_BT_NONDEF_CSET_EXPLICIT_COLL INPUT BLOB CHARACTER SET WIN1251 COLLATE PXW_CYRL - A_BLOB_BINARY INPUT BLOB CHARACTER SET NONE + A_VC_DEFAULT_CSET_IMPLICIT_COLL INPUT VARCHAR(10) CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UNICODE_CI + A_VC_DEFAULT_CSET_EXPLICIT_COLL INPUT VARCHAR(10) CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8 + A_VC_NONDEF_CSET_IMPLICIT_COLL INPUT VARCHAR(10) CHARACTER SET SYSTEM.WIN1251 + A_VC_NONDEF_CSET_EXPLICIT_COLL INPUT VARCHAR(10) CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL + A_NC_FIXED_CHAR_IMPLICIT_COLL INPUT CHAR(10) CHARACTER SET SYSTEM.ISO8859_1 + A_NC_FIXED_CHAR_EXPLICIT_COLL INPUT CHAR(10) CHARACTER SET SYSTEM.ISO8859_1 COLLATE SYSTEM.FR_FR + A_BT_DEFAULT_CSET_IMPLICIT_COLL INPUT BLOB CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UNICODE_CI + A_BT_DEFAULT_CSET_EXPLICIT_COLL INPUT BLOB CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8 + A_BT_NONDEF_CSET_IMPLICIT_COLL INPUT BLOB CHARACTER SET SYSTEM.WIN1251 + A_BT_NONDEF_CSET_EXPLICIT_COLL INPUT BLOB CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL + A_BLOB_BINARY INPUT BLOB CHARACTER SET SYSTEM.NONE + O_VC_DEFAULT_CSET_IMPLICIT_COLL OUTPUT VARCHAR(10) CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UNICODE_CI + O_VC_DEFAULT_CSET_EXPLICIT_COLL OUTPUT VARCHAR(10) CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8 + O_VC_NONDEF_CSET_IMPLICIT_COLL OUTPUT VARCHAR(10) CHARACTER SET SYSTEM.WIN1251 + O_VC_NONDEF_CSET_EXPLICIT_COLL OUTPUT VARCHAR(10) CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL + O_NC_FIXED_CHAR_IMPLICIT_COLL OUTPUT CHAR(10) CHARACTER SET SYSTEM.ISO8859_1 + O_NC_FIXED_CHAR_EXPLICIT_COLL OUTPUT CHAR(10) CHARACTER SET SYSTEM.ISO8859_1 COLLATE SYSTEM.FR_FR + O_BT_DEFAULT_CSET_IMPLICIT_COLL OUTPUT BLOB CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UNICODE_CI + O_BT_DEFAULT_CSET_EXPLICIT_COLL OUTPUT BLOB CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8 + O_BT_NONDEF_CSET_IMPLICIT_COLL OUTPUT BLOB CHARACTER SET SYSTEM.WIN1251 + O_BT_NONDEF_CSET_EXPLICIT_COLL OUTPUT BLOB CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL + O_BLOB_BINARY OUTPUT BLOB CHARACTER SET SYSTEM.NONE + OUTPUT (PUBLIC.DM_VC_DEFAULT_CSET_EXPLICIT_COLL) VARCHAR(10) CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UNICODE_CI_AI + A_VC_DEFAULT_CSET_IMPLICIT_COLL INPUT VARCHAR(10) CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UNICODE_CI + A_VC_DEFAULT_CSET_EXPLICIT_COLL INPUT VARCHAR(10) CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8 + A_VC_NONDEF_CSET_IMPLICIT_COLL INPUT VARCHAR(10) CHARACTER SET SYSTEM.WIN1251 + A_VC_NONDEF_CSET_EXPLICIT_COLL INPUT VARCHAR(10) CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL + A_NC_FIXED_CHAR_IMPLICIT_COLL INPUT CHAR(10) CHARACTER SET SYSTEM.ISO8859_1 + A_NC_FIXED_CHAR_EXPLICIT_COLL INPUT CHAR(10) CHARACTER SET SYSTEM.ISO8859_1 COLLATE SYSTEM.FR_FR + A_BT_DEFAULT_CSET_IMPLICIT_COLL INPUT BLOB CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UNICODE_CI + A_BT_DEFAULT_CSET_EXPLICIT_COLL INPUT BLOB CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8 + A_BT_NONDEF_CSET_IMPLICIT_COLL INPUT BLOB CHARACTER SET SYSTEM.WIN1251 + A_BT_NONDEF_CSET_EXPLICIT_COLL INPUT BLOB CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL + A_BLOB_BINARY INPUT BLOB CHARACTER SET SYSTEM.NONE """ - - act.expected_stdout = isql_show_expected_stdout + act.expected_stdout = isql_show_expected_stdout_6x act.isql(input = test_sql, combine_output = True) assert act.clean_stdout == act.clean_expected_stdout act.reset() @@ -268,98 +267,99 @@ def test_1(act: Action, capsys): # Test N2: check extracted metadata (result of 'isql -x'). # ------- - isql_meta_expected_stdout = """ - CREATE DOMAIN DM_BLOB_BINARY AS BLOB SUB_TYPE 0 SEGMENT SIZE 80; - CREATE DOMAIN DM_BT_DEFAULT_CSET_EXPLICIT_COLL AS BLOB SUB_TYPE TEXT SEGMENT SIZE 80 CHARACTER SET UTF8 COLLATE UTF8; - CREATE DOMAIN DM_BT_DEFAULT_CSET_IMPLICIT_COLL AS BLOB SUB_TYPE TEXT SEGMENT SIZE 80; - CREATE DOMAIN DM_BT_NONDEF_CSET_EXPLICIT_COLL AS BLOB SUB_TYPE TEXT SEGMENT SIZE 80 CHARACTER SET WIN1251 COLLATE PXW_CYRL; - CREATE DOMAIN DM_BT_NONDEF_CSET_IMPLICIT_COLL AS BLOB SUB_TYPE TEXT SEGMENT SIZE 80 CHARACTER SET WIN1251; - CREATE DOMAIN DM_NC_FIXED_CHAR_EXPLICIT_COLL AS CHAR(10) CHARACTER SET ISO8859_1 COLLATE FR_FR; - CREATE DOMAIN DM_NC_FIXED_CHAR_IMPLICIT_COLL AS CHAR(10) CHARACTER SET ISO8859_1; - CREATE DOMAIN DM_VC_DEFAULT_CSET_EXPLICIT_COLL AS VARCHAR(10) CHARACTER SET UTF8 COLLATE UTF8; - CREATE DOMAIN DM_VC_DEFAULT_CSET_IMPLICIT_COLL AS VARCHAR(10); - CREATE DOMAIN DM_VC_NONDEF_CSET_EXPLICIT_COLL AS VARCHAR(10) CHARACTER SET WIN1251 COLLATE PXW_CYRL; - CREATE DOMAIN DM_VC_NONDEF_CSET_IMPLICIT_COLL AS VARCHAR(10) CHARACTER SET WIN1251; - CREATE TABLE TEST (VC_DEFAULT_CSET_IMPLICIT_COLL VARCHAR(10), - VC_DEFAULT_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET UTF8 COLLATE UTF8, - VC_NONDEF_CSET_IMPLICIT_COLL VARCHAR(10) CHARACTER SET WIN1251, - VC_NONDEF_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET WIN1251 COLLATE PXW_CYRL, - NC_FIXED_CHAR_IMPLICIT_COLL CHAR(10) CHARACTER SET ISO8859_1, - NC_FIXED_CHAR_EXPLICIT_COLL CHAR(10) CHARACTER SET ISO8859_1 COLLATE FR_FR, + + isql_meta_expected_stdout_6x = """ + CREATE DOMAIN PUBLIC.DM_BLOB_BINARY AS BLOB SUB_TYPE 0 SEGMENT SIZE 80; + CREATE DOMAIN PUBLIC.DM_BT_DEFAULT_CSET_EXPLICIT_COLL AS BLOB SUB_TYPE TEXT SEGMENT SIZE 80 CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8; + CREATE DOMAIN PUBLIC.DM_BT_DEFAULT_CSET_IMPLICIT_COLL AS BLOB SUB_TYPE TEXT SEGMENT SIZE 80; + CREATE DOMAIN PUBLIC.DM_BT_NONDEF_CSET_EXPLICIT_COLL AS BLOB SUB_TYPE TEXT SEGMENT SIZE 80 CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL; + CREATE DOMAIN PUBLIC.DM_BT_NONDEF_CSET_IMPLICIT_COLL AS BLOB SUB_TYPE TEXT SEGMENT SIZE 80 CHARACTER SET SYSTEM.WIN1251; + CREATE DOMAIN PUBLIC.DM_NC_FIXED_CHAR_EXPLICIT_COLL AS CHAR(10) CHARACTER SET SYSTEM.ISO8859_1 COLLATE SYSTEM.FR_FR; + CREATE DOMAIN PUBLIC.DM_NC_FIXED_CHAR_IMPLICIT_COLL AS CHAR(10) CHARACTER SET SYSTEM.ISO8859_1; + CREATE DOMAIN PUBLIC.DM_VC_DEFAULT_CSET_EXPLICIT_COLL AS VARCHAR(10) CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8; + CREATE DOMAIN PUBLIC.DM_VC_DEFAULT_CSET_IMPLICIT_COLL AS VARCHAR(10); + CREATE DOMAIN PUBLIC.DM_VC_NONDEF_CSET_EXPLICIT_COLL AS VARCHAR(10) CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL; + CREATE DOMAIN PUBLIC.DM_VC_NONDEF_CSET_IMPLICIT_COLL AS VARCHAR(10) CHARACTER SET SYSTEM.WIN1251; + CREATE TABLE PUBLIC.TEST (VC_DEFAULT_CSET_IMPLICIT_COLL VARCHAR(10), + VC_DEFAULT_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8, + VC_NONDEF_CSET_IMPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.WIN1251, + VC_NONDEF_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL, + NC_FIXED_CHAR_IMPLICIT_COLL CHAR(10) CHARACTER SET SYSTEM.ISO8859_1, + NC_FIXED_CHAR_EXPLICIT_COLL CHAR(10) CHARACTER SET SYSTEM.ISO8859_1 COLLATE SYSTEM.FR_FR, BT_DEFAULT_CSET_IMPLICIT_COLL BLOB SUB_TYPE TEXT SEGMENT SIZE 80, - BT_DEFAULT_CSET_EXPLICIT_COLL BLOB SUB_TYPE TEXT SEGMENT SIZE 80 CHARACTER SET UTF8 COLLATE UTF8, - BT_NONDEF_CSET_IMPLICIT_COLL BLOB SUB_TYPE TEXT SEGMENT SIZE 80 CHARACTER SET WIN1251, - BT_NONDEF_CSET_EXPLICIT_COLL BLOB SUB_TYPE TEXT SEGMENT SIZE 80 CHARACTER SET WIN1251 COLLATE PXW_CYRL, + BT_DEFAULT_CSET_EXPLICIT_COLL BLOB SUB_TYPE TEXT SEGMENT SIZE 80 CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8, + BT_NONDEF_CSET_IMPLICIT_COLL BLOB SUB_TYPE TEXT SEGMENT SIZE 80 CHARACTER SET SYSTEM.WIN1251, + BT_NONDEF_CSET_EXPLICIT_COLL BLOB SUB_TYPE TEXT SEGMENT SIZE 80 CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL, BLOB_BINARY BLOB SUB_TYPE 0 SEGMENT SIZE 80); - CREATE OR ALTER FUNCTION FN_TEST (A_VC_DEFAULT_CSET_IMPLICIT_COLL VARCHAR(10), - A_VC_DEFAULT_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET UTF8 COLLATE UTF8, - A_VC_NONDEF_CSET_IMPLICIT_COLL VARCHAR(10) CHARACTER SET WIN1251, - A_VC_NONDEF_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET WIN1251 COLLATE PXW_CYRL, - A_NC_FIXED_CHAR_IMPLICIT_COLL CHAR(10) CHARACTER SET ISO8859_1, - A_NC_FIXED_CHAR_EXPLICIT_COLL CHAR(10) CHARACTER SET ISO8859_1 COLLATE FR_FR, + CREATE OR ALTER FUNCTION PUBLIC.FN_TEST (A_VC_DEFAULT_CSET_IMPLICIT_COLL VARCHAR(10), + A_VC_DEFAULT_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8, + A_VC_NONDEF_CSET_IMPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.WIN1251, + A_VC_NONDEF_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL, + A_NC_FIXED_CHAR_IMPLICIT_COLL CHAR(10) CHARACTER SET SYSTEM.ISO8859_1, + A_NC_FIXED_CHAR_EXPLICIT_COLL CHAR(10) CHARACTER SET SYSTEM.ISO8859_1 COLLATE SYSTEM.FR_FR, A_BT_DEFAULT_CSET_IMPLICIT_COLL BLOB, - A_BT_DEFAULT_CSET_EXPLICIT_COLL BLOB CHARACTER SET UTF8 COLLATE UTF8, - A_BT_NONDEF_CSET_IMPLICIT_COLL BLOB CHARACTER SET WIN1251, - A_BT_NONDEF_CSET_EXPLICIT_COLL BLOB CHARACTER SET WIN1251 COLLATE PXW_CYRL, + A_BT_DEFAULT_CSET_EXPLICIT_COLL BLOB CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8, + A_BT_NONDEF_CSET_IMPLICIT_COLL BLOB CHARACTER SET SYSTEM.WIN1251, + A_BT_NONDEF_CSET_EXPLICIT_COLL BLOB CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL, A_BLOB_BINARY BLOB) - RETURNS DM_VC_DEFAULT_CSET_EXPLICIT_COLL COLLATE UNICODE_CI_AI - CREATE OR ALTER PROCEDURE SP_TEST (A_VC_DEFAULT_CSET_IMPLICIT_COLL VARCHAR(10), - A_VC_DEFAULT_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET UTF8 COLLATE UTF8, - A_VC_NONDEF_CSET_IMPLICIT_COLL VARCHAR(10) CHARACTER SET WIN1251, - A_VC_NONDEF_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET WIN1251 COLLATE PXW_CYRL, - A_NC_FIXED_CHAR_IMPLICIT_COLL CHAR(10) CHARACTER SET ISO8859_1, - A_NC_FIXED_CHAR_EXPLICIT_COLL CHAR(10) CHARACTER SET ISO8859_1 COLLATE FR_FR, + RETURNS PUBLIC.DM_VC_DEFAULT_CSET_EXPLICIT_COLL COLLATE SYSTEM.UNICODE_CI_AI + CREATE OR ALTER PROCEDURE PUBLIC.SP_TEST (A_VC_DEFAULT_CSET_IMPLICIT_COLL VARCHAR(10), + A_VC_DEFAULT_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8, + A_VC_NONDEF_CSET_IMPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.WIN1251, + A_VC_NONDEF_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL, + A_NC_FIXED_CHAR_IMPLICIT_COLL CHAR(10) CHARACTER SET SYSTEM.ISO8859_1, + A_NC_FIXED_CHAR_EXPLICIT_COLL CHAR(10) CHARACTER SET SYSTEM.ISO8859_1 COLLATE SYSTEM.FR_FR, A_BT_DEFAULT_CSET_IMPLICIT_COLL BLOB, - A_BT_DEFAULT_CSET_EXPLICIT_COLL BLOB CHARACTER SET UTF8 COLLATE UTF8, - A_BT_NONDEF_CSET_IMPLICIT_COLL BLOB CHARACTER SET WIN1251, - A_BT_NONDEF_CSET_EXPLICIT_COLL BLOB CHARACTER SET WIN1251 COLLATE PXW_CYRL, + A_BT_DEFAULT_CSET_EXPLICIT_COLL BLOB CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8, + A_BT_NONDEF_CSET_IMPLICIT_COLL BLOB CHARACTER SET SYSTEM.WIN1251, + A_BT_NONDEF_CSET_EXPLICIT_COLL BLOB CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL, A_BLOB_BINARY BLOB) RETURNS (O_VC_DEFAULT_CSET_IMPLICIT_COLL VARCHAR(10), - O_VC_DEFAULT_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET UTF8 COLLATE UTF8, - O_VC_NONDEF_CSET_IMPLICIT_COLL VARCHAR(10) CHARACTER SET WIN1251, - O_VC_NONDEF_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET WIN1251 COLLATE PXW_CYRL, - O_NC_FIXED_CHAR_IMPLICIT_COLL CHAR(10) CHARACTER SET ISO8859_1, - O_NC_FIXED_CHAR_EXPLICIT_COLL CHAR(10) CHARACTER SET ISO8859_1 COLLATE FR_FR, + O_VC_DEFAULT_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8, + O_VC_NONDEF_CSET_IMPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.WIN1251, + O_VC_NONDEF_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL, + O_NC_FIXED_CHAR_IMPLICIT_COLL CHAR(10) CHARACTER SET SYSTEM.ISO8859_1, + O_NC_FIXED_CHAR_EXPLICIT_COLL CHAR(10) CHARACTER SET SYSTEM.ISO8859_1 COLLATE SYSTEM.FR_FR, O_BT_DEFAULT_CSET_IMPLICIT_COLL BLOB, - O_BT_DEFAULT_CSET_EXPLICIT_COLL BLOB CHARACTER SET UTF8 COLLATE UTF8, - O_BT_NONDEF_CSET_IMPLICIT_COLL BLOB CHARACTER SET WIN1251, - O_BT_NONDEF_CSET_EXPLICIT_COLL BLOB CHARACTER SET WIN1251 COLLATE PXW_CYRL, + O_BT_DEFAULT_CSET_EXPLICIT_COLL BLOB CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8, + O_BT_NONDEF_CSET_IMPLICIT_COLL BLOB CHARACTER SET SYSTEM.WIN1251, + O_BT_NONDEF_CSET_EXPLICIT_COLL BLOB CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL, O_BLOB_BINARY BLOB) - ALTER FUNCTION FN_TEST (A_VC_DEFAULT_CSET_IMPLICIT_COLL VARCHAR(10), - A_VC_DEFAULT_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET UTF8 COLLATE UTF8, - A_VC_NONDEF_CSET_IMPLICIT_COLL VARCHAR(10) CHARACTER SET WIN1251, - A_VC_NONDEF_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET WIN1251 COLLATE PXW_CYRL, - A_NC_FIXED_CHAR_IMPLICIT_COLL CHAR(10) CHARACTER SET ISO8859_1, - A_NC_FIXED_CHAR_EXPLICIT_COLL CHAR(10) CHARACTER SET ISO8859_1 COLLATE FR_FR, + ALTER FUNCTION PUBLIC.FN_TEST (A_VC_DEFAULT_CSET_IMPLICIT_COLL VARCHAR(10), + A_VC_DEFAULT_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8, + A_VC_NONDEF_CSET_IMPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.WIN1251, + A_VC_NONDEF_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL, + A_NC_FIXED_CHAR_IMPLICIT_COLL CHAR(10) CHARACTER SET SYSTEM.ISO8859_1, + A_NC_FIXED_CHAR_EXPLICIT_COLL CHAR(10) CHARACTER SET SYSTEM.ISO8859_1 COLLATE SYSTEM.FR_FR, A_BT_DEFAULT_CSET_IMPLICIT_COLL BLOB, - A_BT_DEFAULT_CSET_EXPLICIT_COLL BLOB CHARACTER SET UTF8 COLLATE UTF8, - A_BT_NONDEF_CSET_IMPLICIT_COLL BLOB CHARACTER SET WIN1251, - A_BT_NONDEF_CSET_EXPLICIT_COLL BLOB CHARACTER SET WIN1251 COLLATE PXW_CYRL, + A_BT_DEFAULT_CSET_EXPLICIT_COLL BLOB CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8, + A_BT_NONDEF_CSET_IMPLICIT_COLL BLOB CHARACTER SET SYSTEM.WIN1251, + A_BT_NONDEF_CSET_EXPLICIT_COLL BLOB CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL, A_BLOB_BINARY BLOB) - RETURNS DM_VC_DEFAULT_CSET_EXPLICIT_COLL COLLATE UNICODE_CI_AI - ALTER PROCEDURE SP_TEST (A_VC_DEFAULT_CSET_IMPLICIT_COLL VARCHAR(10), - A_VC_DEFAULT_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET UTF8 COLLATE UTF8, - A_VC_NONDEF_CSET_IMPLICIT_COLL VARCHAR(10) CHARACTER SET WIN1251, - A_VC_NONDEF_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET WIN1251 COLLATE PXW_CYRL, - A_NC_FIXED_CHAR_IMPLICIT_COLL CHAR(10) CHARACTER SET ISO8859_1, - A_NC_FIXED_CHAR_EXPLICIT_COLL CHAR(10) CHARACTER SET ISO8859_1 COLLATE FR_FR, + RETURNS PUBLIC.DM_VC_DEFAULT_CSET_EXPLICIT_COLL COLLATE SYSTEM.UNICODE_CI_AI + ALTER PROCEDURE PUBLIC.SP_TEST (A_VC_DEFAULT_CSET_IMPLICIT_COLL VARCHAR(10), + A_VC_DEFAULT_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8, + A_VC_NONDEF_CSET_IMPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.WIN1251, + A_VC_NONDEF_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL, + A_NC_FIXED_CHAR_IMPLICIT_COLL CHAR(10) CHARACTER SET SYSTEM.ISO8859_1, + A_NC_FIXED_CHAR_EXPLICIT_COLL CHAR(10) CHARACTER SET SYSTEM.ISO8859_1 COLLATE SYSTEM.FR_FR, A_BT_DEFAULT_CSET_IMPLICIT_COLL BLOB, - A_BT_DEFAULT_CSET_EXPLICIT_COLL BLOB CHARACTER SET UTF8 COLLATE UTF8, - A_BT_NONDEF_CSET_IMPLICIT_COLL BLOB CHARACTER SET WIN1251, - A_BT_NONDEF_CSET_EXPLICIT_COLL BLOB CHARACTER SET WIN1251 COLLATE PXW_CYRL, + A_BT_DEFAULT_CSET_EXPLICIT_COLL BLOB CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8, + A_BT_NONDEF_CSET_IMPLICIT_COLL BLOB CHARACTER SET SYSTEM.WIN1251, + A_BT_NONDEF_CSET_EXPLICIT_COLL BLOB CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL, A_BLOB_BINARY BLOB) RETURNS (O_VC_DEFAULT_CSET_IMPLICIT_COLL VARCHAR(10), - O_VC_DEFAULT_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET UTF8 COLLATE UTF8, - O_VC_NONDEF_CSET_IMPLICIT_COLL VARCHAR(10) CHARACTER SET WIN1251, - O_VC_NONDEF_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET WIN1251 COLLATE PXW_CYRL, - O_NC_FIXED_CHAR_IMPLICIT_COLL CHAR(10) CHARACTER SET ISO8859_1, - O_NC_FIXED_CHAR_EXPLICIT_COLL CHAR(10) CHARACTER SET ISO8859_1 COLLATE FR_FR, + O_VC_DEFAULT_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8, + O_VC_NONDEF_CSET_IMPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.WIN1251, + O_VC_NONDEF_CSET_EXPLICIT_COLL VARCHAR(10) CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL, + O_NC_FIXED_CHAR_IMPLICIT_COLL CHAR(10) CHARACTER SET SYSTEM.ISO8859_1, + O_NC_FIXED_CHAR_EXPLICIT_COLL CHAR(10) CHARACTER SET SYSTEM.ISO8859_1 COLLATE SYSTEM.FR_FR, O_BT_DEFAULT_CSET_IMPLICIT_COLL BLOB, - O_BT_DEFAULT_CSET_EXPLICIT_COLL BLOB CHARACTER SET UTF8 COLLATE UTF8, - O_BT_NONDEF_CSET_IMPLICIT_COLL BLOB CHARACTER SET WIN1251, - O_BT_NONDEF_CSET_EXPLICIT_COLL BLOB CHARACTER SET WIN1251 COLLATE PXW_CYRL, + O_BT_DEFAULT_CSET_EXPLICIT_COLL BLOB CHARACTER SET SYSTEM.UTF8 COLLATE SYSTEM.UTF8, + O_BT_NONDEF_CSET_IMPLICIT_COLL BLOB CHARACTER SET SYSTEM.WIN1251, + O_BT_NONDEF_CSET_EXPLICIT_COLL BLOB CHARACTER SET SYSTEM.WIN1251 COLLATE SYSTEM.PXW_CYRL, O_BLOB_BINARY BLOB) """ - act.expected_stdout = isql_meta_expected_stdout + act.expected_stdout = isql_meta_expected_stdout_6x act.extract_meta() assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7767_test.py b/tests/bugs/gh_7767_test.py new file mode 100644 index 00000000..817f750c --- /dev/null +++ b/tests/bugs/gh_7767_test.py @@ -0,0 +1,171 @@ +#coding:utf-8 + +""" +ID: issue-7767 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/7767 +TITLE: Slow drop trigger command execution under FB5.0 +DESCRIPTION: + The issued problem can NOT be stably reproduced if we compare time ratio between 'DROP TRIGGER' vs 'DROP PROCEDURE' statements. + ratio between execution time differed for too small value (e.g. 7.2 before fix and 6.9 after it). + + But regression can be noted if we check ratio between CPU time spent for 'DROP TRIGGER' and some code that does not relate + to any DB operations and makes some evaluation. Such code can be single call to CRYPT_HASH function (doing in loop many times). + This function must be called EVAL_CRYPT_HASH_COUNT times. + Result of evaluating of CRYPT_HASH is stored in var. 'eval_crypt_hash_time' and serves further as "etalone" value. + + Test database is initialized by creation of PSQL_OBJECTS_COUNT triggers and is copied to backup FDB (see 'tmp_fdb'). + Then we call 'DROP TRIGGER' using 'for select ... from rdb$triggers' cursor (so their count is also PSQL_OBJECTS_COUNT). + We repeat this in loop for MEASURES_COUNT iterations, doing restore from DB copy before every iteration (copy 'tmp_fdb' to act.db). + Median of ratios between CPU times obtained in this loop and eval_crypt_hash_time must be less than MAX_RATIO. + Duration is measured as difference between psutil.Process(fb_pid).cpu_times() counters. +NOTES: + [14.08.2024] pzotov + Problem did exist in FB 5.x until commit "Fix #7759 - Routine calling overhead increased by factor 6 vs Firebird 4.0.0." + https://github.com/FirebirdSQL/firebird/commit/d621ffbe0cf2d43e13480628992180c28a5044fe (03-oct-2023 13:32). + Before this commit (up to 5.0.0.1236) median of ratios was more than 6.5. + After fix it was reduced to ~3.5 ... 4.0 (5.0.0.1237 and above). + This ratio seems to be same on Windows and Linux. + + Built-in function CRYPT_HASH appeared in 4.0.0.2180, 27-aug-2020, commit: + https://github.com/FirebirdSQL/firebird/commit/e9f3eb360db41ddff27fa419b908876be0d2daa5 + ("Moved cryptographic hashes to separate function crypt_hash(), crc32 - into function hash()") + + Test duration time: about 50s. + Checked on 6.0.0.436, 5.0.2.1478, 4.0.6.3142 (all SS/CS; both Windows and Linux). +""" +import shutil +from pathlib import Path +import psutil +import pytest +from firebird.qa import * +import time + +########################### +### S E T T I N G S ### +########################### + +# How many times to generate crypt_hash: +EVAL_CRYPT_HASH_COUNT=5000 + +# How many times we call procedures: +MEASURES_COUNT = 11 + +# How many procedures and triggers must be created: +PSQL_OBJECTS_COUNT = 500 + +# Maximal value for ratio between maximal and minimal medians +# +MAX_RATIO = 6 +############# + +init_sql = """ + set bail on; + alter database set linger to 0; + create sequence g; + create table test(id int); + commit; + set term ^; +""" +init_sql = '\n'.join( + ( init_sql + ,'\n'.join( [ f'create trigger tg_{i} for test before insert as declare v int; begin v = gen_id(g,1); end ^' for i in range(PSQL_OBJECTS_COUNT) ] ) + ,'^ set term ;^' + ,'commit;' + ) + ) + +db = db_factory(init = init_sql) +act = python_act('db') + +tmp_fdb = temp_file('tmp_gh_7767_copy.tmp') + +expected_stdout = """ + Medians ratio: acceptable +""" + +eval_crypt_code = f""" + execute block as + declare v_hash varbinary(64); + declare n int = {EVAL_CRYPT_HASH_COUNT}; + begin + while (n > 0) do begin + v_hash = crypt_hash(lpad('', 32765, uuid_to_char(gen_uuid())) using SHA512); + n = n - 1; + end + end +""" + +drop_trg_code = """ + execute block as + declare trg_drop type of column rdb$triggers.rdb$trigger_name; + begin + for select 'DROP TRIGGER '||trim(rdb$trigger_name) + from rdb$triggers + where rdb$system_flag=0 + into :trg_drop do + begin + in autonomous transaction do + begin + execute statement :trg_drop; + end + end + end +""" + +#-------------------------------------------------------------------- +def median(lst): + n = len(lst) + s = sorted(lst) + return (sum(s[n//2-1:n//2+1])/2.0, s[n//2])[n % 2] if n else None + +#-------------------------------------------------------------------- + +def get_server_pid(con): + with con.cursor() as cur: + cur.execute('select mon$server_pid as p from mon$attachments where mon$attachment_id = current_connection') + fb_pid = int(cur.fetchone()[0]) + return fb_pid + +#-------------------------------------------------------------------- + +@pytest.mark.version('>=4.0.0') +def test_1(act: Action, tmp_fdb: Path, capsys): + + shutil.copy2(act.db.db_path, tmp_fdb) + + with act.db.connect() as con: + fb_pid = get_server_pid(con) + fb_info_init = psutil.Process(fb_pid).cpu_times() + con.execute_immediate( eval_crypt_code ) + fb_info_curr = psutil.Process(fb_pid).cpu_times() + eval_crypt_hash_time = max(fb_info_curr.user - fb_info_init.user, 0.000001) + + ddl_time = {} + for iter in range(MEASURES_COUNT): + + with act.db.connect() as con: + fb_pid = get_server_pid(con) + fb_info_init = psutil.Process(fb_pid).cpu_times() + con.execute_immediate( drop_trg_code ) + fb_info_curr = psutil.Process(fb_pid).cpu_times() + ddl_time[ 'tg', iter ] = max(fb_info_curr.user - fb_info_init.user, 0.000001) + + # Quick jump back to database with PSQL_OBJECTS_COUNT triggers that we made on init phase: + shutil.copy2(tmp_fdb, act.db.db_path) + + ratios = [ ddl_time['tg',iter] / eval_crypt_hash_time for iter in range(MEASURES_COUNT) ] + median_ratio = median(ratios) + + SUCCESS_MSG = 'Medians ratio: acceptable' + if median_ratio < MAX_RATIO: + print(SUCCESS_MSG) + else: + print( 'Medians ratio: /* perf_issue_tag */ POOR: %s, more than threshold: %s' % ( '{:.2f}'.format(median_ratio), '{:.2f}'.format(MAX_RATIO) ) ) + print('ratios:',['{:.2f}'.format(r) for r in ratios]) + print('CPU times:') + for k,v in ddl_time.items(): + print(k,':::','{:.2f}'.format(v)) + + act.expected_stdout = SUCCESS_MSG + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7804_test.py b/tests/bugs/gh_7804_test.py index 6e9d767b..8ffe4935 100644 --- a/tests/bugs/gh_7804_test.py +++ b/tests/bugs/gh_7804_test.py @@ -13,25 +13,16 @@ Number of NR must always be 0. Number of IR should increase in proportion to the number of 'OR' terms in the WHERE expression. NOTES: - [01.11.2023] pzotov - 0. ::: NB ::: TEST IS LIKELY TO BE SUPPLEMENTED WITH OTHER CASES. - 1. Explained plan must be displayed with preserved indents, see call to func replace_leading(). - 2. Statistics is gathered using con.info.get_table_access_stats() method. - One need to remember that values are cumulative, so we have make two 'snapshots', - before and after query execution, and then calculate difference between them. - - Checked on 6.0.0.101. - - [16.12.2023] pzotov - Adjusted output to recent changes in FB, currently only for FB 5.x. - See: - https://github.com/FirebirdSQL/firebird/commit/ae427762d5a3e740b69c7239acb9e2383bc9ca83 - More realistic cardinality adjustments for unmatchable booleans, this should also fix #7904: FB5 bad plan for query - - Output for FB 6.x will be changed later (letter from dimitr, 16.12.2023 10:32) + [05.07.2025] pzotov + Separated expected output for FB major versions prior/since 6.x. + No substitutions are used to suppress schema and quotes. Discussed with dimitr, 24.06.2025 12:39. + Checked on 6.0.0.909; 5.0.3.1668. """ + import pytest + from firebird.qa import * +from firebird.driver import DatabaseError init_script = """ recreate table test(x smallint); @@ -51,7 +42,7 @@ #---------------------------------------------------------- -def replace_leading(source, char="#"): +def replace_leading(source, char="."): stripped = source.lstrip() return char * (len(source) - len(stripped)) + stripped @@ -82,10 +73,19 @@ def test_1(act: Action, capsys): result_map = {} for x in q_list: - with cur.prepare(x) as ps: + ps, rs = None, None + try: + ps = cur.prepare(x) tabstat1 = [ p for p in con.info.get_table_access_stats() if p.table_id == test_rel_id ] - cur.execute(x) - for r in cur: + + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + rs = cur.execute(ps) + for r in rs: pass tabstat2 = [ p for p in con.info.get_table_access_stats() if p.table_id == test_rel_id ] @@ -101,219 +101,247 @@ def test_1(act: Action, capsys): tabstat2[0].sequential if tabstat2[0].sequential else 0 ,tabstat2[0].indexed if tabstat2[0].indexed else 0 ) + except DatabaseError as e: + print( e.__str__() ) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() for k,v in result_map.items(): print('Query:', k[0]) - print( '\n'.join([replace_leading(s) for s in k[1].split('\n')]) ) # explained plan, with preserving indents by replacing leading spaces with '#' + + # Show explained plan, with preserving indents by replacing leading spaces with '.': + print( '\n'.join([replace_leading(s) for s in k[1].split('\n')]) ) + print('NR:', v[0]) print('IR:', v[1]) print('') #-------------------------------------------------------------------- - expected_fb5x = f""" + expected_stdout_5x = f""" Query: {q_list[0]} Select Expression - ####-> Filter - ########-> Table "TEST" Access By ID - ############-> Bitmap - ################-> Index "TEST_X_ASC" Range Scan (full match) + ....-> Filter + ........-> Table "TEST" Access By ID + ............-> Bitmap + ................-> Index "TEST_X_ASC" Range Scan (full match) NR: 0 IR: 1000 - Query: select * from test where x = 3 or x = 2 + Query: {q_list[1]} Select Expression - ####-> Filter - ########-> Table "TEST" Access By ID - ############-> Bitmap Or - ################-> Bitmap - ####################-> Index "TEST_X_ASC" Range Scan (full match) - ################-> Bitmap - ####################-> Index "TEST_X_ASC" Range Scan (full match) + ....-> Filter + ........-> Table "TEST" Access By ID + ............-> Bitmap Or + ................-> Bitmap + ....................-> Index "TEST_X_ASC" Range Scan (full match) + ................-> Bitmap + ....................-> Index "TEST_X_ASC" Range Scan (full match) NR: 0 IR: 2000 - Query: select * from test where x = 2 or x = 3 or x = 1 + Query: {q_list[2]} Select Expression - ####-> Filter - ########-> Table "TEST" Access By ID - ############-> Bitmap Or - ################-> Bitmap Or - ####################-> Bitmap - ########################-> Index "TEST_X_ASC" Range Scan (full match) - ####################-> Bitmap - ########################-> Index "TEST_X_ASC" Range Scan (full match) - ################-> Bitmap - ####################-> Index "TEST_X_ASC" Range Scan (full match) + ....-> Filter + ........-> Table "TEST" Access By ID + ............-> Bitmap Or + ................-> Bitmap Or + ....................-> Bitmap + ........................-> Index "TEST_X_ASC" Range Scan (full match) + ....................-> Bitmap + ........................-> Index "TEST_X_ASC" Range Scan (full match) + ................-> Bitmap + ....................-> Index "TEST_X_ASC" Range Scan (full match) NR: 0 IR: 3000 - Query: select * from test where x = 2 or x is null or x = 3 or x = 1 + Query: {q_list[3]} Select Expression - ####-> Filter - ########-> Table "TEST" Access By ID - ############-> Bitmap Or - ################-> Bitmap Or - ####################-> Bitmap Or - ########################-> Bitmap - ############################-> Index "TEST_X_ASC" Range Scan (full match) - ########################-> Bitmap - ############################-> Index "TEST_X_ASC" Range Scan (full match) - ####################-> Bitmap - ########################-> Index "TEST_X_ASC" Range Scan (full match) - ################-> Bitmap - ####################-> Index "TEST_X_ASC" Range Scan (full match) + ....-> Filter + ........-> Table "TEST" Access By ID + ............-> Bitmap Or + ................-> Bitmap Or + ....................-> Bitmap Or + ........................-> Bitmap + ............................-> Index "TEST_X_ASC" Range Scan (full match) + ........................-> Bitmap + ............................-> Index "TEST_X_ASC" Range Scan (full match) + ....................-> Bitmap + ........................-> Index "TEST_X_ASC" Range Scan (full match) + ................-> Bitmap + ....................-> Index "TEST_X_ASC" Range Scan (full match) NR: 0 IR: 4000 - Query: select * from test where x = 5 + Query: {q_list[4]} Select Expression - ####-> Filter - ########-> Table "TEST" Access By ID - ############-> Bitmap - ################-> Index "TEST_X_DEC" Range Scan (full match) + ....-> Filter + ........-> Table "TEST" Access By ID + ............-> Bitmap + ................-> Index "TEST_X_DEC" Range Scan (full match) NR: 0 IR: 1000 - Query: select * from test where x = 6 or x = 5 + Query: {q_list[5]} Select Expression - ####-> Filter - ########-> Table "TEST" Access By ID - ############-> Bitmap Or - ################-> Bitmap - ####################-> Index "TEST_X_DEC" Range Scan (full match) - ################-> Bitmap - ####################-> Index "TEST_X_DEC" Range Scan (full match) + ....-> Filter + ........-> Table "TEST" Access By ID + ............-> Bitmap Or + ................-> Bitmap + ....................-> Index "TEST_X_DEC" Range Scan (full match) + ................-> Bitmap + ....................-> Index "TEST_X_DEC" Range Scan (full match) NR: 0 IR: 2000 - Query: select * from test where x = 5 or x = 4 or x = 6 + Query: {q_list[6]} Select Expression - ####-> Filter - ########-> Table "TEST" Access By ID - ############-> Bitmap Or - ################-> Bitmap Or - ####################-> Bitmap - ########################-> Index "TEST_X_DEC" Range Scan (full match) - ####################-> Bitmap - ########################-> Index "TEST_X_DEC" Range Scan (full match) - ################-> Bitmap - ####################-> Index "TEST_X_DEC" Range Scan (full match) + ....-> Filter + ........-> Table "TEST" Access By ID + ............-> Bitmap Or + ................-> Bitmap Or + ....................-> Bitmap + ........................-> Index "TEST_X_DEC" Range Scan (full match) + ....................-> Bitmap + ........................-> Index "TEST_X_DEC" Range Scan (full match) + ................-> Bitmap + ....................-> Index "TEST_X_DEC" Range Scan (full match) NR: 0 IR: 3000 - Query: select * from test where x = 6 or x is null or x = 4 or x = 5 + Query: {q_list[7]} Select Expression - ####-> Filter - ########-> Table "TEST" Access By ID - ############-> Bitmap Or - ################-> Bitmap Or - ####################-> Bitmap Or - ########################-> Bitmap - ############################-> Index "TEST_X_DEC" Range Scan (full match) - ########################-> Bitmap - ############################-> Index "TEST_X_DEC" Range Scan (full match) - ####################-> Bitmap - ########################-> Index "TEST_X_DEC" Range Scan (full match) - ################-> Bitmap - ####################-> Index "TEST_X_DEC" Range Scan (full match) + ....-> Filter + ........-> Table "TEST" Access By ID + ............-> Bitmap Or + ................-> Bitmap Or + ....................-> Bitmap Or + ........................-> Bitmap + ............................-> Index "TEST_X_DEC" Range Scan (full match) + ........................-> Bitmap + ............................-> Index "TEST_X_DEC" Range Scan (full match) + ....................-> Bitmap + ........................-> Index "TEST_X_DEC" Range Scan (full match) + ................-> Bitmap + ....................-> Index "TEST_X_DEC" Range Scan (full match) NR: 0 IR: 4000 """ - - expected_fb6x = f""" + expected_stdout_6x = f""" Query: {q_list[0]} Select Expression - ####-> Filter - ########-> Table "TEST" Access By ID - ############-> Bitmap - ################-> Index "TEST_X_ASC" Range Scan (full match) + ....-> Filter + ........-> Table "PUBLIC"."TEST" Access By ID + ............-> Bitmap + ................-> Index "PUBLIC"."TEST_X_ASC" Range Scan (full match) NR: 0 IR: 1000 Query: {q_list[1]} Select Expression - ####-> Filter - ########-> Table "TEST" Access By ID - ############-> Bitmap Or - ################-> Bitmap - ####################-> Index "TEST_X_ASC" Range Scan (full match) - ################-> Bitmap - ####################-> Index "TEST_X_ASC" Range Scan (full match) + ....-> Filter + ........-> Table "PUBLIC"."TEST" Access By ID + ............-> Bitmap Or + ................-> Bitmap + ....................-> Index "PUBLIC"."TEST_X_ASC" Range Scan (full match) + ................-> Bitmap + ....................-> Index "PUBLIC"."TEST_X_ASC" Range Scan (full match) NR: 0 IR: 2000 - + Query: {q_list[2]} Select Expression - ####-> Filter - ########-> Table "TEST" Access By ID - ############-> Bitmap Or - ################-> Bitmap Or - ####################-> Bitmap - ########################-> Index "TEST_X_ASC" Range Scan (full match) - ####################-> Bitmap - ########################-> Index "TEST_X_ASC" Range Scan (full match) - ################-> Bitmap - ####################-> Index "TEST_X_ASC" Range Scan (full match) + ....-> Filter + ........-> Table "PUBLIC"."TEST" Access By ID + ............-> Bitmap Or + ................-> Bitmap Or + ....................-> Bitmap + ........................-> Index "PUBLIC"."TEST_X_ASC" Range Scan (full match) + ....................-> Bitmap + ........................-> Index "PUBLIC"."TEST_X_ASC" Range Scan (full match) + ................-> Bitmap + ....................-> Index "PUBLIC"."TEST_X_ASC" Range Scan (full match) NR: 0 IR: 3000 - + Query: {q_list[3]} Select Expression - ####-> Filter - ########-> Table "TEST" Access By ID - ############-> Bitmap - ################-> Index "TEST_X_ASC" Full Scan + ....-> Filter + ........-> Table "PUBLIC"."TEST" Access By ID + ............-> Bitmap Or + ................-> Bitmap Or + ....................-> Bitmap Or + ........................-> Bitmap + ............................-> Index "PUBLIC"."TEST_X_ASC" Range Scan (full match) + ........................-> Bitmap + ............................-> Index "PUBLIC"."TEST_X_ASC" Range Scan (full match) + ....................-> Bitmap + ........................-> Index "PUBLIC"."TEST_X_ASC" Range Scan (full match) + ................-> Bitmap + ....................-> Index "PUBLIC"."TEST_X_ASC" Range Scan (full match) NR: 0 IR: 4000 - + Query: {q_list[4]} Select Expression - ####-> Filter - ########-> Table "TEST" Access By ID - ############-> Bitmap - ################-> Index "TEST_X_DEC" Range Scan (full match) + ....-> Filter + ........-> Table "PUBLIC"."TEST" Access By ID + ............-> Bitmap + ................-> Index "PUBLIC"."TEST_X_DEC" Range Scan (full match) NR: 0 IR: 1000 - + Query: {q_list[5]} Select Expression - ####-> Filter - ########-> Table "TEST" Access By ID - ############-> Bitmap Or - ################-> Bitmap - ####################-> Index "TEST_X_DEC" Range Scan (full match) - ################-> Bitmap - ####################-> Index "TEST_X_DEC" Range Scan (full match) + ....-> Filter + ........-> Table "PUBLIC"."TEST" Access By ID + ............-> Bitmap Or + ................-> Bitmap + ....................-> Index "PUBLIC"."TEST_X_DEC" Range Scan (full match) + ................-> Bitmap + ....................-> Index "PUBLIC"."TEST_X_DEC" Range Scan (full match) NR: 0 IR: 2000 - + Query: {q_list[6]} Select Expression - ####-> Filter - ########-> Table "TEST" Access By ID - ############-> Bitmap Or - ################-> Bitmap Or - ####################-> Bitmap - ########################-> Index "TEST_X_DEC" Range Scan (full match) - ####################-> Bitmap - ########################-> Index "TEST_X_DEC" Range Scan (full match) - ################-> Bitmap - ####################-> Index "TEST_X_DEC" Range Scan (full match) + ....-> Filter + ........-> Table "PUBLIC"."TEST" Access By ID + ............-> Bitmap Or + ................-> Bitmap Or + ....................-> Bitmap + ........................-> Index "PUBLIC"."TEST_X_DEC" Range Scan (full match) + ....................-> Bitmap + ........................-> Index "PUBLIC"."TEST_X_DEC" Range Scan (full match) + ................-> Bitmap + ....................-> Index "PUBLIC"."TEST_X_DEC" Range Scan (full match) NR: 0 IR: 3000 - + Query: {q_list[7]} Select Expression - ####-> Filter - ########-> Table "TEST" Access By ID - ############-> Bitmap - ################-> Index "TEST_X_DEC" Full Scan + ....-> Filter + ........-> Table "PUBLIC"."TEST" Access By ID + ............-> Bitmap Or + ................-> Bitmap Or + ....................-> Bitmap Or + ........................-> Bitmap + ............................-> Index "PUBLIC"."TEST_X_DEC" Range Scan (full match) + ........................-> Bitmap + ............................-> Index "PUBLIC"."TEST_X_DEC" Range Scan (full match) + ....................-> Bitmap + ........................-> Index "PUBLIC"."TEST_X_DEC" Range Scan (full match) + ................-> Bitmap + ....................-> Index "PUBLIC"."TEST_X_DEC" Range Scan (full match) NR: 0 IR: 4000 """ - act.expected_stdout = expected_fb5x if act.is_version('<6') else expected_fb6x + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7846_test.py b/tests/bugs/gh_7846_test.py new file mode 100644 index 00000000..1e7273f6 --- /dev/null +++ b/tests/bugs/gh_7846_test.py @@ -0,0 +1,190 @@ +#coding:utf-8 + +""" +ID: issue-7846 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/7846 +TITLE: FB4 can't backup/restore int128-array +DESCRIPTION: + Test checks ability to make b/r of DB with table that has array-based columns of following types: + smallint; integer; bigint; int128; double. +NOTES: + [16.09.2024] pzotov + 1. Confirmed problem with b/r for INT128 on 4.0.4.3021 (dob: 17-nov-2023). + Got after restore: [Decimal('0'), Decimal('0')] (although no errors were during insert origin data). + Expected values: [Decimal('170141183460469231731687303715884105727'), Decimal('-170141183460469231731687303715884105728')]. + Confirmed fix on 4.0.4.3022 (dob: 19-nov-2023). + 2. Fix for #8100 ("The isc_array_lookup_bounds function returns invalid values...") required in order + this test can pass on FB 5.x and 6.x. + See commits (07-may-2024): + * 5.x: https://github.com/FirebirdSQL/firebird/commit/26ca064202169c0558359fc9ab06b70e827466f0 + * 6.x: https://github.com/FirebirdSQL/firebird/commit/17b007d14f8ccc6cfba0d63a3b2f21622ced20d0 + FB 4.x was not affected by that bug. + 3. INT128 type requires as argument only INTEGER values (from scope -2**127 ... +2**127-1) but NOT decimals. + Rather, NUMERIC / DECIMAL columns allows only Decimal instances. + (letter from Alex, 20.08.2024 16:11, subj: "gh-6544 ("Error writing an array of NUMERIC(24,6)" ) <...>" + 4. DECFLOAT type had a problem in firebird-driver, fixed in v 1.10.5 (26-JUL-2024) + 5. Some features must be implemented in engine and/or in firebird-driver for proper support of NUMERIC datatype + which have big values and use int128 as underlying storage. + Discussed with pcisar, see subj "firebird-driver and its support for FB datatypes", letters since 21-jul-2024. + See also: https://github.com/FirebirdSQL/firebird/issues/6544#issuecomment-2294778138 + + Checked on 6.0.0.457; 5.0.2.1499; 4.0.5.3136. +""" + +import pytest +from firebird.qa import * +from io import BytesIO +from firebird.driver import SrvRestoreFlag, DatabaseError, InterfaceError +from decimal import Decimal +import traceback +import time + +init_script = """ + recreate table test_arr( + id int generated by default as identity constraint test_pk primary key + ,v_smallint smallint[2] + ,v_integer int[2] + ,v_bigint bigint[2] + ,v_int128 int128[2] + ,v_double double precision[2] + ,v_decfloat decfloat[2] + ); +""" +db = db_factory(init = init_script) + +act = python_act('db') + +#-------------------------------------- +def try_insert(con, cur, fld, data): + print(f'\nTrying to add array in {fld}') + try: + print(f'Data: {data}') + with cur.prepare(f"insert into test_arr({fld}) values (?)") as ps: + for x in data: + cur.execute(ps, (x,)) + + cur.execute(f'select {fld} from test_arr order by id desc rows 1') + for r in cur: + for x in r[0]: + print(x, type(x)) + + con.commit() + print('Success.') + #except (ValueError, InterfaceError, DatabaseError) as e: + except Exception as e: + for x in traceback.format_exc().split('\n'): + print(' ',x) +#-------------------------------------- + +@pytest.mark.version('>=4.0.5') +def test_1(act: Action, capsys): + with act.db.connect() as con: + cur = con.cursor() + + # ------------ smallint ------------- + data = [ [32767, -32768] ] + try_insert(con, cur, 'v_smallint', data) + + # ------------ int ------------- + data = [ [2147483647, -2147483648] ] + try_insert(con, cur, 'v_integer', data) + + # ------------ bigint ------------- + data = [ [9223372036854775807, -9223372036854775808] ] + try_insert(con, cur, 'v_bigint', data) + + # ------------ int128 ------------- + # sqltype: 32752 INT128 Nullable scale: 0 subtype: 0 len: 16 + # ValueError: Incorrect ARRAY field value. + # !! WRONG!! >>> data = [ [Decimal('170141183460469231731687303715884105727'), Decimal('-170141183460469231731687303715884105728')] ] + # Only INTEGERS must be specified as arguments: + data = [ [170141183460469231731687303715884105727, -170141183460469231731687303715884105728] ] + try_insert(con, cur, 'v_int128', data) + + # ------------ double ------------- + data = [ [-2.2250738585072014e-308, 1.7976931348623158e+308] ] + try_insert(con, cur, 'v_double', data) + + + # ------------ decfloat ------------- + # ValueError: Incorrect ARRAY field value. + # data = [ [Decimal('-1.0E-6143'), Decimal('9.999999999999999999999999999999999E6144')] ] + # data = [ [-1.0E-6143, 9.999999999999999999999999999999999E6144] ] + data = [ [Decimal('-9.999999999999999999999999999999999E+6144'), Decimal('9.999999999999999999999999999999999E+6144')] ] + try_insert(con, cur, 'v_decfloat', data) + + + backup = BytesIO() + with act.connect_server() as srv: + srv.database.local_backup(database=act.db.db_path, backup_stream=backup) + backup.seek(0) + srv.database.local_restore(backup_stream=backup, database=act.db.db_path, flags = SrvRestoreFlag.REPLACE) + + with act.db.connect() as con: + cur = con.cursor() + for fld_name in ('v_smallint','v_integer','v_bigint','v_int128','v_double', 'v_decfloat'): + cur.execute(f'select {fld_name} from test_arr') + for r in cur: + # type(r): + if any(r): + print(f'Result after restore for column {fld_name}:') + for p in r: + print(p) + + act.expected_stdout = """ + Trying to add array in v_smallint + Data: [[32767, -32768]] + 32767 + -32768 + Success. + + Trying to add array in v_integer + Data: [[2147483647, -2147483648]] + 2147483647 + -2147483648 + Success. + + Trying to add array in v_bigint + Data: [[9223372036854775807, -9223372036854775808]] + 9223372036854775807 + -9223372036854775808 + Success. + + Trying to add array in v_int128 + Data: [[170141183460469231731687303715884105727, -170141183460469231731687303715884105728]] + 170141183460469231731687303715884105727 + -170141183460469231731687303715884105728 + Success. + + Trying to add array in v_double + Data: [[-2.2250738585072014e-308, 1.7976931348623157e+308]] + -2.2250738585072014e-308 + 1.7976931348623157e+308 + Success. + + Trying to add array in v_decfloat + Data: [[Decimal('-9.999999999999999999999999999999999E+6144'), Decimal('9.999999999999999999999999999999999E+6144')]] + -9.999999999999999999999999999999999E+6144 + 9.999999999999999999999999999999999E+6144 + Success. + + Result after restore for column v_smallint: + [32767, -32768] + + Result after restore for column v_integer: + [2147483647, -2147483648] + + Result after restore for column v_bigint: + [9223372036854775807, -9223372036854775808] + + Result after restore for column v_int128: + [Decimal('170141183460469231731687303715884105727'), Decimal('-170141183460469231731687303715884105728')] + + Result after restore for column v_double: + [-2.2250738585072014e-308, 1.7976931348623157e+308] + + Result after restore for column v_decfloat: + [Decimal('-9.999999999999999999999999999999999E+6144'), Decimal('9.999999999999999999999999999999999E+6144')] + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7853_test.py b/tests/bugs/gh_7853_test.py index 7def6d93..3880cffd 100644 --- a/tests/bugs/gh_7853_test.py +++ b/tests/bugs/gh_7853_test.py @@ -2,7 +2,7 @@ """ ID: issue-7853 -ISSUE: 7853 +ISSUE: https://github.com/FirebirdSQL/firebird/pull/7853 TITLE: Do not consider non-deterministic expressions as invariants in pre-filters DESCRIPTION: Change in FB 5.x was pushed 14.12.2023 20:06: @@ -12,23 +12,24 @@ This behavior changed since snapshot 5.0.0.1304 (date: 16-dec-2023). NOTES: [17.12.2023] pzotov - 1. Explained plans for queries where 'tmain' have aliases 'm0' ... 'm4' have: - * 'Filter (preliminary)' - for snapshots before this fix - * 'Filter' (WITHOUT preliminary) - after fix - 2. Last commit in the push is "deterministic uncorrelated subqueries to be considered as invariants" - but there is NO DIFFERENCE - between explained plans for 5.x snapshots that belongs to time points just _before_ and _after_ this push. - This is because push of PR#7853 has several commits, and one of them did broke such functionality whereas next - commit - WITHIN THIS PUSH (!) - did restore previous state (letter by dimitr, 17.12.2023 21:17). - - 3. Actually, NON-correlated subqueries became considered as INVARIANT much earlier, since 5.0.0.890, 10-jan-2023 - ("Merge pull request #7441 from FirebirdSQL/v5-pretty-explained-plan", a6ce0ec1632ec037b41b9cbcad42fd3ce6a9ea5e). - It seems that this old commit (of 10-jan-2023) caused lot of old issues to be considered now as fixed, for example - https://github.com/FirebirdSQL/firebird/issues/3394 - Query where 'tmain' table has alias 'm4' (and 'tdetl' table is involved - in contrary to all other queries) - is from this ticket. - This query explained plan must have "Sub-query (invariant)" since build 5.0.0.890 - - Checked on 5.0.0.1304 - ::: NB ::: Build 6.0.0.180 - FAILS because this PR still not pushed in master branch. Waiting for commit. + 1. Explained plans for queries where 'tmain' have aliases 'm0' ... 'm4' have: + * 'Filter (preliminary)' - for snapshots before this fix + * 'Filter' (WITHOUT preliminary) - after fix + 2. Last commit in the push is "deterministic uncorrelated subqueries to be considered as invariants" - but there is NO DIFFERENCE + between explained plans for 5.x snapshots that belongs to time points just _before_ and _after_ this push. + This is because push of PR#7853 has several commits, and one of them did broke such functionality whereas next + commit - WITHIN THIS PUSH (!) - did restore previous state (letter by dimitr, 17.12.2023 21:17). + + 3. Actually, NON-correlated subqueries became considered as INVARIANT much earlier, since 5.0.0.890, 10-jan-2023 + ("Merge pull request #7441 from FirebirdSQL/v5-pretty-explained-plan", a6ce0ec1632ec037b41b9cbcad42fd3ce6a9ea5e). + It seems that this old commit (of 10-jan-2023) caused lot of old issues to be considered now as fixed, for example + https://github.com/FirebirdSQL/firebird/issues/3394 + Query where 'tmain' table has alias 'm4' (and 'tdetl' table is involved - in contrary to all other queries) - is from this ticket. + This query explained plan must have "Sub-query (invariant)" since build 5.0.0.890 + + [05.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' and variables to be substituted in expected_* on FB 6.x + Checked on 6.0.0.909; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -61,42 +62,12 @@ act = python_act('db') -expected_stdout = """ - Select Expression - ####-> Aggregate - ########-> Filter - ############-> Table "TMAIN" as "M0" Full Scan - - Select Expression - ####-> Aggregate - ########-> Filter - ############-> Table "TMAIN" as "M1" Full Scan - - Select Expression - ####-> Aggregate - ########-> Filter - ############-> Table "TMAIN" as "M2" Full Scan - - Select Expression - ####-> Aggregate - ########-> Filter - ############-> Table "TMAIN" as "M3" Full Scan - - Sub-query (invariant) - ####-> Singularity Check - ########-> Aggregate - ############-> Table "TDETL" as "D" Full Scan - Select Expression - ####-> Aggregate - ########-> Filter - ############-> Table "TMAIN" as "M4" Full Scan -""" #--------------------------------------------------------- -def replace_leading(source, char="#"): +def replace_leading(source, char="."): stripped = source.lstrip() return char * (len(source) - len(stripped)) + stripped #--------------------------------------------------------- -@pytest.mark.version('>=5.0,<6') +@pytest.mark.version('>=5.0') def test_1(act: Action, capsys): with act.db.connect() as con: cur = con.cursor() @@ -104,6 +75,40 @@ def test_1(act: Action, capsys): with cur.prepare(q) as ps: print( '\n'.join([replace_leading(s) for s in ps.detailed_plan .split('\n')]) ) + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + TABLE_TMAIN_NAME = '"TMAIN"' if act.is_version('<6') else f'{SQL_SCHEMA_PREFIX}"TMAIN"' + TABLE_TDETL_NAME = '"TDETL"' if act.is_version('<6') else f'{SQL_SCHEMA_PREFIX}"TDETL"' + expected_stdout = f""" + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Table {TABLE_TMAIN_NAME} as "M0" Full Scan + + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Table {TABLE_TMAIN_NAME} as "M1" Full Scan + + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Table {TABLE_TMAIN_NAME} as "M2" Full Scan + + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Table {TABLE_TMAIN_NAME} as "M3" Full Scan + + Sub-query (invariant) + ....-> Singularity Check + ........-> Aggregate + ............-> Table {TABLE_TDETL_NAME} as "D" Full Scan + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Table {TABLE_TMAIN_NAME} as "M4" Full Scan + """ + act.expected_stdout = expected_stdout act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7863_test.py b/tests/bugs/gh_7863_test.py index 16e714db..70555e01 100644 --- a/tests/bugs/gh_7863_test.py +++ b/tests/bugs/gh_7863_test.py @@ -6,13 +6,22 @@ TITLE: Non-correlated sub-query is evaluated multiple times if it is based on a VIEW rathe than on appropriate derived table. DESCRIPTION: NOTES: + [18.01.2025] pzotov + Resultset of cursor that executes using instance of selectable PreparedStatement must be stored + in some variable in order to have ability close it EXPLICITLY (before PS will be freed). + Otherwise access violation raises during Python GC and pytest hangs at final point (does not return control to OS). + This occurs at least for: Python 3.11.2 / pytest: 7.4.4 / firebird.driver: 1.10.6 / Firebird.Qa: 0.19.3 + The reason of that was explained by Vlad, 26.10.24 17:42 ("oddities when use instances of selective statements"). + Confirmed bug on 6.0.0.222 Checked on 6.0.0.223, 5.0.1.1322. """ -import pytest from pathlib import Path + +import pytest from firebird.qa import * +from firebird.driver import DatabaseError init_sql = """ create view v_test_nr as select 1 i from rdb$fields rows 50; @@ -58,12 +67,21 @@ def test_1(act: Action, capsys): result_map = {} for qry_txt in q_map.keys(): - with cur.prepare(qry_txt) as ps: + ps, rs = None, None + try: + ps = cur.prepare(qry_txt) q_map[qry_txt] = ps.detailed_plan for tab_nm,tab_id in t_map.items(): tabstat1 = [ p for p in con.info.get_table_access_stats() if p.table_id == tab_id ] - cur.execute(qry_txt) - for r in cur: + + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + rs = cur.execute(ps) + for r in rs: pass tabstat2 = [ p for p in con.info.get_table_access_stats() if p.table_id == tab_id ] @@ -77,6 +95,14 @@ def test_1(act: Action, capsys): seq -= (tabstat1[0].sequential if tabstat1[0].sequential else 0) idx -= (tabstat1[0].indexed if tabstat1[0].indexed else 0) result_map[qry_txt, tab_nm] = (seq, idx) + except DatabaseError as e: + print( e.__str__() ) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() for k,v in result_map.items(): print(k[0]) # query diff --git a/tests/bugs/gh_7865_test.py b/tests/bugs/gh_7865_test.py new file mode 100644 index 00000000..0c69fb14 --- /dev/null +++ b/tests/bugs/gh_7865_test.py @@ -0,0 +1,84 @@ +#coding:utf-8 + +""" +ID: issue-7865 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/7865 +TITLE: Consider the return value of deterministic functions to be invariant if all its arguments are invariant +DESCRIPTION: + Test uses example provided in the ticket: we check performance of query which has `WHERE` clause + with deterministic function in one of its parts. +NOTES: + [12.05.2025] pzotov + Further checks may be included into this test later. + One may find this link useful (for those who can read in Russian): + https://murcode.ru/search/deterministic/?message=True&topic=True&user=False&forum=2&orderby=byDefault + + Confirmed improvement on 6.0.0.779-136fa13: number of NR = 2 * + Before this fix NR was * + (checked on 6.0.0.770-82c4a08) +""" + +import pytest +from firebird.qa import * + +###################################### +ROWS_COUNT = 30 +MAX_ALLOWED_NAT_READS = 2 * ROWS_COUNT +###################################### + +init_sql = f""" + create table test(id int, x bigint); + insert into test(id, x) select i, i*i from (select row_number()over() as i from rdb$types rows {ROWS_COUNT}); + commit; + + set term ^; + create function fb_get_x_for_id(a_id int) returns bigint deterministic as + begin + return (select t.x from test t where t.id = :a_id); + end + ^ + commit + ^ + set term ;^ +""" + +db = db_factory(init = init_sql) + +act = python_act('db') + +#----------------------------------------------------------- + +@pytest.mark.version('>=6.0') +def test_1(act: Action, capsys): + msg_prefix = 'Number of natural reads:' + expected_txt = 'EXPECTED' + nat_reads = {} + with act.db.connect() as con: + cur = con.cursor() + + cur.execute("select rdb$relation_id from rdb$relations where rdb$relation_name = upper('test')") + src_relation_id = cur.fetchone()[0] + nat_reads[src_relation_id] = 0 + + for x_table in con.info.get_table_access_stats(): + if x_table.table_id == src_relation_id: + nat_reads[src_relation_id] = -x_table.sequential + + cur.execute(f"select * from test where x = fb_get_x_for_id({ROWS_COUNT})") + # cur.execute(f"select * from test where fb_get_x_for_id({ROWS_COUNT}) = x") -- checked; result is the same. + data = cur.fetchall() + + for x_table in con.info.get_table_access_stats(): + if x_table.table_id == src_relation_id: + nat_reads[src_relation_id] += x_table.sequential + + if nat_reads[src_relation_id] <= MAX_ALLOWED_NAT_READS: + print(f'{msg_prefix} {expected_txt}') + else: + print(f'{msg_prefix} UNEXPECTED: {nat_reads[src_relation_id]} - greater than threshold = {MAX_ALLOWED_NAT_READS}.') + + act.expected_stdout = f""" + {msg_prefix} {expected_txt} + """ + + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7899_test.py b/tests/bugs/gh_7899_test.py index 5129a42a..f04ab99a 100644 --- a/tests/bugs/gh_7899_test.py +++ b/tests/bugs/gh_7899_test.py @@ -11,6 +11,9 @@ 6.0.0.186, commit 305c40a05b1d64c14dbf5f25f36c42c44c6392d9 5.0.1.1307, commit e35437e00687db9ed6add279cecb816dcdf8b07a 4.0.5.3042, commit f7b090043e8886ab6286f8d626dd1684dc09e3b8 + [05.07.2025] pzotov + Added 'SQL_SCHEMA_PREFIX' to be substituted in expected_* on FB 6.x + Checked on 6.0.0.909; 5.0.3.1668; 4.0.6.3214. """ import pytest @@ -20,18 +23,6 @@ act = python_act('db') -expected_stdout = """ - Statement failed, SQLSTATE = 23000 - violation of FOREIGN KEY constraint "TDETL_FK" on table "PERSISTENT_DETL" - -Foreign key reference target does not exist - -Problematic key value is ("ID" = 1) - - Statement failed, SQLSTATE = 23000 - violation of FOREIGN KEY constraint "TDETL_FK" on table "PERSISTENT_DETL" - -Foreign key reference target does not exist - -Problematic key value is ("ID" = 2) -""" - @pytest.mark.version('>=4.0.5') def test_1(act: Action): @@ -67,6 +58,19 @@ def test_1(act: Action): where not exists(select * from persistent_main m where m.id = d.id); """ + SQL_SCHEMA_PREFIX = '' if act.is_version('<6') else '"PUBLIC".' + expected_stdout = f""" + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint "TDETL_FK" on table {SQL_SCHEMA_PREFIX}"PERSISTENT_DETL" + -Foreign key reference target does not exist + -Problematic key value is ("ID" = 1) + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint "TDETL_FK" on table {SQL_SCHEMA_PREFIX}"PERSISTENT_DETL" + -Foreign key reference target does not exist + -Problematic key value is ("ID" = 2) + """ + act.expected_stdout = expected_stdout act.isql(switches=['-q'], input = test_script, combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7904_test.py b/tests/bugs/gh_7904_test.py index 274f0666..77cd52fc 100644 --- a/tests/bugs/gh_7904_test.py +++ b/tests/bugs/gh_7904_test.py @@ -8,6 +8,14 @@ NOTES: Confirmed problem on 5.0.0.1291 (for UMOWA_ROWS = 700K number of fetches = 6059386, elapsed time = 9.609s) Checked on 5.0.0.1303, 6.0.0.180 (for UMOWA_ROWS = 700K number of fetches = 270208, elapsed time = 0.741s) + + [24.09.2024] pzotov + Changed substitutions: one need to suppress '(keys: N, total key length: M)' in FB 6.x (and ONLY there), + otherwise actual and expected output become differ. + Commit: https://github.com/FirebirdSQL/firebird/commit/c50b0aa652014ce3610a1890017c9dd436388c43 + ("Add key info to the hash join plan output", 23.09.2024 18:26) + Discussed with dimitr. + Checked on 6.0.0.467-cc183f5, 5.0.2.1513 """ import pytest @@ -16,6 +24,7 @@ UMOWA_ROWS = 7000 ROZL_MULTIPLIER = 10 + init_sql = f""" set bail on; @@ -178,9 +187,20 @@ commit; """ +#----------------------------------------------------------- db = db_factory(init = init_sql) +substitutions = \ + [ + ( r'\(record length: \d+, key length: \d+\)', '' ) # (record length: 132, key length: 16) + ,( r'\(keys: \d+, total key length: \d+\)', '' ) # (keys: 1, total key length: 2) + ] + +act = python_act('db', substitutions = substitutions) + +#----------------------------------------------------------- + query_lst = [ # Query from https://github.com/FirebirdSQL/firebird/issues/7904: """ @@ -207,9 +227,6 @@ """, ] -substitutions = [ ('record length: \\d+.*', 'record length'), ('key length: \\d+.*', 'key length') ] -act = python_act('db', substitutions = substitutions) - #--------------------------------------------------------- def replace_leading(source, char="."): stripped = source.lstrip() diff --git a/tests/bugs/gh_7917_test-obj-in-use-on-drop-db.py b/tests/bugs/gh_7917_test-obj-in-use-on-drop-db.py new file mode 100644 index 00000000..ee66ad67 --- /dev/null +++ b/tests/bugs/gh_7917_test-obj-in-use-on-drop-db.py @@ -0,0 +1,419 @@ +#coding:utf-8 + +""" +ID: issue-7917 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/7917 +TITLE: Hang in a case of error when sweep thread is attaching to database +DESCRIPTION: + Test uses preliminary created alias with KeyHolderPlugin that points to special configuration with name 'KH2'. + This configuration makes crypt plugin accept key ONLY from client app (in contrary to some other encryption-related tests). + DB-level triggers (ON CONNECT, ON DISCONNECT) are created for logging appropriate events (see table 'att_log'). + Then we run special SQL that uses autonomous transactions and ES/EDS. + This script will hang because of update-conflict after some number of transactions (see TX_NUMBER_BEFORE_HANG). + We run this script asynchronously, then reduce sweep interval to value SWP_INTERVAL_TO_CHECK that is less than TX_NUMBER_BEFORE_HANG, + change DB state to full shutdown and return it online. + At this point any connection to DB will fire AUTO SWEEP (normally this can be seen in firebird.log as 'Sweep is started by SWEEPER'). + We run ISQL that queries 'att_log' table and causes AUTO SWEEP. That ISQL has to normally detach from DB and we must see its results. +NOTES: + [28.12.2023] pzotov + 1. To make crypt plugin accept key only from client app, $FB_HOME/plugins.conf must contain following lines: + ================== + Plugin = KH2 { + Module = $(dir_plugins)/fbSampleKeyHolder + RegisterName = fbSampleKeyHolder + Config = KH2 + } + Config = KH2 { + Auto = false + } + ================== + QA-scenario (.bat and .sh) must add in advance content of $QA_ROOT/files/qa-plugins-supplement.conf to $FB_HOME/plugins.conf. + + 2. Demo-plugin (fbSampleKeyHolder) can transfer key over network only for default key which has no-name. + Because of this, command 'alter database encrypt with ' has no '... key ' tail. + See letter from Alex, 15.12.2023 16:16 + + 3. In case of regression caused by that bug, we have to be ready that FB will hang on this test! + + Great thanks to Alex for suggestions (discussion started 13.12.2023 13:18). + + Confirmed bug on 6.0.0.173. + Checked on 6.0.0.175, 5.0.0.1305, 4.0.5.3049 - but currenlyt only SuperServer works fine. + Classic has the same problem. Sent report to Alex, 28.12.2023 13:10. + + On 3.0.12.33726 error raises: + unsuccessful metadata update / -ALTER DATABASE failed / -Missing correct crypt key / -Plugin fbSampleDbCrypt: / -Crypt key not set + This problem not [yet] investogated. +""" +import os +import time +import datetime as py_dt +import subprocess +import locale +import re +from pathlib import Path + +import pytest +from firebird.qa import * +from firebird.driver import DatabaseError + +REQUIRED_ALIAS = 'tmp_gh_7917_alias' + +########################### +### S E T T I N G S ### +########################### + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +enc_settings = QA_GLOBALS['encryption'] + +# ACHTUNG: this must be carefully tuned on every new host: +# +MAX_WAITING_ENCR_FINISH = int(enc_settings['MAX_WAIT_FOR_ENCR_FINISH_WIN' if os.name == 'nt' else 'MAX_WAIT_FOR_ENCR_FINISH_NIX']) +assert MAX_WAITING_ENCR_FINISH > 0 + +ENCRYPTION_PLUGIN = enc_settings['encryption_plugin'] # fbSampleDbCrypt +ENCRYPTION_KEY = enc_settings['encryption_key'] # Red + +SWP_INTERVAL_TO_CHECK = 100 +TX_NUMBER_BEFORE_HANG = SWP_INTERVAL_TO_CHECK + 10 + +MAX_WAIT_FOR_ISQL_TERMINATE=11 + +db = db_factory(filename = '#' + REQUIRED_ALIAS, do_not_drop = True) + +act = python_act('db', substitutions = [('^((?!(ISQL|Attributes)).)*$', ''), ('[ \t]+', ' '), ('TCPv(4|6)$', 'TCP')]) +#act = python_act('db') + +tmp_sql_file = temp_file('tmp_7917.sql') +tmp_log_file = temp_file('tmp_7917.log') + +#----------------------------------------------------------------------- + +def run_encr_decr(act: Action, mode, max_wait_encr_thread_finish, capsys): + if mode == 'encrypt': + # See letter from Alex, 15.12.2023 16:16 demo-plugin can not transfer named key over network. + # Because of that, we have to use following command WITHOUT adding 'key "{ENCRYPTION_KEY}"': + # ::: NB ::: One need to be sure that $FB_HOME/plugins.conf contains following lines: + # Plugin = KH2 { + # Module = $(dir_plugins)/fbSampleKeyHolder + # RegisterName = fbSampleKeyHolder + # Config = KH2 + # } + # Config = KH2 { + # Auto = false + # } + # Otherwise error will raise: + # unsuccessful metadata update + # -ALTER DATABASE failed + # -Missing database encryption key for your attachment + # -Plugin fbSampleDbCrypt: + # -Crypt key not set + # + alter_db_sttm = f'alter database encrypt with "{ENCRYPTION_PLUGIN}"' + wait_for_state = 'Database encrypted' + elif mode == 'decrypt': + alter_db_sttm = 'alter database decrypt' + wait_for_state = 'Database not encrypted' + + + e_thread_started = False + e_thread_finished = False + with act.db.connect() as con: + + t1=py_dt.datetime.now() + d1 = t1-t1 + try: + con.execute_immediate(alter_db_sttm) + con.commit() + e_thread_started = True + except DatabaseError as e: + print( e.__str__() ) + + while e_thread_started: + t2=py_dt.datetime.now() + d1=t2-t1 + if d1.seconds*1000 + d1.microseconds//1000 > max_wait_encr_thread_finish: + print(f'TIMEOUT EXPIRATION. Mode="{mode}" took {d1.seconds*1000 + d1.microseconds//1000} ms which exceeds limit = {max_wait_encr_thread_finish} ms.') + break + + # Possible output: + # Database [not] encrypted + # Database encrypted, crypt thread not complete + act.isql(switches=['-q'], input = 'show database;', combine_output = True) + if wait_for_state in act.stdout: + if 'not complete' in act.stdout: + pass + else: + e_thread_finished = True + break + act.reset() + + act.expected_stdout = '' + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() + + assert e_thread_finished + + +#----------------------------------------------------------------------- + +@pytest.mark.es_eds +@pytest.mark.encryption +@pytest.mark.version('>=4.0.5') +@pytest.mark.platform('Windows') +def test_1(act: Action, tmp_sql_file: Path, tmp_log_file: Path, capsys): + + ''' + with act.db.connect() as con: + if act.vars['server-arch'] == 'SuperServer': + pass + else: + pytest.skip("Currently fixed only for SuperServer. Temporary SKIPPED.") + ''' + + # Scan line-by-line through databases.conf, find line starting with REQUIRED_ALIAS and extract name of file that + # must be created in the $(dir_sampleDb)/qa/ folder. This name will be used further as target database (tmp_fdb). + # NOTE: we have to SKIP lines which are commented out, i.e. if they starts with '#': + p_required_alias_ptn = re.compile( '^(?!#)((^|\\s+)' + REQUIRED_ALIAS + ')\\s*=\\s*\\$\\(dir_sampleDb\\)/qa/', re.IGNORECASE ) + fname_in_dbconf = None + + with open(act.home_dir/'databases.conf', 'r') as f: + for line in f: + if p_required_alias_ptn.search(line): + # If databases.conf contains line like this: + # tmp_7598_alias = $(dir_sampleDb)/qa/tmp_gh_7598.fdb + # - then we extract filename: 'tmp_gh_7598.fdb' (see below): + fname_in_dbconf = Path(line.split('=')[1].strip()).name + break + + # if 'fname_in_dbconf' remains undefined here then propably REQUIRED_ALIAS not equals to specified in the databases.conf! + # + assert fname_in_dbconf + + #---------------------------------------------------------------- + + run_encr_decr(act, 'encrypt', MAX_WAITING_ENCR_FINISH, capsys) + + test_script = f""" + set bail on; + create table att_log( + att_prot varchar(15) + ,who_ami varchar(31) default current_user + ,att_id bigint default current_connection + ,trn_id bigint default current_transaction + ,evt_time time default 'now' + ,evt_name varchar(20) + ,swp_interval int + ); + set term ^; + create procedure sp_fill_dblevel_log(a_evt_name type of column att_log.evt_name) as + declare v_swp_interval int; + declare v_protocol type of column att_log.att_prot; + begin + insert into att_log( + att_prot + ,evt_name + ) values ( + rdb$get_context('SYSTEM', 'NETWORK_PROTOCOL') + ,:a_evt_name + ); + + end + ^ + create or alter trigger trg_detach on disconnect as + begin + execute procedure sp_fill_dblevel_log('detach'); + end + ^ + create or alter trigger trg_attach on connect as + begin + execute procedure sp_fill_dblevel_log('attach'); + end + ^ + set term ;^ + commit; + + recreate table test(s varchar(36) unique); + insert into test(s) values('LOCKED_FOR_PAUSE'); + commit; + + set transaction read committed WAIT; + + update test set s = s where s = 'LOCKED_FOR_PAUSE'; + + set term ^; + execute block as + declare n int = {TX_NUMBER_BEFORE_HANG}; + declare v_role varchar(31); + begin + while (n > 0) do + in autonomous transaction do + insert into test(s) values( rpad('', 36, uuid_to_char(gen_uuid()) ) ) + returning :n-1 into n; + + v_role = left(replace( uuid_to_char(gen_uuid()), '-', ''), 31); + + begin + execute statement ('update /* ES/EDS */ test set s = s where s = ?') ('LOCKED_FOR_PAUSE') + on external + 'localhost:' || rdb$get_context('SYSTEM', 'DB_NAME') + as user 'SYSDBA' password 'masterkey' role v_role + with autonomous transaction; + when any do + begin + end + end + + end + ^ + set term ;^ + set heading off; + select '-- shutdown me now --' from rdb$database; + """ + + tmp_sql_file.write_text(test_script) + + #---------------------------------------------------------------- + + # Reduce sweep interval to small value (that must be less than SQL_HANG_AFTER_TX_CNT): + # + act.gfix(switches=['-h', f'{SWP_INTERVAL_TO_CHECK}', act.db.dsn], combine_output = True, io_enc = locale.getpreferredencoding()) + + with open(tmp_log_file, 'w') as f: + # Launch ISQL which will hang because update conflict. + # This ISQl will be 'self-terminated' further because we will change DB state to full shutdown: + # + p_handed_isql = subprocess.Popen([act.vars['isql'], '-nod', '-i', str(tmp_sql_file), + '-user', act.db.user, + '-password', act.db.password, act.db.dsn], + stdout = f, + stderr = subprocess.STDOUT) + + # Let ISQL time to establish connection and fall in hanging state: + time.sleep(3) + + try: + act.gfix(switches=['-shut', 'full', '-force', '0', act.db.dsn], combine_output = True, io_enc = locale.getpreferredencoding()) + finally: + p_handed_isql.terminate() + + p_handed_isql.wait(MAX_WAIT_FOR_ISQL_TERMINATE) + if p_handed_isql.poll() is None: + print(f'Hanged ISQL process WAS NOT terminated in {MAX_WAIT_FOR_ISQL_TERMINATE} second(s).!') + else: + print(f'Hanged ISQL process terminated with retcode = {p_handed_isql.poll()}') + + # Result: log of hanged ISQL must contain now: + # Statement failed, SQLSTATE = 08003 + # connection shutdown + # -Database is shutdown. + + act.gfix(switches=['-online', act.db.dsn], combine_output = True, io_enc = locale.getpreferredencoding()) + assert act.clean_stdout == '' + act.reset() + + # Must show: Attributes encrypted, plugin {ENCRYPTION_PLUGIN} - without 'shutdown'! + act.gstat(switches=['-h']) + print(act.stdout) + + #act.stdout = capsys.readouterr().out + #assert act.clean_stdout == act.clean_expected_stdout + #act.reset() + + + #---------------------------------------------------------------- + + TEST_QUERY = 'select att_prot,who_ami,evt_name from att_log order by trn_id' + final_sql = f""" + set count on; + set list on; + set echo on; + {TEST_QUERY}; + commit; + connect '{act.db.dsn}'; + drop database; + quit; + """ + tmp_sql_file.write_text(final_sql) + + with open(tmp_log_file, 'w') as f: + # Explained by Alex, letter 13-dec-2023 13:18. + # Following ISQL will create attach that provokes AUTO SWEEP (because Next - OST now greater than SWP_INTERVAL_TO_CHECK). + # Problem raised when other attachments were prohibited to use encryption key (and this is default behaviour). + # Before fix, SWEEEP was not allowed to use key from this ISQL-attachment. + # Following message was added in firebird.log: "Automatic sweep error /Missing database encryption key for your attachment" + # But despite problem with establishing connection by SWEEP, its thread already created appropriate lock at that point. + # As result, engine remained in wrong state after this: existied attachments could not be closed. + # Also, FB process could not be normally stopped. + + MAX_WAIT_AUTO_SWEEP_FINISH = 3 + p_chk_sql = subprocess.Popen( [ act.vars['isql'], + '-nod', '-i', str(tmp_sql_file), + '-user', act.db.user, + '-password', act.db.password, + act.db.dsn + ], + stdout = f, + stderr = subprocess.STDOUT, + ) + + # If the process does not terminate after timeout seconds, raise a TimeoutExpired exception. + # It is safe to catch this exception and retry the wait. + try: + p_chk_sql.wait(timeout = MAX_WAIT_AUTO_SWEEP_FINISH) + except subprocess.TimeoutExpired as e: + print(f'Could not obtain result for {MAX_WAIT_AUTO_SWEEP_FINISH} seconds:') + print(e.__str__()) + + p_chk_sql.terminate() + p_chk_sql.wait(MAX_WAIT_FOR_ISQL_TERMINATE) + + # Check if child process has terminated. Set and return returncode attribute. Otherwise, returns None. + if p_chk_sql.poll() is None: + print(f'Final ISQL process WAS NOT terminated in {MAX_WAIT_FOR_ISQL_TERMINATE} second(s).!') + else: + print(f'Final ISQL process terminated') + #print(f'Final ISQL process terminated with retcode = {p_chk_sql.poll()}') + + + # do NOT put here this: + #run_encr_decr(act, 'decrypt', MAX_WAITING_ENCR_FINISH, capsys) + # - otherwise pytest will not return control + + with open(tmp_log_file, 'r') as f: + for line in f: + if line.strip(): + print('final ISQL log:',line) + + act.expected_stdout = f""" + Hanged ISQL process terminated with retcode = 1 + Attributes encrypted, plugin {ENCRYPTION_PLUGIN} + + Final ISQL process terminated + final ISQL log: {TEST_QUERY}; + final ISQL log: ATT_PROT TCP + final ISQL log: WHO_AMI {act.db.user.upper()} + final ISQL log: EVT_NAME attach + + final ISQL log: ATT_PROT TCP + final ISQL log: WHO_AMI {act.db.user.upper()} + final ISQL log: EVT_NAME detach + + final ISQL log: Records affected: 2 + + final ISQL log: commit; + final ISQL log: drop database; + final ISQL log: quit; + """ + + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() + + # NB! We have to decrypt database now. Otherwise teardown will fail with: + # firebird.driver.types.DatabaseError: Missing database encryption key for your attachment + # -Plugin fbSampleDbCrypt: + # -Crypt key not set + #run_encr_decr(act, 'decrypt', MAX_WAITING_ENCR_FINISH, capsys) diff --git a/tests/bugs/gh_7917_test.py b/tests/bugs/gh_7917_test.py index cfa692b4..aacaafc2 100644 --- a/tests/bugs/gh_7917_test.py +++ b/tests/bugs/gh_7917_test.py @@ -36,7 +36,6 @@ 3. In case of regression caused by that bug, we have to be ready that FB will hang on this test! Great thanks to Alex for suggestions (discussion started 13.12.2023 13:18). - Confirmed bug on 6.0.0.173. [30.01.2024] pzotov @@ -49,6 +48,14 @@ [01.02.2024] pzotov Added check for testing vanilla FB (temporary, until appropriate commits not merged with HQbird fork). + + [18.01.2025] pzotov + Resultset of cursor that executes using instance of selectable PreparedStatement must be stored + in some variable in order to have ability close it EXPLICITLY (before PS will be freed). + Otherwise access violation raises during Python GC and pytest hangs at final point (does not return control to OS). + This occurs at least for: Python 3.11.2 / pytest: 7.4.4 / firebird.driver: 1.10.6 / Firebird.Qa: 0.19.3 + The reason of that was explained by Vlad, 26.10.24 17:42 ("oddities when use instances of selective statements"). + """ import os import time @@ -134,10 +141,10 @@ def run_encr_decr(act: Action, mode, max_wait_encr_thread_finish, capsys): d1 = py_dt.timedelta(0) with act.db.connect() as con: cur = con.cursor() - ps = cur.prepare('select mon$crypt_state from mon$database') - - t1=py_dt.datetime.now() + ps, rs = None, None try: + ps = cur.prepare('select mon$crypt_state from mon$database') + t1=py_dt.datetime.now() d1 = t1-t1 con.execute_immediate(alter_db_sttm) con.commit() @@ -147,11 +154,18 @@ def run_encr_decr(act: Action, mode, max_wait_encr_thread_finish, capsys): if d1.seconds*1000 + d1.microseconds//1000 > max_wait_encr_thread_finish: break - ###################################################### - ### C H E C K M O N $ C R Y P T _ S T A T E ### - ###################################################### - cur.execute(ps) - current_crypt_state = cur.fetchone()[0] + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + rs = cur.execute(ps) + for r in rs: + ###################################################### + ### C H E C K M O N $ C R Y P T _ S T A T E ### + ###################################################### + current_crypt_state = r[0] con.commit() if current_crypt_state == REQUIRED_CRYPT_STATE: e_thread_finished = True @@ -160,12 +174,20 @@ def run_encr_decr(act: Action, mode, max_wait_encr_thread_finish, capsys): time.sleep(0.5) except DatabaseError as e: print( e.__str__() ) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() + assert e_thread_finished, f'TIMEOUT EXPIRATION. Mode="{mode}" took {d1.seconds*1000 + d1.microseconds//1000} ms which exceeds limit = {max_wait_encr_thread_finish} ms; current_crypt_state={current_crypt_state}' #----------------------------------------------------------------------- +@pytest.mark.es_eds @pytest.mark.encryption @pytest.mark.version('>=4.0.5') def test_1(act: Action, tmp_sql_file: Path, tmp_log_file: Path, tmp_gstat_log: Path, capsys): diff --git a/tests/bugs/gh_7921_test.py b/tests/bugs/gh_7921_test.py index ad6b188b..aa87a133 100644 --- a/tests/bugs/gh_7921_test.py +++ b/tests/bugs/gh_7921_test.py @@ -9,6 +9,13 @@ Confirmed problem on 5.0.0.1303. Checked on 6.0.0.180 (intermediate build 18.12.2023). Checked on 5.0.1.1322 after backporting (commit fef5af38, 23.01.2024). + + [17.11.2024] pzotov + Query text was replaced after https://github.com/FirebirdSQL/firebird/commit/26e64e9c08f635d55ac7a111469498b3f0c7fe81 + ( Cost-based decision between ORDER and SORT plans (#8316) ): 'OPTIMIZE FOR FIRST ROWS' is used for 6.x + Suggested by dimitr, letter 16.11.2024 15:15 + + Checked on 6.0.0.532; 5.0.2.1567 """ import pytest @@ -182,21 +189,6 @@ db = db_factory(init = init_sql) -query_lst = [ - """ - select - q2_rozl.dyr_id as "dyrekcja" - ,count(*) as "q2_rozl" - from - rozliczenie q2_rozl - where - q2_rozl.okres_numer = '15' - and q2_rozl.dok_rozliczeniowy_id in ('1') - group by - q2_rozl.dyr_id - """, -] - act = python_act('db') #--------------------------------------------------------- @@ -207,6 +199,24 @@ def replace_leading(source, char="."): @pytest.mark.version('>=5.0.1') def test_1(act: Action, capsys): + + OPT_CLAUSE = '' if act.is_version('<6') else 'optimize for first rows' + query_lst = [ + f""" + select + q2_rozl.dyr_id as "dyrekcja" + ,count(*) as "q2_rozl" + from + rozliczenie q2_rozl + where + q2_rozl.okres_numer = '15' + and q2_rozl.dok_rozliczeniowy_id in ('1') + group by + q2_rozl.dyr_id + {OPT_CLAUSE} + """, + ] + with act.db.connect() as con: cur = con.cursor() for q in query_lst: diff --git a/tests/bugs/gh_7937_test.py b/tests/bugs/gh_7937_test.py index ba0307ce..e5fa59a3 100644 --- a/tests/bugs/gh_7937_test.py +++ b/tests/bugs/gh_7937_test.py @@ -10,6 +10,9 @@ Confirmed bug on 5.0.0.1305, 6.0.0.279 Checked on 6.0.0.286 -- all OK. Thanks to dimitr for providing simplest test case. + + [17.06.2024] pzotov + Reduced min_version to 5.0.1 after check on 5.0.1.1416-b4b3559. """ import pytest @@ -42,7 +45,7 @@ COUNT 1 """ -@pytest.mark.version('>=6.0.0') +@pytest.mark.version('>=5.0.1') def test_1(act: Action): act.expected_stdout = expected_stdout act.execute(combine_output = True) diff --git a/tests/bugs/gh_7980_test.py b/tests/bugs/gh_7980_test.py new file mode 100644 index 00000000..657c5031 --- /dev/null +++ b/tests/bugs/gh_7980_test.py @@ -0,0 +1,62 @@ +#coding:utf-8 + +""" +ID: issue-7980 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/7980 +TITLE: Option for GEN_UUID to generate v7 UUID +DESCRIPTION: + Test verifies: + * ability to call function GEN_UUID() with argument (value = 7); + * orderliness of generated UUID values according to requirements of v7. + + UUID v7 values have prefix (first 13 bytes) which monotonically grows. + If we generate some number of such UUIDs and write them into the table with integer PK + then this PK and value of row_number()over(order by left(uuid_v7,13)) must be EQUAL + for all records. + Otherwise generated UUID does not match to v7 requirement. +NOTES: + [10.07.2024] pzotov + Checked on 6.0.0.386 +""" + +import pytest +from firebird.qa import * + +init_script = f""" + recreate table test ( + id int generated by default as identity constraint pk_test primary key + ,uuid_v7 varchar(36) unique + ); + + recreate view v_check as + with + a as ( + select id, row_number()over(order by left(uuid_v7,13)) rn, uuid_v7 from test + ) + select * from a where id <> rn + ; + + insert into test(uuid_v7) select uuid_to_char(gen_uuid(7)) from rdb$types rows 50; + commit; +""" + +db = db_factory(init=init_script) +act = python_act('db') + +expected_stdout = f""" +""" + +@pytest.mark.version('>=6.0') +def test_1(act: Action, capsys): + with act.db.connect() as con: + cur = con.cursor() + cur.execute('select 1 from v_check') + violates_v7 = cur.fetchone() + if violates_v7: + print('SOME UUIDs DO NOT MEET UUID v7 REQUIREMENTS:') + cur.execute('select id, uuid_v7 from v_check') + act.print_data(cur) + + act.expected_stdout = expected_stdout + act.stdout = capsys.readouterr().out # must be EMPTY! + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_7989_test.py b/tests/bugs/gh_7989_test.py new file mode 100644 index 00000000..59b7d9c0 --- /dev/null +++ b/tests/bugs/gh_7989_test.py @@ -0,0 +1,152 @@ +#coding:utf-8 + +""" +ID: issue-7989 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/7989 +TITLE: Improve performance of external (UDR) functions +DESCRIPTION: + We can estimate perfomance by comparison of time that is spent to call UDR vs result of some crypt function. + Function crypt_hash( using SHA512) has been selected for that because of notable CPU consumation. + Stored procedured SP_GEN_HASH is created for evaluation of crypt hash, it will run loop for N_HASH_EVALUATE_COUNT times. + Duration for each measure is difference between psutil.Process(fb_pid).cpu_times() counters. + + We do to call SP vs UDR, with adding results to map. + UDR that determines whether date relates to LEAP year is used for this test. + It is defined as 'udf_compat!UC_isLeapYear' in UDR engine, see $FB_HOME/upgrade/v4.0/udf_replace.sql + + Finally, we get ratio between medians of these measures (see 'median_ratio') + Test is considered as passed if median_ratio less than threshold . +NOTES: + [18.05.2025] pzotov. + Initial commit that introduces inmprovement: 29.02.2024 00:43 + https://github.com/FirebirdSQL/firebird/commit/547cb8388b9c72a329ed9bfe8c25f8ee696a112e + Postfix for #7989 - Improve performance of external (UDR) functions: 17.03.2024 04:14 + https://github.com/FirebirdSQL/firebird/commit/e4377213b4c9767cafe92502797db4f040a9e6be + + 1. Medians ratio on Windows: + * before: 0.46 + * after: 0.31 + 2. Test DB must NOT have charset = utf8, otherwise 'implementation limit exceeded' will raise; 'ascii' charset was selected for work. + 3. Just for info: isLeapYear() can be implemented using pure PSQL: + create or alter function isLeapPSQL (a_year int) returns boolean as + begin + return bin_and( (a_year * 1073750999), 3221352463) <= 126976; + end + Benchmark shows that this code *was* faster than UDR before #547cb838 but now it runs slower (with raio ~ 2450/3050 ms). + See: + [EN]: https://hueffner.de/falk/blog/a-leap-year-check-in-three-instructions.html + [RU]: https://habr.com/ru/articles/910188/ + + Checked on 6.0.0.269; 6.0.0.273; 6.0.0.783. +""" +import os +import psutil +import pytest +from firebird.qa import * + +#-------------------------------------------------------------------- +def median(lst): + n = len(lst) + s = sorted(lst) + return (sum(s[n//2-1:n//2+1])/2.0, s[n//2])[n % 2] if n else None +#-------------------------------------------------------------------- + +########################### +### S E T T I N G S ### +########################### + +# How many times we do measure: +N_MEASURES = 21 + +N_UDR_CALLS_COUNT = 50000 + +# How many iterations must be done for hash evaluation: +N_HASH_EVALUATE_COUNT = 3000 + +# Maximal value for ratio between maximal and minimal medians +# +################ +MAX_RATIO = 0.40 +################ + +init_script = \ +f''' + set term ^; + create or alter procedure sp_gen_hash (n_cnt int) as + declare v_hash varbinary(64); + declare s varchar(32765); + begin + s = lpad('', 32765, uuid_to_char(gen_uuid())); + while (n_cnt > 0) do + begin + v_hash = crypt_hash(s using SHA512); + n_cnt = n_cnt - 1; + end + end + ^ + + -- from $FB_HOME/upgrade/v4.0/udf_replace.sql: + create or alter function isLeapUDR (a_timestamp timestamp) returns boolean + external name 'udf_compat!UC_isLeapYear' + engine udr; + ^ + create or alter procedure sp_udr_call (n_cnt int) as + declare a_date date; + declare b boolean; + begin + a_date = current_date; + while (n_cnt > 0) do + begin + b = isLeapUDR(a_date); + n_cnt = n_cnt - 1; + end + end + ^ + commit + ^ +''' + +db = db_factory(init = init_script, charset = 'ascii') +act = python_act('db') + +expected_stdout = """ + Medians ratio: acceptable +""" + +@pytest.mark.version('>=6.0') +def test_1(act: Action, capsys): + + with act.db.connect() as con: + cur=con.cursor() + cur.execute('select mon$server_pid as p from mon$attachments where mon$attachment_id = current_connection') + fb_pid = int(cur.fetchone()[0]) + + times_map = {} + for i in range(0, N_MEASURES): + fb_info_init = psutil.Process(fb_pid).cpu_times() + cur.callproc( 'sp_gen_hash', (N_HASH_EVALUATE_COUNT,) ) + fb_info_curr = psutil.Process(fb_pid).cpu_times() + times_map[ 'hash_eval', i ] = max(fb_info_curr.user - fb_info_init.user, 0.000001) + + fb_info_init = psutil.Process(fb_pid).cpu_times() + cur.callproc( 'sp_udr_call', (N_UDR_CALLS_COUNT,) ) + fb_info_curr = psutil.Process(fb_pid).cpu_times() + times_map[ 'udr_call', i ] = max(fb_info_curr.user - fb_info_init.user, 0.000001) + + + sp_gen_hash_median = median([v for k,v in times_map.items() if k[0] == 'hash_eval']) + sp_udr_call_median = median([v for k,v in times_map.items() if k[0] == 'udr_call']) + + median_ratio = sp_udr_call_median / sp_gen_hash_median + + print( 'Medians ratio: ' + ('acceptable' if median_ratio <= MAX_RATIO else '/* perf_issue_tag */ POOR: %s, more than threshold: %s' % ( '{:9g}'.format(median_ratio), '{:9g}'.format(MAX_RATIO) ) ) ) + if median_ratio > MAX_RATIO: + print(f'CPU times for each of {N_MEASURES} measures:') + for sp_name in ('hash_eval', 'udr_call', ): + print(f'{sp_name=}:') + for p in [v for k,v in times_map.items() if k[0] == sp_name]: + print(p) + + act.expected_stdout = expected_stdout + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8015_test.py b/tests/bugs/gh_8015_test.py index c1e78eb8..094061f1 100644 --- a/tests/bugs/gh_8015_test.py +++ b/tests/bugs/gh_8015_test.py @@ -161,6 +161,7 @@ ánoc """ +@pytest.mark.intl @pytest.mark.version('>=6.0.0') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/bugs/gh_8057_test.py b/tests/bugs/gh_8057_test.py new file mode 100644 index 00000000..c6a53c50 --- /dev/null +++ b/tests/bugs/gh_8057_test.py @@ -0,0 +1,527 @@ +#coding:utf-8 + +""" +ID: issue-8057 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8057 +TITLE: Let optimizer automatically update empty index statistics for relatively small system tables +DESCRIPTION: + Only tables from list are checked (with additional filtering only those which have indices). + For each index of selected RDB tables we get name of its first field (i.e. started part of index key). + Then we construct query with INNER JOIN that involves such index to be included in execution plan. + For example, for table RDB$DEPENDENCIES with two indices: + CREATE INDEX RDB$INDEX_27 ON RDB$DEPENDENCIES (RDB$DEPENDENT_NAME, RDB$DEPENDENT_TYPE); + CREATE INDEX RDB$INDEX_28 ON RDB$DEPENDENCIES (RDB$DEPENDED_ON_NAME, RDB$DEPENDED_ON_TYPE, RDB$FIELD_NAME); + - following queries will be generated: + ------------------------------------- + 1) Index for columns (RDB$DEPENDENT_NAME, RDB$DEPENDENT_TYPE) will be involved here: + select a.rdb$dependent_name, count(*) + from rdb$dependencies a + join rdb$dependencies b on a.rdb$dependent_name = b.rdb$dependent_name + where a.rdb$dependent_name = ? and a.rdb$dependent_name < b.rdb$dependent_name + group by 1 + + 2) Index for column (RDB$DEPENDED_ON_NAME, RDB$DEPENDED_ON_TYPE, RDB$FIELD_NAME) will be involved here: + select a.rdb$depended_on_name, count(*) + from rdb$dependencies a + join rdb$dependencies b on a.rdb$depended_on_name = b.rdb$depended_on_name + where a.rdb$depended_on_name = ? and a.rdb$depended_on_name < b.rdb$depended_on_name + group by 1 + ------------------------------------- + + When every such query is prepared, optimizer must automatically update appropriate index statistics. + KEY NOTE: this will be done only when system table is not empty and has no more than 100 data pages, + see FB (see in Optimizer.cpp: "... (relPages->rel_data_pages > 0) && (relPages->rel_data_pages < 100)"). + + Because of need to have some data in RDB tables, we must run DDL which force these tables to be fulfilled. + For this purpose test creates DB objects for each object type (mapping, sequences etc). + Also, "NBACKUP -B ..." is called times in order to fulfill table RDB$BACKUP_HISTORY table. + + Then, we get IndexRoot page number for each of selected RDB tables and start to parse this page content. + During this parsing (see func 'parse_index_root_page') we get values of th statistics for each of starting + parts of compound index (or for single segment for usual index). We accumulate this data in rel_sel_map{}. + Finally, we check that all indices has NON-zero statistics: rel_sel_map{} must have NO items with zero values. +NOTES: + [21.12.2024] pzotov + Currently only FB 6.x has this feature (since 22-mar-2024 11:46). + Commit: https://github.com/FirebirdSQL/firebird/commit/ef66a9b4d803d5129a10350c54f00bc637c09b48 + + ::: ACHTUNG ::: Index statistics must be searched in the Index Root page rather than in RDB$INDICES! + Internals of misc FB page types can be found here: + https://firebirdsql.org/file/documentation/html/en/firebirddocs/firebirdinternals/firebird-internals.html + + It is supposed that there are no expression-based indices for selected system tables (this case was not investigated). + + Confirmed ticket issue on 6.0.0.294-c353de4 (21-mar-2024 16:45): some of system tables remain with non-updated index statistics. + Checked on 6.0.0.295-ef66a9b (22-mar-2024 13:48): all OK,every of checked system tables has non-zero statistics for its indices. + Test executiuon time: ~8...10 seconds. + + [24.02.2025] + Changed offset calculations according to #93db88: ODS14: header page refactoring (#8401) + See: https://github.com/FirebirdSQL/firebird/commit/93db88084d5a04aaa3f98179e76cdfa092431fa8 + + Thanks to Vlad for explanations. +""" +import sys +import binascii +import struct +from typing import List +from pathlib import Path + +import pytest +from firebird.qa import * +from firebird.driver import DatabaseError, DbInfoCode + +import locale +import time + +EXPECTED_MSG = 'Expected: every checked system table has non-zero index statistics.' + +IRT_PAGE_TYPE = 6 +CHECKED_SYS_TABLES = """ + ( + 'RDB$AUTH_MAPPING' + ,'RDB$BACKUP_HISTORY' + ,'RDB$CHARACTER_SETS' + ,'RDB$CHECK_CONSTRAINTS' + ,'RDB$COLLATIONS' + ,'RDB$DEPENDENCIES' + ,'RDB$EXCEPTIONS' + ,'RDB$FIELDS' + ,'RDB$FIELD_DIMENSIONS' + ,'RDB$FILTERS' + ,'RDB$FORMATS' + ,'RDB$FUNCTIONS' + ,'RDB$FUNCTION_ARGUMENTS' + ,'RDB$GENERATORS' + ,'RDB$INDEX_SEGMENTS' + ,'RDB$INDICES' + ,'RDB$PACKAGES' + ,'RDB$PROCEDURES' + ,'RDB$PROCEDURE_PARAMETERS' + ,'RDB$PUBLICATIONS' + ,'RDB$PUBLICATION_TABLES' + ,'RDB$REF_CONSTRAINTS' + ,'RDB$RELATIONS' + ,'RDB$RELATION_CONSTRAINTS' + ,'RDB$RELATION_FIELDS' + ,'RDB$ROLES' + ,'RDB$SECURITY_CLASSES' + ,'RDB$TRIGGERS' + ,'RDB$TYPES' + ,'RDB$USER_PRIVILEGES' + ,'RDB$VIEW_RELATIONS' + ) +""" + +NBACKUP_RUNS_CNT = 10 +INIT_DB_OBJECTS_CNT = 30 + +# SQL script which will cause filling of RDB tables with some data: +INIT_DB_OBJECTS_SQL = f""" + alter database enable publication; + set term ^; + execute block as + declare n_obj_cnt smallint = {INIT_DB_OBJECTS_CNT}; + declare i int; + declare v_sttm varchar(8190); + begin + i = 0; + while (i < n_obj_cnt) do + begin + v_sttm = 'recreate sequence g_' || i; + execute statement v_sttm; + v_sttm = 'create or alter mapping local_map_' || i || ' using any plugin from group musicians to role guitarist'; + execute statement v_sttm; + + begin + v_sttm = 'drop collation name_coll_' || i; + execute statement v_sttm; + when any do + begin + end + end + + begin + v_sttm = 'drop role r_manager_' || i; + execute statement v_sttm; + when any do + begin + end + end + + begin + v_sttm = 'drop table tbl_' || i; + execute statement v_sttm; + when any do + begin + end + end + + begin + v_sttm = 'drop domain dm_' || i; + execute statement v_sttm; + when any do + begin + end + end + + begin + v_sttm = 'drop filter jpg_' || i; + execute statement v_sttm; + when any do + begin + end + end + + -- examples\api\api9f.sql + -- declare filter desc_filter_01 -- ==> will be saved in indexed column rdb$filters.rdb$function_name + -- input_type 1 + -- output_type -4 + -- entry_point 'desc_filter' + -- module_name 'api9f' + -- ; + + v_sttm = 'declare filter jpg_' || i || ' input_type ' || i || ' output_type -4 entry_point ''desc_filter'' module_name ''api9f'''; + execute statement v_sttm; + + + v_sttm = 'create collation name_coll_' || i || ' for utf8 from unicode case insensitive'; + execute statement v_sttm; + + v_sttm = 'recreate exception exc_' || i || ' ''missing element with index @1'''; + execute statement v_sttm; + + v_sttm = 'create role r_manager_' || i; + execute statement v_sttm; + + v_sttm = 'create domain dm_' || i || ' as int not null check(value > 0)'; + execute statement v_sttm; + + v_sttm = 'recreate table tbl_' || i + || '( id int generated by default as identity constraint pk_tbl_'|| i || ' primary key' + || ', pid int' + || ', f_0 dm_' || i + || ', f_01 int' + || ', f_02 int[3,4]' + || ', constraint fktbl_' || i || ' foreign key(pid) references tbl_' || i || '(id)' + || ', constraint chk_tbl_' || i || ' check (f_01 > 0)' + || ')' + ; + execute statement v_sttm; + + v_sttm = 'recreate trigger trg_' || i || '_bi for tbl_' || i + || ' active before insert as' + || ' begin' + || ' end' + ; + execute statement v_sttm; + + execute statement 'alter database include table tbl_' || i || ' to publication'; + + + v_sttm = 'recreate view vew_' || i || ' as select 1 x from rdb$database'; + execute statement v_sttm; + + v_sttm = 'create or alter procedure sp_' || i || '(a_0 int, a_1 varchar(10)) as begin end'; + execute statement v_sttm; + + v_sttm = 'create or alter function fn_' || i || '(a_0 int, a_1 int) returns int as begin return a_0 + a_1; end'; + execute statement v_sttm; + + v_sttm = 'create or alter package pg_' || i || ' as begin function pg_fn() returns int; end'; + execute statement v_sttm; + + v_sttm = 'recreate package body pg_' || i || ' as begin function pg_fn() returns int as begin return 1; end end'; + execute statement v_sttm; + + i = i + 1; + end + end + ^ + ------------------------------------ + set term ;^ + commit; +""" + +db = db_factory(init = INIT_DB_OBJECTS_SQL) +act = python_act('db', substitutions = [('[ \t]+', ' ')]) +tmp_nbk_lst = temp_files( [ f'tmp_8057.{i}.nbk' for i in range(NBACKUP_RUNS_CNT) ] ) + +#----------------------------------------------------------------------- + +def parse_index_root_page(db_file, pg_size, rel_name, irt_page_number, rel_sel_map, verbose = False): + + # rel_sel_map -- byref + min_irtd_selec = sys.float_info.max + + with open( db_file, "rb") as db_handle: + db_handle.seek( irt_page_number * pg_size ) + page_content = db_handle.read( pg_size ) + page_as_hex=binascii.hexlify( page_content ) + if verbose: + print(f'{pg_size=}, page_as_hex:') + print(page_as_hex.decode("utf-8")) + + # https://firebirdsql.org/file/documentation/html/en/firebirddocs/firebirdinternals/firebird-internals.html#fbint-page-6 + # https://docs.python.org/3/library/struct.html#format-characters + """ + See src/jrd/ods.h, https://github.com/FirebirdSQL/firebird/pull/8340 + *** BEFORE *** + struct index_root_page + { + pag irt_header; + USHORT irt_relation; + USHORT irt_count; + struct irt_repeat { + SLONG irt_root; + union { + float irt_selectivity; + SLONG irt_transaction; + } irt_stuff; + USHORT irt_desc; + UCHAR irt_keys; + UCHAR irt_flags; + } irt_rpt[1]; + }; + + *** AFTER *** + struct index_root_page + { + pag irt_header; + USHORT irt_relation; // 2 relation id (for consistency) + USHORT irt_count; // 2 number of indices + ULONG irt_dummy; // 4 so far used as a padding to ensure the same alignment in 32-bit and 64-bit builds + struct irt_repeat + { + FB_UINT64 irt_transaction; // 8 transaction in progress + ULONG irt_page_num; // 4 page number + ULONG irt_page_space_id; // 4 page space + USHORT irt_desc; // 2 offset to key descriptions + USHORT irt_flags; // 2 index flags + UCHAR irt_state; // 1 index state + UCHAR irt_keys; // 1 number of keys in index + USHORT irt_dummy; // 2 alignment to 8-byte boundary + } irt_rpt[1]; + }; + + """ + + # Two bytes, UNsigned. Offset 0x10 on the page. The relation id. This is the value of RDB$RELATIONS.RDB$RELATION_ID. + irt_relation = struct.unpack_from('@H', page_content[0x10:0x12])[0] # (128,) --> 128 + + # Two bytes, UNsigned. Offset 0x12 on the page. The number of indices defined for this table. + irt_count = struct.unpack_from('@H', page_content[0x12:0x14])[0] + + # 24.02.2025 + irt_dummy = struct.unpack_from('@H', page_content[0x14:0x18])[0] + + if verbose: + print(f'{irt_relation=}, {rel_name.strip()=}, {irt_count=}') + + for i in range(irt_count): + + ################### + IRT_REPEAT_LEN = 24 + ################### + + irt_tran_offset_i = i * IRT_REPEAT_LEN + int(0x18) + irt_page_num_offset = i * IRT_REPEAT_LEN + int(0x26) # 18 + 8 + irt_page_space_id_offset = i * IRT_REPEAT_LEN + int(0x30) # 26 + 4 + irt_desc_offset_i = i * IRT_REPEAT_LEN + int(0x34) # 30 + 4 + irt_flags_offset_i = i * IRT_REPEAT_LEN + int(0x36) # 34 + 2 + irt_state_offset_i = i * IRT_REPEAT_LEN + int(0x38) # 36 + 2 + irt_keys_offset_i = i * IRT_REPEAT_LEN + int(0x39) # 38 + 1 + irt_dummy_offset = i * IRT_REPEAT_LEN + int(0x40) # 39 + 1 + + + # Four bytes, SIGNED. Offset 0x00 in each descriptor array entry. + # This field is the page number where the root page for the individual index (page type 0x07) is located. + # 24.02.2025: this field no more exists in new ODS: + irt_root_i = -1 # before 24.02.2025: struct.unpack_from('@i', page_content[irt_root_offset_i : irt_root_offset_i + 4])[0] + + # Normally this field will be zero but if an index is in the process of being created, the transaction id will be found here. + # irt_tran_i = struct.unpack_from('@i', page_content[irt_tran_offset_i : irt_tran_offset_i + 4])[0] + # 24.02.2025: this field now is FB_UINT64, i.e 8 bytes: + irt_tran_i = struct.unpack_from('@i', page_content[irt_tran_offset_i : irt_tran_offset_i + 8])[0] + + # Two bytes, UNsigned. Offset 0x08 in each descriptor array entry. This field holds the offset, from the start of the page, + # to the index field descriptors which are located at the bottom end (ie, highest addresses) of the page. + # To calculate the starting address, add the value in this field to the address of the start of the page. + irt_desc_i = struct.unpack_from('@H', page_content[irt_desc_offset_i : irt_desc_offset_i + 2])[0] + + # One byte, UNsigned. This defines the number of keys (columns) in this index. + irt_keys_i = struct.unpack_from('@B', page_content[ irt_keys_offset_i : irt_keys_offset_i + 1])[0] + + # One byte, UNsigned. The flags define various attributes for this index, these are encoded into various bits in the field, as follows: + # See src/jrd/btr.h + # Bit 0 : Index is unique (set) or not (unset). + # Bit 1 : Index is descending (set) or ascending (unset). + # Bit 2 : Index [creation?] is in progress (set) or not (unset). + # Bit 3 : Index is a foreign key index (set) or not (unset). + # Bit 4 : Index is a primary key index (set) or not (unset). + # Bit 5 : Index is expression based (set) or not (unset). + # Bit 6 : Index is conditional + irt_flags_i = struct.unpack_from('@B', page_content[ irt_flags_offset_i : irt_flags_offset_i + 1])[0] + + if verbose: + print(f' {i=} ::: {irt_root_i=}, {irt_tran_i=}, {irt_desc_i=}, {irt_keys_i=}, {irt_flags_i=}, bin(irt_flags_i)=','{0:08b}'.format(irt_flags_i)) + + for j in range(irt_keys_i): + + # Two bytes, UNsigned. Offset 0x00 in each field descriptor. This field defines the field number of the table that makes up 'this' field in the index. + # This number is equivalent to RDB$RELATION_FIELDS.RDB$FIELD_ID. + irtd_field = struct.unpack_from('@H', page_content[ j*8 + irt_desc_i : j*8 + irt_desc_i + 2])[0] + #print(f' column: {j} ::: {irtd_field=}') + + # Two bytes, UNsigned. Offset 0x02 in each field descriptor. This determines the data type of the appropriate field in the index. + irtd_itype = struct.unpack_from('@H', page_content[ j*8 + irt_desc_i + 2 : j*8 + irt_desc_i + 4])[0] + #print(f' {irtd_itype=}') + + # Four bytes, floating point format. Offset 0x04 in each field descriptor. This field holds the selectivity of this particular column in the index. + irtd_selec = struct.unpack_from('@f', page_content[ j*8 + irt_desc_i + 4 : j*8 + irt_desc_i + 8])[0] + min_irtd_selec = min(min_irtd_selec, irtd_selec) + #print(f' {irtd_selec=}') + + # Input, byRef: + rel_sel_map[rel_name.strip()] = min_irtd_selec + +#----------------------------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------------------- + +@pytest.mark.version('>=6.0') +def test_1(act: Action, tmp_nbk_lst: List[Path], capsys): + + try: + for i,tmp_nbk_i in enumerate(tmp_nbk_lst): + act.expected_stderr = '' + tmp_nbk_i.unlink(missing_ok = True) + act.nbackup(switches=['-b', str(i), act.db.dsn, tmp_nbk_i], io_enc = locale.getpreferredencoding()) + assert act.clean_stderr == act.clean_expected_stderr + act.reset() + except DatabaseError as e: + print(e.__str__()) + + #----------------------------------------------------------- + sql_get_sys_tables_index_info = f""" + with recursive r + as ( + select + rr.rdb$relation_id as rel_id + ,rr.rdb$relation_name rel_name + ,ri.rdb$index_id-1 as idx_id + ,ri.rdb$index_name idx_name + ,ri.rdb$segment_count seg_cnt + ,rs.rdb$field_position fpos + ,rs.rdb$field_name fname + ,',' || cast( trim(rs.rdb$field_name) as varchar(8190) ) as idx_key + ,sign(octet_length(ri.rdb$expression_blr)) as idx_on_expr + from rdb$relations rr + join rdb$indices ri on rr.rdb$relation_name = ri.rdb$relation_name + join rdb$index_segments rs on ri.rdb$index_name = rs.rdb$index_name + where + ri.rdb$system_flag = 1 + and ri.rdb$relation_name in {CHECKED_SYS_TABLES} + and ri.rdb$index_inactive is distinct from 1 + and (rs.rdb$field_position = 0 or ri.rdb$expression_blr is not null) + + union all + + select + r.rel_id + ,r.rel_name + ,ri.rdb$index_id-1 + ,ri.rdb$index_name + ,r.seg_cnt + ,rs.rdb$field_position + ,rs.rdb$field_name fname + ,r.idx_key || ',' || trim(rs.rdb$field_name ) + ,r.idx_on_expr + from rdb$indices ri + join rdb$index_segments rs on ri.rdb$index_name = rs.rdb$index_name + join r on ri.rdb$relation_name = r.rel_name + and ri.rdb$index_name = r.idx_name + and (rs.rdb$field_position = r.fpos+1 or r.idx_on_expr = 1) + ) + --select * from r + + ,m as ( + select + rel_id + ,rel_name + ,idx_id + ,idx_name + ,iif(idx_on_expr = 1, '', substring(idx_key from 2)) as idx_key + from r + where fpos = seg_cnt-1 or idx_on_expr = 1 + ) + select m.rel_id, m.rel_name, m.idx_id, m.idx_name, m.idx_key, p.rdb$page_number as irt_page + from m + join rdb$pages p on m.rel_id = p.rdb$relation_id and p.rdb$page_type = {IRT_PAGE_TYPE} + order by rel_id, idx_id + """ + + with act.db.connect() as con: + cur = con.cursor() + cur.execute(sql_get_sys_tables_index_info) + rel_irt_map = {} + for r in cur: + rel_id, rel_name, idx_id, idx_name, idx_key, irt_page = r[:6] + idx_starting_fld = idx_key.split(",")[0] + + #print(f'{rel_id=}, {rel_name.strip()=}, {irt_page=}') + #print(f'{idx_id=}, {idx_name.strip()=}') + #print(f'{idx_key=}, starting field: {idx_starting_fld}') + + sql_to_make_recalc_idx_stat = f""" + select a.{idx_starting_fld}, count(*) + from {rel_name.strip()} a + join {rel_name.strip()} b on a.{idx_starting_fld} = b.{idx_starting_fld} + where a.{idx_starting_fld} = ? and a.{idx_starting_fld} < b.{idx_starting_fld} + group by 1 + """ + + #print(f'{sql_to_make_recalc_idx_stat=}') + ps = None + try: + # This must cause update index statistics of : + ps = cur.prepare(sql_to_make_recalc_idx_stat) + + # Print explained plan with padding eash line by dots in order to see indentations: + #print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + #print('') + except DatabaseError as e: + print(e.__str__()) + print(e.gds_codes) + finally: + if ps: + ps.free() + + rel_irt_map[ rel_id ] = (rel_name, irt_page) + + rel_sel_map = {} # K = rel_name, V = minimal selectivity (i.e. min irtd_selec for all indices) + + # NB: we have to re-connect in order to see updated indices statistics! + with act.db.connect() as con: + cur = con.cursor() + + for rel_id, (rel_name, irt_no) in rel_irt_map.items(): + parse_index_root_page(act.db.db_path, con.info.page_size, rel_name, irt_no, rel_sel_map, verbose = False) + + if min(rel_sel_map.values()) > 0: + print(EXPECTED_MSG) + else: + print('UNEXPECTED: AT LEAST ONE OF SYSTEM TABLES HAS ZERO INDEX STATISTICS') + for rel_name, min_idx_selectivity in rel_sel_map.items(): + print(f'{rel_name=}, {min_idx_selectivity=}') + + act.expected_stdout = f""" + {EXPECTED_MSG} + """ + + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8061_addi_test.py b/tests/bugs/gh_8061_addi_test.py new file mode 100644 index 00000000..c99b81ea --- /dev/null +++ b/tests/bugs/gh_8061_addi_test.py @@ -0,0 +1,225 @@ +#coding:utf-8 + +""" +ID: issue-8061 +ISSUE: https://github.com/FirebirdSQL/firebird/pull/8061 +TITLE: UNNEST subqueries invalidation. Examples when unnesting can NOT be used. +DESCRIPTION: + Test uses DDL and data from employee DB but they have been extracted to .sql and stored in + files/standard_sample_databases.zip (file: "sample-DB_-_firebird.sql"). + Default names of all constraints were replaced in order to easy find appropriate table. + + Examples for this test based on + 1) https://blogs.oracle.com/optimizer/post/optimizer-transformations-subquery-unesting-part-2 + (paragraph "Validity of Unnesting") + 2) https://jonathanlewis.wordpress.com/2007/02/26/subquery-with-or/ +NOTES: + 1. One need to change config parameter SubQueryConversion to 'true' when check FB 5.x. + 2. Commits: + 6.x: + 22.03.2025 10:47 + https://github.com/FirebirdSQL/firebird/commit/fc12c0ef392fec9c83d41bc17da3dc233491498c + (Unnest IN/ANY/EXISTS subqueries and optimize them using semi-join algorithm (#8061)) + 5.x + 31.07.2024 09:46 + https://github.com/FirebirdSQL/firebird/commit/4943b3faece209caa93cc9573803677019582f1c + (Added support for semi/anti and outer joins to hash join algorithm ...) + Also: + 14.09.2024 09:24 + https://github.com/FirebirdSQL/firebird/commit/5fa4ae611d18fd4ce9aac1c8dbc79e5fea2bc1f2 + (Fix bug #8252: Incorrect subquery unnesting with complex dependencies) + + Checked on 6.0.0.735, 5.0.3.1647 +""" + +import pytest +import zipfile +from pathlib import Path +from firebird.qa import * +from firebird.driver import driver_config, connect + +db = db_factory() +# Hash Join (semi) (keys: 1, total key length: 4) +substitutions = [(r'Hash Join \(semi\) \(keys: \d+, total key length: \d+\)', 'Hash Join (semi)'), (r'record length: \d+', 'record length: NN')] + +act = python_act('db', substitutions = substitutions) + +tmp_sql = temp_file('gh_8061.tmp.sql') + +query_map = { + 1000 : ( + """ + select c3.cust_no + from customer c3 + where exists ( + select s3.cust_no + from sales s3 + where s3.cust_no = c3.cust_no and + exists ( + select x.emp_no + from employee x + where + x.job_country = c3.country + ) + ) + """ + ,""" + Subqueries that are correlated to non-parent; for example, + subquery SQ3 is contained by SQ2 (parent of SQ3) and SQ2 in turn is contained + by SQ1 and SQ3 is correlated to tables defined in SQ1. + """ + ) + ,2000 : ( + """ + select c3.cust_no + from customer c3 + where exists ( + select s3.cust_no + from sales s3 + where s3.cust_no = c3.cust_no + group by s3.cust_no + ) + """ + ,""" + A group-by subquery is correlated; in this case, unnesting implies doing join + after group-by. Changing the given order of the two operations may not be always legal. + """ + ) + ,3000 : ( + """ + select s1.cust_no + from sales s1 + where exists ( + select 1 from customer c1 where s1.cust_no = c1.cust_no + union all + select 1 from employee x1 where s1.sales_rep = x1.emp_no + ) + """ + ,""" + For disjunctive subqueries, the outer columns in the connecting + or correlating conditions are not the same. + """ + ) + ,4000 : ( + """ + select x1.emp_no + from employee x1 + where + ( + x1.job_country = 'USA' or + exists ( + select 1 + from sales s1 + where s1.sales_rep = x1.emp_no + ) + ) + """ + ,'An `OR` condition in compound WHERE expression, see https://jonathanlewis.wordpress.com/2007/02/26/subquery-with-or/' + ) +} + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- + +@pytest.mark.version('>=5.0.1') +def test_1(act: Action, tmp_sql: Path, capsys): + employee_data_sql = zipfile.Path(act.files_dir / 'standard_sample_databases.zip', at='sample-DB_-_firebird.sql') + tmp_sql.write_bytes(employee_data_sql.read_bytes()) + + act.isql(switches = ['-q'], charset='utf8', input_file = tmp_sql, combine_output = True) + + if act.return_code == 0: + + srv_cfg = driver_config.register_server(name = f'srv_cfg_8061_addi', config = '') + db_cfg_name = f'db_cfg_8061_addi' + db_cfg_object = driver_config.register_database(name = db_cfg_name) + db_cfg_object.server.value = srv_cfg.name + db_cfg_object.database.value = str(act.db.db_path) + if act.is_version('<6'): + db_cfg_object.config.value = f""" + SubQueryConversion = true + """ + + with connect(db_cfg_name, user = act.db.user, password = act.db.password) as con: + cur = con.cursor() + for q_idx, q_tuple in query_map.items(): + test_sql, qry_comment = q_tuple[:2] + ps = cur.prepare(test_sql) + print(q_idx) + print(test_sql) + print(qry_comment) + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + ps.free() + + else: + # If retcode !=0 then we can print the whole output of failed gbak: + print('Initial script failed, check output:') + for line in act.clean_stdout.splitlines(): + print(line) + act.reset() + + act.expected_stdout = f""" + 1000 + {query_map[1000][0]} + {query_map[1000][1]} + Sub-query + ....-> Filter + ........-> Table "EMPLOYEE" as "X" Full Scan + Sub-query + ....-> Filter (preliminary) + ........-> Filter + ............-> Table "SALES" as "S3" Access By ID + ................-> Bitmap + ....................-> Index "SALES_CUSTOMER_FK_CUST_NO" Range Scan (full match) + Select Expression + ....-> Filter + ........-> Table "CUSTOMER" as "C3" Full Scan + + 2000 + {query_map[2000][0]} + {query_map[2000][1]} + Sub-query + ....-> Aggregate + ........-> Filter + ............-> Table "SALES" as "S3" Access By ID + ................-> Index "SALES_CUSTOMER_FK_CUST_NO" Range Scan (full match) + Select Expression + ....-> Filter + ........-> Table "CUSTOMER" as "C3" Full Scan + + 3000 + {query_map[3000][0]} + {query_map[3000][1]} + Sub-query + ....-> Union + ........-> Filter + ............-> Table "CUSTOMER" as "C1" Access By ID + ................-> Bitmap + ....................-> Index "CUSTOMER_PK" Unique Scan + ........-> Filter + ............-> Table "EMPLOYEE" as "X1" Access By ID + ................-> Bitmap + ....................-> Index "EMPLOYEE_PK" Unique Scan + Select Expression + ....-> Filter + ........-> Table "SALES" as "S1" Full Scan + + 4000 + {query_map[4000][0]} + {query_map[4000][1]} + Sub-query + ....-> Filter + ........-> Table "SALES" as "S1" Access By ID + ............-> Bitmap + ................-> Index "SALES_EMPLOYEE_FK_SALES_REP" Range Scan (full match) + Select Expression + ....-> Filter + ........-> Table "EMPLOYEE" as "X1" Full Scan + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8061_test.py b/tests/bugs/gh_8061_test.py new file mode 100644 index 00000000..8b6ee94e --- /dev/null +++ b/tests/bugs/gh_8061_test.py @@ -0,0 +1,232 @@ +#coding:utf-8 + +""" +ID: issue-8061 +ISSUE: https://github.com/FirebirdSQL/firebird/pull/8061 +TITLE: Unnest IN/ANY/EXISTS subqueries and optimize them using semi-join algorithm +DESCRIPTION: + Test uses DDL and data from employee DB but they have been extracted to .sql and stored in + files/standard_sample_databases.zip (file: "sample-DB_-_firebird.sql"). + Default names of all constraints were replaced in order to easy find appropriate table. + + Some examples for this test were taken from: + https://blogs.oracle.com/optimizer/post/optimizer-transformations-subquery-unnesting-part-1 +NOTES: + 1. One need to change config parameter SubQueryConversion to 'true' when check FB 5.x. + 2. Explained plan in FB 5.x has no details about keys and total key length, so we have to apply + substitution in order to ignore these data when make comparison with expected output. + 3. Commits: + 6.x: + 22.03.2025 10:47 + https://github.com/FirebirdSQL/firebird/commit/fc12c0ef392fec9c83d41bc17da3dc233491498c + (Unnest IN/ANY/EXISTS subqueries and optimize them using semi-join algorithm (#8061)) + 5.x + 31.07.2024 09:46 + https://github.com/FirebirdSQL/firebird/commit/4943b3faece209caa93cc9573803677019582f1c + (Added support for semi/anti and outer joins to hash join algorithm ...) + Also: + 14.09.2024 09:24 + https://github.com/FirebirdSQL/firebird/commit/5fa4ae611d18fd4ce9aac1c8dbc79e5fea2bc1f2 + (Fix bug #8252: Incorrect subquery unnesting with complex dependencies) + + 4. Following tests also relate to unnesting but they check only FB 5.x (and not FB 6.x): + bugs/gh_8265_test.py; // additional examples related to ability of subquery unnesting; + bugs/gh_8252_test.py; // example when unnesting must NOT be performed; + bugs/gh_8233_test.py; + bugs/gh_8231_test.py; + bugs/gh_8225_test.py; + bugs/gh_8223_test.py; + All these tests will be reimplemented soon in order to check FB 6.x also. + + Confirmed old execution plan in 6.0.0.680 (19.03.2025), it had no 'hash join (semi)' in any explanied plan. + Checked on 6.0.0.687-730aa8f (22-mar-2025), 5.0.1.1464-d1033cc (01-aug-2024). +""" + +import pytest +import zipfile +from pathlib import Path +from firebird.qa import * +from firebird.driver import driver_config, connect + +db = db_factory() +# Hash Join (semi) (keys: 1, total key length: 4) +substitutions = [(r'Hash Join \(semi\) \(keys: \d+, total key length: \d+\)', 'Hash Join (semi)'), (r'record length: \d+', 'record length: NN')] + +act = python_act('db', substitutions = substitutions) + +tmp_sql = temp_file('gh_8061.tmp.sql') + +query_map = { + 1000 : ( + """ + select c1.cust_no + from customer c1 + where exists ( + select 1 from sales s1 where s1.cust_no = c1.cust_no and s1.qty_ordered > 10 + ) + """ + ,'Check unnesting of single EXISTS' + ) + ,2000 : ( + """ + select c2.cust_no + from customer c2 + where c2.cust_no = any ( + select s2.cust_no + from sales s2 + where s2.qty_ordered > 10 + ) + """ + ,'Check unnesting of ANY' + ) + ,3000 : ( + """ + select c3.cust_no + from customer c3 + where exists ( + select s3.cust_no + from sales s3 + where s3.cust_no = c3.cust_no and + exists ( + select x.emp_no + from employee x + where + x.emp_no = s3.sales_rep + and ( + x.dept_no > 0 + or + x.job_code > '' + ) + ) + ) + """ + ,'Check unnesting of two nested EXISTS' + ) + ,4000 : ( + """ + select c4.cust_no + from customer c4 + where c4.cust_no in + ( + select s4.cust_no + from sales s4 + where + s4.paid > '' + or + s4.sales_rep in ( + select x.emp_no + from employee x + where + x.dept_no > 0 + or + x.job_code > '' + ) + ) + """ + ,'Check unnesting of IN (NOTE: inner sub-query cannot be unnested due to OR condition present, but the outer sub-query *can*; see also bugs/gh_8265_test.py)' + ) +} + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- + +@pytest.mark.version('>=5.0.1') +def test_1(act: Action, tmp_sql: Path, capsys): + employee_data_sql = zipfile.Path(act.files_dir / 'standard_sample_databases.zip', at='sample-DB_-_firebird.sql') + tmp_sql.write_bytes(employee_data_sql.read_bytes()) + + act.isql(switches = ['-q'], charset='utf8', input_file = tmp_sql, combine_output = True) + + if act.return_code == 0: + + srv_cfg = driver_config.register_server(name = f'srv_cfg_8061', config = '') + db_cfg_name = f'db_cfg_8061' + db_cfg_object = driver_config.register_database(name = db_cfg_name) + db_cfg_object.server.value = srv_cfg.name + db_cfg_object.database.value = str(act.db.db_path) + if act.is_version('<6'): + db_cfg_object.config.value = f""" + SubQueryConversion = true + """ + + with connect(db_cfg_name, user = act.db.user, password = act.db.password) as con: + cur = con.cursor() + for q_idx, q_tuple in query_map.items(): + test_sql, qry_comment = q_tuple[:2] + ps = cur.prepare(test_sql) + print(q_idx) + print(test_sql) + print(qry_comment) + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + ps.free() + + else: + # If retcode !=0 then we can print the whole output of failed gbak: + print('Initial script failed, check output:') + for line in act.clean_stdout.splitlines(): + print(line) + act.reset() + + act.expected_stdout = f""" + 1000 + {query_map[1000][0]} + {query_map[1000][1]} + Select Expression + ....-> Filter + ........-> Hash Join (semi) (keys: 1, total key length: 4) + ............-> Table "CUSTOMER" as "C1" Full Scan + ............-> Record Buffer (record length: 33) + ................-> Filter + ....................-> Table "SALES" as "S1" Full Scan + + 2000 + {query_map[2000][0]} + {query_map[2000][1]} + Select Expression + ....-> Filter + ........-> Hash Join (semi) (keys: 1, total key length: 4) + ............-> Table "CUSTOMER" as "C2" Full Scan + ............-> Record Buffer (record length: 33) + ................-> Filter + ....................-> Table "SALES" as "S2" Full Scan + + 3000 + {query_map[3000][0]} + {query_map[3000][1]} + Select Expression + ....-> Filter + ........-> Hash Join (semi) (keys: 1, total key length: 4) + ............-> Table "CUSTOMER" as "C3" Full Scan + ............-> Record Buffer (record length: 58) + ................-> Filter + ....................-> Hash Join (semi) (keys: 1, total key length: 2) + ........................-> Table "SALES" as "S3" Full Scan + ........................-> Record Buffer (record length: 41) + ............................-> Filter + ................................-> Table "EMPLOYEE" as "X" Full Scan + + 4000 + {query_map[4000][0]} + {query_map[4000][1]} + Sub-query + ....-> Filter + ........-> Filter + ............-> Table "EMPLOYEE" as "X" Access By ID + ................-> Bitmap + ....................-> Index "EMPLOYEE_PK" Unique Scan + Select Expression + ....-> Filter + ........-> Hash Join (semi) (keys: 1, total key length: 4) + ............-> Table "CUSTOMER" as "C4" Full Scan + ............-> Record Buffer (record length: 33) + ................-> Filter + ....................-> Table "SALES" as "S4" Full Scan + + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8077_test.py b/tests/bugs/gh_8077_test.py new file mode 100644 index 00000000..3ae6a0f5 --- /dev/null +++ b/tests/bugs/gh_8077_test.py @@ -0,0 +1,200 @@ +#coding:utf-8 + +""" +ID: issue-8077 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8077 +TITLE: Error "Too many recursion levels" does not stop execuition of code that uses ON DISCONNECT trigger (FB 4.x+) +DESCRIPTION: + Test creates DB-triggers 'ON CONNECT' and 'ON DISCONNECT', plus table 'LOG' for logging actions performed by DB-triggers. + Trigger 'ON DISCONNECT' makes EDS with new role thus creating new connection which will then be immediately finished and, + in turn, fire again this trigger. Eventually this must cause ISQL to terminate and firebird.log must contain errors. + Difference between old and new firebird.log must contain exactly two errors about detahc problem + (Error at disconnect: / Execute statement error at attach : / 335544830 : Too many recursion levels of EXECUTE STATEMENT) +NOTES: + [27.05.2024] pzotov + Time of ISQL execution is limited by MAX_WAIT_FOR_ISQL_TERMINATE seconds. Currently it is ~6s for SS and ~18s for CS. + + [08.06.2024] pzotov + Added threshold in order to prevent infinite recursion in case of regression. + Otherwise this test can cause collapse of test machine because of infinite launch of firebird processes (in case of Classic). + See notes in the code below, variable 'STOP_RECURSIVE_ES_AFTER_ITER'. + Checked on snapshot 5.x that was not yet fixed. + + Checked on 6.0.0.362, 5.0.1.1408, 4.0.5.3103 (all SS/CS). +""" + +import re +from difflib import unified_diff +import pytest +import time +from pathlib import Path +import subprocess +import locale + +import firebird.driver +from firebird.qa import * + +db = db_factory(do_not_drop = True) +#db = db_factory() +substitutions = [ + ('^((?!(E|e)rror|exception|statement|recursion|source|rdb_trg|trigger|ext_pool_active|isql_outcome).)*$', '') + ,('Execute statement error.*', 'Execute statement error') + ,('Firebird::.*', 'Firebird::') + ,('line(:)?\\s+\\d+.*', '') + ,('[ \t]+', ' ') + ] +act = python_act('db', substitutions = substitutions) + +tmp_sql = temp_file('tmp_8077.sql') +tmp_log = temp_file('tmp_8077.log') + +MAX_WAIT_FOR_ISQL_BEGIN_WORK=0.5 +MAX_WAIT_FOR_ISQL_TERMINATE=30 + +# ### NOTE ### +# We have to define generator and increment it on each DB-level trigger invocation in order to prevent infinite loop +# if its value will be greater than some threshold (in case if regression will occur and engine for some reason will +# not able to detect too deep recursion). +# Value of this generator will be compared with threshold ('STOP_RECURSIVE_ES_AFTER_ITER') and no further recursive +# calls will be executed if generator exceeds this threshold. +# But value of threshold STOP_RECURSIVE_ES_AFTER_ITER must NOT be too small. +# Otherwise following message will be MESSED from output: +# Error at disconnect: +# Execute statement error +# 335544830 : Too many recursion levels of EXECUTE STATEMENT +# Data source : Firebird:: +# At trigger 'TRG_DETACH' +# Currently last value of this sequence (after exception raising) is 99 for both SS and CS. +# +STOP_RECURSIVE_ES_AFTER_ITER = 101 + +#-------------------------------------------------------------------- + +@pytest.mark.es_eds +@pytest.mark.version('>=4.0.5') +def test_1(act: Action, tmp_sql: Path, tmp_log: Path, capsys): + + test_sql = f""" + set list on; + set bail on; + + set term ^; + create or alter trigger trg_detach on disconnect as begin end + ^ + create or alter trigger trg_attach on connect as begin end + ^ + create sequence g_attach + ^ + create sequence g_detach + ^ + create sequence g_diff + ^ + create exception exc_too_deep_recursion 'Execution terminated by @1: recursion depth exceeded the threshold @2 by @3' + ^ + recreate table log(id int primary key, att bigint default current_connection, event_name varchar(6) ) + ^ + create or alter trigger trg_attach on connect as + declare v_pool_size int; + begin + if ( gen_id(g_attach,0) <= {STOP_RECURSIVE_ES_AFTER_ITER} ) then + in autonomous transaction do + insert into log(id, event_name) values (-gen_id(g_attach,1), 'attach'); + else + exception exc_too_deep_recursion using ('trg_attach', {STOP_RECURSIVE_ES_AFTER_ITER}, gen_id(g_attach,1) - {STOP_RECURSIVE_ES_AFTER_ITER} - 1) + ; + end + ^ + create or alter trigger trg_detach on disconnect as + begin + if ( gen_id(g_detach,0) <= {STOP_RECURSIVE_ES_AFTER_ITER} ) then + execute statement ('insert into log(id, event_name) values(?, ?)') (gen_id(g_detach,1), 'detach') + with autonomous transaction + on external 'localhost:' || rdb$get_context('SYSTEM', 'DB_NAME') + as user '{act.db.user}' password '{act.db.password}' role 'R' || replace(uuid_to_char(gen_uuid()),'-','') + ; + else + exception exc_too_deep_recursion using ('trg_detach', {STOP_RECURSIVE_ES_AFTER_ITER}, gen_id(g_detach,1) - {STOP_RECURSIVE_ES_AFTER_ITER} - 1) + ; + end + ^ + set term ;^ + commit; + select + rdb$trigger_name as "rdb_trg_name" + ,rdb$trigger_type as "rdb_trg_type" + from rdb$triggers + where rdb$system_flag is distinct from 1 + order by rdb$trigger_name; + rollback; + connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; + quit; + """ + with open(tmp_sql, 'w') as f: + f.write(test_sql) + + # Get Firebird log before test + # ---------------------------- + fb_log_init = act.get_firebird_log() + failed_finish_isql_msg = '' + with act.db.connect() as con: + with open(tmp_log, 'w') as f: + try: + p_handed_isql = subprocess.Popen( [act.vars['isql'], '-i', str(tmp_sql), + '-user', act.db.user, + '-password', act.db.password, act.db.dsn], + stdout = f, + stderr = subprocess.STDOUT + ) + + for i in range(MAX_WAIT_FOR_ISQL_TERMINATE): + time.sleep(1) + # Check if child process has terminated. + # Set and return returncode attribute. Otherwise, returns None. + if p_handed_isql.poll() is not None: + break + + finally: + p_handed_isql.terminate() + + if p_handed_isql.poll() is None: + failed_finish_isql_msg = f'isql_outcome: process WAS NOT terminated in {MAX_WAIT_FOR_ISQL_TERMINATE} second. Probably MAX_WAIT_FOR_ISQL_TERMINATE value must be increased.' + + # Get Firebird log after test + # ---------------------------- + fb_log_curr = act.get_firebird_log() + + with open(tmp_log, 'a') as f: + if failed_finish_isql_msg: + f.write(failed_finish_isql_msg+'\n') + for line in unified_diff(fb_log_init, fb_log_curr): + if line.startswith('+'): + f.write(line[1:] + '\n') + + + expected_stdout = f""" + rdb_trg_name TRG_ATTACH + rdb_trg_type 8192 + rdb_trg_name TRG_DETACH + rdb_trg_type 8193 + + Error at disconnect: + Execute statement error at attach : + 335544830 : Too many recursion levels of EXECUTE STATEMENT + Data source : Firebird:: + At trigger 'TRG_DETACH' + + Error at disconnect: + Execute statement error at attach : + 335544830 : Too many recursion levels of EXECUTE STATEMENT + Data source : Firebird:: + At trigger 'TRG_DETACH' + """ + + with open(tmp_log, 'r') as f: + for line in f: + print(line) + + act.expected_stdout = expected_stdout + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() diff --git a/tests/bugs/gh_8091_test.py b/tests/bugs/gh_8091_test.py new file mode 100644 index 00000000..166e48bb --- /dev/null +++ b/tests/bugs/gh_8091_test.py @@ -0,0 +1,144 @@ +#coding:utf-8 + +""" +ID: issue-8091 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8091 +TITLE: Ability to create an inactive index +DESCRIPTION: + Test creates a table with several indices, all of them are specified as INACTIVE. + Then we check that these indices actually can *not* be used: explained plans for any query + to this table that could relate to indexed columns must now contain 'Full Scan'. + After this we extract metadata (with saving it to 'init_meta' variable) and drop test table. + Applying of metada to the test database (which is empty now) must pass without errors and, + more important, all indices must remain inactive after that. + Finally, we change DB dialect to 1, make b/r and again do same actions. + Result must be the same as for iteration with default dialect = 3. +NOTES: + [25.10.2024] pzotov + Checked on 6.0.0.508-67d8e39 (intermediate build). +""" +import time +from io import BytesIO +from firebird.driver import SrvRestoreFlag +import pytest +from firebird.qa import * + +init_sql = """ + set bail on; + recreate table test(id int generated by default as identity, x int, y int, z int); + set term ^; + execute block as + declare n int = 100000; + declare i int = 0; + begin + while (i < n) do + begin + insert into test(x, y, z) values( :i, null, :i); + i = i + 1; + end + end^ + set term ;^ + commit; + + create unique ascending index test_x_asc inactive on test(x); + create descending index test_y_desc inactive on test(y); + create unique descending index test_x_plus_y inactive on test computed by (x+y); + + create index test_z_partial inactive on test(z) where mod(id,2) = 0; + create unique index test_x_minus_y_partial inactive on test computed by (x-y) where mod(id,3) <= 1; + commit; +""" +db = db_factory(init = init_sql) + +act = python_act('db') + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- + +def check_indices_inactive(act, qry_map, nr_block, capsys): + with act.db.connect() as con: + cur = con.cursor() + for k,v in qry_map.items(): + ps = cur.prepare(v) + # Print explained plan with padding eash line by dots in order to see indentations: + print(v) + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + print('') + ps.free() + + expected_out = '\n'.join( [''.join( (qry_map[i],'\n',nr_block) ) for i in range(len(qry_map))] ) + act.expected_stdout = expected_out + + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() + +#----------------------------------------------------------- + +@pytest.mark.version('>=6.0') +def test_1(act: Action, capsys): + + qry_map = { + 0 : 'select count(*) from test where x is null' + ,1 : 'select count(*) from test where y is null' + ,2 : 'select count(*) from test where x+y is null' + ,3 : 'select count(*) from test where z is null and mod(id,2) = 0' + ,4 : 'select count(*) from test where x-y is null and mod(id,3) <= 1' + ,5 : 'select count(*) from test where x is not distinct from null' + ,6 : 'select count(*) from test where y is not distinct from null' + ,7 : 'select count(*) from test where x+y is not distinct from null' + ,8 : 'select count(*) from test where z is not distinct from null and mod(id,2) = 0' + ,9 : 'select count(*) from test where x-y is not distinct from null and mod(id,3) <= 1' + } + nr_block = """ + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Table "TEST" Full Scan + """ + + for iter in range(2): + + # check-1: ensure that all indices actually are INACTIVE, i.e. all queries will use full scan. + ########## + check_indices_inactive(act, qry_map, nr_block, capsys) + + #---===+++---===+++---===+++---===+++---===+++---===+++---===+++---===+++---===+++---===+++--- + + # check-2: extract metadata, drop table and apply metadata which now contains 'INACTIVE' clause for indices. + ########## + act.isql(switches=['-x']) + init_meta = '\n'.join( ('set bail on;', act.stdout) ) + + with act.db.connect() as con: + con.execute_immediate('drop table test') + con.commit() + + # Apply metadata to main test database. + act.isql(switches = [], input = init_meta) + # NO errors must occur now: + assert act.clean_stdout == '' + act.reset() + + #---===+++---===+++---===+++---===+++---===+++---===+++---===+++---===+++---===+++---===+++--- + + # check-3: ensure that all indices REMAIN INACTIVE, i.e. all queries will use full scan. + ########## + check_indices_inactive(act, qry_map, nr_block, capsys) + + if iter == 0: + # change dialect to 1, make backup / restore and repeat all prev actions. + act.gfix(switches = ['-sql_dialect', '1', act.db.dsn], combine_output = True) + assert act.clean_stdout == '' + act.reset() + + backup = BytesIO() + with act.connect_server() as srv: + srv.database.local_backup(database = act.db.db_path, backup_stream = backup) + backup.seek(0) + srv.database.local_restore(backup_stream = backup, database = act.db.db_path, flags = SrvRestoreFlag.REPLACE) diff --git a/tests/bugs/gh_8100_test.py b/tests/bugs/gh_8100_test.py new file mode 100644 index 00000000..bbd7291f --- /dev/null +++ b/tests/bugs/gh_8100_test.py @@ -0,0 +1,53 @@ +#coding:utf-8 + +""" +ID: issue-8100 +ISSUE: 8100 +TITLE: The isc_array_lookup_bounds function returns invalid values for low and high array bounds +DESCRIPTION: + Test verifies ability to create table with array-type column, store data in it and obtain array by query. + Script based on example provided in firebird-driver doc: + https://firebird-driver.readthedocs.io/en/latest/usage-guide.html#firebird-array-type +NOTES: + [11.05.2024] pzotov + Confirmed problem on 5.0.0.1391, 6.0.0.344: got "ValueError: Incorrect ARRAY field value" + Checked on 6.0.0.345 #17b007d, 5.0.1.1394 #aa3cafb - all OK. +""" + +import pytest +from pathlib import Path +from firebird.qa import * + +db = db_factory() +act = python_act('db', substitutions = [('[ \t]+', ' ')]) + +@pytest.mark.version('>=5.0.1') +def test_1(act: Action, capsys): + + with act.db.connect() as con: + cur = con.cursor() + con.execute_immediate("recreate table array_table (id int generated by default as identity constraint pk_arr primary key, arr int[3,4])") + con.commit() + + data = ( + [ [87, 13, 16, 19], [25, 52, 73, 24], [81, 92, 63, 14] ] + ,[ [21, 79, 63, 57], [34, 42, 13, 34], [71, 15, 73, 34] ] + ,[ [31, 33, 55, 47], [17, 22, 33, 14], [91, 21, 93, 24] ] + ) + + ps = cur.prepare("insert into array_table(arr) values (?)") + for x in data: + cur.execute(ps, (x,)) + con.commit() + + cur.execute("select id, arr from array_table order by (arr[1,2]) desc") + for r in cur: + print(r[0], r[1]) + + act.expected_stdout = """ + 2 [[21, 79, 63, 57], [34, 42, 13, 34], [71, 15, 73, 34]] + 3 [[31, 33, 55, 47], [17, 22, 33, 14], [91, 21, 93, 24]] + 1 [[87, 13, 16, 19], [25, 52, 73, 24], [81, 92, 63, 14]] + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8108_test.py b/tests/bugs/gh_8108_test.py new file mode 100644 index 00000000..357aad93 --- /dev/null +++ b/tests/bugs/gh_8108_test.py @@ -0,0 +1,49 @@ +#coding:utf-8 + +""" +ID: issue-8108 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8108 +TITLE: Engine returns empty string when unable to translate Unicode symbol into ICU-codepage. +DESCRIPTION: +NOTES: + [15.05.2024] pzotov + Confirmed ticket notes on 4.0.5.3092: empty string is returned instead of error with SQLSTATE = 22018. + Checked on intermediate snapshots: 6.0.0.351 #02ef0c8, 5.0.1.1399 #5b8b57c, 4.0.5.3099 #bc1ad78 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + + -- Hangul Choseong Filler + -- x115F; + -- FB3 returns an error, FB4 OK (an error is expected) + select '>' || cast(_utf8 'ᅟ' as varchar(1) character set tis620) || '<' from rdb$database; + --select '>' || cast( cast(unicode_char(0x115f) as varchar(1) character set utf8) as varchar(1) character set tis620) || '<' from rdb$database; + + -- FB3 and FB4 return an error (it is OK) + select '>' || cast(_utf8 'ᅟ' as varchar(1) character set win1251) || '<' from rdb$database; + --select '>' || cast( cast(unicode_char(0x115f) as varchar(1) character set utf8) as varchar(1) character set win1251) || '<' from rdb$database; +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +expected_stdout = """ + Statement failed, SQLSTATE = 22018 + arithmetic exception, numeric overflow, or string truncation + -Cannot transliterate character between character sets + + Statement failed, SQLSTATE = 22018 + arithmetic exception, numeric overflow, or string truncation + -Cannot transliterate character between character sets +""" + +@pytest.mark.version('>=4.0.5') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8109_test.py b/tests/bugs/gh_8109_test.py new file mode 100644 index 00000000..8608ea54 --- /dev/null +++ b/tests/bugs/gh_8109_test.py @@ -0,0 +1,155 @@ +#coding:utf-8 + +""" +ID: issue-8109 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8109 +TITLE: Plan/Performance regression when using special construct for IN in FB5.x compared to FB3.x +DESCRIPTION: +NOTES: + [03.02.2025] pzotov + Confirmed problem (regression) on 6.0.0.595-2c5b146, 5.0.2.1601-f094936 + Checked on 6.0.0.601-5dee439, 5.0.2.1606-fd31e52 (intermediate snapshots). +""" + +import pytest +from firebird.qa import * +from firebird.driver import DatabaseError + +init_sql = """ + create table test(x int, y int, u int, v int, p int, q int); + insert into test(x,y, u,v, p,q) select r,r, r,r, r,r from ( select rand()*10000 r from rdb$types, rdb$types ); + commit; + create index test_x_asc on test(x); + create index test_y_asc on test(y); + create index test_c_asc on test computed by (x+y); + create index test_p_asc on test(p) where p < 5001; -- partial index + create index test_q_asc on test(q) where q > 4999; -- partial index + + create descending index test_u_dec on test(u); + create descending index test_v_dec on test(v); + create descending index test_c_dec on test computed by (u-v); + create descending index test_p_dec on test(p) where p > 4999; -- partial index + create descending index test_q_dec on test(q) where q < 5001; -- partial index + commit; +""" + +db = db_factory(init = init_sql) +act = python_act('db') + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- + +@pytest.mark.version('>=5.0.2') +def test_1(act: Action, capsys): + queries_map = { i : x for i,x in enumerate + ( + [ + 'select * from test where 5000 in (x, y)' + ,'select * from test where 5000 in (u, v)' + ,'select * from test where 5000 in (x, u)' + ,'select * from test where 5000 in (v, y)' + ,'select * from test where 5000 in (x+y, u-v)' + ,'select * from test where 5000 in (p, q) and p < 5001 and q > 4999' + ,'select * from test where 5000 in (p, q) and p > 4999 and q < 5001' + ] + ) + } + with act.db.connect() as con: + cur = con.cursor() + for qry_idx, qry_txt in queries_map.items(): + ps = None + try: + ps = cur.prepare(qry_txt) + + # Print explained plan with padding eash line by dots in order to see indentations: + print(qry_txt) + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + print('\n') + except DatabaseError as e: + print(e.__str__()) + print(e.gds_codes) + finally: + if ps: + ps.free() + + + expected_stdout = f""" + {queries_map[ 0 ]} + Select Expression + ....-> Filter + ........-> Table "TEST" Access By ID + ............-> Bitmap Or + ................-> Bitmap + ....................-> Index "TEST_X_ASC" Range Scan (full match) + ................-> Bitmap + ....................-> Index "TEST_Y_ASC" Range Scan (full match) + + {queries_map[ 1 ]} + Select Expression + ....-> Filter + ........-> Table "TEST" Access By ID + ............-> Bitmap Or + ................-> Bitmap + ....................-> Index "TEST_U_DEC" Range Scan (full match) + ................-> Bitmap + ....................-> Index "TEST_V_DEC" Range Scan (full match) + + {queries_map[ 2 ]} + Select Expression + ....-> Filter + ........-> Table "TEST" Access By ID + ............-> Bitmap Or + ................-> Bitmap + ....................-> Index "TEST_X_ASC" Range Scan (full match) + ................-> Bitmap + ....................-> Index "TEST_U_DEC" Range Scan (full match) + + {queries_map[ 3 ]} + Select Expression + ....-> Filter + ........-> Table "TEST" Access By ID + ............-> Bitmap Or + ................-> Bitmap + ....................-> Index "TEST_V_DEC" Range Scan (full match) + ................-> Bitmap + ....................-> Index "TEST_Y_ASC" Range Scan (full match) + + {queries_map[ 4 ]} + Select Expression + ....-> Filter + ........-> Table "TEST" Access By ID + ............-> Bitmap Or + ................-> Bitmap + ....................-> Index "TEST_C_ASC" Range Scan (full match) + ................-> Bitmap + ....................-> Index "TEST_C_DEC" Range Scan (full match) + + {queries_map[ 5 ]} + Select Expression + ....-> Filter + ........-> Table "TEST" Access By ID + ............-> Bitmap Or + ................-> Bitmap + ....................-> Index "TEST_P_ASC" Range Scan (full match) + ................-> Bitmap + ....................-> Index "TEST_Q_ASC" Range Scan (full match) + + {queries_map[ 6 ]} + Select Expression + ....-> Filter + ........-> Table "TEST" Access By ID + ............-> Bitmap Or + ................-> Bitmap + ....................-> Index "TEST_P_DEC" Range Scan (full match) + ................-> Bitmap + ....................-> Index "TEST_Q_DEC" Range Scan (full match) + """ + + act.expected_stdout = expected_stdout + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8112_test.py b/tests/bugs/gh_8112_test.py new file mode 100644 index 00000000..53d49097 --- /dev/null +++ b/tests/bugs/gh_8112_test.py @@ -0,0 +1,45 @@ +#coding:utf-8 + +""" +ID: issue-8112 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8112 +TITLE: Error isc_read_only_trans (335544361) should report SQLSTATE 25006. +DESCRIPTION: +NOTES: + Confirmed problem on 6.0.0.345, 5.0.1.1395. + Checked on 6.0.0.351, 5.0.1.1399. +""" +import pytest +from firebird.qa import * +from firebird.driver import tpb, Isolation, TraLockResolution, TraAccessMode, DatabaseError + +init_sql = """ + recreate table test(id int default current_transaction); +""" +db = db_factory(init = init_sql) +act = python_act('db', substitutions = [('[ \t]+', ' ')]) + +@pytest.mark.version('>=5.0.1') +def test_1(act: Action, capsys): + + try: + custom_tpb = tpb(isolation = Isolation.SERIALIZABLE, access_mode = TraAccessMode.READ) + with act.db.connect() as con: + tx = con.transaction_manager(custom_tpb) + cur = tx.cursor() + cur.execute('insert into test default values') + con.commit() + + except DatabaseError as e: + print(e.sqlstate) # must be '25006' after fix. + print(e.__str__()) + for g in e.gds_codes: + print(g) + + act.expected_stdout = f""" + 25006 + attempted update during read-only transaction + 335544361 + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8113_test.py b/tests/bugs/gh_8113_test.py new file mode 100644 index 00000000..d6e7c9b6 --- /dev/null +++ b/tests/bugs/gh_8113_test.py @@ -0,0 +1,78 @@ +#coding:utf-8 + +""" +ID: issue-8113 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8113 +TITLE: UNION ALL optimization with constant false condition +DESCRIPTION: + Test uses script based on example from ticket. + Number of UNIONed parts is defined via UNION_MEMBERS_CNT variable. + We compare number of natural reads with threshold = 1 (see MAX_ALLOWED_NAT_READS). +NOTES: + [18.11.2024] pzotov + Confirmed excessive reads on 6.0.0.520. + Checked on 6.0.0.532 -- all OK. +""" + +import pytest +from firebird.qa import * + +######################### +MAX_ALLOWED_NAT_READS = 1 +UNION_MEMBERS_CNT = 254 +######################### + +view_ddl = f'recreate view v_test as ' + '\nunion all '.join( [f'select {i} as x from test' for i in range(UNION_MEMBERS_CNT)] ) + '\n;' +init_sql = f""" + recreate table test(id int); + insert into test(id) values(0); + commit; + {view_ddl} +""" + +db = db_factory(init = init_sql) + +act = python_act('db') + +#----------------------------------------------------------- + +@pytest.mark.version('>=6.0') +def test_1(act: Action, capsys): + msg_prefix = 'Number of natural reads:' + expected_txt = 'EXPECTED' + nat_reads = {} + with act.db.connect() as con: + cur = con.cursor() + + cur.execute("select rdb$relation_id from rdb$relations where rdb$relation_name = upper('test')") + src_relation_id = cur.fetchone()[0] + nat_reads[src_relation_id] = 0 + + for x_table in con.info.get_table_access_stats(): + if x_table.table_id == src_relation_id: + nat_reads[src_relation_id] = -x_table.sequential + + cur.execute(f"select /* trace_me */ x from v_test where x = {UNION_MEMBERS_CNT-1}") + data = cur.fetchall() + + for x_table in con.info.get_table_access_stats(): + if x_table.table_id == src_relation_id: + nat_reads[src_relation_id] += x_table.sequential + + if nat_reads[src_relation_id] <= MAX_ALLOWED_NAT_READS: + print(f'{msg_prefix} {expected_txt}') + else: + print(f'{msg_prefix} UNEXPECTED: {nat_reads[src_relation_id]} - greater than threshold = {MAX_ALLOWED_NAT_READS}.') + print('Check view source:') + cur.execute("select rdb$view_source from rdb$relations where rdb$relation_name = upper('v_test')") + v_source = cur.fetchall()[0] + for line in v_source[0].split('\n'): + print(line) + + + act.expected_stdout = f""" + {msg_prefix} {expected_txt} + """ + + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8115_test.py b/tests/bugs/gh_8115_test.py new file mode 100644 index 00000000..9d57fffc --- /dev/null +++ b/tests/bugs/gh_8115_test.py @@ -0,0 +1,115 @@ +#coding:utf-8 + +""" +ID: issue-8115 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8115 +TITLE: Avoid reading/hashing the inner stream(s) if the leader stream is empty +DESCRIPTION: + Original title: "FB 5.0.0.1306 - unexpected results using LEFT JOIN with When " +NOTES: + [16.09.2024] pzotov + Confirmed bug in 5.0.1.1369-8c31082 (17.03.2024) + Bug was fixed in 5.0.1.1369-bbd35ab (20.03.2024) + Commit: + https://github.com/FirebirdSQL/firebird/commit/bbd35ab07c129e9735f081fcd29172a8187aa8ab + Avoid reading/hashing the inner stream(s) if the leader stream is empty + Checked on 6.0.0.457, 5.0.2.1499 + + [18.01.2025] pzotov + Resultset of cursor that executes using instance of selectable PreparedStatement must be stored + in some variable in order to have ability close it EXPLICITLY (before PS will be freed). + Otherwise access violation raises during Python GC and pytest hangs at final point (does not return control to OS). + This occurs at least for: Python 3.11.2 / pytest: 7.4.4 / firebird.driver: 1.10.6 / Firebird.Qa: 0.19.3 + The reason of that was explained by Vlad, 26.10.24 17:42 ("oddities when use instances of selective statements"). + + [27.03.2025] pzotov + Explained plan for 6.x became the same as for 5.x, see commit: + https://github.com/FirebirdSQL/firebird/commit/6c21404c6ef800ceb7d3bb9c97dc8249431dbc5b + Comparison of actual output must be done with single expected_out in both 5.x and 6.x. + Plan for 5.x (with TWO 'Filter' clauses) must be considered as more effective. + Disussed with dimitr, letter 16.09.2024 17:55. + + Checked on 6.0.0.698-6c21404. +""" +import zipfile +from pathlib import Path +import locale +import re + +from firebird.driver import DatabaseError +import pytest +from firebird.qa import * + +db = db_factory() + +substitutions = [('INDEX_\\d+', 'INDEX_nn'),] + +act = python_act('db', substitutions = substitutions) +tmp_fbk = temp_file('gh_8115.tmp.fbk') + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- + +@pytest.mark.version('>=5.0.1') +def test_1(act: Action, tmp_fbk: Path, capsys): + zipped_fbk_file = zipfile.Path(act.files_dir / 'gh_8115.zip', at = 'gh_8115.fbk') + tmp_fbk.write_bytes(zipped_fbk_file.read_bytes()) + + act.gbak(switches = ['-rep', str(tmp_fbk), act.db.db_path], combine_output = True, io_enc = locale.getpreferredencoding()) + print(act.stdout) # must be empty + + test_sql = """ + select /* trace_me */ aa.id, ab.CNP_USER, ab.ID_USER + from sal_inperioada2('7DC51501-0DF2-45BE-93E5-382A541505DE', '15.05.2024') aa + left join user_cnp(aa.cnp, '15.05.2024') ab on ab.CNP_USER = aa.cnp + where ab.ID_USER = '04B23787-2C7F-451A-A12C-309F79D6F13A' + """ + + with act.db.connect() as con: + cur = con.cursor() + ps, rs = None, None + try: + ps = cur.prepare(test_sql) + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + rs = cur.execute(ps) + for r in rs: + for p in r: + print(p) + except DatabaseError as e: + print(e.__str__()) + print(e.gds_codes) + except Error as x: + print(x) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() + + expected_stdout = """ + Select Expression + ....-> Nested Loop Join (inner) + ........-> Procedure "SAL_INPERIOADA2" as "AA" Scan + ........-> Filter + ............-> Filter + ................-> Procedure "USER_CNP" as "AB" Scan + + 000DD4E1-B4D0-4D6E-9D9F-DE9A7D0D6492 + E574F734-CECB-4A8F-B9BE-FAF51BC61FAD + 04B23787-2C7F-451A-A12C-309F79D6F13A + """ + act.expected_stdout = expected_stdout + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8120_test.py b/tests/bugs/gh_8120_test.py new file mode 100644 index 00000000..823f8113 --- /dev/null +++ b/tests/bugs/gh_8120_test.py @@ -0,0 +1,34 @@ +#coding:utf-8 + +""" +ID: issue-8120 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8120 +TITLE: Cast dies with numeric value is out of range error +DESCRIPTION: +NOTES: + [27.05.2024] pzotov + Confirmed bug on 4.0.5.3077. + Checked on 6.0.0.362; 5.0.1.1408; 4.0.5.3103 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set heading off; + select cast('27' as numeric(4,2)) from rdb$database; +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +expected_stdout = """ + 27.00 +""" + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8123_test.py b/tests/bugs/gh_8123_test.py new file mode 100644 index 00000000..ac25d1e8 --- /dev/null +++ b/tests/bugs/gh_8123_test.py @@ -0,0 +1,163 @@ +#coding:utf-8 + +""" +ID: issue-8123 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8123 +TITLE: Procedure manipulation can lead to wrong dependencies removal +DESCRIPTION: + Test verifies ticket notes for standalone procedure, standalone function and package which contains SP. + Before fix problem did exist in procedure (standalone and packaged): DROP command led to missed record + in rdb$dependencies and if we further try to drop TABLE then error with wrong SQLSTATE raised: + "SQLSTATE = 42S22 / invalid request BLR at offset 5 / -column N1 is not defined in table ..." + Expected error: + "SQLSTATE = 42000 / ... / -cannot delete ... / -there are 1 dependencies" +NOTES: + [21.05.2024] pzotov + Confirmed bug on 6.0.0.357-bf6c467 (regular daily snapshot, 18-may-2024). + Checked on intermediate snapshots 6.0.0.357-f94343e, 5.0.1.1404-88bf561. +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + -- DO NOT set bail on! + set list on; + set count on; + + create table tb_test1 (n0 int, n1 integer, n2 integer computed by (n1)); + commit; + + create procedure sp_test (i1 type of column tb_test1.n2) as begin end; + commit; + + drop procedure sp_test; + commit; + + select + rdb$depended_on_name + ,rdb$field_name + ,rdb$dependent_type + ,rdb$depended_on_type + from rdb$dependencies + where rdb$depended_on_name = upper('tb_test1') and rdb$field_name = upper('n1'); + commit; + + alter table tb_test1 drop n1; + commit; + + -------------------------------------------------------------------------------- + + create table tb_test2 (n0 int, n1 integer, n2 integer computed by (n1)); + commit; + set term ^; + create function fn_test (i1 type of column tb_test2.n2) returns int as + begin + return 1; + end + ^ + set term ;^ + commit; + + drop function fn_test; + commit; + + select + rdb$depended_on_name + ,rdb$field_name + ,rdb$dependent_type + ,rdb$depended_on_type + from rdb$dependencies + where rdb$depended_on_name = upper('tb_test2') and rdb$field_name = upper('n1'); + commit; + + alter table tb_test2 drop n1; + commit; + + ----------------------------------------------------------------------------------- + + create table tb_test3 (n0 int, n1 integer, n2 integer computed by (n1)); + commit; + + set term ^; + create or alter package pg_test as + begin + procedure sp_worker (i1 type of column tb_test3.n2); + end + ^ + recreate package body pg_test as + begin + procedure sp_worker (i1 type of column tb_test3.n2) as + begin + + end + end + ^ + set term ;^ + commit; + + drop package pg_test; + commit; + + select + rdb$depended_on_name + ,rdb$field_name + ,rdb$dependent_type + ,rdb$depended_on_type + from rdb$dependencies + where rdb$depended_on_name = upper('tb_test3') and rdb$field_name = upper('n1'); + commit; + + alter table tb_test3 drop n1; + commit; +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +expected_stdout = """ + RDB$DEPENDED_ON_NAME TB_TEST1 + RDB$FIELD_NAME N1 + RDB$DEPENDENT_TYPE 3 + RDB$DEPENDED_ON_TYPE 0 + Records affected: 1 + + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -cannot delete + -COLUMN TB_TEST1.N1 + -there are 1 dependencies + + + RDB$DEPENDED_ON_NAME TB_TEST2 + RDB$FIELD_NAME N1 + RDB$DEPENDENT_TYPE 3 + RDB$DEPENDED_ON_TYPE 0 + Records affected: 1 + + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -cannot delete + -COLUMN TB_TEST2.N1 + -there are 1 dependencies + + + RDB$DEPENDED_ON_NAME TB_TEST3 + RDB$FIELD_NAME N1 + RDB$DEPENDENT_TYPE 3 + RDB$DEPENDED_ON_TYPE 0 + Records affected: 1 + + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -cannot delete + -COLUMN TB_TEST3.N1 + -there are 1 dependencies +""" + +@pytest.mark.version('>=5.0.1') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8136_test.py b/tests/bugs/gh_8136_test.py new file mode 100644 index 00000000..d755cc03 --- /dev/null +++ b/tests/bugs/gh_8136_test.py @@ -0,0 +1,38 @@ +#coding:utf-8 + +""" +ID: issue-8136 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8136 +TITLE: Server crashes with IN (dbkey1, dbkey2, ...) condition +DESCRIPTION: +NOTES: + [28.05.2024] pzotov + Confirmed crash on 6.0.0.36, 5.0.1.1408 + Checked on 6.0.0.363-40d0b41, 5.0.1.1408-c432bd0 + + [09.06.2024] pzotov + Added temporary mark 'disabled_in_forks' to SKIP this test when QA verifies *fork* rather than standard FB. +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set heading off; + select 1 from rdb$database where rdb$db_key in (MAKE_DBKEY(1, 0), MAKE_DBKEY(1, 1)); +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +expected_stdout = """ + 1 +""" + +@pytest.mark.disabled_in_forks +@pytest.mark.version('>=5.0.1') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8161_test.py b/tests/bugs/gh_8161_test.py new file mode 100644 index 00000000..7d50a85b --- /dev/null +++ b/tests/bugs/gh_8161_test.py @@ -0,0 +1,111 @@ +#coding:utf-8 + +""" +ID: issue-8161 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8161 +TITLE: Cardinality estimation should use primary record versions only +DESCRIPTION: + Test must use .fbk file from core-5602. It was copied to separate file and packed into gh_8161.zip + We check explained plan of query (making 'padding' of every line with '.' character for preserving indentation). + Statistics is not checked. + Numeric suffixes of index names from RDB tables are suppressed because they can change in the future. + Also, we suppress output of rows with 'line NNN, column NNN' (FB 5.x+) because they have no matter in this test. +NOTES: + [20.06.2024] pzotov + Despite that we use 'clean' (i.e. just restored) DB, back-versions CAN exists there for system tables, + particularly (for this .fbk) - in rdb$dependencies and rdb$procedures. + For that test not only back-versions but also blobs and fragments matter. + See letters from dimitr and hvlad: 20.06.2024 10:36, 10:39. + + Confirmed bug (regression) on 3.0.12.33735 (date of build: 09-mar-2024). + Checked on 3.0.12.33764, 4.0.5.3112, 5.0.1.1416, 6.0.0.374. + + [31.10.2024] pzotov + Adjusted expected_out discuss with dimitr: explained plan for FB 6.x became identical to FB 5.x and earlier after + https://github.com/FirebirdSQL/firebird/commit/e7e9e01fa9d7c13d8513fcadca102d23ad7c5e2a + ("Rework fix for #8290: Unique scan is incorrectly reported in the explained plan for unique index and IS NULL predicate") + + Checked on 3.0.13.33794, 4.0.6.3165, 5.0.2.1551, 6.0.0.515 +""" +import zipfile +from pathlib import Path +import locale +import re + +import pytest +from firebird.qa import * + +db = db_factory() + +substitutions = [('INDEX_\\d+', 'INDEX_nn'), ('\\(?line \\d+, column \\d+\\)?', '')] + +act = python_act('db', substitutions = substitutions) +tmp_fbk = temp_file('gh_8161.tmp.fbk') + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- + +@pytest.mark.version('>=3.0.12') +def test_1(act: Action, tmp_fbk: Path, capsys): + zipped_fbk_file = zipfile.Path(act.files_dir / 'gh_8161.zip', at = 'gh_8161.fbk') + tmp_fbk.write_bytes(zipped_fbk_file.read_bytes()) + + act.gbak(switches = ['-rep', str(tmp_fbk), act.db.db_path], combine_output = True, io_enc = locale.getpreferredencoding()) + print(act.stdout) # must be empty + + test_sql = """ + execute block as + declare relname varchar(32); + declare cnt int; + begin + for select X.RDB$RELATION_NAME + from RDB$RELATION_FIELDS X + where X.RDB$FIELD_SOURCE = 'BOOL_EMUL' + into :relname + do begin + select count(*) + from RDB$DEPENDENCIES DEP, RDB$PROCEDURES PRC + where DEP.RDB$DEPENDED_ON_NAME = :relname + AND DEP.RDB$FIELD_NAME = :relname + AND DEP.RDB$DEPENDED_ON_TYPE = 0 /* obj_relation */ + AND DEP.RDB$DEPENDENT_TYPE = 5 /* obj_procedure */ + AND DEP.RDB$DEPENDENT_NAME = PRC.RDB$PROCEDURE_NAME + AND PRC.RDB$PACKAGE_NAME IS NULL + into :cnt; + end + end + """ + + with act.db.connect() as con: + cur = con.cursor() + ps = cur.prepare(test_sql) + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + + expected_stdout = """ + Select Expression + ....-> Singularity Check + ........-> Aggregate + ............-> Nested Loop Join (inner) + ................-> Filter + ....................-> Table "RDB$DEPENDENCIES" as "DEP" Access By ID + ........................-> Bitmap + ............................-> Index "RDB$INDEX_nn" Range Scan (full match) + ................-> Filter + ....................-> Table "RDB$PROCEDURES" as "PRC" Access By ID + ........................-> Bitmap + ............................-> Index "RDB$INDEX_nn" Unique Scan + Select Expression + ....-> Filter + ........-> Table "RDB$RELATION_FIELDS" as "X" Access By ID + ............-> Bitmap + ................-> Index "RDB$INDEX_nn" Range Scan (full match) + """ + + act.expected_stdout = expected_stdout + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8168_test.py b/tests/bugs/gh_8168_test.py new file mode 100644 index 00000000..605aa073 --- /dev/null +++ b/tests/bugs/gh_8168_test.py @@ -0,0 +1,147 @@ +#coding:utf-8 + +""" +ID: issue-8168 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8168 +TITLE: MAKE_DBKEY bug after backup/restore +DESCRIPTION: + Test creates two tables (tab_1, tab_2) and stores their relation_id in appropriate context variables. + Then we drop these tables and create them again but in 'reverse' order: tab_2, tab_1. + At this point we have to check that values of relation_id must differ for both tables and raise error + (see 'exc_rel_id_not_changed') if this is not so. Test logic must be changed if this error raises. + Then we add one row into each table and create SP (sp_chk) that uses make_dbkey() for returning these + rows. Key note: this SP *must* find appropriate record for each table. + Otherwise (if record not found) we raise exception exc_invalid_make_dbkey. + Finally, we do backup / restore and repeat call of sp_chk. It must return same ID values as before b/r. +NOTES: + [16.07.2024] pzotov + Confirmed bug on 6.0.0.386, 5.0.1.1425. + Checked on 6.0.0.387, 5.0.1.1428. + + Thanks to Vlad for suggestion about test implementation. +""" +import pytest +from firebird.qa import * +from io import BytesIO +from firebird.driver import SrvRestoreFlag +import locale + +db = db_factory() +act = python_act('db', substitutions=[('[ \t]+', ' ')]) + +@pytest.mark.version('>=5.0.1') +def test_1(act: Action, capsys): + + init_sql = f""" + set bail on; + set list on; + create exception exc_rel_id_not_changed 'RELATION_ID not changed table(s): @1. One need to change test logic!'; + create exception exc_invalid_make_dbkey 'Invalid make_dbkey() detected for table(s): @1'; + + create view v_get_rel_id as + select + max( iif( upper(rdb$relation_name) = 'TAB_1', rdb$relation_id, null ) ) as t1_rel_id + ,max( iif( upper(rdb$relation_name) = 'TAB_2', rdb$relation_id, null ) ) as t2_rel_id + from rdb$relations + where rdb$relation_name starting with upper('tab_') + ; + create table tab_1(id int); + create table tab_2(id int); + commit; + + set term ^; + execute block as + begin + for + select t1_rel_id, t2_rel_id + from v_get_rel_id + as cursor c + do begin + rdb$set_context('USER_SESSION', 'TAB1_REL_ID', c.t1_rel_id); + rdb$set_context('USER_SESSION', 'TAB2_REL_ID', c.t2_rel_id); + end + end + ^ + set term ;^ + + drop table tab_1; + drop table tab_2; + + recreate table tab_2(id int); + recreate table tab_1(id int); + commit; + + set term ^; + execute block as + declare v_list varchar(100) = ''; + begin + for + select t1_rel_id, t2_rel_id + from v_get_rel_id + as cursor c + do begin + if (c.t1_rel_id = rdb$get_context('USER_SESSION', 'TAB1_REL_ID')) then + v_list = v_list || 'TAB_1; '; + if (c.t2_rel_id = rdb$get_context('USER_SESSION', 'TAB2_REL_ID')) then + v_list = v_list || 'TAB_2'; + + if (v_list > '') then + exception exc_rel_id_not_changed using(v_list); + end + end + ^ + set term ;^ + + ----------------------------------------- + + insert into tab_1(id) values(1); + insert into tab_2(id) values(2); + commit; + + set term ^; + create or alter procedure sp_chk returns (id1 int, id2 int) as + declare v_list varchar(100) = ''; + begin + select id from tab_1 where rdb$db_key = make_dbkey('TAB_1', 0) into id1; + if (row_count = 0) then v_list = v_list || 'TAB_1; '; + + select id from tab_2 where rdb$db_key = make_dbkey('TAB_2', 0) into id2; + if (row_count = 0) then v_list = v_list || 'TAB_2'; + + if (v_list > '') then + exception exc_invalid_make_dbkey using(v_list); + + suspend; + end + ^ + set term ;^ + commit; + select * from sp_chk; + + """ + + expected_stdout = """ + ID1 1 + ID2 2 + """ + act.expected_stdout = expected_stdout + act.isql(input = init_sql, combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout + act.reset() + + #--------------------------------------------------- + + backup = BytesIO() + + with act.connect_server() as srv: + srv.database.local_backup(database=act.db.db_path, backup_stream=backup) + backup.seek(0) + srv.database.local_restore(backup_stream=backup, database=act.db.db_path, flags = SrvRestoreFlag.REPLACE) + + act.expected_stdout = """ + ID1 1 + ID2 2 + """ + act.isql(input = "set list on; select * from sp_chk;", combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout + act.reset() diff --git a/tests/bugs/gh_8176_test.py b/tests/bugs/gh_8176_test.py new file mode 100644 index 00000000..484428e8 --- /dev/null +++ b/tests/bugs/gh_8176_test.py @@ -0,0 +1,201 @@ +#coding:utf-8 + +""" +ID: issue-8176 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8176 +TITLE: Firebird hangs after starting remote profiling session +DESCRIPTION: + We create two users: tmp_worker_usr and tmp_profiler_usr. + First of them will perform some 'useful work', second will PROFILE attachment of tmp_worker_usr. + Also, we create role 'tmp_profiler_role' and grant it to 'tmp_profiler_usr'. + Ability to profile ather attachment (by tmp_profiler_usr) is achieved via granting to role + tmp_profiler_role system privilege PROFILE_ANY_ATTACHMENT. + Then we create three connections to test DB belonging to tmp_worker_usr, tmp_profiler_usr and SYSDBA. + We pass attachment_id of tmp_worker_usr to the call 'select rdb$profiler.start_session(...)' performed + by tmp_profiler_usr. Executing of this causes creation of PLG$PROFILE* tables and views in the test DB + (they are created by special internal connection - this can be seen in the trace). + Unfortunately, usedr who calls rdb$profiler.start_session() will *NOT* have any access rights to created + PLG* objects. This must be donw explicitly to SYSDBA (and this is the reason why we create 3rd connection). + Finally, we do somewhat similar to example described doc/sql.extensions/README.profiler.md: run two dummy + procedures by tmp_worker_usr, and after that - call proc rdb$profiler.finish_session by tmp_profiler_usr. + At the last step, we query data from plg$prof_sessions (it must have number of records equal to the number + of profiling sessions that we have created). +NOTES: + [10.07.2024] pzotov + ::: WARNING ::: + 1. On CLASSIC problem still exists: firebird hangs, although new connections to test DB are allowed (found both on 5.x and 6.x). + One may even to run 'delete from mon$attachments' (using new ISQL session) but there is no effect: server does not perform that. + Because of this, test currently can be run only on Super. + 2. It looks weird that user (NON-sysdba) who has necessary rights to start profiling, must be explicitly granted to access PLG* tables/views. + + Confirmed bug on 6.0.0.389-cc71c6f (SS), 5.0.1.1432-9d5a60a (SS) - server hanged and did not allow to make new connections to test DB. + Checked on 6.0.0.392-3fa8e6b, 5.0.1.1434-2593b3b. + + [24.07.2024] pzotov + Checked fixed problem with hang on Windows, Servermode = CLASSIC + Commits: + https://github.com/FirebirdSQL/firebird/commit/fa90256cf080965f92eae11eba8d897c2d02e1b9 + Merge pull request #8186 from FirebirdSQL/work/ProfilerIPC + Fixed a few issues with IPC used by remote profiler + https://github.com/FirebirdSQL/firebird/commit/f59905fc29f0b9288d61fc6113fd24301dce1327 + Frontported PR #8186 : Fixed a few issues with IPC used by remote profiler + Snapshots: 6.0.0.398-f59905f, 5.0.1.1440-7b1b824 +""" + +import pytest +from firebird.qa import * +from firebird.driver import tpb, Isolation # , TraLockResolution, TraAccessMode, DatabaseError +import time + +N_COUNT = 1000 +init_script = f""" + create or alter procedure sp_ins as begin end; + create or alter procedure sp_del as begin end; + + recreate table test ( + id integer primary key, + val integer not null + ); + + set term ^; + + create or alter function fn_mult(p1 integer, p2 integer) returns integer + as + begin + return p1 * p2; + end^ + + create or alter procedure sp_ins sql security definer + as + declare i integer = 1; + begin + while (i <= {N_COUNT}) + do + begin + if (mod(i, 2) = 1) then + insert into test values (:i, fn_mult(:i, 2)); + i = i + 1; + end + end + ^ + + create or alter procedure sp_del sql security definer + as + declare i integer = 1; + begin + while (i <= {N_COUNT} / 2) + do + begin + if (mod(i, 2) = 1) then + delete from test where id = fn_mult(:i, 2); + i = i + 1; + end + end + ^ + set term ;^ + commit; + + grant execute on procedure sp_ins to public; + grant execute on procedure sp_del to public; + commit; +""" + +tmp_worker_usr = user_factory('db', name='tmp_worker_8176', password='123') +tmp_profiler_usr = user_factory('db', name='tmp_profiler_8176', password='456') +tmp_profiler_role = role_factory('db', name='tmp_profiler_role') + +db = db_factory(init=init_script) +act = python_act('db') + +@pytest.mark.disabled_in_forks +@pytest.mark.version('>=5.0.1') +def test_1(act: Action, tmp_worker_usr: User, tmp_profiler_usr: User, tmp_profiler_role: Role, capsys): + + addi_script = f""" + set wng off; + set bail on; + alter user {tmp_profiler_usr.name} revoke admin role; + revoke all on all from {tmp_profiler_usr.name}; + commit; + -- doc/sql.extensions/README.profiler.md: + -- If the remote attachment is from a different user, the calling user must have the system privilege `PROFILE_ANY_ATTACHMENT`. + alter role {tmp_profiler_role.name} + set system privileges to PROFILE_ANY_ATTACHMENT; + commit; + grant default {tmp_profiler_role.name} to user {tmp_profiler_usr.name}; + commit; + """ + act.isql(switches=['-q'], input=addi_script) + + custom_tpb = tpb(isolation = Isolation.READ_COMMITTED_READ_CONSISTENCY, lock_timeout = 0) + + with act.db.connect() as con_admin, \ + act.db.connect(user = tmp_worker_usr.name, password = tmp_worker_usr.password) as con_worker, \ + act.db.connect(user = tmp_profiler_usr.name, password = tmp_profiler_usr.password, role = tmp_profiler_role.name) as con_profiler: + + cur_worker = con_worker.cursor() + cur_worker.execute('select current_connection from rdb$database') + worker_attach_id = cur_worker.fetchone()[0] + + tx_profiler = con_profiler.transaction_manager(custom_tpb) + cur_profiler = tx_profiler.cursor() + + #.............................................................................. + + tx_profiler.begin() + cur_profiler.execute(f"select rdb$profiler.start_session('remote_profiling_of_inserts', null, {worker_attach_id}) from rdb$database;") + cur_profiler.fetchall() + + cur_worker.callproc('sp_ins') + + cur_profiler.callproc(f'rdb$profiler.finish_session(true, {worker_attach_id})') + tx_profiler.commit() + + #.............................................................................. + + tx_profiler.begin() + cur_profiler.execute(f"select rdb$profiler.start_session('remote_profiling_of_deletions', null, {worker_attach_id}) from rdb$database;") + cur_profiler.fetchall() + + cur_worker.callproc('sp_del') + + cur_profiler.callproc(f'rdb$profiler.finish_session(true, {worker_attach_id})') + tx_profiler.commit() + + #------------------------------------------------------------------------------ + # firebird.driver.types.DatabaseError: no permission for SELECT access to TABLE PLG$PROF_SESSIONS + # -Effective user is TMP_USER_PROFILER_ANY_ATT + ################################# + # ::: NB ::: Why this is needed ? + ################################# + con_admin.execute_immediate(f'grant select on plg$prof_sessions to role {tmp_profiler_role.name}') + con_admin.execute_immediate(f'grant select on plg$prof_psql_stats_view to role {tmp_profiler_role.name}') + con_admin.commit() + #------------------------------------------------------------------------------ + + tx_profiler.begin() + cur_profiler.execute('select description as profiler_session_descr, attachment_id as profiled_attachment, trim(user_name) as who_was_profiled from plg$prof_sessions order by profile_id') + cur_cols = cur_profiler.description + for r in cur_profiler: + for i in range(0,len(cur_cols)): + print( cur_cols[i][0], ':', r[i] ) + + #cur_profiler.execute('select * from plg$prof_psql_stats_view') + #cur_cols = cur_profiler.description + #for r in cur_profiler: + # for i in range(0,len(cur_cols)): + # print( cur_cols[i][0], ':', r[i] ) + + tx_profiler.commit() + + act.expected_stdout = f""" + PROFILER_SESSION_DESCR : remote_profiling_of_inserts + PROFILED_ATTACHMENT : {worker_attach_id} + WHO_WAS_PROFILED : {tmp_worker_usr.name.upper()} + + PROFILER_SESSION_DESCR : remote_profiling_of_deletions + PROFILED_ATTACHMENT : {worker_attach_id} + WHO_WAS_PROFILED : {tmp_worker_usr.name.upper()} + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8178_addi_test.py b/tests/bugs/gh_8178_addi_test.py new file mode 100644 index 00000000..cbceb042 --- /dev/null +++ b/tests/bugs/gh_8178_addi_test.py @@ -0,0 +1,117 @@ +#coding:utf-8 + +""" +ID: issue-8178 +ISSUE: https://github.com/FirebirdSQL/firebird/pull/8178 +TITLE: Check result of conversion to string in COALESCE that involves all families of data types. +DESCRIPTION: + Additional test for gh_8178 +NOTES: + [12.07.2024] pzotov + Checked on 6.0.0.392, 5.0.1.1434, 4.0.5.3127, 3.0.12.33765 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + recreate table dvalues( + t_int int default 12345 + ,t_boo boolean default true + ,t_chr char default 'A' + ,t_blb blob default 'bbbbbbblllllllloooooooooobbbbbbbb' + ,t_dat date default '01.01.1991' + ,t_tim time default '01:02:03.456' + ,t_tst timestamp default '01.02.2003 23:34:56' + ,t_nul char default null + ); + insert into dvalues default values; + commit; + --------------------------------------------------------- + recreate table dtypes(f smallint, t varchar(20)); + insert into dtypes(f,t) values( 1,'int'); + insert into dtypes(f,t) values( 2,'boo'); + insert into dtypes(f,t) values( 3,'chr'); + insert into dtypes(f,t) values( 4,'blb'); + insert into dtypes(f,t) values( 5,'dat'); + insert into dtypes(f,t) values( 6,'tim'); + insert into dtypes(f,t) values( 7,'tst'); + --insert into dtypes(f,t) values( 8,'nul'); + commit; + + set count on; + + set term ^; + execute block returns( checked_expr varchar(1024), raised_gds int) as + declare v_expr varchar(1024); + declare v_done smallint; + begin + for + select + --a.t as a_t, b.t as b_t, c.t as c_t, d.t as d_t, e.t as e_t, f.t as f_t, g.t as g_t + --,a.f as a_f, b.f as b_f, c.f as c_f, d.f as d_f, e.f as e_f, f.f as f_f, g.f as g_f, + 'select iif( coalesce(' + || + iif( a.t = 'nul', 'null', 't_'||a.t) + || + iif( b.t = 'nul', ', null', ', t_'||b.t) + || + iif( c.t = 'nul', ', null', ', t_'||c.t) + || + iif( d.t = 'nul', ', null', ', t_'||d.t) + || + iif( e.t = 'nul', ', null', ', t_'||e.t) + || + iif( f.t = 'nul', ', null', ', t_'||f.t) + || + iif( g.t = 'nul', ', null', ', t_'||g.t) + || + ') is distinct from null, 1, 0 ) as x from dvalues' as expr + from dtypes a + cross join dtypes b + cross join dtypes c + cross join dtypes d + cross join dtypes e + cross join dtypes f + cross join dtypes g + where + a.f not in (b.f, c.f, d.f, e.f, f.f, g.f) + and b.f not in (c.f, d.f, e.f, f.f, g.f) + and c.f not in (d.f, e.f, f.f, g.f) + and d.f not in (e.f, f.f, g.f) + and e.f not in (f.f, g.f) + and f.f not in (g.f) + order by a.t, b.t, c.t, d.t, e.t, f.t, g.t + as cursor k + do begin + checked_expr = k.expr; + raised_gds = 0; + begin + execute statement checked_expr into v_done; + when any do + begin + raised_gds = gdscode; + end + end + if (raised_gds <> 0) then + suspend; + end + end + ^ + set term ;^ +""" + +act = isql_act('db', test_script) + +expected_stdout = """ + Records affected: 0 +""" + +@pytest.mark.version('>=3.0.12') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8178_test.py b/tests/bugs/gh_8178_test.py new file mode 100644 index 00000000..763d674a --- /dev/null +++ b/tests/bugs/gh_8178_test.py @@ -0,0 +1,77 @@ +#coding:utf-8 + +""" +ID: issue-8178 +ISSUE: https://github.com/FirebirdSQL/firebird/pull/8178 +TITLE: Problem with boolean conversion to string inside DataTypeUtil::makeFromList() +DESCRIPTION: +NOTES: + [11.07.2024] pzotov + Confirmed problem on 6.0.0.389. + Checked on 6.0.0.392, 5.0.1.1434, 4.0.5.3127, 3.0.12.33765 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set heading off; + -- set echo on; + select coalesce(1, 'c1') from rdb$database; + + select coalesce('c2', 2) from rdb$database; + + select coalesce('c3', true) from rdb$database; + + select coalesce(true, 'c4') from rdb$database; + + select coalesce(5, true) from rdb$database; + + select coalesce(true, 6) from rdb$database; + + ---------------------------------------------------- + + select coalesce('c7', true, 7) from rdb$database; + + select coalesce('c8', 8, true) from rdb$database; + + select coalesce(true, 'c9', 9) from rdb$database; + + select coalesce(true, 10, 'c10') from rdb$database; + + select coalesce(11, 'c11', true) from rdb$database; + + select coalesce(12, true, 'c12') from rdb$database; +""" + +act = isql_act('db', test_script) + +expected_stdout = """ + 1 + c2 + c3 + TRUE + + Statement failed, SQLSTATE = HY004 + SQL error code = -104 + -Datatypes are not comparable in expression COALESCE + + Statement failed, SQLSTATE = HY004 + SQL error code = -104 + -Datatypes are not comparable in expression COALESCE + + c7 + c8 + TRUE + TRUE + 11 + 12 +""" + +@pytest.mark.version('>=3.0.12') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8182_test.py b/tests/bugs/gh_8182_test.py new file mode 100644 index 00000000..e32b11fb --- /dev/null +++ b/tests/bugs/gh_8182_test.py @@ -0,0 +1,43 @@ +#coding:utf-8 + +""" +ID: issue-8182 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8182 +TITLE: IN predicate incorrectly handles single parenthesized subquery as IN-list, instead of table subquery +DESCRIPTION: +NOTES: + [15.06.2025] pzotov + Confirmed bug on 6.0.0.835-3da8317; 5.0.3.1661-cfcf0e8 + Checked on 6.0.0.838-0b49fa8; 5.0.3.1666-97178d0; 4.0.6.3213-f015c28; 3.0.13.33813-222a910 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set bail on; + set list on; + select sign(count(*)) + from rdb$character_sets + where rdb$character_set_id in ( + ( + select rdb$character_set_id + from rdb$collations + ) + ); + +""" + +act = isql_act('db', test_script, substitutions=[ ('[ \\t]+', ' ') ]) + +expected_stdout = """ + SIGN 1 +""" + +@pytest.mark.version('>=3.0.13') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8185_test.py b/tests/bugs/gh_8185_test.py new file mode 100644 index 00000000..6c73d2dc --- /dev/null +++ b/tests/bugs/gh_8185_test.py @@ -0,0 +1,346 @@ +#coding:utf-8 + +""" +ID: issue-8185 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8185 +TITLE: SIGSEGV in Firebird 5.0.0.1306 embedded during update on cursor +DESCRIPTION: + Test implements sequence of actions described by Dimitry Sibiryakov in the ticket, + see: https://github.com/FirebirdSQL/firebird/issues/8185#issuecomment-2258598579 +NOTES: + [01.11.2024] pzotov + 1. Bug was fixed on following commits: + 5.x: 27.07.2024 11:48, 08dc25f8c45342a73c786bc60571c8a5f2c8c6e3 + ("Simplest fix for #8185: SIGSEGV in Firebird 5.0.0.1306 embedded during update on cursor - disallow caching for positioned updates/deletes") + 6.x: 29.07.2024 00:53, a7d10a40147d326e56540498b50e40b2da0e5850 + ("Fix #8185 - SIGSEGV with WHERE CURRENT OF statement with statement cache turned on") + 2. In current version of firebird-driver we can *not* set cursor name without executing it first. + But such execution leads to 'update conflict / deadlock' for subsequent UPDATE statement. + Kind of 'hack' is used to solve this problem: ps1._istmt.set_cursor_name(CURSOR_NAME) + 3. GREAT thanks to: + * Vlad for providing workaround and explanation of problem with AV for code like this: + with connect(f'localhost:{DB_NAME}', user = DBA_USER, password = DBA_PSWD) as con: + cur1 = con.cursor() + ps1 = cur1.prepare('update test set id = -id rows 0 returning id') + cur1.execute(ps1) + ps1.free() + It is mandatory to store result of cur1.eecute in some variable, i.e. rs1 = cur1.execute(ps1), + and call then rs1.close() __BEFORE__ ps1.free(). + Discussed 26.10.2024, subj: + "Oddities when using instance of selectable Statement // related to interfaces, VTable, iResultSet, iVersioned , CLOOP" + * Dimitry Sibiryakov for describe the 'step-by-step' algorithm for reproducing problem and providing working example in .cpp + + Confirmed problem on 5.0.1.1452-b056f5b (last snapshot before it was fixed). + Checked on 5.0.1.1452-08dc25f (27.07.2024 11:50); 6.0.0.401-a7d10a4 (29.07.2024 01:33) -- all OK. +""" + +import pytest +from firebird.qa import * +from firebird.driver import driver_config, connect, tpb, TraAccessMode, Isolation, DatabaseError + +init_sql = """ + set bail on; + recreate table test(id int, f01 int); + commit; + insert into test(id, f01) select row_number()over(), row_number()over() * 10 from rdb$types rows 3; + commit; +""" +db = db_factory(init = init_sql) +act = python_act('db') + +@pytest.mark.version('>=5.0.1') +def test_1(act: Action, capsys): + + srv_cfg = driver_config.register_server(name = 'test_srv_gh_8185', config = '') + db_cfg_name = f'db_cfg_8185' + db_cfg_object = driver_config.register_database(name = db_cfg_name) + db_cfg_object.server.value = srv_cfg.name + db_cfg_object.database.value = str(act.db.db_path) + db_cfg_object.config.value = f""" + MaxStatementCacheSize = 1M + """ + + # Pre-check: + with connect(db_cfg_name, user = act.db.user, password = act.db.password) as con: + cur = con.cursor() + cur.execute("select a.mon$remote_protocol, g.rdb$config_value from mon$attachments a left join rdb$config g on g.rdb$config_name = 'MaxStatementCacheSize' where a.mon$attachment_id = current_connection") + for r in cur: + conn_protocol = r[0] + db_sttm_cache_size = int(r[1]) + assert conn_protocol is None, "Test must use LOCAL protocol." + assert db_sttm_cache_size > 0, "Parameter 'MaxStatementCacheSize' (per-database) must be greater than zero for this test." + + #--------------------------------------------- + + CURSOR_NAME = 'k1' + SELECT_STTM = 'select /* ps-1*/ id, f01 from test where id > 0 for update' + UPDATE_STTM = f'update /* ps-2 */ test set id = -id where current of {CURSOR_NAME} returning id' + + update_tpb = tpb( access_mode = TraAccessMode.WRITE, + isolation = Isolation.READ_COMMITTED_RECORD_VERSION, + lock_timeout = 1) + + with connect(db_cfg_name, user = act.db.user, password = act.db.password) as con: + + tx2 = con.transaction_manager(update_tpb) + tx2.begin() + + with con.cursor() as cur1, tx2.cursor() as cur2, con.cursor() as cur3: + + ps1, rs1, ps2, rs2, ps3, rs3 = None, None, None, None, None, None + try: + ps1 = cur1.prepare(SELECT_STTM) # 1. [ticket, DS] Prepare statement 1 "select ... for update" + ps1._istmt.set_cursor_name(CURSOR_NAME) # 2. [ticket, DS] Set cursor name for statement 1 // ~hack. + + # DO NOT use it because subsequent update statement will get 'deadlock / update conflict' and not able to start: + #rs1 = cur1.execute(ps1) + #cur1.set_cursor_name(CURSOR_NAME) + + # DS example: "// Prepare positioned update statement" + ps2 = cur2.prepare(UPDATE_STTM) # 3. [ticket, DS] Prepare statement 2 "update ... where current of " + + # DS .cpp: // fetch records from cursor and print them + rs1 = cur1.execute(ps1) + rs1.fetchall() + + # DS .cpp: // IStatement* stmt2 = att->prepare(&status, tra, 0, "select * from pos where a > 1 for update", + ps3 = cur3.prepare(SELECT_STTM) # 4. [ticket, DS] Prepare statement 3 similar to statement 1 + + rs1.close() # 5. [ticket, DS] Release statement 1 // see hvlad recipe, 26.10.2024 + ps1.free() + + # DS .cpp: updStmt->free(&status); + ps2.free() # 6. [ticket, DS] Release statement 2 // see hvlad recipe, 26.10.2024 + + # DS .cpp: stmt = stmt2 + ps3._istmt.set_cursor_name(CURSOR_NAME) # 7. [ticket, DS] Set cursor name to statement 3 as in step 2 + + ps2 = cur2.prepare(UPDATE_STTM) # 8. [ticket, DS] Prepare statement 2 again (it will be got from cache keeping reference to statement 1) + + rs3 = cur3.execute(ps3) + rs3.fetchone() # 9. [ticket, DS] Run statement 3 and fetch one record + + # At step 10 you can get "Invalid handle" error or a crash if you swap steps 5 and 6. + rs2 = cur2.execute(ps2) # 10. [ticket, DS] Execute statement 2 + data2 = rs2.fetchone() + print('Changed ID:', data2[0]) + # print(f'{rs2.rowcount=}') + + except DatabaseError as e: + print(e.__str__()) + print('gds codes:') + for i in e.gds_codes: + print(i) + + finally: + if rs1: + rs1.close() + if ps1: + ps1.free() + + if rs2: + rs2.close() + if ps2: + ps2.free() + + if rs3: + rs3.close() + if ps3: + ps3.free() + + #--------------------------------------------- + + act.expected_stdout = 'Changed ID: -1' + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + + +# Example in .cpp (provided by Dimitry Sibiryakov): +################################################### +# +# #include +# #include "ifaceExamples.h" +# static IMaster* master = fb_get_master_interface(); +# +# int main() +# { +# int rc = 0; +# +# // status vector and main dispatcher +# ThrowStatusWrapper status(master->getStatus()); +# IProvider* prov = master->getDispatcher(); +# IUtil* utl = master->getUtilInterface(); +# +# // declare pointers to required interfaces +# IAttachment* att = NULL; +# ITransaction* tra = NULL; +# IStatement* stmt = NULL; +# IMessageMetadata* meta = NULL; +# IMetadataBuilder* builder = NULL; +# IXpbBuilder* tpb = NULL; +# +# // Interface provides access to data returned by SELECT statement +# IResultSet* curs = NULL; +# +# try +# { +# // IXpbBuilder is used to access various parameters blocks used in API +# IXpbBuilder* dpb = NULL; +# +# // create DPB - use non-default page size 4Kb +# dpb = utl->getXpbBuilder(&status, IXpbBuilder::DPB, NULL, 0); +# dpb->insertString(&status, isc_dpb_user_name, "sysdba"); +# dpb->insertString(&status, isc_dpb_password, "masterkey"); +# +# // create empty database +# att = prov->attachDatabase(&status, "ctest", dpb->getBufferLength(&status), +# dpb->getBuffer(&status)); +# +# dpb->dispose(); +# +# printf("database attached.\n"); +# +# att->execute(&status, nullptr, 0, "set debug option dsql_keep_blr = true", SAMPLES_DIALECT, nullptr, nullptr, nullptr, nullptr); +# // start read only transaction +# tpb = utl->getXpbBuilder(&status, IXpbBuilder::TPB, NULL, 0); +# tpb->insertTag(&status, isc_tpb_read_committed); +# tpb->insertTag(&status, isc_tpb_no_rec_version); +# tpb->insertTag(&status, isc_tpb_wait); +# tra = att->startTransaction(&status, tpb->getBufferLength(&status), tpb->getBuffer(&status)); +# +# // prepare statement +# stmt = att->prepare(&status, tra, 0, "select * from pos where a > 1 for update", +# SAMPLES_DIALECT, IStatement::PREPARE_PREFETCH_METADATA); +# +# // get list of columns +# meta = stmt->getOutputMetadata(&status); +# unsigned cols = meta->getCount(&status); +# unsigned messageLength = meta->getMessageLength(&status); +# +# std::unique_ptr buffer(new char[messageLength]); +# +# stmt->setCursorName(&status, "abc"); +# +# // open cursor +# printf("Opening cursor...\n"); +# curs = stmt->openCursor(&status, tra, NULL, NULL, meta, 0); +# +# // Prepare positioned update statement +# printf("Preparing update statement...\n"); +# IStatement* updStmt = att->prepare(&status, tra, 0, "update pos set b=b+1 where current of abc", +# SAMPLES_DIALECT, 0); +# +# const unsigned char items[] = {isc_info_sql_exec_path_blr_text, isc_info_sql_explain_plan}; +# unsigned char infoBuffer[32000]; +# updStmt->getInfo(&status, sizeof items, items, sizeof infoBuffer, infoBuffer); +# +# IXpbBuilder* pb = utl->getXpbBuilder(&status, IXpbBuilder::INFO_RESPONSE, infoBuffer, sizeof infoBuffer); +# for (pb->rewind(&status); !pb->isEof(&status); pb->moveNext(&status)) +# { +# switch (pb->getTag(&status)) +# { +# case isc_info_sql_exec_path_blr_text: +# printf("BLR:\n%s\n", pb->getString(&status)); +# break; +# case isc_info_sql_explain_plan: +# printf("Plan:\n%s\n", pb->getString(&status)); +# break; +# case isc_info_truncated: +# printf(" truncated\n"); +# // fall down... +# case isc_info_end: +# break; +# default: +# printf("Unexpected item %d\n", pb->getTag(&status)); +# } +# } +# pb->dispose(); +# +# // fetch records from cursor and print them +# for (int line = 0; curs->fetchNext(&status, buffer.get()) == IStatus::RESULT_OK; ++line) +# { +# printf("Fetched record %d\n", line); +# updStmt->execute(&status, tra, nullptr, nullptr, nullptr, nullptr); +# printf("Update executed\n"); +# } +# +# IStatement* stmt2 = att->prepare(&status, tra, 0, "select * from pos where a > 1 for update", +# SAMPLES_DIALECT, IStatement::PREPARE_PREFETCH_METADATA); +# +# // close interfaces +# curs->close(&status); +# curs = NULL; +# +# stmt->free(&status); +# stmt = NULL; +# +# updStmt->free(&status); +# +# stmt = stmt2; +# stmt->setCursorName(&status, "abc"); +# +# // open cursor +# printf("Opening cursor2...\n"); +# curs = stmt->openCursor(&status, tra, NULL, NULL, meta, 0); +# +# // Prepare positioned update statement +# printf("Preparing update statement again...\n"); +# updStmt = att->prepare(&status, tra, 0, "update pos set b=b+1 where current of abc", +# SAMPLES_DIALECT, 0); +# +# // fetch records from cursor and print them +# for (int line = 0; curs->fetchNext(&status, buffer.get()) == IStatus::RESULT_OK; ++line) +# { +# printf("Fetched record %d\n", line); +# updStmt->execute(&status, tra, nullptr, nullptr, nullptr, nullptr); +# printf("Update executed\n"); +# } +# +# curs->close(&status); +# curs = NULL; +# +# stmt->free(&status); +# stmt = NULL; +# +# updStmt->free(&status); +# +# meta->release(); +# meta = NULL; +# +# tra->commit(&status); +# tra = NULL; +# +# att->detach(&status); +# att = NULL; +# } +# catch (const FbException& error) +# { +# // handle error +# rc = 1; +# +# char buf[256]; +# master->getUtilInterface()->formatStatus(buf, sizeof(buf), error.getStatus()); +# fprintf(stderr, "%s\n", buf); +# } +# +# // release interfaces after error caught +# if (meta) +# meta->release(); +# if (builder) +# builder->release(); +# if (curs) +# curs->release(); +# if (stmt) +# stmt->release(); +# if (tra) +# tra->release(); +# if (att) +# att->release(); +# if (tpb) +# tpb->dispose(); +# +# prov->release(); +# status.dispose(); +# +# return rc; +# } diff --git a/tests/bugs/gh_8187_test.py b/tests/bugs/gh_8187_test.py new file mode 100644 index 00000000..f9389116 --- /dev/null +++ b/tests/bugs/gh_8187_test.py @@ -0,0 +1,133 @@ +#coding:utf-8 + +""" +ID: issue-8187 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8187 +TITLE: Performance regression in generating of UUID values after introducing GUID-v7 +DESCRIPTION: + We can estimate perfomance by comparison of time that is spent to generate UUIDs vs result of some crypt function + (commit: https://github.com/FirebirdSQL/firebird/commit/43e40886856ace39e0b3a1de7b00c53325e67225). + Function crypt_hash( using SHA512) has been selected for that. + Two procedures are created for generating appropriate results, SP_GEN_UUID and SP_GEN_HASH. + Duration for each of them is measured as difference between psutil.Process(fb_pid).cpu_times() counters. + We do these measures times for each SP, and each result is added to the list + which, in turn, is the source for median evaluation. + Finally, we get ratio between minimal and maximal medians (see 'median_ratio') + + Test is considered as passed if median_ratio less than threshold . +NOTES: + [05.08.2024] pzotov. + It seems that SP_CRYPT_HASH consumes lot of CPU resources thus it must be called much less times than SP_GEN_UUID, + see settings N_COUNT_PER_MEASURE_HASH vs N_COUNT_PER_MEASURE_GUID. + On Windows 10 usually ratio between cpu_times() medians of these two SPs is about 0.5 (before fix it was about 30). + + Confirmed bug on 6.0.0.405. + Checked on Windows, 6.0.0.406, 5.0.1.1469, 4.0.5.3139 (SS and CS). + + [08.08.2024] pzotov + Separated definition of MAX_RATIO value, it depends on OS. + Linux results show that medians ratio is about 2.3 ... 2.6. +""" +import os +import psutil +import pytest +from firebird.qa import * + +#-------------------------------------------------------------------- +def median(lst): + n = len(lst) + s = sorted(lst) + return (sum(s[n//2-1:n//2+1])/2.0, s[n//2])[n % 2] if n else None +#-------------------------------------------------------------------- + +########################### +### S E T T I N G S ### +########################### + +# How many times we call procedures: +N_MEASURES = 11 + +# How many iterations must be done: +N_COUNT_PER_MEASURE_HASH = 1000 + +N_COUNT_PER_MEASURE_GUID = 100000 + +# Maximal value for ratio between maximal and minimal medians +# +########################################### +MAX_RATIO = 1.0 if os.name == 'nt' else 5.5 +########################################### + +init_script = \ +f''' + set term ^; + create or alter procedure sp_gen_uuid (n_cnt int) as + declare v_guid varchar(16) character set octets; + begin + while (n_cnt > 0) do + begin + v_guid = gen_uuid(); + n_cnt = n_cnt - 1; + end + end + ^ + create or alter procedure sp_gen_hash (n_cnt int) as + declare v_hash varbinary(64); + declare s varchar(32765); + begin + s = lpad('', 32765, uuid_to_char(gen_uuid())); + while (n_cnt > 0) do + begin + v_hash = crypt_hash(s using SHA512); + n_cnt = n_cnt - 1; + end + end + ^ + commit + ^ +''' + +db = db_factory(init = init_script, charset = 'win1251') +act = python_act('db') + +expected_stdout = """ + Medians ratio: acceptable +""" + +@pytest.mark.version('>=4.0.5') +def test_1(act: Action, capsys): + + with act.db.connect(charset = 'win1251') as con: + cur=con.cursor() + cur.execute('select mon$server_pid as p from mon$attachments where mon$attachment_id = current_connection') + fb_pid = int(cur.fetchone()[0]) + + sp_time = {} + for i in range(0, N_MEASURES): + for sp_name in ('sp_gen_hash', 'sp_gen_uuid', ): + fb_info_init = psutil.Process(fb_pid).cpu_times() + n_cnt = N_COUNT_PER_MEASURE_HASH if sp_name == 'sp_gen_hash' else N_COUNT_PER_MEASURE_GUID + cur.callproc( sp_name, (n_cnt,) ) + fb_info_curr = psutil.Process(fb_pid).cpu_times() + sp_time[ sp_name, i ] = max(fb_info_curr.user - fb_info_init.user, 0.000001) + + + sp_gen_hash_median = median([v for k,v in sp_time.items() if k[0] == 'sp_gen_hash']) + sp_gen_uuid_median = median([v for k,v in sp_time.items() if k[0] == 'sp_gen_uuid']) + #---------------------------------- + #print(f'{sp_gen_hash_median=}') + #print(f'{sp_gen_uuid_median=}') + + median_ratio = sp_gen_uuid_median / sp_gen_hash_median + + print( 'Medians ratio: ' + ('acceptable' if median_ratio <= MAX_RATIO else '/* perf_issue_tag */ POOR: %s, more than threshold: %s' % ( '{:9g}'.format(median_ratio), '{:9g}'.format(MAX_RATIO) ) ) ) + if median_ratio > MAX_RATIO: + print('CPU times for each of {N_MEASURES} measures:') + for sp_name in ('sp_gen_hash', 'sp_gen_uuid', ): + print(f'{sp_name=}:') + for p in [v for k,v in sp_time.items() if k[0] == sp_name]: + print(p) + + act.expected_stdout = expected_stdout + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8194_test.py b/tests/bugs/gh_8194_test.py new file mode 100644 index 00000000..bf208584 --- /dev/null +++ b/tests/bugs/gh_8194_test.py @@ -0,0 +1,100 @@ +#coding:utf-8 + +#coding:utf-8 + +""" +ID: issue-8194 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8194 +TITLE: Internal consistency check (page in use during flush) with small number of DefaultDbCachePages +DESCRIPTION: + Test uses pre-created databases.conf which has alias (see variable REQUIRED_ALIAS) with DefaultDbCachePages = 128. + Database file for that alias must NOT exist in the QA_root/files/qa/ subdirectory: it will be created here. +NOTES: + [02.08.2024] pzotov + 1. One need to be sure that firebird.conf does NOT contain DatabaseAccess = None. + 2. Value of REQUIRED_ALIAS must be EXACTLY the same as alias specified in the pre-created databases.conf + (for LINUX this equality is case-sensitive, even when aliases are compared!) + 3. Content of databases.conf must be taken from $QA_ROOT/files/qa-databases.conf (one need to replace it before every test session). + Discussed with pcisar, letters since 30-may-2022 13:48, subject: + "new qa, core_4964_test.py: strange outcome when use... shutil.copy() // comparing to shutil.copy2()" + 4. Custom driver config object ('db_cfg_object') is used: we have to use DB with predefined alias instead of temporary one. + 5. It is enough to set number of iterations to small number about 10...20 (see 'LOOP_LIMIT'): bugcheck raised after first 2...3 iter. + + Confirmed bug on 6.0.0.403, got in firebird.log: + internal Firebird consistency check (page in use during flush (210), file: cch.cpp line: 2827) + Checked on 6.0.0.406 - all fine. + + [23.04.2025] pzotov + Confirmed bug on 5.0.3.1648-ca2f3e7, got: + internal Firebird consistency check (page in use during flush (210), file: cch.cpp line: 2828) + Checked on 5.0.3.1649-afcfa1a (intermediate snapshot) - all OK. Reduced min_version to 5.0.3. + + [20.05.2024] pzotov + Added temporary mark 'disabled_in_forks' to SKIP this test when QA runs agains *fork* of standard FB. +""" + +import re +from pathlib import Path +import pytest +from firebird.qa import * +from firebird.driver import driver_config, create_database, NetProtocol + +substitutions = [('[ \t]+', ' '), ] +REQUIRED_ALIAS = 'tmp_gh_8194_alias' +LOOP_LIMIT = 20 +SUCCESS_MSG = 'OK.' + +db = db_factory() +act = python_act('db') + +@pytest.mark.disabled_in_forks +@pytest.mark.version('>=5.0.3') +def test_1(act: Action, capsys): + + if act.get_server_architecture() != 'SuperServer': + pytest.skip('Applies only to SuperServer') + + # Scan line-by-line through databases.conf, find line starting with REQUIRED_ALIAS and extract name of file that + # must be created in the $(dir_sampleDb)/qa/ folder. This name will be used further as target database (tmp_fdb). + # NOTE: we have to SKIP lines which are commented out, i.e. if they starts with '#': + p_required_alias_ptn = re.compile( '^(?!#)((^|\\s+)' + REQUIRED_ALIAS + ')\\s*=\\s*\\$\\(dir_sampleDb\\)/qa/', re.IGNORECASE ) + fname_in_dbconf = None + + with open(act.home_dir/'databases.conf', 'r') as f: + for line in f: + if p_required_alias_ptn.search(line): + # If databases.conf contains line like this: + # tmp_8194_alias = $(dir_sampleDb)/qa/tmp_qa_8194.fdb + # - then we extract filename: 'tmp_qa_8194.fdb' (see below): + fname_in_dbconf = Path(line.split('=')[1].strip()).name + break + + # if 'fname_in_dbconf' remains undefined here then propably REQUIRED_ALIAS not equals to specified in the databases.conf! + # + assert fname_in_dbconf + + srv_cfg = """ + [local] + host = localhost + user = SYSDBA + password = masterkey + """ + srv_cfg = driver_config.register_server(name = 'test_srv_gh_8194', config = '') + + db_cfg_name = 'tmp_8194' + db_cfg_object = driver_config.register_database(name = db_cfg_name) + db_cfg_object.server.value = srv_cfg.name + db_cfg_object.protocol.value = NetProtocol.INET + db_cfg_object.database.value = REQUIRED_ALIAS + for i in range(LOOP_LIMIT): + # ::: NB ::: + # charset must be 'utf8' otherwise problem can not be reproduced! + # (see also note by the author of ticket in his starting message) + with create_database(db_cfg_name, user = act.db.user, password = act.db.password, charset = 'utf8') as con: + con.drop_database() + + print(SUCCESS_MSG) + + act.expected_stdout = SUCCESS_MSG + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8203_test.py b/tests/bugs/gh_8203_test.py new file mode 100644 index 00000000..5604102c --- /dev/null +++ b/tests/bugs/gh_8203_test.py @@ -0,0 +1,239 @@ +#coding:utf-8 + +""" +ID: issue-8203 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8203 +TITLE: MAKE_DBKEY can raise 'malformed string' for some table names +DESCRIPTION: + Test verifies ability to create table with random name for each of Unicode ranges + defined in https://jrgraphix.net/r/Unicode/, except following: + (0xD800, 0xDB7F), # High Surrogates + (0xDB80, 0xDBFF), # High Private Use Surrogates + (0xDC00, 0xDFFF), # Low Surrogates + Random name is generated for each range, with random length from scope NAME_MIN_LEN ... NAME_MAX_LEN scope. + Then we create table with such name and stored procedure that attempts to use make_dbkey() with 1st argument + equals to just created table. + This action is repeated REPEAT_CHECKS_FOR_SELECTED_UNICODE_RANGE times for each Unicode range. + Some characters from 'Basic Latin' are NOT included in any table names - see CHARS_TO_SKIP. + No error must raise for any of checked Unicode scopes. + Example of output when problem does exist: + iter=4 of REPEAT_CHECKS_FOR_SELECTED_UNICODE_RANGE=5: SUCCESS + range_name='Basic Latin', ..., table_random_unicode_name='}JIry@frnWdzb]5[:A=IomGozwyM*rmJ' + Error while parsing procedure SP_CHK's BLR + -Malformed string + err.gds_codes=(335544876, 335544849) + err.sqlcode=-901 + err.sqlstate='2F000' +NOTES: + [11.08.2024] pzotov + Confirmed bug on 6.0.0.421, 5.0.1.1469 + Checked on 6.0.0.423, 5.0.2.1477 +""" +import pytest +from firebird.qa import * +from io import BytesIO +from firebird.driver import SrvRestoreFlag, DatabaseError +import locale +import random + +db = db_factory() +act = python_act('db', substitutions=[('[ \t]+', ' ')]) + +######################### +### s e t t i n g s ### +######################### +CHARS_TO_SKIP = set('<>|"\'^') +NAME_MIN_LEN = 32 +NAME_MAX_LEN = 63 +REPEAT_CHECKS_FOR_SELECTED_UNICODE_RANGE = 5 # duration: ~60" + +#------------------------------------------------ + +def get_random_unicode(length, bound_points): + # https://stackoverflow.com/questions/1477294/generate-random-utf-8-string-in-python + try: + get_char = unichr + except NameError: + get_char = chr + + alphabet = [ + get_char(code_point) for code_point in range(bound_points[0],bound_points[1]) + ] + return ''.join(random.choice(alphabet) for i in range(length)) + +#------------------------------------------------ + +@pytest.mark.version('>=5.0.2') +def test_1(act: Action, capsys): + + # https://jrgraphix.net/r/Unicode/ + UNICODE_RANGES_MAP = { + (0x0020, 0x007F) : 'Basic Latin', + (0x00A0, 0x00FF) : 'Latin-1 Supplement', + (0x0100, 0x017F) : 'Latin Extended-A', + (0x0180, 0x024F) : 'Latin Extended-B', + (0x0400, 0x04FF) : 'Cyrillic', + (0x0500, 0x052F) : 'Cyrillic Supplementary', + (0x0300, 0x036F) : 'Combining Diacritical Marks', + (0x0250, 0x02AF) : 'IPA Extensions', + (0x0370, 0x03FF) : 'Greek and Coptic', + (0x0530, 0x058F) : 'Armenian', + (0x02B0, 0x02FF) : 'Spacing Modifier Letters', + (0x0590, 0x05FF) : 'Hebrew', + (0x0600, 0x06FF) : 'Arabic', + (0x0700, 0x074F) : 'Syriac', + (0x0780, 0x07BF) : 'Thaana', + (0x0900, 0x097F) : 'Devanagari', + (0x0980, 0x09FF) : 'Bengali', + (0x0A00, 0x0A7F) : 'Gurmukhi', + (0x0A80, 0x0AFF) : 'Gujarati', + (0x0B00, 0x0B7F) : 'Oriya', + (0x0B80, 0x0BFF) : 'Tamil', + (0x0C00, 0x0C7F) : 'Telugu', + (0x0C80, 0x0CFF) : 'Kannada', + (0x0D00, 0x0D7F) : 'Malayalam', + (0x0D80, 0x0DFF) : 'Sinhala', + (0x0E00, 0x0E7F) : 'Thai', + (0x0E80, 0x0EFF) : 'Lao', + (0x0F00, 0x0FFF) : 'Tibetan', + (0x1000, 0x109F) : 'Myanmar', + (0x10A0, 0x10FF) : 'Georgian', + (0x1100, 0x11FF) : 'Hangul Jamo', + (0x1200, 0x137F) : 'Ethiopic', + (0x13A0, 0x13FF) : 'Cherokee', + (0x1400, 0x167F) : 'Unified Canadian Aboriginal Syllabics', + (0x1680, 0x169F) : 'Ogham', + (0x16A0, 0x16FF) : 'Runic', + (0x1700, 0x171F) : 'Tagalog', + (0x1720, 0x173F) : 'Hanunoo', + (0x1740, 0x175F) : 'Buhid', + (0x1760, 0x177F) : 'Tagbanwa', + (0x1780, 0x17FF) : 'Khmer', + (0x1800, 0x18AF) : 'Mongolian', + (0x1900, 0x194F) : 'Limbu', + (0x1950, 0x197F) : 'Tai Le', + (0x19E0, 0x19FF) : 'Khmer Symbols', + (0x1D00, 0x1D7F) : 'Phonetic Extensions', + (0x1E00, 0x1EFF) : 'Latin Extended Additional', + (0x1F00, 0x1FFF) : 'Greek Extended', + (0x2000, 0x206F) : 'General Punctuation', + (0x2070, 0x209F) : 'Superscripts and Subscripts', + (0x20A0, 0x20CF) : 'Currency Symbols', + (0x20D0, 0x20FF) : 'Combining Diacritical Marks for Symbols', + (0x2100, 0x214F) : 'Letterlike Symbols', + (0x2150, 0x218F) : 'Number Forms', + (0x2190, 0x21FF) : 'Arrows', + (0x2200, 0x22FF) : 'Mathematical Operators', + (0x2300, 0x23FF) : 'Miscellaneous Technical', + (0x2400, 0x243F) : 'Control Pictures', + (0x2440, 0x245F) : 'Optical Character Recognition', + (0x2460, 0x24FF) : 'Enclosed Alphanumerics', + (0x2500, 0x257F) : 'Box Drawing', + (0x2580, 0x259F) : 'Block Elements', + (0x25A0, 0x25FF) : 'Geometric Shapes', + (0x2600, 0x26FF) : 'Miscellaneous Symbols', + (0x2700, 0x27BF) : 'Dingbats', + (0x27C0, 0x27EF) : 'Miscellaneous Mathematical Symbols-A', + (0x27F0, 0x27FF) : 'Supplemental Arrows-A', + (0x2800, 0x28FF) : 'Braille Patterns', + (0x2900, 0x297F) : 'Supplemental Arrows-B', + (0x2980, 0x29FF) : 'Miscellaneous Mathematical Symbols-B', + (0x2A00, 0x2AFF) : 'Supplemental Mathematical Operators', + (0x2B00, 0x2BFF) : 'Miscellaneous Symbols and Arrows', + (0x2E80, 0x2EFF) : 'CJK Radicals Supplement', + (0x2F00, 0x2FDF) : 'Kangxi Radicals', + (0x2FF0, 0x2FFF) : 'Ideographic Description Characters', + (0x3000, 0x303F) : 'CJK Symbols and Punctuation', + (0x3040, 0x309F) : 'Hiragana', + (0x30A0, 0x30FF) : 'Katakana', + (0x3100, 0x312F) : 'Bopomofo', + (0x3130, 0x318F) : 'Hangul Compatibility Jamo', + (0x3190, 0x319F) : 'Kanbun', + (0x31A0, 0x31BF) : 'Bopomofo Extended', + (0x31F0, 0x31FF) : 'Katakana Phonetic Extensions', + (0x3200, 0x32FF) : 'Enclosed CJK Letters and Months', + (0x3300, 0x33FF) : 'CJK Compatibility', + (0x3400, 0x4DBF) : 'CJK Unified Ideographs Extension A', + (0x4DC0, 0x4DFF) : 'Yijing Hexagram Symbols', + (0x4E00, 0x9FFF) : 'CJK Unified Ideographs', + (0xA000, 0xA48F) : 'Yi Syllables', + (0xA490, 0xA4CF) : 'Yi Radicals', + (0xAC00, 0xD7AF) : 'Hangul Syllables', + (0xE000, 0xF8FF) : 'Private Use Area', + (0xF900, 0xFAFF) : 'CJK Compatibility Ideographs', + (0xFB00, 0xFB4F) : 'Alphabetic Presentation Forms', + (0xFB50, 0xFDFF) : 'Arabic Presentation Forms-A', + (0xFE00, 0xFE0F) : 'Variation Selectors', + (0xFE20, 0xFE2F) : 'Combining Half Marks', + (0xFE30, 0xFE4F) : 'CJK Compatibility Forms', + (0xFE50, 0xFE6F) : 'Small Form Variants', + (0xFE70, 0xFEFF) : 'Arabic Presentation Forms-B', + (0xFF00, 0xFFEF) : 'Halfwidth and Fullwidth Forms', + (0xFFF0, 0xFFFF) : 'Specials', + (0x10000, 0x1007F) : 'Linear B Syllabary', + (0x10080, 0x100FF) : 'Linear B Ideograms', + (0x10100, 0x1013F) : 'Aegean Numbers', + (0x10300, 0x1032F) : 'Old Italic', + (0x10330, 0x1034F) : 'Gothic', + (0x10380, 0x1039F) : 'Ugaritic', + (0x10400, 0x1044F) : 'Deseret', + (0x10450, 0x1047F) : 'Shavian', + (0x10480, 0x104AF) : 'Osmanya', + (0x10800, 0x1083F) : 'Cypriot Syllabary', + (0x1D000, 0x1D0FF) : 'Byzantine Musical Symbols', + (0x1D100, 0x1D1FF) : 'Musical Symbols', + (0x1D300, 0x1D35F) : 'Tai Xuan Jing Symbols', + (0x1D400, 0x1D7FF) : 'Mathematical Alphanumeric Symbols', + (0x20000, 0x2A6DF) : 'CJK Unified Ideographs Extension B', + (0x2F800, 0x2FA1F) : 'CJK Compatibility Ideographs Supplement', + (0xE0000, 0xE007F) : 'Tags', + } + + for bound_points, range_name in UNICODE_RANGES_MAP.items(): + + for iter in range(1,REPEAT_CHECKS_FOR_SELECTED_UNICODE_RANGE+1): + + + table_random_unicode_name = get_random_unicode( random.randint(NAME_MIN_LEN, NAME_MAX_LEN), bound_points ) + table_random_unicode_name = ''.join(c for c in table_random_unicode_name if c not in CHARS_TO_SKIP) + + test_sql = f""" + recreate table "{table_random_unicode_name.replace('"','""')}"(id int) + ^ + create or alter procedure sp_chk as + declare id1 int; + begin + select /* {range_name=} {iter=} */ id from "{table_random_unicode_name.replace('"','""')}" where rdb$db_key = make_dbkey('{table_random_unicode_name}', 0) into id1; + end + ^ + """ + # select id from "{table_random_unicode_name.replace('"','""')}" where rdb$db_key = make_dbkey('{table_random_unicode_name.replace("'","''")}', 0) into id1; + + expected_txt = f'{iter=} of {REPEAT_CHECKS_FOR_SELECTED_UNICODE_RANGE=}: SUCCESS' + with act.db.connect(charset = 'utf-8') as con: + try: + for line in test_sql.split('^'): + if (expr := line.strip()): + if expr != '^': + con.execute_immediate(expr) + else: + con.commit() + con.commit() + print(expected_txt) + except DatabaseError as err: + print(f'{range_name=}, {iter=} of {REPEAT_CHECKS_FOR_SELECTED_UNICODE_RANGE=}, {table_random_unicode_name=}') + print(err) + print(f'{err.gds_codes=}') + print(f'{err.sqlcode=}') + print(f'{err.sqlstate=}') + ''' + backup = BytesIO() + with act.connect_server() as srv: + srv.database.local_backup(database=act.db.db_path, backup_stream=backup) + backup.seek(0) + srv.database.local_restore(backup_stream=backup, database=act.db.db_path, flags = SrvRestoreFlag.REPLACE) + ''' + act.expected_stdout = expected_txt + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() diff --git a/tests/bugs/gh_8211_test.py b/tests/bugs/gh_8211_test.py new file mode 100644 index 00000000..4650a235 --- /dev/null +++ b/tests/bugs/gh_8211_test.py @@ -0,0 +1,50 @@ +#coding:utf-8 + +""" +ID: issue-8211 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8211 +TITLE: DATEADD truncates milliseconds for month and year deltas. +DESCRIPTION: +NOTES: + [11.08.2024] pzotov + Confirmed bug on 6.0.0.423 + Checked on intermediate snapshots: 6.0.0.431-16bb157; 5.0.2.1477-c71eb20; 4.0.6.3141 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + select + ''||dateadd(0 millisecond to cast('01.01.2001 01:01:01.1111' as timestamp)) a_millisecond, + ''||dateadd(0 second to cast('01.01.2001 01:01:01.1111' as timestamp)) a_second, + ''||dateadd(0 minute to cast('01.01.2001 01:01:01.1111' as timestamp)) a_minute, + ''||dateadd(0 hour to cast('01.01.2001 01:01:01.1111' as timestamp)) a_hour, + ''||dateadd(0 day to cast('01.01.2001 01:01:01.1111' as timestamp)) a_day, + ''||dateadd(0 week to cast('01.01.2001 01:01:01.1111' as timestamp)) a_week, + ''||dateadd(0 month to cast('01.01.2001 01:01:01.1111' as timestamp)) a_month, + ''||dateadd(0 year to cast('01.01.2001 01:01:01.1111' as timestamp)) a_year + from rdb$database; +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +expected_stdout = """ + A_MILLISECOND 2001-01-01 01:01:01.1111 + A_SECOND 2001-01-01 01:01:01.1111 + A_MINUTE 2001-01-01 01:01:01.1111 + A_HOUR 2001-01-01 01:01:01.1111 + A_DAY 2001-01-01 01:01:01.1111 + A_WEEK 2001-01-01 01:01:01.1111 + A_MONTH 2001-01-01 01:01:01.1111 + A_YEAR 2001-01-01 01:01:01.1111 +""" + +@pytest.mark.version('>=4.0.6') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8213_test.py b/tests/bugs/gh_8213_test.py new file mode 100644 index 00000000..9dea50ed --- /dev/null +++ b/tests/bugs/gh_8213_test.py @@ -0,0 +1,276 @@ +#coding:utf-8 + +""" +ID: issue-8213 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8213 +TITLE: WHEN NOT MATCHED BY SOURCE - does not work with a direct table as source +DESCRIPTION: +NOTES: + [20.08.2024] pzotov + Checked on 6.0.0.438-d40d01b, 5.0.2.1479-47aa3b1 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + recreate procedure sp_main as begin end; + recreate table test (id smallint); + recreate generator g; + recreate table test ( + id smallint primary key, + typ smallint, + cat smallint + ); + commit; + + set term ^ ; + create or alter trigger test_bi0 for test active before insert position 0 as + begin + new.id = coalesce(new.id, gen_id(g, 1)); + end + ^ + set term ; ^ + commit; + + insert into test(typ, cat) values(1, 10); + insert into test(typ, cat) values(1, 20); + insert into test(typ, cat) values(2, 10); + insert into test(typ, cat) values(2, 30); + commit; + + set term ^; + recreate procedure sp_main ( + a_insert_using_sp boolean, + a_delete_using_sp boolean, + a_source_typ smallint, + a_target_typ smallint + ) as + + declare procedure inner_sp_data_for_source_typ + returns ( + id smallint, + typ smallint, + cat smallint + ) as + begin + for select t.id, + t.typ, + t.cat + from test t + where t.typ = :a_source_typ + into :id, + :typ, + :cat + do + begin + suspend; + end + end + + begin + + if ( a_insert_using_sp or :a_delete_using_sp ) then + begin + if (a_insert_using_sp) then + merge into test t + using inner_sp_data_for_source_typ s + on t.typ = :a_target_typ and + t.cat = s.cat + when not matched by target then + insert (typ, cat) values (:a_target_typ, s.cat); + else + merge into test t + using test s + on t.typ = :a_target_typ and + t.cat = s.cat + when not matched by target then + insert (typ, cat) values (:a_target_typ, s.cat); + + if (a_delete_using_sp) then + merge into test t + using inner_sp_data_for_source_typ s on t.cat = s.cat + when not matched by source and t.typ = :a_target_typ then + delete; + else + merge into test t + using test s on t.cat = s.cat + when not matched by source and t.typ = :a_target_typ then + delete; + + end + else + begin + -- works as expected + merge into test t + using ( select t.id, + t.typ, + t.cat + from test t + where t.typ = :a_source_typ + ) s + on t.typ = :a_target_typ and + t.cat = s.cat + when not matched by target then + insert (typ, cat) values (:a_target_typ, s.cat); + + merge into test t + using ( select t.id, + t.typ, + t.cat + from test t + where t.typ = :a_source_typ + ) s + on t.cat = s.cat + when not matched by source and t.typ = :a_target_typ then + delete; + end + end + ^ + set term ;^ + commit; + + -- select * from test; + set count on; + + alter sequence g restart with 1000; + execute procedure sp_main(true, true, 1, 10); + select 'INS:SP, DEL:SP' msg, t.id, t.typ, t.cat from test t order by id; + rollback; + + alter sequence g restart with 1000; + execute procedure sp_main(true, false, 1, 10); + select 'INS:SP, DEL:TAB' msg, t.id, t.typ, t.cat from test t order by id; + rollback; + + alter sequence g restart with 1000; + execute procedure sp_main(false, true, 1, 10); + select 'INS:TAB, DEL:SP' msg, t.id, t.typ, t.cat from test t order by id; + rollback; + + alter sequence g restart with 1000; + execute procedure sp_main(false, false, 1, 10); + select 'INS:TAB, DEL:TAB' msg, t.id, t.typ, t.cat from test t order by id; + rollback; +""" + +act = isql_act('db', test_script) + +expected_stdout = """ + MSG INS:SP, DEL:SP + ID 1 + TYP 1 + CAT 10 + MSG INS:SP, DEL:SP + ID 2 + TYP 1 + CAT 20 + MSG INS:SP, DEL:SP + ID 3 + TYP 2 + CAT 10 + MSG INS:SP, DEL:SP + ID 4 + TYP 2 + CAT 30 + MSG INS:SP, DEL:SP + ID 1000 + TYP 10 + CAT 10 + MSG INS:SP, DEL:SP + ID 1001 + TYP 10 + CAT 20 + Records affected: 6 + + MSG INS:SP, DEL:TAB + ID 1 + TYP 1 + CAT 10 + MSG INS:SP, DEL:TAB + ID 2 + TYP 1 + CAT 20 + MSG INS:SP, DEL:TAB + ID 3 + TYP 2 + CAT 10 + MSG INS:SP, DEL:TAB + ID 4 + TYP 2 + CAT 30 + MSG INS:SP, DEL:TAB + ID 1000 + TYP 10 + CAT 10 + MSG INS:SP, DEL:TAB + ID 1001 + TYP 10 + CAT 20 + Records affected: 6 + + MSG INS:TAB, DEL:SP + ID 1 + TYP 1 + CAT 10 + MSG INS:TAB, DEL:SP + ID 2 + TYP 1 + CAT 20 + MSG INS:TAB, DEL:SP + ID 3 + TYP 2 + CAT 10 + MSG INS:TAB, DEL:SP + ID 4 + TYP 2 + CAT 30 + MSG INS:TAB, DEL:SP + ID 1000 + TYP 10 + CAT 10 + MSG INS:TAB, DEL:SP + ID 1001 + TYP 10 + CAT 20 + MSG INS:TAB, DEL:SP + ID 1002 + TYP 10 + CAT 10 + Records affected: 7 + + MSG INS:TAB, DEL:TAB + ID 1 + TYP 1 + CAT 10 + MSG INS:TAB, DEL:TAB + ID 2 + TYP 1 + CAT 20 + MSG INS:TAB, DEL:TAB + ID 3 + TYP 2 + CAT 10 + MSG INS:TAB, DEL:TAB + ID 4 + TYP 2 + CAT 30 + MSG INS:TAB, DEL:TAB + ID 1000 + TYP 10 + CAT 10 + MSG INS:TAB, DEL:TAB + ID 1001 + TYP 10 + CAT 20 + Records affected: 6 +""" + +@pytest.mark.version('>=5.0.2') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8214_test.py b/tests/bugs/gh_8214_test.py new file mode 100644 index 00000000..56304b19 --- /dev/null +++ b/tests/bugs/gh_8214_test.py @@ -0,0 +1,441 @@ +#coding:utf-8 + +""" +ID: issue-8214 +ISSUE: 8214 +TITLE: Incorrect result of index list scan for a composite index, the second segment of which is a text field with COLLATE UNICODE_CI +DESCRIPTION: + Test adds check for: + * collation with attributes 'case insensitive accent insensitive'; + * null values of some records (the must not appear in any query); + * non-ascii values; + * both asc and desc indices - results must be identical; + * miscelaneous predicates +NOTES: + [31.10.2024] pzotov + Confirmed bug on 5.0.2.1547. + Checked on 5.0.2.1551, 6.0.0.515. +""" + +import pytest +from firebird.qa import * + +db = db_factory(charset = 'utf8') + +test_script = """ + set bail on; + set list on; + + create collation txt_coll_ci for utf8 from unicode case insensitive; + create collation txt_coll_ci_ai for utf8 from unicode case insensitive accent insensitive; + + recreate table mans ( + id bigint not null, + code_sex smallint not null, + name_1 varchar(50) collate txt_coll_ci, + name_2 varchar(50) collate txt_coll_ci_ai, + constraint pk_mans primary key(id) + ); + + commit; + insert into mans (id, code_sex, name_1, name_2) values (1, 1, 'BoB', 'BØb'); + insert into mans (id, code_sex, name_1, name_2) values (2, 1, 'jOhN', 'jŐhŇ'); + insert into mans (id, code_sex, name_1, name_2) values (3, 2, 'BArbArA', 'BÄŔBĄŕă'); + insert into mans (id, code_sex, name_1, name_2) values (4, 2, 'aNNA', 'âŃŃÁ'); + insert into mans (id, code_sex, name_1, name_2) values (5, 1, null, null); + insert into mans (id, code_sex, name_1, name_2) values (6, 2, null, null); + insert into mans (id, code_sex, name_1, name_2) values (7, 1, 'danIEL', 'ĐÁniel'); + insert into mans (id, code_sex, name_1, name_2) values (8, 2, 'debora', 'ĐeborÁ'); + commit; + + create index mans_sex_name_1_asc on mans(code_sex, name_1); + create index mans_sex_name_2_asc on mans(code_sex, name_2); + + create view v_test_1 as + select msg, id, name_1 + from ( + select 'chk-a' as msg, id, code_sex, name_1 + from mans where code_sex between 1 and 2 and name_1 starts 'b' + UNION ALL + select 'chk-b' as msg, id, code_sex, name_1 + from mans where code_sex > 0 and code_sex < 3 and name_1 starts 'b' + UNION ALL + select 'chk-c' as msg, id, code_sex, name_1 + from mans where (code_sex =1 or code_sex =2) and name_1 starts 'b' + UNION ALL + select 'chk-d' as msg, id, code_sex, name_1 + from mans where code_sex in(1,2) and name_1 starts 'b' + UNION ALL + select 'chk-e' as msg, id, code_sex, name_1 + from mans where code_sex in(1,2) and name_1 like 'b%' + UNION ALL + select 'chk-f' as msg, id, code_sex, name_1 + from mans where code_sex in(1,2) and name_1 similar to 'b%' + UNION ALL + select 'chk-g' as msg, id, code_sex, name_1 + from mans where code_sex in(1,2) and name_1 in ('boB', 'barbarA') + UNION ALL + select 'chk-h' as msg, id, code_sex, name_1 + from mans where code_sex in(1,2) and (name_1 is not distinct from 'boB' or name_1 is not distinct from 'barbarA') + UNION ALL + select 'chk-i' as msg, id, code_sex, name_1 + from mans where code_sex in(1,2) and (name_1 >= 'D' and name_1 <= 'E') + ) + order by msg, id + ; + + create view v_test_2 as + select msg, id, name_2 + from ( + select 'chk-a' as msg, id, code_sex, name_2 + from mans where code_sex between 1 and 2 and name_2 starts 'b' + UNION ALL + select 'chk-b' as msg, id, code_sex, name_2 + from mans where code_sex > 0 and code_sex < 3 and name_2 starts 'b' + UNION ALL + select 'chk-c' as msg, id, code_sex, name_2 + from mans where (code_sex =1 or code_sex =2) and name_2 starts 'b' + UNION ALL + select 'chk-d' as msg, id, code_sex, name_2 + from mans where code_sex in(1,2) and name_2 starts 'b' + UNION ALL + select 'chk-e' as msg, id, code_sex, name_2 + from mans where code_sex in(1,2) and name_2 like 'b%' + UNION ALL + select 'chk-f' as msg, id, code_sex, name_2 + from mans where code_sex in(1,2) and name_2 similar to 'b%' + UNION ALL + select 'chk-g' as msg, id, code_sex, name_2 + from mans where code_sex in(1,2) and name_2 in ('boB', 'barbarA') + UNION ALL + select 'chk-h' as msg, id, code_sex, name_2 + from mans where code_sex in(1,2) and (name_2 is not distinct from 'boB' or name_2 is not distinct from 'barbarA') + UNION ALL + select 'chk-i' as msg, id, code_sex, name_2 + from mans where code_sex in(1,2) and (name_2 >= 'D' and name_2 <= 'E') + ) + order by msg, id + ; + + + select * from v_test_1; + select * from v_test_2; + commit; + + ----------------------------------------------------------- + + alter index mans_sex_name_1_asc inactive; + alter index mans_sex_name_2_asc inactive; + + create descending index mans_sex_name_1_dec on mans(code_sex, name_1); + create descending index mans_sex_name_2_dec on mans(code_sex, name_2); + commit; + + select * from v_test_1; + select * from v_test_2; +""" + +act = isql_act('db', test_script, substitutions = [ ('[ \t]+',' ') ]) + +expected_stdout = """ + MSG chk-a + ID 1 + NAME_1 BoB + + MSG chk-a + ID 3 + NAME_1 BArbArA + + MSG chk-b + ID 1 + NAME_1 BoB + + MSG chk-b + ID 3 + NAME_1 BArbArA + + MSG chk-c + ID 1 + NAME_1 BoB + + MSG chk-c + ID 3 + NAME_1 BArbArA + + MSG chk-d + ID 1 + NAME_1 BoB + + MSG chk-d + ID 3 + NAME_1 BArbArA + + MSG chk-e + ID 1 + NAME_1 BoB + + MSG chk-e + ID 3 + NAME_1 BArbArA + + MSG chk-f + ID 1 + NAME_1 BoB + + MSG chk-f + ID 3 + NAME_1 BArbArA + + MSG chk-g + ID 1 + NAME_1 BoB + + MSG chk-g + ID 3 + NAME_1 BArbArA + + MSG chk-h + ID 1 + NAME_1 BoB + + MSG chk-h + ID 3 + NAME_1 BArbArA + + MSG chk-i + ID 7 + NAME_1 danIEL + + MSG chk-i + ID 8 + NAME_1 debora + + + + MSG chk-a + ID 1 + NAME_2 BØb + + MSG chk-a + ID 3 + NAME_2 BÄŔBĄŕă + + MSG chk-b + ID 1 + NAME_2 BØb + + MSG chk-b + ID 3 + NAME_2 BÄŔBĄŕă + + MSG chk-c + ID 1 + NAME_2 BØb + + MSG chk-c + ID 3 + NAME_2 BÄŔBĄŕă + + MSG chk-d + ID 1 + NAME_2 BØb + + MSG chk-d + ID 3 + NAME_2 BÄŔBĄŕă + + MSG chk-e + ID 1 + NAME_2 BØb + + MSG chk-e + ID 3 + NAME_2 BÄŔBĄŕă + + MSG chk-f + ID 1 + NAME_2 BØb + + MSG chk-f + ID 3 + NAME_2 BÄŔBĄŕă + + MSG chk-g + ID 1 + NAME_2 BØb + + MSG chk-g + ID 3 + NAME_2 BÄŔBĄŕă + + MSG chk-h + ID 1 + NAME_2 BØb + + MSG chk-h + ID 3 + NAME_2 BÄŔBĄŕă + + MSG chk-i + ID 7 + NAME_2 ĐÁniel + + MSG chk-i + ID 8 + NAME_2 ĐeborÁ + + + + MSG chk-a + ID 1 + NAME_1 BoB + + MSG chk-a + ID 3 + NAME_1 BArbArA + + MSG chk-b + ID 1 + NAME_1 BoB + + MSG chk-b + ID 3 + NAME_1 BArbArA + + MSG chk-c + ID 1 + NAME_1 BoB + + MSG chk-c + ID 3 + NAME_1 BArbArA + + MSG chk-d + ID 1 + NAME_1 BoB + + MSG chk-d + ID 3 + NAME_1 BArbArA + + MSG chk-e + ID 1 + NAME_1 BoB + + MSG chk-e + ID 3 + NAME_1 BArbArA + + MSG chk-f + ID 1 + NAME_1 BoB + + MSG chk-f + ID 3 + NAME_1 BArbArA + + MSG chk-g + ID 1 + NAME_1 BoB + + MSG chk-g + ID 3 + NAME_1 BArbArA + + MSG chk-h + ID 1 + NAME_1 BoB + + MSG chk-h + ID 3 + NAME_1 BArbArA + + MSG chk-i + ID 7 + NAME_1 danIEL + + MSG chk-i + ID 8 + NAME_1 debora + + + + MSG chk-a + ID 1 + NAME_2 BØb + + MSG chk-a + ID 3 + NAME_2 BÄŔBĄŕă + + MSG chk-b + ID 1 + NAME_2 BØb + + MSG chk-b + ID 3 + NAME_2 BÄŔBĄŕă + + MSG chk-c + ID 1 + NAME_2 BØb + + MSG chk-c + ID 3 + NAME_2 BÄŔBĄŕă + + MSG chk-d + ID 1 + NAME_2 BØb + + MSG chk-d + ID 3 + NAME_2 BÄŔBĄŕă + + MSG chk-e + ID 1 + NAME_2 BØb + + MSG chk-e + ID 3 + NAME_2 BÄŔBĄŕă + + MSG chk-f + ID 1 + NAME_2 BØb + + MSG chk-f + ID 3 + NAME_2 BÄŔBĄŕă + + MSG chk-g + ID 1 + NAME_2 BØb + + MSG chk-g + ID 3 + NAME_2 BÄŔBĄŕă + + MSG chk-h + ID 1 + NAME_2 BØb + + MSG chk-h + ID 3 + NAME_2 BÄŔBĄŕă + + MSG chk-i + ID 7 + NAME_2 ĐÁniel + + MSG chk-i + ID 8 + NAME_2 ĐeborÁ +""" + +@pytest.mark.version('>=5.0.2') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout + diff --git a/tests/bugs/gh_8219_test.py b/tests/bugs/gh_8219_test.py new file mode 100644 index 00000000..0e248de0 --- /dev/null +++ b/tests/bugs/gh_8219_test.py @@ -0,0 +1,145 @@ +#coding:utf-8 + +""" +ID: issue-8219 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8219 +TITLE: Database creation in 3.0.12, 4.0.5 and 5.0.1 slower than in previous releases +DESCRIPTION: + We can estimate perfomance by comparison of time that is spent to create DB vs result of some crypt function. + Function crypt_hash( using SHA512) has been selected for that because of notable CPU consumation. + Stored procedured SP_GEN_HASH is created for evaluation of crypt hash, it will run loop for N_HASH_EVALUATE_COUNT times. + Duration for each measure is difference between psutil.Process(fb_pid).cpu_times() counters. + We do times call of SP and create_database(), with adding results to map. + Finally, we get ratio between medians of these measures (see 'median_ratio') + + Test is considered as passed if median_ratio less than threshold . +NOTES: + [05.09.2024] pzotov. + 1. Confirmed problem on snapshotrs before 20-aug-2024. + Medians ratio on Windows: + 1. Before fix: + 6.0.0.423: 0.39; 6.0.0.436: 0.39; 6.0.0.437: 0.35; + 5.0.1.1464: 0.42; 5.0.1.1469: 0.39; 5.0.1.1479: 0.35 + 4.0.5.3136: 0.42; 4.0.6.3142: 0.39 + 2. After fix ratio reduced to ~0.25: + 6.0.0.438: 0.21; 6.0.0.442: 0.21; 6.0.0.438: 0.21; 6.0.0.442: 0.21; 6.0.0.450: 0.24 + 5.0.2.1481: 0.25; 5.0.2.1482: 0.21; 5.0.2.1493: 0.22 + 4.0.6.3144: 0.25; 4.0.6.3149: 0.29 + + Medians ratio on Windows: + 1. Before fix: + 6.0.0.397-c734c96: 0.48; 6.0.0.438-088b529: 0.49 + 2. After fix ratio reduced to ~0.25: + 6.0.0.441-75042b5: 0.23 + 5.0.2.1481-fc71044: 0.24 + 4.0.6.3144-5a3b718: 0.27 + 2. Test DB must NOT have charset = utf8, otherwise 'implementation limit exceeded' will raise; win1251 was selected for work. + 3. Test can be used only for ServerMode = Super or SuperClassic + (because in CS a new process is made and we have no value of cpu_times() *before* DB creation). +""" +import os +import psutil +import pytest +from firebird.qa import * +from firebird.driver import driver_config, create_database, NetProtocol +from pathlib import Path + +#-------------------------------------------------------------------- +def median(lst): + n = len(lst) + s = sorted(lst) + return (sum(s[n//2-1:n//2+1])/2.0, s[n//2])[n % 2] if n else None +#-------------------------------------------------------------------- + +########################### +### S E T T I N G S ### +########################### + +# How many times we create databases +N_MEASURES = 31 + +# How many iterations must be done for hash evaluation: +N_HASH_EVALUATE_COUNT = 3000 + +# Maximal value for ratio between maximal and minimal medians +# +############################################# +MAX_RATIO = 0.30 if os.name == 'nt' else 0.33 +############################################# + +init_script = \ +f''' + set term ^; + create or alter procedure sp_gen_hash (n_cnt int) as + declare v_hash varbinary(64); + declare s varchar(32765); + begin + s = lpad('', 32765, uuid_to_char(gen_uuid())); + while (n_cnt > 0) do + begin + v_hash = crypt_hash(s using SHA512); + n_cnt = n_cnt - 1; + end + end + ^ + commit + ^ +''' + +db = db_factory(init = init_script, charset = 'win1251') +act = python_act('db') +tmp_fdb = temp_file('tmp_gh_8219.tmp') + +expected_stdout = """ + Medians ratio: acceptable +""" + +@pytest.mark.version('>=4.0.5') +def test_1(act: Action, tmp_fdb: Path, capsys): + + if act.vars['server-arch'].lower() == 'classic': + pytest.skip('Can be used only for SS / SC.') + + srv_cfg = driver_config.register_server(name = 'test_srv_gh_8219', config = '') + + db_cfg_name = 'tmp_8219' + db_cfg_object = driver_config.register_database(name = db_cfg_name) + db_cfg_object.server.value = srv_cfg.name + db_cfg_object.protocol.value = NetProtocol.INET + db_cfg_object.database.value = str(tmp_fdb) + + with act.db.connect() as con: + cur=con.cursor() + cur.execute('select mon$server_pid as p from mon$attachments where mon$attachment_id = current_connection') + fb_pid = int(cur.fetchone()[0]) + + times_map = {} + for i in range(0, N_MEASURES): + fb_info_init = psutil.Process(fb_pid).cpu_times() + cur.callproc( 'sp_gen_hash', (N_HASH_EVALUATE_COUNT,) ) + fb_info_curr = psutil.Process(fb_pid).cpu_times() + times_map[ 'hash_eval', i ] = max(fb_info_curr.user - fb_info_init.user, 0.000001) + + fb_info_init = psutil.Process(fb_pid).cpu_times() + with create_database(db_cfg_name, user = act.db.user, password = act.db.password, overwrite = True) as dbc: + pass + fb_info_curr = psutil.Process(fb_pid).cpu_times() + times_map[ 'db_create', i ] = max(fb_info_curr.user - fb_info_init.user, 0.000001) + + + sp_gen_hash_median = median([v for k,v in times_map.items() if k[0] == 'hash_eval']) + sp_db_create_median = median([v for k,v in times_map.items() if k[0] == 'db_create']) + + median_ratio = sp_db_create_median / sp_gen_hash_median + + print( 'Medians ratio: ' + ('acceptable' if median_ratio <= MAX_RATIO else '/* perf_issue_tag */ POOR: %s, more than threshold: %s' % ( '{:9g}'.format(median_ratio), '{:9g}'.format(MAX_RATIO) ) ) ) + if median_ratio > MAX_RATIO: + print(f'CPU times for each of {N_MEASURES} measures:') + for sp_name in ('hash_eval', 'db_create', ): + print(f'{sp_name=}:') + for p in [v for k,v in times_map.items() if k[0] == sp_name]: + print(p) + + act.expected_stdout = expected_stdout + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8221_test.py b/tests/bugs/gh_8221_test.py new file mode 100644 index 00000000..b97e724d --- /dev/null +++ b/tests/bugs/gh_8221_test.py @@ -0,0 +1,38 @@ +#coding:utf-8 + +""" +ID: issue-8221 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8221 +TITLE: Crash when MAKE_DBKEY() is called with 0 or 1 arguments +DESCRIPTION: +NOTES: + [20.08.2024] pzotov + Confirmed crash on 6.0.0.438-d40d01b (dob: 20.08.2024 04:44). + Checked on 6.0.0.438-d9f9b28, 5.0.2.1479-adfe97a, 4.0.6.3142-984ccb9 +""" +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set bail OFF; + select 1 from rdb$database where rdb$db_key = make_dbkey(); + select 1 from rdb$database where rdb$db_key = make_dbkey('RDB$DATABASE'); +""" + +act = isql_act('db', test_script, substitutions=[ ('[ \\t]+', ' ') ]) + +@pytest.mark.version('>=4.0.6') +def test_1(act: Action): + + expected_stdout = f""" + Statement failed, SQLSTATE = 39000 + function MAKE_DBKEY could not be matched + + Statement failed, SQLSTATE = 39000 + function MAKE_DBKEY could not be matched + """ + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8223_test.py b/tests/bugs/gh_8223_test.py new file mode 100644 index 00000000..b22b7a8f --- /dev/null +++ b/tests/bugs/gh_8223_test.py @@ -0,0 +1,152 @@ +#coding:utf-8 + +""" +ID: issue-8223 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8223 +TITLE: SubQueryConversion = true: error "no current record for fetch operation" with complex joins +DESCRIPTION: +NOTES: + [27.08.2024] pzotov + 1. Confirmed bug on 5.0.1.1469-1d792e4 (Release (15.08.2024), got for SubQueryConversion=true: + no current record for fetch operation / gdscode = 335544348. + + 2. Parameter 'SubQueryConversion' currently presents only in FB 5.x and _NOT_ in FB 6.x. + Because of that, testing version are limited only for 5.0.2. FB 6.x currently is NOT tested. + 3. Custom driver config objects are created here, one with SubQueryConversion = true and second with false. + + [18.01.2025] pzotov + Resultset of cursor that executes using instance of selectable PreparedStatement must be stored + in some variable in order to have ability close it EXPLICITLY (before PS will be freed). + Otherwise access violation raises during Python GC and pytest hangs at final point (does not return control to OS). + This occurs at least for: Python 3.11.2 / pytest: 7.4.4 / firebird.driver: 1.10.6 / Firebird.Qa: 0.19.3 + The reason of that was explained by Vlad, 26.10.24 17:42 ("oddities when use instances of selective statements"). + + Checked on 5.0.2.1483-0bf2de0 -- all ok. + Thanks to dimitr for the advice on implementing the test. + + [16.04.2025] pzotov + Re-implemented in order to check FB 5.x with set 'SubQueryConversion = true' and FB 6.x w/o any changes in its config. + Checked on 6.0.0.687-730aa8f, 5.0.3.1647-8993a57 +""" + +import pytest +from firebird.qa import * +from firebird.driver import driver_config, connect, DatabaseError + +init_script = """ + create table t1(id int); + create table t2(id int); + create table t3(id int); + create table t4(id int); + create table t5(id int); + + insert into t1(id) values(1); + insert into t2(id) values(1); + insert into t3(id) values(1); + insert into t4(id) values(1); + insert into t5(id) values(1); + commit; + + create view v as + select a.id as a_id, b.id as b_id, c.id as c_id + from t1 a + left join t2 b on a.id = b.id + left join t3 c on b.id = c.id; + commit; +""" + +db = db_factory(init=init_script) + +# Substitusions are needed here in order to ignore concrete numbers in explained plan parts, e.g.: +# Hash Join (semi) (keys: 1, total key length: 4) +# Sort (record length: 28, key length: 8) +# Record Buffer (record length: 25) +substitutions = [ + (r'Hash Join \(semi\) \(keys: \d+, total key length: \d+\)','Hash Join (semi)') + ,(r'Hash Join \(inner\) \(keys: \d+, total key length: \d+\)','Hash Join (inner)') + ,(r'record length: \d+', 'record length: NN') + ,(r'key length: \d+', 'key length: NN') +] + +act = python_act('db', substitutions = substitutions) + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- + +@pytest.mark.version('>=5.0.2') +def test_1(act: Action, capsys): + + test_sql = """ + select v.a_id, v.b_id, v.c_id + from v + join t4 d on v.c_id = d.id + where exists ( + select 1 + from t5 e where e.id = d.id + ) + """ + + srv_cfg = driver_config.register_server(name = f'srv_cfg_8223', config = '') + db_cfg_name = f'db_cfg_8223' + db_cfg_object = driver_config.register_database(name = db_cfg_name) + db_cfg_object.server.value = srv_cfg.name + db_cfg_object.database.value = str(act.db.db_path) + if act.is_version('<6'): + db_cfg_object.config.value = f""" + SubQueryConversion = true + """ + + with connect(db_cfg_name, user = act.db.user, password = act.db.password) as con: + ps, rs = None, None + try: + cur = con.cursor() + ps = cur.prepare(test_sql) + + # Print explained plan with padding eash line by dots in order to see indentations: + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + rs = cur.execute(ps) + for r in rs: + print(r[0], r[1], r[2]) + except DatabaseError as e: + print(e.__str__()) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() + + + act.expected_stdout = f""" + Select Expression + ....-> Filter + ........-> Hash Join (semi) + ............-> Filter + ................-> Hash Join (inner) + ....................-> Nested Loop Join (outer) + ........................-> Nested Loop Join (outer) + ............................-> Table "T1" as "V A" Full Scan + ............................-> Filter + ................................-> Table "T2" as "V B" Full Scan + ........................-> Filter + ............................-> Table "T3" as "V C" Full Scan + ....................-> Record Buffer (record length: 25) + ........................-> Table "T4" as "D" Full Scan + ............-> Record Buffer (record length: 25) + ................-> Table "T5" as "E" Full Scan + 1 1 1 + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8225_test.py b/tests/bugs/gh_8225_test.py new file mode 100644 index 00000000..a0074886 --- /dev/null +++ b/tests/bugs/gh_8225_test.py @@ -0,0 +1,152 @@ +#coding:utf-8 + +""" +ID: issue-8225 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8225 +TITLE: Problematic queries when SubQueryConversion = true +DESCRIPTION: +NOTES: + [03.09.2024] pzotov + Parameter 'SubQueryConversion' currently presents only in FB 5.x and _NOT_ in FB 6.x. + Because of that, testing version are limited only for 5.0.2. FB 6.x currently is NOT tested. + + [18.01.2025] pzotov + Resultset of cursor that executes using instance of selectable PreparedStatement must be stored + in some variable in order to have ability close it EXPLICITLY (before PS will be freed). + Otherwise access violation raises during Python GC and pytest hangs at final point (does not return control to OS). + This occurs at least for: Python 3.11.2 / pytest: 7.4.4 / firebird.driver: 1.10.6 / Firebird.Qa: 0.19.3 + The reason of that was explained by Vlad, 26.10.24 17:42 ("oddities when use instances of selective statements"). + + Confirmed bug on 5.0.2.1479-adfe97a. + Checked on 5.0.2.1482-604555f. + + [16.04.2025] pzotov + Re-implemented in order to check FB 5.x with set 'SubQueryConversion = true' and FB 6.x w/o any changes in its config. + Checked on 6.0.0.687-730aa8f, 5.0.3.1647-8993a57 +""" + +import pytest +from firebird.qa import * +from firebird.driver import driver_config, connect, DatabaseError + +init_script = """ + create domain dm_emp_id smallint; + create domain dm_dep_id smallint; + create domain dm_name varchar(20); + + create table department ( + dept_no dm_dep_id not null + ,dept_name dm_name not null + ); + + create table employee ( + emp_no dm_emp_id not null + ,last_name dm_name not null + ,dept_no dm_dep_id not null + ,constraint emp_key primary key (emp_no) + ); + commit; + insert into department( dept_no, dept_name) values (1, 'd1'); + insert into department( dept_no, dept_name) values (2, 'd2'); + insert into department( dept_no, dept_name) values (3, 'd3'); + insert into employee( emp_no, last_name, dept_no) values (1, 'e1', 1); + insert into employee( emp_no, last_name, dept_no) values (2, 'e2', 2); + insert into employee( emp_no, last_name, dept_no) values (3, 'e3', 3); + insert into employee( emp_no, last_name, dept_no) values (4, 'e4', 1); + insert into employee( emp_no, last_name, dept_no) values (5, 'e5', 1); + insert into employee( emp_no, last_name, dept_no) values (6, 'e6', 1); + insert into employee( emp_no, last_name, dept_no) values (7, 'e7', 2); + insert into employee( emp_no, last_name, dept_no) values (8, 'e8', 3); + insert into employee( emp_no, last_name, dept_no) values (9, 'e9', 3); + commit; + + update department d set dept_no = -dept_no where exists(select * from employee e where e.dept_no = d.dept_no) rows 1; + insert into employee( emp_no, last_name, dept_no) values (12, 'e12', -(select max(dept_no)+1 from department) ); + commit; +""" + +db = db_factory(init=init_script) + + +# Substitusions are needed here in order to ignore concrete numbers in explained plan parts, e.g.: +# Hash Join (semi) (keys: 1, total key length: 4) +# Sort (record length: 28, key length: 8) +# Record Buffer (record length: 25) +substitutions = [ + (r'Hash Join \(semi\) \(keys: \d+, total key length: \d+\)','Hash Join (semi)') + ,(r'record length: \d+', 'record length: NN') + ,(r'key length: \d+', 'key length: NN') +] + +act = python_act('db', substitutions = substitutions) + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- + +@pytest.mark.version('>=5.0.2') +def test_1(act: Action, capsys): + + test_sql = """ + select d.dept_no, d.dept_name from department d + where exists(select * from employee e where e.dept_no = d.dept_no) + order by dept_no rows 1 + """ + + srv_cfg = driver_config.register_server(name = f'srv_cfg_8225', config = '') + db_cfg_name = f'db_cfg_8225' + db_cfg_object = driver_config.register_database(name = db_cfg_name) + db_cfg_object.server.value = srv_cfg.name + db_cfg_object.database.value = str(act.db.db_path) + if act.is_version('<6'): + db_cfg_object.config.value = f""" + SubQueryConversion = true + """ + + with connect(db_cfg_name, user = act.db.user, password = act.db.password) as con: + cur = con.cursor() + ps, rs = None, None + try: + ps = cur.prepare(test_sql) + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + rs = cur.execute(ps) + for r in rs: + print(r[0],r[1]) + + except DatabaseError as e: + print(e.__str__()) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() + + con.rollback() + + act.expected_stdout = f""" + Select Expression + ....-> First N Records + ........-> Filter + ............-> Hash Join (semi) + ................-> Refetch + ....................-> Sort (record length: 28, key length: 8) + ........................-> Table "DEPARTMENT" as "D" Full Scan + ................-> Record Buffer (record length: 25) + ....................-> Table "EMPLOYEE" as "E" Full Scan + 2 d2 + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + diff --git a/tests/bugs/gh_8230_test.py b/tests/bugs/gh_8230_test.py new file mode 100644 index 00000000..20901eef --- /dev/null +++ b/tests/bugs/gh_8230_test.py @@ -0,0 +1,36 @@ +#coding:utf-8 + +""" +ID: issue-8230 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8230 +TITLE: Ability to obtain PID of server process for current connection without querying mon$ tables +DESCRIPTION: + Test verifies ability to call appropriate rdb$get_context() and compare its value with + mon$attachments.mon$server_pid. They must be equal (and no error must raise). +NOTES: + [29.10.2024] pzotov + Checked on 6.0.0.511-781e5d9 (intermediate build). +""" +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + select cast(rdb$get_context('SYSTEM', 'SERVER_PID') as int) - a.mon$server_pid as result + from mon$attachments a + where a.mon$attachment_id = current_connection; +""" + +act = isql_act('db', test_script, substitutions=[ ('[ \\t]+', ' ') ]) + +@pytest.mark.version('>=6.0') +def test_1(act: Action): + + expected_stdout = f""" + RESULT 0 + """ + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8231_test.py b/tests/bugs/gh_8231_test.py new file mode 100644 index 00000000..415c8c16 --- /dev/null +++ b/tests/bugs/gh_8231_test.py @@ -0,0 +1,112 @@ +#coding:utf-8 + +""" +ID: issue-8231 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8231 +TITLE: SubQueryConversion = true causes "request size limit exceeded" / "... unavailable resource. Unable to allocate memory ..." +DESCRIPTION: +NOTES: + [26.08.2024] pzotov + Two tables must be joined by columns which has different charset or collates. + Confirmed bug on 5.0.2.1484-3cdfd38 (25.08.2024), got: + Statement failed, SQLSTATE = HY000 + request size limit exceeded + Checked on 5.0.2.1485-274af35 -- all ok. + + [18.01.2025] pzotov + Resultset of cursor that executes using instance of selectable PreparedStatement must be stored + in some variable in order to have ability close it EXPLICITLY (before PS will be freed). + Otherwise access violation raises during Python GC and pytest hangs at final point (does not return control to OS). + This occurs at least for: Python 3.11.2 / pytest: 7.4.4 / firebird.driver: 1.10.6 / Firebird.Qa: 0.19.3 + The reason of that was explained by Vlad, 26.10.24 17:42 ("oddities when use instances of selective statements"). + + Thanks to dimitr for the advice on implementing the test. + + [16.04.2025] pzotov + Re-implemented in order to check FB 5.x with set 'SubQueryConversion = true' and FB 6.x w/o any changes in its config. + Checked on 6.0.0.687-730aa8f, 5.0.3.1647-8993a57 +""" + +import pytest +from firebird.qa import * +from firebird.driver import driver_config, connect, DatabaseError + +init_script = """ + create table t1(fld varchar(10) character set win1252); + create table t2(fld varchar(10) character set utf8); + + insert into t1(fld) values('Ð'); + insert into t2(fld) values('Ð'); + commit; +""" + +db = db_factory(init=init_script) + +act = python_act('db') + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- + +@pytest.mark.version('>=5.0.2') +def test_1(act: Action, capsys): + + test_sql = """ + select 1 as x + from t1 + where exists (select 1 from t2 where t1.fld = t2.fld) + """ + + srv_cfg = driver_config.register_server(name = f'srv_cfg_8231', config = '') + db_cfg_name = f'db_cfg_8231' + db_cfg_object = driver_config.register_database(name = db_cfg_name) + db_cfg_object.server.value = srv_cfg.name + db_cfg_object.database.value = str(act.db.db_path) + if act.is_version('<6'): + db_cfg_object.config.value = f""" + SubQueryConversion = true + """ + + with connect(db_cfg_name, user = act.db.user, password = act.db.password) as con: + cur = con.cursor() + ps, rs = None, None + try: + ps = cur.prepare(test_sql) + + # Print explained plan with padding eash line by dots in order to see indentations: + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + rs = cur.execute(ps) + for r in rs: + print(r[0]) + except DatabaseError as e: + print(e.__str__()) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() + + con.rollback() + + act.expected_stdout = f""" + Select Expression + ....-> Nested Loop Join (semi) + ........-> Table "T1" Full Scan + ........-> Filter + ............-> Table "T2" Full Scan + 1 + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8233_test.py b/tests/bugs/gh_8233_test.py new file mode 100644 index 00000000..696227f7 --- /dev/null +++ b/tests/bugs/gh_8233_test.py @@ -0,0 +1,134 @@ +#coding:utf-8 + +""" +ID: issue-8233 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8233 +TITLE: SubQueryConversion = true - multiple rows in singleton select +DESCRIPTION: +NOTES: + [27.08.2024] pzotov + 1. Confirmed bug on 5.0.1.1485-274af35 (26.08.2024), got for SubQueryConversion=true: + "multiple rows in singleton select", gdscodes: (335544652, 335544842) + 2. Parameter 'SubQueryConversion' currently presents only in FB 5.x and _NOT_ in FB 6.x. + Because of that, testing version are limited only for 5.0.2. FB 6.x currently is NOT tested. + 3. Table 't1' must have more than one row for bug reproducing. Query must be enclosed in execute block. + 4. Custom driver config objects are created here, one with SubQueryConversion = true and second with false. + + [18.01.2025] pzotov + Resultset of cursor that executes using instance of selectable PreparedStatement must be stored + in some variable in order to have ability close it EXPLICITLY (before PS will be freed). + Otherwise access violation raises during Python GC and pytest hangs at final point (does not return control to OS). + This occurs at least for: Python 3.11.2 / pytest: 7.4.4 / firebird.driver: 1.10.6 / Firebird.Qa: 0.19.3 + The reason of that was explained by Vlad, 26.10.24 17:42 ("oddities when use instances of selective statements"). + + Checked on 5.0.2.1487-6934878 -- all ok. + Thanks to dimitr for the advice on implementing the test. + + [16.04.2025] pzotov + Re-implemented in order to check FB 5.x with set 'SubQueryConversion = true' and FB 6.x w/o any changes in its config. + Checked on 6.0.0.687-730aa8f, 5.0.3.1647-8993a57 +""" + +import pytest +from firebird.qa import * +from firebird.driver import driver_config, connect, DatabaseError + +init_script = """ +create table t1(id int, fld int); +create table t2(id int, fld int); + +insert into t1(id, fld) values(1, 111); +insert into t1(id, fld) values(2, 222); +insert into t1(id, fld) values(3, 333); +insert into t2(id, fld) values(3, 999); +commit; +""" + +db = db_factory(init=init_script) + +# Substitusions are needed here in order to ignore concrete numbers in explained plan parts, e.g.: +# Hash Join (semi) (keys: 1, total key length: 4) +# Sort (record length: 28, key length: 8) +# Record Buffer (record length: 25) +substitutions = [ + (r'Hash Join \(semi\) \(keys: \d+, total key length: \d+\)','Hash Join (semi)') + ,(r'record length: \d+', 'record length: NN') + ,(r'key length: \d+', 'key length: NN') +] + +act = python_act('db', substitutions = substitutions) + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- + +@pytest.mark.version('>=5.0.2') +def test_1(act: Action, capsys): + + test_sql = """ + execute block returns (res int) + as + begin + select first 1 id from t1 + where exists (select 1 from t2 where t1.id = t2.id) + order by t1.id + into :res; + suspend; + end + """ + + srv_cfg = driver_config.register_server(name = f'srv_cfg_8233', config = '') + db_cfg_name = f'db_cfg_8233' + db_cfg_object = driver_config.register_database(name = db_cfg_name) + db_cfg_object.server.value = srv_cfg.name + db_cfg_object.database.value = str(act.db.db_path) + if act.is_version('<6'): + db_cfg_object.config.value = f""" + SubQueryConversion = true + """ + + with connect(db_cfg_name, user = act.db.user, password = act.db.password) as con: + ps, rs = None, None + try: + cur = con.cursor() + ps = cur.prepare(test_sql) + + # Print explained plan with padding eash line by dots in order to see indentations: + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + rs = cur.execute(ps) + for r in rs: + print(r[0]) + except DatabaseError as e: + print(e.__str__()) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() + + act.expected_stdout = f""" + Select Expression (line 5, column 12) + ....-> Singularity Check + ........-> First N Records + ............-> Filter + ................-> Hash Join (semi) + ....................-> Sort (record length: 28, key length: 8) + ........................-> Table "T1" Full Scan + ....................-> Record Buffer (record length: 25) + ........................-> Table "T2" Full Scan + 3 + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8241_test.py b/tests/bugs/gh_8241_test.py new file mode 100644 index 00000000..8fcd4b5b --- /dev/null +++ b/tests/bugs/gh_8241_test.py @@ -0,0 +1,54 @@ +#coding:utf-8 +""" +ID: issue-8241 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8241 +TITLE: gbak may lose NULLs in restore +DESCRIPTION: +NOTES: + Restore must use gbak utility, target DB must be prefixed by 'localhost:'. + + Confirmed bug on 6.0.0.447, 5.0.2.1487. + Checked on 6.0.0.450-8591572, 5.0.2.1493-eb720e8. +""" + +import pytest +from firebird.qa import * +from pathlib import Path +import time + +init_sql = """ + create table t ("TABLE" integer); + insert into t values (null); + insert into t values (null); + commit; +""" +db = db_factory(init = init_sql) +act = python_act('db') + +tmp_fbk = temp_file('tmp_8241.fbk') +tmp_res = temp_file('tmp_8241.fdb') + +@pytest.mark.version('>=5.0.2') +def test_1(act: Action, tmp_fbk: Path, tmp_res: Path, capsys): + + with act.connect_server() as srv: + srv.database.set_sql_dialect(database=act.db.db_path, dialect=1) + + act.gbak(switches=['-b', act.db.db_path, str(tmp_fbk)]) + act.gbak(switches=['-rep', str(tmp_fbk), f'localhost:{act.db.db_path}']) + + # NOTE! THIS PREVENTS FROM REPRODUCING BUG: + # DO NOT USE >>> act.gbak(switches=['-se', 'localhost:service_mgr', '-rep', str(tmp_fbk), act.db.db_path]) + + with act.db.connect() as con: + cur = con.cursor() + cur.execute('select "TABLE" from t') + for r in cur: + print(r[0]) + + act.expected_stdout = f""" + None + None + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8249_test.py b/tests/bugs/gh_8249_test.py new file mode 100644 index 00000000..30009f1d --- /dev/null +++ b/tests/bugs/gh_8249_test.py @@ -0,0 +1,127 @@ +#coding:utf-8 + +""" +ID: issue-8249 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8249 +TITLE: CAST() ignores collation of target data type +NOTES: + [22.10.2024] pzotov + Commit related to this test (04.10.2024 13:13): + https://github.com/FirebirdSQL/firebird/commit/aa167e2b36122684796d7b34935b0340be6f5074 + See also: gh_7748_test.py + + Confirmed problem on 6.0.0.483: queries to view, function, SP and EB complete OK (rather than expectedly raise error). + Checked on 6.0.0.485 -- all OK. No output to STDOUT, all queries finish with errors. +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + create view v_test_1 as + select cast('x' as varchar(10) character set utf8 collate missed_coll) as view_output from rdb$database + ; + commit; + + set term ^; + create procedure sp_test_1 returns(sp_output varchar(10) character set utf8) as + begin + sp_output = cast('x' as varchar(10) character set utf8 collate missed_coll); + suspend; + end + ^ + create procedure sp_test_2 returns(sp_output varchar(10) character set utf8) as + declare v_text varchar(10) character set utf8; + begin + v_text = cast('x' as varchar(10) character set utf8 collate missed_coll); + sp_output = v_text; + suspend; + end + ^ + create function fn_test_1 returns varchar(10) character set utf8 as + begin + return cast('x' as varchar(10) character set utf8 collate missed_coll); + end + ^ + set term ;^ + commit; + + select * from v_test_1; + select fn_test_1() as fn_output from rdb$database; + select * from sp_test_1; + + set term ^; + execute block returns(eb_text varchar(10) character set utf8) as + begin + execute procedure sp_test_2 returning_values :eb_text; + suspend; + end + ^ + set term ;^ + +""" + +act = isql_act('db', test_script, substitutions = [('[-]?At line \\d+.*', '')]) + +expected_stdout = """ + Statement failed, SQLSTATE = 22021 + unsuccessful metadata update + -CREATE VIEW V_TEST_1 failed + -Dynamic SQL Error + -SQL error code = -204 + -COLLATION MISSED_COLL for CHARACTER SET UTF8 is not defined + + Statement failed, SQLSTATE = 22021 + unsuccessful metadata update + -CREATE PROCEDURE SP_TEST_1 failed + -Dynamic SQL Error + -SQL error code = -204 + -COLLATION MISSED_COLL for CHARACTER SET UTF8 is not defined + + Statement failed, SQLSTATE = 22021 + unsuccessful metadata update + -CREATE PROCEDURE SP_TEST_2 failed + -Dynamic SQL Error + -SQL error code = -204 + -COLLATION MISSED_COLL for CHARACTER SET UTF8 is not defined + + Statement failed, SQLSTATE = 22021 + unsuccessful metadata update + -CREATE FUNCTION FN_TEST_1 failed + -Dynamic SQL Error + -SQL error code = -204 + -COLLATION MISSED_COLL for CHARACTER SET UTF8 is not defined + + Statement failed, SQLSTATE = 42S02 + Dynamic SQL Error + -SQL error code = -204 + -Table unknown + -V_TEST_1 + + Statement failed, SQLSTATE = 39000 + Dynamic SQL Error + -SQL error code = -804 + -Function unknown + -FN_TEST_1 + + Statement failed, SQLSTATE = 42S02 + Dynamic SQL Error + -SQL error code = -204 + -Table unknown + -SP_TEST_1 + + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -204 + -Procedure unknown + -SP_TEST_2 +""" + +@pytest.mark.version('>=6.0') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8250_test.py b/tests/bugs/gh_8250_test.py new file mode 100644 index 00000000..121a3e6d --- /dev/null +++ b/tests/bugs/gh_8250_test.py @@ -0,0 +1,132 @@ +#coding:utf-8 + +""" +ID: 8250 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8250 +TITLE: Bad performance on simple two joins query on tables with composed index +DESCRIPTION: +NOTES: + [25.09.2024] pzotov + 0. Tables in explained plans are specified in the same order for snapshots before and after fix. + Difference can be seen only in added "keys: " clause. + This clause currently exists only in FB 6.x, commit: + https://github.com/FirebirdSQL/firebird/commit/c50b0aa652014ce3610a1890017c9dd436388c43 + Because of this, test min_version = 6.0. + 1. Improvement can be checked by parsing of explained plan if its top-level hash join looks like + "Hash Join (inner) (keys: N, total key length: ...)" and value of is NOT LESS THAN some + minimal threshold defined by variable MIN_REQUIRED_KEYS_IN_TOP_LEVEL_HJ. + As explained by dimitr, for this test such value must be 5 (letter 24.09.2024 07:22). + 2. Tables must have indices (defined by PK and FK constraints) for explained plan to show 'keys: N' + with N > 1. Otherwise top-level HJ line will be 'keys: 1' and query will run as slow as it was + specified in the ticket. The reason is that in case of missed indices optimizer uses other criteria + for estimation of cost and plan will remain ineffective (at least for current FB 6.x). + Explained by dimitr, letter 24.09.2024 12:29. + + Confirmed problem on 6.0.0.461. + Checked on 6.0.0.467. +""" + +import re +import time + +import pytest +from firebird.qa import * + +MIN_REQUIRED_KEYS_IN_TOP_LEVEL_HJ = 5 +ROWS_LIMIT_FOR_CHILD_TABLES = 5000 + +init_sql = f""" + create table test1(id1 int not null, id2 int not null, name varchar(30) not null); + create table test2(id1 int not null, id2 int not null, code30 varchar(36) not null, descr varchar(36)); + create table test3(id1 int not null, id2 int not null, code30 varchar(36) not null, code15 varchar(15) not null, price double precision); + + set term ^; + execute block as + declare n_cnt int = {ROWS_LIMIT_FOR_CHILD_TABLES}; + declare i int; + declare v_code30 type of column test2.code30; + begin + insert into test1(id1, id2, name) select 1, i, left(uuid_to_char(gen_uuid()), 30) from (select row_number()over()-1 i from rdb$types rows 16); + i = 0; + while (i < n_cnt) do + begin + v_code30 = lpad('', 36, uuid_to_char(gen_uuid())); + insert into test2(id1, id2, code30, descr) values( 1, mod(:i, 4), :v_code30, :v_code30); + insert into test3(id1, id2, code30, code15, price) values( 1, mod(:i, 4) , :v_code30, left(uuid_to_char(gen_uuid()), 15), round(1000)); + i = i + 1; + end + + end + ^ + set term ;^ + commit; + set echo on; + alter table test1 add constraint t1_pk primary key(id1,id2); + alter table test2 add constraint t2_pk primary key(id1,id2,code30); + alter table test3 add constraint t3_pk primary key(id1,id2,code30,code15); +""" +db = db_factory(init = init_sql, page_size = 8192) + +substitutions = [ ('[ \t]+', ' ') + ,('keys: \\d+, total key length: \\d+', 'keys, total key length') + ,('record length: \\d+', 'record length') + ] + +act = python_act('db', substitutions = substitutions) + + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- + +@pytest.mark.version('>=6.0') +def test_1(act: Action, capsys): + + test_sql = """ + select 1 x + from test2 t2 + join test3 t3 on t3.id1 = t2.id1 and t3.id2 = t2.id2 and t3.code30 = t2.code30 + join test1 t1 on t1.id1 = t2.id1 and t1.id2 = t2.id2 + """ + + # 'Hash Join (inner) (keys: 3, total key length: 38)' + p_hj_keys = re.compile(r'Hash Join \(inner\) \(keys: \d+', re.IGNORECASE) + top_level_keys_found = -1 + with act.db.connect() as con: + cur = con.cursor() + ps = cur.prepare(test_sql) + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + for s in ps.detailed_plan.split('\n'): + if (pm := p_hj_keys.search(s)): + top_level_keys_found = max(top_level_keys_found, int(pm.group().split()[-1])) + break + + max_keys_msg = 'Top-level hash join keys: ' + expected_keys = '' + if top_level_keys_found >= MIN_REQUIRED_KEYS_IN_TOP_LEVEL_HJ: + max_keys_msg += 'EXPECTED' + expected_keys = max_keys_msg + else: + max_keys_msg += f'UNEXPECTED, {" too low: "+str(top_level_keys_found)+" - less than "+str(MIN_REQUIRED_KEYS_IN_TOP_LEVEL_HJ) if top_level_keys_found > 0 else "NOT FOUND"}' + + print(max_keys_msg) + + act.expected_stdout = f""" + Select Expression + ....-> Filter + ........-> Hash Join (inner) (keys, total key length) + ............-> Hash Join (inner) (keys, total key length) + ................-> Table "TEST3" as "T3" Full Scan + ................-> Record Buffer (record length) + ....................-> Table "TEST1" as "T1" Full Scan + ............-> Record Buffer (record length) + ................-> Table "TEST2" as "T2" Full Scan + {expected_keys} + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() diff --git a/tests/bugs/gh_8252_test.py b/tests/bugs/gh_8252_test.py new file mode 100644 index 00000000..c0748d4e --- /dev/null +++ b/tests/bugs/gh_8252_test.py @@ -0,0 +1,111 @@ +#coding:utf-8 + +""" +ID: issue-8252 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8252 +TITLE: Incorrect subquery unnesting with complex dependencies (SubQueryConversion = true) +DESCRIPTION: +NOTES: + [14.09.2024] pzotov + 1. Parameter 'SubQueryConversion' currently presents only in FB 5.x and _NOT_ in FB 6.x. + Because of that, testing version are limited only for 5.0.2. FB 6.x currently is NOT tested. + 2. Custom driver config object is created here for using 'SubQueryConversion = true'. + 3. Additional test was made for this issue: tests/functional/tabloid/test_aae2ae32.py + + [18.01.2025] pzotov + Resultset of cursor that executes using instance of selectable PreparedStatement must be stored + in some variable in order to have ability close it EXPLICITLY (before PS will be freed). + Otherwise access violation raises during Python GC and pytest hangs at final point (does not return control to OS). + This occurs at least for: Python 3.11.2 / pytest: 7.4.4 / firebird.driver: 1.10.6 / Firebird.Qa: 0.19.3 + The reason of that was explained by Vlad, 26.10.24 17:42 ("oddities when use instances of selective statements"). + + Confirmed bug on 5.0.2.1497. + Checked on 5.0.2.1499-5fa4ae6. + + [16.04.2025] pzotov + Re-implemented in order to check FB 5.x with set 'SubQueryConversion = true' and FB 6.x w/o any changes in its config. + Checked on 6.0.0.687-730aa8f, 5.0.3.1647-8993a57 +""" + +import pytest +from firebird.qa import * +from firebird.driver import driver_config, connect, NetProtocol + +db = db_factory() + +act = python_act('db', substitutions = [ ('[ \t]+',' ') ]) + +test_sql = """ + select /* TRACE_ME */ first 5 1 x + from sales s + where exists ( + select 1 from customer c + where + s.cust_no = c.cust_no + and ( s.cust_no = c.cust_no + or + s.cust_no = c.cust_no + ) + ); +""" + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- + +@pytest.mark.version('>=5.0.2') +def test_1(act: Action, capsys): + + srv_cfg = driver_config.register_server(name = 'test_srv_gh_8252', config = '') + db_cfg_name = f'db_cfg_8252' + db_cfg_object = driver_config.register_database(name = db_cfg_name) + db_cfg_object.server.value = srv_cfg.name + db_cfg_object.database.value = 'employee' + if act.is_version('<6'): + db_cfg_object.config.value = f""" + SubQueryConversion = true + """ + + with connect(db_cfg_name, user = act.db.user, password = act.db.password) as con: + cur = con.cursor() + ps = cur.prepare(test_sql) + # Show explained plan: + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + rs = cur.execute(ps) + for r in rs: + print(r[0]) + + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + ps.free() + + con.rollback() + + act.expected_stdout = """ + Sub-query + ....-> Filter + ........-> Table "CUSTOMER" as "C" Access By ID + ............-> Bitmap + ................-> Index "RDB$PRIMARY22" Unique Scan + Select Expression + ....-> First N Records + ........-> Filter + ............-> Table "SALES" as "S" Full Scan + 1 + 1 + 1 + 1 + 1 + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8253_test.py b/tests/bugs/gh_8253_test.py new file mode 100644 index 00000000..c184c34f --- /dev/null +++ b/tests/bugs/gh_8253_test.py @@ -0,0 +1,112 @@ +#coding:utf-8 + +""" +ID: issue-8253 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8253 +TITLE: Incorrect handling of non-ASCII object names in CREATE MAPPING statement +DESCRIPTION: + Test uses pre-created databases.conf which has alias (see variable REQUIRED_ALIAS) and SecurityDatabase in its details + which points to that alias, thus making such database be self-security. + Database file for that alias must NOT exist in the QA_root/files/qa/ subdirectory: it will be created here. + Self-security database allows us to create GLOBAL mapping without worrying about how it will be removed on test finish. +NOTES: + [23.09.2024] pzotov + 1. One need to be sure that firebird.conf does NOT contain DatabaseAccess = None. + 2. Value of REQUIRED_ALIAS must be EXACTLY the same as alias specified in the pre-created databases.conf + (for LINUX this equality is case-sensitive, even when aliases are compared!) + 3. Content of databases.conf must be taken from $QA_ROOT/files/qa-databases.conf (one need to replace it before every test session). + Discussed with pcisar, letters since 30-may-2022 13:48, subject: + "new qa, core_4964_test.py: strange outcome when use... shutil.copy() // comparing to shutil.copy2()" + 4. Confirmed bug on 6.0.0.461, data from rdb$auth_mapping.rdb$map_from: + * is displayed as '???...???' for global mapping; + * is mojibake for local mapping when group name is enclosed in double quotes. + + Checked on 6.0.0.466, 5.0.2.1513, 4.0.6.3156 +""" + +import re +from pathlib import Path + +import pytest +from firebird.qa import * + +REQUIRED_ALIAS = 'tmp_gh_8253_alias' + +db = db_factory() + +act = python_act('db', substitutions=[('[ \t]+', ' ')]) + +tmp_sql = temp_file('tmp_8253_non_ascii_ddl.sql') +tmp_log = temp_file('tmp_8253_non_ascii_ddl.log') + +@pytest.mark.intl +@pytest.mark.version('>=4.0.6') +def test_1(act: Action, tmp_sql: Path, tmp_log: Path, capsys): + + # Scan line-by-line through databases.conf, find line starting with REQUIRED_ALIAS and extract name of file that + # must be created in the $(dir_sampleDb)/qa/ folder. This name will be used further as target database (tmp_fdb). + # NOTE: we have to SKIP lines which are commented out, i.e. if they starts with '#': + p_required_alias_ptn = re.compile( '^(?!#)((^|\\s+)' + REQUIRED_ALIAS + ')\\s*=\\s*\\$\\(dir_sampleDb\\)/qa/', re.IGNORECASE ) + fname_in_dbconf = None + + with open(act.home_dir/'databases.conf', 'r') as f: + for line in f: + if p_required_alias_ptn.search(line): + # If databases.conf contains line like this: + # tmp_8253_alias = $(dir_sampleDb)/qa/tmp_qa_8253.fdb + # - then we extract filename: 'tmp_qa_8253.fdb' (see below): + fname_in_dbconf = Path(line.split('=')[1].strip()).name + break + + # if 'fname_in_dbconf' remains undefined here then propably REQUIRED_ALIAS not equals to specified in the databases.conf! + # + assert fname_in_dbconf + + dba_pswd = 'alterkey' + g_name = 'лондонский симфонический оркестр' + r_name = 'настройщик роялей' + non_ascii_ddl = f''' + set list on; + create database '{REQUIRED_ALIAS}' user {act.db.user}; + create user {act.db.user} password '{dba_pswd}'; + commit; + connect 'localhost:{REQUIRED_ALIAS}' user {act.db.user} password '{dba_pswd}'; + select mon$sec_database from mon$database; -- must be: 'Self' + create role "{r_name}"; + commit; + create mapping "локальная_апостроф" using any plugin from group '{g_name}' to role "{r_name}"; + create mapping "локальная_кавычки" using any plugin from group "{g_name}" to role "{r_name}"; + create global mapping "глобальная_апостроф" using any plugin from group '{g_name}' to role "{r_name}"; + create global mapping "глобальная_кавычки" using any plugin from group "{g_name}" to role "{r_name}"; + commit; + set count on; + select rdb$map_name,rdb$map_from,rdb$map_to from rdb$auth_mapping order by rdb$map_name; + commit; + ''' + + tmp_sql.write_bytes(non_ascii_ddl.encode('cp866')) + act.isql(switches=['-q'], input_file=tmp_sql, credentials = False, connect_db = False, combine_output = True, charset='dos866', io_enc = 'cp866') + tmp_log.write_bytes(act.clean_stdout.encode('utf-8')) + with open(tmp_log, 'r', encoding = 'utf-8', errors = 'backslashreplace') as f: + for line in f: + print(line) + + act.expected_stdout = f""" + MON$SEC_DATABASE Self + RDB$MAP_NAME глобальная_апостроф + RDB$MAP_FROM {g_name} + RDB$MAP_TO {r_name} + RDB$MAP_NAME глобальная_кавычки + RDB$MAP_FROM {g_name} + RDB$MAP_TO {r_name} + RDB$MAP_NAME локальная_апостроф + RDB$MAP_FROM {g_name} + RDB$MAP_TO {r_name} + RDB$MAP_NAME локальная_кавычки + RDB$MAP_FROM {g_name} + RDB$MAP_TO {r_name} + Records affected: 4 + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + diff --git a/tests/bugs/gh_8255_test.py b/tests/bugs/gh_8255_test.py new file mode 100644 index 00000000..835532b7 --- /dev/null +++ b/tests/bugs/gh_8255_test.py @@ -0,0 +1,60 @@ +#coding:utf-8 + +""" +ID: issue-8255 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8255 +TITLE: Catch possible stack overflow when preparing and compiling user statements +DESCRIPTION: + Test generates SQL like 'select 1+1+1....+1 from rdb$database' and tries to execte it. + Exception 'SQLSTATE = HY001 / Stack overflow' must raise instead of crash (that was before fix). + Commits: + * 4.x: https://github.com/FirebirdSQL/firebird/commit/04c586d4ea4bafb50818bcf7f46188afc67ab1c5 (20-sep-2024) + * 5.x: https://github.com/FirebirdSQL/firebird/commit/f0670f90cc7d1fc93db22336fd43abc6d348e31e (18-sep-2024) + * 6.x: https://github.com/FirebirdSQL/firebird/commit/6b445c0dc53f1c5778258bd673c0b61f6dd93a69 (20-sep-2024) +NOTES: + [23.09.2024] pzotov + Initially query contained expression of 15'000 terms ("1+1+1...+1") was used to check. + This query causes 'stack overflow' only in FB 5.x and 6.x. + But in FB 4.0.6.3156 it successfully COMPLETES calculation and issues result. + For FB 4.x this 'threshold' is 16'287 (last number of terms where FB can evaluate result w/o 'stack overflow'). + Because of this, it was decided to increase number of terms to 100'000. + + ::: NB ::: + Fix currently exists only for Windows, see: + https://github.com/FirebirdSQL/firebird/pull/8255#issuecomment-2354781108 + On Linux this query still crashes server. + + Checked on 6.0.0.466, 5.0.2.1513, 4.0.6.3156 +""" + +from pathlib import Path +import platform +import pytest +from firebird.qa import * + +db = db_factory() +act = python_act('db', substitutions=[('[ \t]+', ' '), ('After line \\d+.*', '')]) + +tmp_sql = temp_file('tmp_8255_non_ascii_ddl.sql') + +@pytest.mark.skipif(platform.system() != 'Windows', reason='See ticket note: fix was only for Windows.') +@pytest.mark.version('>=4.0.6') +def test_1(act: Action, tmp_sql: Path, capsys): + + long_expr = '\n'.join( ( + 'select' + ,'+'.join( ('1') * 100000 ) + ,'from rdb$database;' + ) + ) + + tmp_sql.write_bytes(long_expr.encode('utf-8')) + + act.isql(switches=['-q'], input_file=tmp_sql, combine_output = True, charset='win1251') + + act.expected_stdout = f""" + Statement failed, SQLSTATE = HY001 + Stack overflow. The resource requirements of the runtime stack have exceeded the memory available to it. + """ + assert act.clean_stdout == act.clean_expected_stdout + diff --git a/tests/bugs/gh_8263_test.py b/tests/bugs/gh_8263_test.py new file mode 100644 index 00000000..df739190 --- /dev/null +++ b/tests/bugs/gh_8263_test.py @@ -0,0 +1,145 @@ +#coding:utf-8 + +""" +ID: issue-8263 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8263 +TITLE: gbak on Classic with ParallelWorkers > 1 doesn't restore indices, giving a cryptic error message +DESCRIPTION: + Following conditions must be met to reproduce ticket issue: + * firebird.conf contains ParallelWorkers > 1 and MaxParallelWorkers >= ParallelWorkers; + * test database has a table with indexed column and number of POINTER PAGES more than 1. + Test creates table with PK-column (type = int) and wide text column ('data_filler') of len = . + We add records into this table and make backup of this table. + Set ROWS_COUNT = 50'000 and DATA_FILLER_WID = 1'000 leads to allocating 5 pointer pages for test table + (database must be created with page_size = 8192). + + Then we run restore WITHOUT '-par' switch and without verbosing. + Before fix this issued error described in the ticket and index remained inactive. + After fix this restore must complete silently (w/o any output). + Finally, we run query that must use PK index and compare its explained plan with expected which must contain + 'Index "..." Full Scan' line. +NOTES: + [28.09.2024] pzotov + ::: NB ::: + This test forced to change prototypes of firebird.conf for 5.x and 6.x, see in $QA_HOME/firebird-qa/configs/ + files 'fb50_all.conf' and 'fb60_all.conf': they now contain ParallelWorkers > 1. + This change may affect on entire QA run result! Some other tests may need to be adjusted after this. + + Thanks to Vlad for suggestions about this test implementation. + + Confirmed bug on 6.0.0.471, 5.0.2.1516 + Checked on 6.0.0.474, 5.0.2.1519 -- all Ok. +""" +import locale +from pathlib import Path +import time + +import pytest +from firebird.qa import * +from firebird.driver import driver_config, connect, DatabaseError, SrvRestoreFlag + +tmp_fbk = temp_file('tmp_gh_8263.fbk') +tmp_fdb = temp_file('tmp_gh_8263_restored.tmp') + +######################### +### S E T T I N G S ### +######################### +ROWS_COUNT = 50000 +DATA_FILLER_WID = 1000 + +init_script = f""" + create table test( + id int generated by default as identity -- constraint pk_wares primary key using index test_pk + ,data_filler varchar({DATA_FILLER_WID}) + ); + commit; + + set term ^; + execute block as + declare n int = {ROWS_COUNT}; + begin + while (n>0) do + begin + insert into test(data_filler) values( lpad('', {DATA_FILLER_WID}, uuid_to_char(gen_uuid())) ); + n = n - 1; + end + end + ^ + set term ;^ + commit; + create index test_id on test(id); + commit; +""" + +db = db_factory(init = init_script, page_size = 8192) + +act = python_act('db') + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- + +@pytest.mark.version('>=5.0') +def test_1(act: Action, tmp_fbk: Path, tmp_fdb: Path, capsys): + + if act.vars['server-arch'].lower() != 'classic': + pytest.skip('Only Classic was affected.') + + #srv_cfg = driver_config.register_server(name = f'srv_cfg_8263', config = '') + #db_cfg_name = f'db_cfg_8263' + #db_cfg_object = driver_config.register_database(name = db_cfg_name) + #db_cfg_object.server.value = srv_cfg.name + #db_cfg_object.database.value = str(act.db.db_path) + ## db_cfg_object.parallel_workers.value = 3 + + with act.db.connect() as con: # connect(db_cfg_name, user = act.db.user, password = act.db.password) as con: + chk_sql = """ + select + max(iif(rdb$config_name = 'ServerMode', rdb$config_value, null)) as srv_mode + ,cast(max(iif(rdb$config_name = 'ParallelWorkers', rdb$config_value, null)) as int) as par_workers + ,cast(max(iif(rdb$config_name = 'MaxParallelWorkers', rdb$config_value, null)) as int) as max_workers + from rdb$config + """ + cur = con.cursor() + cur.execute(chk_sql) + srv_mode, par_workers, max_workers = cur.fetchone() + assert srv_mode == 'Classic', f'Not applicable ServerMode: {srv_mode}' + assert par_workers > 1, f'ParallelWorkers = {par_workers} must be greater than 1 for this test' + assert max_workers > par_workers, f'MaxParallelWorkers = {maxworkers} must be greater than ParallelWorkers = {par_workers}' + + act.gfix(switches=['-shutdown','single', '-force', '0', act.db.dsn]) + print(act.stdout) # must be empty + + act.gbak(switches=['-b', act.db.dsn, str(tmp_fbk)], combine_output = True, io_enc = locale.getpreferredencoding()) + print(act.stdout) # must be empty + + # BEFORE fix following restore failed with: + # gbak:cannot commit index TEST_ID + # gbak: ERROR:invalid database handle (no active connection) + # gbak: ERROR:Database is not online due to failure to activate one or more indices. + # gbak: ERROR: Run gfix -online to bring database online without active indices. + # + act.gbak(switches=['-rep', str(tmp_fbk), str(tmp_fdb)], combine_output = True, io_enc = locale.getpreferredencoding()) + print(act.stdout) # must be empty! + + ############################################################### + + with connect(str(tmp_fdb), user = act.db.user, password = act.db.password) as con: + chk_sql = 'select 1 from test order by id' + cur = con.cursor() + ps = cur.prepare(chk_sql) + # Print explained plan with padding eash line by dots in order to see indentations: + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + + act.expected_stdout = f""" + Select Expression + ....-> Table "TEST" Access By ID + ........-> Index "TEST_ID" Full Scan + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + \ No newline at end of file diff --git a/tests/bugs/gh_8265_test.py b/tests/bugs/gh_8265_test.py new file mode 100644 index 00000000..7c122234 --- /dev/null +++ b/tests/bugs/gh_8265_test.py @@ -0,0 +1,272 @@ +#coding:utf-8 + +""" +ID: issue-8265 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8265 +TITLE: Nested IN/EXISTS subqueries should not be converted into semi-joins if the outer context is a sub-query which wasn't unnested +DESCRIPTION: +NOTES: + [26.09.2024] pzotov + 0. Commits: + 6.x: + 22.03.2025 10:47 + https://github.com/FirebirdSQL/firebird/commit/fc12c0ef392fec9c83d41bc17da3dc233491498c + (Unnest IN/ANY/EXISTS subqueries and optimize them using semi-join algorithm (#8061)) + 5.x + 31.07.2024 09:46 + https://github.com/FirebirdSQL/firebird/commit/4943b3faece209caa93cc9573803677019582f1c + (Added support for semi/anti and outer joins to hash join algorithm ...) + Also: + 14.09.2024 09:24 + https://github.com/FirebirdSQL/firebird/commit/5fa4ae611d18fd4ce9aac1c8dbc79e5fea2bc1f2 + (Fix bug #8252: Incorrect subquery unnesting with complex dependencies) + 1. Parameter 'SubQueryConversion' currently presents only in FB 5.x and _NOT_ in FB 6.x. + 2. Custom driver config objects are created here, one with SubQueryConversion = true and second with false. + 3. First example of this test is also used in tests/functional/tabloid/test_aae2ae32.py + + Confirmed problem on 5.0.2.1516-fe6ba50 (23.09.2024). + Checked on 5.0.2.1516-92316F0 (25.09.2024). + + [15.01.2025] pzotov + + ### CRITICAL ISSUE ### PROBABLY MUST BE APPLIED TO ALL TESTS WITH SIMILAR BEHAVOUR ### + + Resultset of cursor that executes using instance of selectable PreparedStatement must be stored + in some variable in order to have ability close it EXPLICITLY (before PS will be freed). + Otherwise access violation raises. + This occurs at least for: Python 3.11.2 / pytest: 7.4.4 / firebird.driver: 1.10.6 / Firebird.Qa: 0.19.3 + + Example of broken pytest log when AV occurs: + ========== + tests/bugs/gh_8265_test.py::test_1 Windows fatal exception: access violation + + Current thread 0x0000385c (most recent call first): + Garbage-collecting + File "C:/Python3x/Lib/site-packages/firebird/driver/interfaces.py", line 831 in free + File "C:/Python3x/Lib/site-packages/firebird/driver/core.py", line 2736 in free + ... + File "C:/Python3x/Scripts/pytest.exe/__main__.py", line 7 in + File "", line 88 in _run_code + File "", line 198 in _run_module_as_main + PASSED [1773/2565] + ========== + The reason of that was explained by Vlad, letter 26.10.24 17:42 + (subject: "oddities when use instances of selective statements"): + * line 'cur1.execute(ps1)' creates a new cursor but looses reference on it; + * but this cursor is linked with instance of ps1 which *has* reference on that cursor; + * call 'ps1.free()' delete this anonimous cursor but Python runtime + (or - maybe - code that makes connection cleanup) does not know about it + and tries to delete this anon cursor AGAIN when code finishes 'with' block. + This attempt causes AV. + + [16.04.2025] pzotov + Re-implemented in order to check FB 5.x with set 'SubQueryConversion = true' and FB 6.x w/o any changes in its config. + Checked on 6.0.0.687-730aa8f, 5.0.3.1647-8993a57 +""" + +import pytest +from firebird.qa import * +from firebird.driver import driver_config, connect, DatabaseError + +init_script = """ + create table test1(id int not null); + create table test2(id int not null, pid int not null); + create table test3(id int not null, pid int not null, name varchar(30) not null); + commit; + + insert into test1(id) select row_number()over()-1 from rdb$types rows 10; + insert into test2(id, pid) select row_number()over()-1, mod(row_number()over()-1, 10) from rdb$types rows 100; + insert into test3(id, pid, name) select row_number()over()-1, mod(row_number()over()-1, 100), 'QWEABCRTY' from rdb$types, rdb$types rows 1000; + commit; +""" + +db = db_factory(init=init_script) + +# Hash Join (semi) (keys: 1, total key length: 4) +substitutions = [(r'Hash Join \(semi\) \(keys: \d+, total key length: \d+\)', 'Hash Join (semi)'), (r'record length: \d+', 'record length: NN')] + +act = python_act('db', substitutions = substitutions) + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- + +query_map = { + 1000 : ( + """ + select count(*) from test1 q1_a + where + q1_a.id in ( + select q1_b.pid from test2 q1_b + where + q1_b.id in ( + select q1_c.pid from test3 q1_c + where q1_c.name like '%ABC%' + ) + ) + """ + ,'Both sub-queries can (and should) be unnested.' + ) + ,2000 : ( + """ + select count(*) from test1 q2_a + where + q2_a.id in ( + select q2_b.pid from test2 q2_b + where + 1=1 or q2_b.id in ( + select q2_c.pid from test3 q2_c + where q2_c.name like '%ABC%' + ) + ) + """ + ,'Inner sub-query can NOT be unnested due to `OR` condition present, but the outer sub-query can' + ) + ,3000 : ( + """ + select count(*) from test1 q3_a + where + 1=1 or q3_a.id in ( + select q3_b.pid from test2 q3_b + where q3_b.id in ( + select id from test3 q3_c + where q3_c.name like '%ABC%' + ) + ) + """ + ,'Outer sub-query can NOT be unnested due to `OR` condition present, so the inner sub-query should NOT be unnested too' + ) + ,4000 : ( + """ + select count(*) from test1 q4_a + where + 1=1 or q4_a.id in ( + select id from test2 q4_b + where + 1=1 or q4_b.id in ( + select id from test3 q4_c + where q4_c.name like '%ABC%' + ) + ) + """ + ,'Both sub-queries can NOT be unnested due to OR conditions present' + ) +} + +#----------------------------------------------------------- + +@pytest.mark.version('>=5.0.2') +def test_1(act: Action, capsys): + + srv_cfg = driver_config.register_server(name = f'srv_cfg_8265', config = '') + db_cfg_name = f'db_cfg_8265' + db_cfg_object = driver_config.register_database(name = db_cfg_name) + db_cfg_object.server.value = srv_cfg.name + db_cfg_object.database.value = str(act.db.db_path) + if act.is_version('<6'): + db_cfg_object.config.value = f""" + SubQueryConversion = true + """ + + with connect(db_cfg_name, user = act.db.user, password = act.db.password) as con: + cur = con.cursor() + for q_idx, q_tuple in query_map.items(): + test_sql, qry_comment = q_tuple[:2] + ps,rs = None, None + try: + ps = cur.prepare(test_sql) + print(q_idx) + print(test_sql) + print(qry_comment) + + # Print explained plan with padding eash line by dots in order to see indentations: + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + rs = cur.execute(ps) + # Print data: + for r in rs: + print(r[0]) + except DatabaseError as e: + print(e.__str__()) + print(e.gds_codes) + finally: + # explained by hvlad, 26.10.24 17:42 + if rs: + rs.close() + if ps: + ps.free() + + act.expected_stdout = f""" + 1000 + {query_map[1000][0]} + {query_map[1000][1]} + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Hash Join (semi) (keys: 1, total key length: 4) + ................-> Table "TEST1" as "Q1_A" Full Scan + ................-> Record Buffer (record length: 82) + ....................-> Filter + ........................-> Hash Join (semi) (keys: 1, total key length: 4) + ............................-> Table "TEST2" as "Q1_B" Full Scan + ............................-> Record Buffer (record length: 57) + ................................-> Filter + ....................................-> Table "TEST3" as "Q1_C" Full Scan + 10 + + 2000 + {query_map[2000][0]} + {query_map[2000][1]} + Sub-query + ....-> Filter + ........-> Filter + ............-> Table "TEST3" as "Q2_C" Full Scan + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Hash Join (semi) (keys: 1, total key length: 4) + ................-> Table "TEST1" as "Q2_A" Full Scan + ................-> Record Buffer (record length: 33) + ....................-> Filter + ........................-> Table "TEST2" as "Q2_B" Full Scan + 10 + + 3000 + {query_map[3000][0]} + {query_map[3000][1]} + Sub-query + ....-> Filter + ........-> Filter + ............-> Table "TEST3" as "Q3_C" Full Scan + Sub-query + ....-> Filter + ........-> Filter + ............-> Table "TEST2" as "Q3_B" Full Scan + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Table "TEST1" as "Q3_A" Full Scan + 10 + + 4000 + {query_map[4000][0]} + {query_map[4000][1]} + Sub-query + ....-> Filter + ........-> Filter + ............-> Table "TEST3" as "Q4_C" Full Scan + Sub-query + ....-> Filter + ........-> Filter + ............-> Table "TEST2" as "Q4_B" Full Scan + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Table "TEST1" as "Q4_A" Full Scan + 10 + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8278_test.py b/tests/bugs/gh_8278_test.py new file mode 100644 index 00000000..7cc12bdf --- /dev/null +++ b/tests/bugs/gh_8278_test.py @@ -0,0 +1,98 @@ +#coding:utf-8 + +""" +ID: issue-8278 +ISSUE: 8278 +TITLE: Avoid index lookup for a NULL key if the condition is known to always be FALSE in this case +DESCRIPTION: Test uses ticket example but with reduced number of records that will be inserted into tables. + Number of fetches is compared with MAX_FETCHES_ALERT. After fix trace shows that it is ~5600. +NOTES: + [24.01.2025] pzotov + Commits that fixed problem: + 6.x: https://github.com/FirebirdSQL/firebird/commit/58633c81ea490326d880f42780bb7f293c2a0ae8 + 5.x: https://github.com/FirebirdSQL/firebird/commit/22d23c17d94e390f4ca058afff6ac0338d014225 + + Confirmed problem on 6.0.0.647-9fccb55. + Checked on intermediate snapshots: 6.0.0.652-58633c8; 5.0.3.1622-22d23c1 +""" + +from pathlib import Path + +import pytest +from firebird.qa import * +from firebird.driver import DatabaseError + +T1_COUNT = 30000 +T2_COUNT = 100 +T3_COUNT = 10 +MAX_FETCHES_ALERT = 6000 + +init_sql = f""" + recreate table t1 (t1_id int not null); + recreate table t2 (t2_id int not null, t1_id int); + recreate table t3 (t3_id int not null, t1_id int); + + set term ^; + execute block as + declare l_t1_id int; + declare l_t2_id int; + declare l_t3_id int; + declare n1 int = {T1_COUNT}; + declare n2 int = {T2_COUNT}; + declare n3 int = {T3_COUNT}; + begin + l_t3_id = 1; + while (l_t3_id <= n1) do + begin + l_t1_id = iif(mod(l_t3_id, n2) = 0, trunc(l_t3_id/n2), null); + l_t2_id = iif(mod(l_t3_id, n3) = 0, trunc(l_t3_id/n3), null); + + if (l_t1_id is not null) then + insert into t1 (t1_id) values (:l_t1_id); + + insert into t3 (t3_id, t1_id) values (:l_t3_id, :l_t1_id); + + if (l_t2_id is not null) then + insert into t2 (t2_id, t1_id) values (:l_t2_id, :l_t1_id); + + l_t3_id = l_t3_id + 1; + end + end + ^ + set term ;^ + commit; + + alter table t1 add constraint t1_pk primary key (t1_id); + alter table t2 add constraint t2_pk primary key (t2_id); + alter table t3 add constraint t3_pk primary key (t3_id); + alter table t2 add constraint t2_fk foreign key (t1_id) references t1 (t1_id); + alter table t3 add constraint t3_fk foreign key (t1_id) references t1 (t1_id); + commit; +""" +db = db_factory(init = init_sql) +act = python_act('db') + +@pytest.mark.version('>=5.0.3') +def test_1(act: Action, capsys): + + expected_msg = 'Number of fetches: EXPECTED' + with act.db.connect() as con: + cur = con.cursor() + test_sql = """ + select count(*) /* trace_me */ + from t2 + left outer join t1 on t1.t1_id = t2.t1_id + left outer join t3 on t3.t1_id = t1.t1_id + """ + + fetches_ini = con.info.fetches + cur.execute(test_sql) + cur.fetchall() + sql_fetches = con.info.fetches - fetches_ini + print(expected_msg if sql_fetches <= MAX_FETCHES_ALERT else f'Number of fetches UNEXPECTED: {sql_fetches} - greater than {MAX_FETCHES_ALERT}' ) + + act.expected_stdout = f""" + {expected_msg} + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8290_test.py b/tests/bugs/gh_8290_test.py new file mode 100644 index 00000000..d0efda58 --- /dev/null +++ b/tests/bugs/gh_8290_test.py @@ -0,0 +1,158 @@ +#coding:utf-8 + +""" +ID: issue-8290 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8290 +TITLE: "Unique scan" is incorrectly reported in the explained plan for unique index and IS NULL predicate +DESCRIPTION: + Test creates a table and checks several cases related to issue: asc/desc, computed-by and partial indices. + For each case we ask engine to show explained plan. Every case must have 'Range Scan (full match)'. +NOTES: + [25.10.2024] pzotov + Confirmed problem on 6.0.0.485, 5.0.2.1519. + Checked on 6.0.0.502-d2f4cf6, 5.0.2.1542-ab50e20 (intermediate builds). +""" + +import pytest +from firebird.qa import * + +init_sql = """ + recreate table test(id int generated by default as identity, x int, y int, z int); + insert into test(x, y, z) select null, null, null from rdb$types, rdb$types rows 1000; + commit; + create unique index test_x_asc on test(x); + create unique descending index test_y_desc on test(y); + create unique index test_x_plus_y on test computed by (x+y); + + create unique index test_z_partial on test(z) where mod(id,2) = 0; + create unique index test_x_minus_y_partial on test computed by (x-y) where mod(id,3) <= 1; + commit; +""" +db = db_factory(init = init_sql) + +act = python_act('db') + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- + +@pytest.mark.version('>=5.0.2') +def test_1(act: Action, capsys): + + qry_map = { + 0 : 'select count(*) from test where x is null' + ,1 : 'select count(*) from test where y is null' + ,2 : 'select count(*) from test where x+y is null' + ,3 : 'select count(*) from test where z is null and mod(id,2) = 0' + ,4 : 'select count(*) from test where x-y is null and mod(id,3) <= 1' + ,5 : 'select count(*) from test where x is not distinct from null' + ,6 : 'select count(*) from test where y is not distinct from null' + ,7 : 'select count(*) from test where x+y is not distinct from null' + ,8 : 'select count(*) from test where z is not distinct from null and mod(id,2) = 0' + ,9 : 'select count(*) from test where x-y is not distinct from null and mod(id,3) <= 1' + } + + with act.db.connect() as con: + cur = con.cursor() + for k,v in qry_map.items(): + ps = cur.prepare(v) + # Print explained plan with padding eash line by dots in order to see indentations: + print(v) + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + print('') + + # 26.10.2024. ::: ACHTUNG ::: + # MANDATORY OTHERWISE PYTEST WILL HANG AT FINAL POINT: + ps.free() + + + expected_out = f""" + {qry_map[0]} + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Table "TEST" Access By ID + ................-> Bitmap + ....................-> Index "TEST_X_ASC" Range Scan (full match) + + {qry_map[1]} + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Table "TEST" Access By ID + ................-> Bitmap + ....................-> Index "TEST_Y_DESC" Range Scan (full match) + + {qry_map[2]} + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Table "TEST" Access By ID + ................-> Bitmap + ....................-> Index "TEST_X_PLUS_Y" Range Scan (full match) + + {qry_map[3]} + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Table "TEST" Access By ID + ................-> Bitmap + ....................-> Index "TEST_Z_PARTIAL" Range Scan (full match) + + {qry_map[4]} + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Table "TEST" Access By ID + ................-> Bitmap + ....................-> Index "TEST_X_MINUS_Y_PARTIAL" Range Scan (full match) + + {qry_map[5]} + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Table "TEST" Access By ID + ................-> Bitmap + ....................-> Index "TEST_X_ASC" Range Scan (full match) + + {qry_map[6]} + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Table "TEST" Access By ID + ................-> Bitmap + ....................-> Index "TEST_Y_DESC" Range Scan (full match) + + {qry_map[7]} + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Table "TEST" Access By ID + ................-> Bitmap + ....................-> Index "TEST_X_PLUS_Y" Range Scan (full match) + + {qry_map[8]} + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Table "TEST" Access By ID + ................-> Bitmap + ....................-> Index "TEST_Z_PARTIAL" Range Scan (full match) + + {qry_map[9]} + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Table "TEST" Access By ID + ................-> Bitmap + ....................-> Index "TEST_X_MINUS_Y_PARTIAL" Range Scan (full match) + """ + + act.expected_stdout = expected_out + + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8291_test.py b/tests/bugs/gh_8291_test.py new file mode 100644 index 00000000..cb01fab0 --- /dev/null +++ b/tests/bugs/gh_8291_test.py @@ -0,0 +1,96 @@ +#coding:utf-8 + +""" +ID: issue-8291 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8291 +TITLE: NULLs should be skipped during index navigation when there's no lower bound and matched conditions are known to ignore NULLs +DESCRIPTION: + Test uses script from ticket. We compare number of indexed reads with threshold - see variable MAX_ALLOWED_IDX_READS. + BEFORE fix value of indexed was 500002 (both on 5.x and 6.x), after fix it became 886 (using default page_size = 8k). +NOTES: + [25.10.2024] pzotov + Confirmed problem on 6.0.0.485, 5.0.2.1519. + Checked on 6.0.0.502-d2f4cf6, 5.0.2.1542-ab50e20 (intermediate builds). + + [25.02.2025] pzotov + Splitted code that defines value of MAX_ALLOWED_IDX_READS: for 6.x it can be safely reduced to 6...10 since commit #5767b9. + Checked on intermediate snapshot 6.0.0.654-5767b9e. +""" + +import pytest +from firebird.qa import * + +init_sql = """ + create table test (id int); + + set term ^; + execute block as + declare n int = 1000000; + declare i int = 0; + begin + while (i < n) do + begin + insert into test(id) values( iif(mod(:i, 2) = 0, null, :i) ); + i = i + 1; + end + end^ + set term ;^ + commit; + + create index test_id on test(id); + commit; +""" +db = db_factory(page_size = 8192, init = init_sql) + +act = python_act('db') + +#----------------------------------------------------------- + +@pytest.mark.version('>=5.0.2') +def test_1(act: Action, capsys): + + # :::::::::::::::::::::::::::::: + # ::: T H R E S H O L D ::: + # :::::::::::::::::::::::::::::: + if act.is_version('<6'): + MAX_ALLOWED_IDX_READS = 1000 + else: + # NB. For the query that is used here number of indexed reads have been drastically reduced + # since #5767b9 (Ignore NULLs (if desired) while scanning keys during index navigation (#8446)). + # https://github.com/FirebirdSQL/firebird/commit/5767b9e522aa0b0ef36790f041a26bfd4f2fe738 + # https://github.com/FirebirdSQL/firebird/pull/8446 + # For 6.0.0.652 this value is 3 (three), so we can safely set it to 6...10 + MAX_ALLOWED_IDX_READS = 6 + + msg_prefix = 'Number of indexed reads:' + expected_txt = 'EXPECTED' + idx_reads = {} + with act.db.connect() as con: + cur = con.cursor() + cur.execute("select rdb$relation_id from rdb$relations where rdb$relation_name = 'TEST'") + test_rel_id = cur.fetchone()[0] + idx_reads[test_rel_id] = 0 + + for x_table in con.info.get_table_access_stats(): + if x_table.table_id == test_rel_id: + idx_reads[test_rel_id] = -x_table.indexed + + cur.execute('select count(*) from (select id from test where id < 3 order by id)') + data = cur.fetchall() + + for x_table in con.info.get_table_access_stats(): + if x_table.table_id == test_rel_id: + idx_reads[test_rel_id] += x_table.indexed + + # BEFORE fix value of indexed was 500002. After fix: 886 + if idx_reads[test_rel_id] < MAX_ALLOWED_IDX_READS: + print(f'{msg_prefix} {expected_txt}') + else: + print(f'{msg_prefix} UNEXPECTED: {idx_reads[test_rel_id]} - greater than threshold = {MAX_ALLOWED_IDX_READS}.') + + act.expected_stdout = f""" + {msg_prefix} {expected_txt} + """ + + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8304_test.py b/tests/bugs/gh_8304_test.py new file mode 100644 index 00000000..6260f53e --- /dev/null +++ b/tests/bugs/gh_8304_test.py @@ -0,0 +1,96 @@ +#coding:utf-8 + +""" +ID: issue-8304 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8304 +TITLE: wrong results using minvalue/maxvalue in join condition +DESCRIPTION: +NOTES: + [04.11.2024] pzotov + Confirmed bug on 6.0.0.515-d53f368 (dob: 30.10.2024). + Checked on 6.0.0.515-1c3dc43; 5.0.2.1551-90fdb97; 4.0.6.3165 (intermediate build). +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + create domain dm_sml smallint default 0 not null; + create domain dm_txt varchar(100) not null; + create table tbl1 ( + ds int, + ru dm_sml, + wi dm_sml, + ko dm_sml + ); + + create table tbl2 ( + id int, + ru dm_sml, + ru_txt dm_txt, + wi dm_sml, + wi_txt dm_txt, + ko dm_sml, + ko_txt dm_txt + ); + + commit; + + insert into tbl1 (ds, ru, wi, ko) values(50, 1, 1, 0); + + insert into tbl2 (id, ru, ru_txt, wi, wi_txt, ko, ko_txt) values(1, 1, 'a', 1, 'a', 1, 'a'); + insert into tbl2 (id, ru, ru_txt, wi, wi_txt, ko, ko_txt) values(2, 1, 'b', 1, 'b', 0, 'b'); + insert into tbl2 (id, ru, ru_txt, wi, wi_txt, ko, ko_txt) values(3, 1, 'c', 0, 'c', 1, 'c'); + insert into tbl2 (id, ru, ru_txt, wi, wi_txt, ko, ko_txt) values(4, 1, 'd', 0, 'd', 0, 'd'); + insert into tbl2 (id, ru, ru_txt, wi, wi_txt, ko, ko_txt) values(5, 0, 'e', 1, 'e', 1, 'e'); + insert into tbl2 (id, ru, ru_txt, wi, wi_txt, ko, ko_txt) values(6, 0, 'f', 1, 'f', 0, 'f'); + insert into tbl2 (id, ru, ru_txt, wi, wi_txt, ko, ko_txt) values(7, 0, 'g', 0, 'g', 1, 'g'); + insert into tbl2 (id, ru, ru_txt, wi, wi_txt, ko, ko_txt) values(8, 0, 'h', 0, 'h', 0, 'h'); + + commit; + + set count on; + set list on; + + -- no record - wrong: + select 'case-1' as msg, a.* + from tbl1 a + join tbl2 b on minvalue(a.ko, 1) = b.ko and + minvalue(a.ru, 1) = b.ru and + minvalue(a.wi, 1) = b.wi + ; + + -- one record - correct: + select 'case-2' as msg, a.* + from tbl1 a + join tbl2 b on decode(a.ko, 0, 0, 1) = b.ko and + decode(a.ru, 0, 0, 1) = b.ru and + decode(a.wi, 0, 0, 1) = b.wi + ; +""" + +act = isql_act('db', test_script) + +expected_stdout = """ + MSG case-1 + DS 50 + RU 1 + WI 1 + KO 0 + Records affected: 1 + + MSG case-2 + DS 50 + RU 1 + WI 1 + KO 0 + Records affected: 1 +""" + +@pytest.mark.version('>=4.0.6') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8309_test.py b/tests/bugs/gh_8309_test.py new file mode 100644 index 00000000..ac456856 --- /dev/null +++ b/tests/bugs/gh_8309_test.py @@ -0,0 +1,90 @@ +#coding:utf-8 + +""" +ID: issue-8309 +ISSUE: https://github.com/FirebirdSQL/firebird/pull/8309 +TITLE: Add ALTER PACKAGE BODY and CRAETE OR ALTER PACKAGE BODY parse rules +DESCRIPTION: + We create package with body. Then we change its body two times: + * using 'ALTER PACKAGE'; + * using 'CREATE OR ALTER PACKAGE' clause. + Both changes must complete without error. +NOTES: + [10.11.2024] pzotov + Checked on 6.0.0.523-8ca2314. +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +act = isql_act('db', substitutions=[('[ \\t]+', ' ')]) + +@pytest.mark.version('>=6.0.0') +def test_1(act: Action): + + msg_map = { + 0: 'Initial' + ,1: 'Changed via "ALTER PACKAGE BODY"' + ,2: 'Changed via "CREATE OR ALTER PACKAGE BODY"' + } + + test_sql = f""" + set term ^; + set heading off + ^ + set bail on + ^ + create or alter package pg_test as + begin + function fn_dummy returns varchar(50); + end + ^ + recreate package body pg_test as + begin + function fn_dummy returns varchar(50) as + begin + return '{msg_map[0]}'; + end + end + ^ + select pg_test.fn_dummy() from rdb$database + ^ + + set bail off + ^ + + -- must NOT fail since 6.0.0.523: + alter package body pg_test as + begin + function fn_dummy returns varchar(50) as + begin + return '{msg_map[1]}'; + end + end + ^ + select pg_test.fn_dummy() from rdb$database + ^ + + -- must NOT fail since 6.0.0.523: + create or alter package body pg_test as + begin + function fn_dummy returns varchar(50) as + begin + return '{msg_map[2]}'; + end + end + ^ + select pg_test.fn_dummy() from rdb$database + ^ + """ + + act.expected_stdout = f""" + {msg_map[0]} + {msg_map[1]} + {msg_map[2]} + """ + act.isql(switches = ['-q'], input = test_sql, combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout + diff --git a/tests/bugs/gh_8310_test.py b/tests/bugs/gh_8310_test.py new file mode 100644 index 00000000..f67093e1 --- /dev/null +++ b/tests/bugs/gh_8310_test.py @@ -0,0 +1,65 @@ +#coding:utf-8 + +""" +ID: issue-8310 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8310 +TITLE: Collect network statistics and make it available for the user applications +DESCRIPTION: +NOTES: + [18.11.2024] pzotov + ::: NB ::: Currently the ticket is incompletely checked. + Test verifies only ability to obtain in ISQL wire counters and statistics as described in the doc. + More complex test(s) will be implemented after firebird-driver become to recognize appropriate API changes. + + Checked on 6.0.0.532; 5.0.2.1567. +""" +import os +import pytest +from firebird.qa import * + +db = db_factory() + +test_sql = f""" + set bail on; + set list on; + set wire; + out {os.devnull}; + select count(*) from rdb$fields; + show wire_stat; + out; +""" + +act = isql_act('db', test_sql, substitutions=[ ('\\d+', ''), ('[ \t]+', ' ')]) + +@pytest.mark.version('>=5.0.2') +def test_1(act: Action): + + act.expected_stdout = """ + Wire logical statistics: + send packets = 6 + recv packets = 5 + send bytes = 184 + recv bytes = 224 + Wire physical statistics: + send packets = 3 + recv packets = 2 + send bytes = 184 + recv bytes = 224 + roundtrips = 2 + + Wire logical statistics: + send packets = 18 + recv packets = 15 + send bytes = 1480 + recv bytes = 944 + Wire physical statistics: + send packets = 15 + recv packets = 11 + send bytes = 1480 + recv bytes = 944 + roundtrips = 11 + """ + act.execute(combine_output = True) + + assert act.clean_stdout == act.clean_expected_stdout + diff --git a/tests/bugs/gh_8318_test.py b/tests/bugs/gh_8318_test.py new file mode 100644 index 00000000..cf1cc257 --- /dev/null +++ b/tests/bugs/gh_8318_test.py @@ -0,0 +1,177 @@ +#coding:utf-8 + +""" +ID: issue-8318 +ISSUE: https://github.com/FirebirdSQL/firebird/pull/8318 +TITLE: Send blob contents in the same data stream as main resultset +DESCRIPTION: We create four tables: one pair with data which can be compressed with maximal degree ('tblob_max*', 'ttext_max*') + and second whith data that is almost incompressible ('tblob_min*', 'ttext_min*'). + Each row in these tables is fulfilled by long binary data (see 'TEXT_LEN' setting), thus DB must have 1-byte charset. + Then we run queries using ISQL feature that allows to see network statistics (SET WIRE ON), with redirecting output + to appropriate OS null device. + Finally, we parse network statistics and gather only roundtrip values from it. + RATIO between rountrips that were performed during selects BLOB vs VARCHAR is compared to MAX_ALLOWED_ROUND_TRIPS_RATIO. + This ratio BEFORE improvement was about 38 for maximal compressability and 17 for incompressible data. + AFTER improvement ratio reduced to ~1. or can be even less than this. +NOTES: + [28.02.2025] pzotov + Thanks to Vlad for suggestion about this test implementation. + Confirmed poor network statistics (RATIO between rountrips) for 6.0.0.607-1985b88 (03.02.2025). + Checked on 6.0.0.656-25fb454 - all fine. + + [09.04.2025] pzotov + Checked on 5.0.3.1639-f47fcd9 (intermediate snapshot). Reduced min_version to 5.0.3. +""" + +import os +import re +import time + +import pytest +from firebird.qa import * +from firebird.driver import DatabaseError + +TEXT_LEN = 32765 +ROWS_CNT = 50 +MAX_ALLOWED_ROUND_TRIPS_RATIO = 2 + +init_sql = f""" + recreate table tblob_max_compressible(b blob sub_type 0); + recreate table ttext_max_compressible(c varchar({TEXT_LEN})); + recreate table tblob_min_compressible(b blob sub_type 0); + recreate table ttext_min_compressible(c varchar({TEXT_LEN})); + + set term ^; + execute block as + declare n int = {ROWS_CNT}; + declare s varchar({TEXT_LEN}) character set octets; + declare x blob character set octets; + begin + x = lpad('', {TEXT_LEN}, 'A' ); + while (n>0) do + begin + insert into tblob_max_compressible(b) values( :x ); + insert into ttext_max_compressible(c) values( :x ); + n = n - 1; + end + end + ^ + execute block as + declare n int = {ROWS_CNT}; + declare s varchar({TEXT_LEN}) character set octets; + declare x blob character set octets; + begin + while (n>0) do + begin + x = ''; + while (octet_length(x) < {TEXT_LEN}-16) do + begin + x = blob_append(x, gen_uuid() ); + end + insert into tblob_min_compressible(b) values( :x ); + insert into ttext_min_compressible(c) values( :x ); + n = n - 1; + end + end + ^ + set term ;^ + commit; + alter database set linger to 0; + commit; +""" + +db = db_factory(init = init_sql, charset = 'win1251') +act = python_act('db') # , substitutions = [(r'record length: \d+, key length: \d+', 'record length: NN, key length: MM')]) + +#----------------------------------------------------------- + +@pytest.mark.version('>=5.0.3') +def test_1(act: Action, capsys): + test_sql = f""" + rollback; + set list on; + set blob all; + -- set echo on; + + connect '{act.db.dsn}'; + set wire on; + out {os.devnull}; -- tmp_ttext_max_compressible.tmp; + select * from ttext_max_compressible; + out; + set wire off; + rollback; + + connect '{act.db.dsn}'; + set wire on; + out {os.devnull}; -- tmp_blob_max_compressible.tmp; + select * from tblob_max_compressible; + out; + set wire off; + rollback; + + connect '{act.db.dsn}'; + set wire on; + out {os.devnull}; -- tmp_ttext_min_compressible.tmp; + select * from ttext_min_compressible; + out; + set wire off; + rollback; + + connect '{act.db.dsn}'; + set wire on; + out {os.devnull}; -- tmp_tblob_min_compressible.tmp; + select * from tblob_min_compressible; + out; + set wire off; + """ + + act.isql(switches = ['-q'], input = test_sql, combine_output = True) + + rt_pattern = re.compile('roundtrips', re.IGNORECASE); + + keys = iter(('ttext_max_compressible', 'tblob_max_compressible', 'ttext_min_compressible', 'tblob_min_compressible', )) + rountrips_map = {} + + if act.return_code == 0: + # Print only interesting lines from ISQl output tail: + for line in act.clean_stdout.splitlines(): + if rt_pattern.search(line): + # print(line) + rountrips_map[ next(keys) ] = int(line.split('=')[1]) + + rtrips_max_compr_ratio = rountrips_map[ 'tblob_max_compressible' ] / rountrips_map[ 'ttext_max_compressible' ] + rtrips_min_compr_ratio = rountrips_map[ 'tblob_min_compressible' ] / rountrips_map[ 'ttext_min_compressible' ] + + msg_prefix = 'Ratio between roundtrips when data compressibility is' + poor_ratio_found = 0 + if rtrips_max_compr_ratio <= MAX_ALLOWED_ROUND_TRIPS_RATIO: + print(f'{msg_prefix} maximal: EXPECTED.') + else: + print(f"{msg_prefix} maximal: UNEXPECTED, {rountrips_map[ 'tblob_max_compressible' ]} / {rountrips_map[ 'ttext_max_compressible' ]} = {rtrips_max_compr_ratio} - greater than {MAX_ALLOWED_ROUND_TRIPS_RATIO=}") + poor_ratio_found = 1 + + if rtrips_min_compr_ratio <= MAX_ALLOWED_ROUND_TRIPS_RATIO: + print(f'{msg_prefix} minimal: EXPECTED.') + else: + print(f"{msg_prefix} maximal: UNEXPECTED, {rountrips_map[ 'tblob_min_compressible' ]} / {rountrips_map[ 'ttext_min_compressible' ]} = {rtrips_min_compr_ratio} - greater than {MAX_ALLOWED_ROUND_TRIPS_RATIO=}") + poor_ratio_found = 1 + + if poor_ratio_found == 1: + print('Check full output:') + for line in act.clean_stdout.splitlines(): + print(line) + + else: + # If retcode !=0 then we can print the whole output of failed gbak: + for line in act.clean_stdout.splitlines(): + print(line) + act.reset() + + expected_stdout = f""" + {msg_prefix} maximal: EXPECTED. + {msg_prefix} minimal: EXPECTED. + """ + + act.expected_stdout = expected_stdout + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8323_test.py b/tests/bugs/gh_8323_test.py new file mode 100644 index 00000000..22713c54 --- /dev/null +++ b/tests/bugs/gh_8323_test.py @@ -0,0 +1,49 @@ +#coding:utf-8 + +""" +ID: issue-8323 +ISSUE: https://github.com/FirebirdSQL/firebird/pull/8323 +TITLE: Add AUTO RELEASE TEMP BLOBID transaction option +NOTES: + [28.11.2024] pzotov + 1. Test verifies only syntax extension of SET TRANSACTION, i.e. ability to use 'AUTO RELEASE TEMP BLOBID' clause. + Handling with temporary BLOBID can not be tested in ISQL and will be checked when firebird-driver will support this. + 2. Presense of mon$transactions.mon$auto_release_temp_blobid column not checked: FB 5.x currently missed it. + + Discussed with Vlad, letters 28.11.2024. + + Checked on 6.0.0.535, 5.0.2.1569 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set bail on; + set list on; + set blob all; + recreate table test(b blob); + commit; + + -- makes the transaction release a temporary ID of a user BLOB just after its materialization + set transaction AUTO RELEASE TEMP BLOBID; + + insert into test values('qwerty') returning b as blob_column_id; + -- TEMPORARY (?) DISABLED: FB 5.X HAS NO SUCH FIELD: + -- select mon$auto_release_temp_blobid from mon$transactions where mon$transaction_id = current_transaction; + commit; +""" + +act = isql_act('db', test_script, substitutions = [('BLOB_COLUMN_ID .*', '')]) + +expected_stdout = """ + qwerty +""" + +@pytest.mark.version('>=5.0.2') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8353_test.py b/tests/bugs/gh_8353_test.py new file mode 100644 index 00000000..4d8cbe7f --- /dev/null +++ b/tests/bugs/gh_8353_test.py @@ -0,0 +1,73 @@ +#coding:utf-8 + +""" +ID: issue-8353 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8353 +TITLE: Report unique usernames for isc_info_user_names +DESCRIPTION: + Test uses DbInfoCode.USER_NAMES property to obtain AGGREGATED info for every connected user (instead of getting list). + This info looks like: + : + - where: + = name of connected user + = number of attachments created by , total for BOTH auth plugins: Srp + Legacy. + +NOTES: + [19.12.2024] pzotov + Confirmed ticket issue on 6.0.0.552: N_COUNT > 1 are shown for both SYSDBA and non-dba users when they make more than one attachment. + Checked on 6.0.0.553-7ebb66b, 5.0.2.1580-7961de2, 4.0.6.3172-4119f625: every user is specified only once, i.e. N_COUNT = 1 + + [21.12.2024] pzotov + Added pytest.skip() directive if ServerMode is not Super because there is no way to get info about other users for NON-dba. + See letter from Vlad, 21.12.2024 13:07. +""" + +import pytest +from firebird.qa import * +from firebird.driver import DatabaseError, DbInfoCode + +db = db_factory() +act = python_act('db', substitutions = [('[ \t]+', ' ')]) + +N_CONNECTIONS = 3 +TMP_USER_NAME = 'TMP$8353' + +tmp_user_leg = user_factory('db', name = TMP_USER_NAME, password = '123', plugin = 'Legacy_UserManager') +tmp_user_srp = user_factory('db', name = TMP_USER_NAME, password = '456', plugin = 'Srp') + +# set width mon_user 16; +# set width auth_method 20; +# select mon$attachment_id, trim(mon$user) as mon_user, mon$auth_method as auth_method, count(*)over(partition by mon$user), count(*)over(partition by mon$user, mon$auth_method) from mon$attachments; + +#----------------------------------------------------------------------- + +@pytest.mark.version('>=4.0.6') +def test_1(act: Action, tmp_user_leg: User, tmp_user_srp: User, capsys): + + if act.vars['server-arch'] != 'SuperServer': + pytest.skip("Can not be checked on CS/SC.") + + try: + with act.db.connect() as con1, \ + act.db.connect() as con2: + conn_lst = [] + for i in range(N_CONNECTIONS): + for u in (tmp_user_leg, tmp_user_srp): + conn_lst.append( act.db.connect(user = u.name, password = u.password) ) + + for k,v in sorted(con1.info.get_info(DbInfoCode.USER_NAMES).items()): + print(k,':',v) + + for c in conn_lst: + c.close() + + except DatabaseError as e: + print(e.__str__()) + + act.expected_stdout = f""" + {act.db.user} : 1 + {TMP_USER_NAME} : 1 + """ + + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8356_test.py b/tests/bugs/gh_8356_test.py new file mode 100644 index 00000000..dded932f --- /dev/null +++ b/tests/bugs/gh_8356_test.py @@ -0,0 +1,88 @@ +#coding:utf-8 + +""" +ID: issue-8356 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8356 +TITLE: Make Trace use HEX representation for parameter values of types [VAR]CHAR CHARACTER SET OCTETS and [VAR]BINARY +DESCRIPTION: +NOTES: + [21.01.2025] pzotov + After fix parameters type '[va]rchar(N) character set octets' is shown in the trace as '[var]binary(N)'. + + Confirmed problem on 6.0.0.585: parameter values in the trace are shown in binary (non-readable) form. + Checked on 6.0.0.590-7e96b33, 5.0.2.1597-4fa00f1 - all fine. +""" +import re + +import pytest +from firebird.qa import * + +init_sql = """ + set term ^; + create procedure sp_test ( + a_vchr varchar(16) character set octets + ,a_chr char(16) character set octets + ,a_vbin varbinary(16) + ,a_bin binary(16) + ) as + declare n smallint; + begin + n = 1; + end + ^ + set term ;^ + commit; +""" + +db = db_factory(init = init_sql) + +act = python_act('db') + +test_sql = """ + execute procedure sp_test( + gen_uuid() + ,gen_uuid() + ,gen_uuid() + ,gen_uuid() + ); +""" + +trace_events_lst = \ + [ 'time_threshold = 0' + ,'log_procedure_start = true' + ,'log_initfini = false' + ] + +@pytest.mark.trace +@pytest.mark.version('>=5.0.2') +def test_1(act: Action, capsys): + + with act.trace(db_events = trace_events_lst): + act.reset() + act.isql(switches = ['-q'], input = test_sql, combine_output = True) + + # Process trace + # BEFORE FIX: param0 = varchar(16), "" + # AFTER FIX: param0 = varbinary(16), "D0EC952EC11A4C209011CF95C1712D2F" + + param_name_pattern = re.compile( r'\s?param\d+\s?=\s?(var)?(binary|char)\(\d+\)', re.IGNORECASE ) + # param_hexvalue_ptn = re.compile('') + + for line in act.trace_log: + if param_name_pattern.search(line.lower()): + param_name = line.split("=")[0].strip() + param_val = line.split('"')[1] + try: + _ = int(param_val, 16) + print(f'Parameter: {param_name}, value is in HEX form.') + except ValueError as e: + print(f'Parameter: {param_name}, value: "{param_val}" - not in HEX form.') + + act.expected_stdout = """ + Parameter: param0, value is in HEX form. + Parameter: param1, value is in HEX form. + Parameter: param2, value is in HEX form. + Parameter: param3, value is in HEX form. + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8379_test.py b/tests/bugs/gh_8379_test.py new file mode 100644 index 00000000..a012b991 --- /dev/null +++ b/tests/bugs/gh_8379_test.py @@ -0,0 +1,128 @@ +#coding:utf-8 + +""" +ID: issue-8379 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8379 +TITLE: Incorrect cardinality estimation for retrievals with multiple compound indices having common set of fields +DESCRIPTION: + +NOTES: + [15.01.2025] pzotov + + 1. This ticket can NOT be checked on FB 5.x: there are no differences neither in explained plans nor in trace statistics + for snapshots before and after fix. Only cardinality estimation can be checked but this feature avaliable only in FB-6 + via rdb$sql.explain() package (see doc/sql.extensions/README.sql_package.md). + 2. Cardinality change can be seen only for second example provided in the ticket + ("Test #2: three conditions, two of them are mapped ... the third one is not mapped to an index") + // see also letter from dimitr 14.01.2025 15:06 + + 3. Query : + select + plan_line as p_line + ,cardinality + ,access_path + from rdb$sql.explain(q'# + select count(*) as cnt_1 + from t + where f1 = 99 and f2 = 99 and f3 = 99 + #') + - must return rows with *different* cardinality for plan lines with "Filter" and "Table ... Access By ID": + P_LINE CARDINALITY ACCESS_PATH | + 1 Select expression | + 2 1 -> Aggregate | + 3 1.78675004326506 -> Filter | <<< THIS VALUE MUST BE MUCH LESS THAN ONE FOR P_LINE = 4 ('card_afte') + 4 17.8675004326506 -> Table ... Access By ID | <<< THIS IS VALUE BEFORE 'FILTER' IS APPLIED ('card_befo') + Before fix this was not so: cardinality value in line with "Filter" was the same as in line for "Table ... Access By ID": + Thanks to dimitr for suggestions. + + Confirmed bug on 6.0.0.576. + Checked on 6.0.0.577 - all OK. +""" + +import pytest +from firebird.qa import * + +############# +N_ROWS = 5000 +OK_MSG = 'Cardinality estimation: EXPECTED.' +############# + +init_script = f""" + recreate view v_check_card as select 1 x from rdb$database; + recreate table test (id int primary key, f1 int, f2 int, f3 int); + + set term ^; + execute block as + declare n int = {N_ROWS}; + declare i int = 0; + begin + while (i < n) do + begin + insert into test(id, f1, f2, f3) values(:i, mod(:i, 100), mod(:i, 200), mod(:i, 300)); + i = i + 1; + end + end + ^ + set term ;^ + commit; + + create index it1 on test(f1, f2); + create index it2 on test(f1, f3); + commit; + + recreate view v_check_card as + with + a as ( + select + plan_line + ,record_source_id + ,parent_record_source_id + ,level + ,cardinality + ,record_length + ,key_length + ,access_path + from rdb$sql.explain(q'# + select count(*) as cnt_1 + from test + where f1 = 99 and f2 = 99 and f3 = 99 + #') + ) + ,b as ( + select + a.plan_line + ,a.cardinality as card_afte + ,lead(a.cardinality)over(order by a.plan_line) card_befo + ,a.access_path + from a + ) + select + iif( b.card_afte / nullif(b.card_befo,0) < 1 + ,'{OK_MSG}' + ,'Cardinality estimation INCORRECT, HAS NOT REDUCED: ' + || 'card_befo = ' || b.card_befo + || ', card_afte = ' || b.card_afte + || ', card_afte / card_befo = ' || (b.card_afte / nullif(b.card_befo,0)) + ) as msg + from b + where b.access_path containing 'filter'; +""" + +db = db_factory(init=init_script) + +test_script = """ + set heading off; + select * from v_check_card; +""" + +act = isql_act('db', test_script) + +expected_stdout = f""" + {OK_MSG} +""" + +@pytest.mark.version('>=6.0') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8386_test.py b/tests/bugs/gh_8386_test.py new file mode 100644 index 00000000..cf15f52c --- /dev/null +++ b/tests/bugs/gh_8386_test.py @@ -0,0 +1,118 @@ +#coding:utf-8 + +""" +ID: issue-8386 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8386 +TITLE: Crash when creating index on table that uses UDR and ParallelWorkers > 1 +DESCRIPTION: +NOTES: + [18.01.2025] pzotov + Confirmed bug on 5.0.2.1589, 6.0.0.584 - got: "SQLSTATE = 08006 / Error reading data ...". + Checked on 5.0.2.1592-2d11769, 6.0.0.585-6f17277 -- all fine. +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set bail on; + set list on; + create exception exc_parallel_workers_required q'[Config parameter ParallelWorkers = @1 must be set to 2 or greater.]'; + create exception exc_udr_func_seems_not_working q'[UDR function 'sum_args' either not defined or is invalid]'; + create exception exc_not_enough_data_for_test q'[At least 2 PP must be allocated for data of 'TEST' table. Currently only @1 PP are used.]'; + + -- create UDR + create function sum_args ( + n1 integer, + n2 integer, + n3 integer + ) returns integer + external name 'udrcpp_example!sum_args' + engine udr + ; + + set term ^; + execute block as + declare n int; + begin + select rdb$config_value from rdb$config where rdb$config_name = 'ParallelWorkers' into n; + if (n is null or n <= 1) then + exception exc_parallel_workers_required using (n); + -------------------------------------------- + select sum_args(1, 2, 3) from rdb$database into n; + if (n is distinct from 6) then + exception exc_udr_func_seems_not_working; + + end + ^ + set term ;^ + commit; + + -- create table with dependency on UDR + create table test ( + f1 int, + f2 int, + f3 int, + f_sum computed by (sum_args(f1, f2, f3)) + ); + + -- fill it with some data + insert into test values (1, 1, 1); + insert into test select f1, f2, f3 from test; + insert into test select f1, f2, f3 from test; + insert into test select f1, f2, f3 from test; + insert into test select f1, f2, f3 from test; + insert into test select f1, f2, f3 from test; + insert into test select f1, f2, f3 from test; + insert into test select f1, f2, f3 from test; + insert into test select f1, f2, f3 from test; + insert into test select f1, f2, f3 from test; + insert into test select f1, f2, f3 from test; + insert into test select f1, f2, f3 from test; + insert into test select f1, f2, f3 from test; + insert into test select f1, f2, f3 from test; + insert into test select f1, f2, f3 from test; + insert into test select f1, f2, f3 from test; + insert into test select f1, f2, f3 from test; + insert into test select f1, f2, f3 from test; + insert into test select f1, f2, f3 from test; + commit; + + set term ^; + execute block as + declare n int; + begin + -- make sure there are at least 2 pointer pages + select count(*) from rdb$pages p join rdb$relations r on p.rdb$relation_id = r.rdb$relation_id + where r.rdb$relation_name = upper('test') and p.rdb$page_type = 4 + into n; + if (n < 2) then + exception exc_not_enough_data_for_test using (n); + + end + ^ + set term ;^ + commit; + + -- create index + create index test_idx_f1 on test(f1); + + -- THIS MUST BE DISPLAYED. CRASH WAS HERE BEFORE FIX: + select 'Completed' as msg from rdb$database; +""" + +act = isql_act('db', test_script, substitutions = [('[ \t]+', ' ')]) + +@pytest.mark.version('>=5.0.2') +def test_1(act: Action): + + # DISABLED 17.01.2025 13:35, requested by dimitr: + #if act.vars['server-arch'] != 'SuperServer': + # pytest.skip("Only SuperServer affected") + + act.expected_stdout = 'MSG Completed' + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout + diff --git a/tests/bugs/gh_8391_test.py b/tests/bugs/gh_8391_test.py new file mode 100644 index 00000000..0f978940 --- /dev/null +++ b/tests/bugs/gh_8391_test.py @@ -0,0 +1,136 @@ +#coding:utf-8 + +#coding:utf-8 + +""" +ID: issue-8391 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8391 +TITLE: gbak: Incorrect total statistics for restore +DESCRIPTION: + Test uses pre-created databases.conf which has alias (see variable REQUIRED_ALIAS) with DefaultDbCachePages = 128. + Database file for that alias must NOT exist in the QA_root/files/qa/ subdirectory: it will be created here. + + We add data to test DB (script was taken from gh_8394_test.py - it is enough to use it instead of bug DB from ticket). + Then we make backup and start restore with '-v -st w' option. + Parsing of restore log must give us total number of writes that were done, see variable 'gbak_total_writes'. + Value of gbak_total_writes must be NOT LESS than ratio restored_file_size / PAGE_SIZE. +NOTES: + [23.01.2025] pzotov + 1. One need to be sure that firebird.conf does NOT contain DatabaseAccess = None. + 2. Value of REQUIRED_ALIAS must be EXACTLY the same as alias specified in the pre-created databases.conf + (for LINUX this equality is case-sensitive, even when aliases are compared!) + 3. Content of databases.conf must be taken from $QA_ROOT/files/qa-databases.conf (one need to replace it before every test session). + Discussed with pcisar, letters since 30-may-2022 13:48, subject: + "new qa, core_4964_test.py: strange outcome when use... shutil.copy() // comparing to shutil.copy2()" + 4. Value of DefaultDBCachePages must be LESS than result of division restored_file_size/PAGE_SIZE otherwise fix can not be observed. + + Confirmed problem on 6.0.0.584, 5.0.2.1592 (only when Servermode = 'Super'; SC and CS not affected). + Checked on 6.0.0.590, 5.0.2.1601 -- all OK; 4.x. seems not affected (checked on 4.0.6.3174-ffd396f, date: 23-dec-2024). +""" + +import locale +import re +import os +from pathlib import Path + +import pytest +from firebird.qa import * + +substitutions = [('[ \t]+', ' '), ] + +PAGE_SIZE = 8192 +REQUIRED_ALIAS = 'tmp_gh_8391_alias' + +db = db_factory(filename = '#' + REQUIRED_ALIAS, do_not_create = True, do_not_drop = True) +act = python_act('db', substitutions = substitutions) + +tmp_fbk = temp_file('tmp_gh_8391.restored.fbk') +tmp_log = temp_file('tmp_gh_8391.restored.log') + +@pytest.mark.version('>=4.0.6') +def test_1(act: Action, tmp_fbk: Path, tmp_log: Path, capsys): + + if act.get_server_architecture() != 'SuperServer': + pytest.skip('Applies only to SuperServer') + + # Scan line-by-line through databases.conf, find line starting with REQUIRED_ALIAS and extract name of file that + # must be created in the $(dir_sampleDb)/qa/ folder. + # NOTE: we have to SKIP lines which are commented out, i.e. if they starts with '#': + p_required_alias_ptn = re.compile( '^(?!#)((^|\\s+)' + REQUIRED_ALIAS + ')\\s*=\\s*\\$\\(dir_sampleDb\\)/qa/', re.IGNORECASE ) + fname_in_dbconf = None + + with open(act.home_dir/'databases.conf', 'r') as f: + for line in f: + if p_required_alias_ptn.search(line): + # If databases.conf contains line like this: + # tmp_8391_alias = $(dir_sampleDb)/qa/tmp_qa_8391.fdb + # - then we extract filename: 'tmp_qa_8391.fdb' (see below): + fname_in_dbconf = Path(line.split('=')[1].strip()).name + break + + # if 'fname_in_dbconf' remains undefined here then propably REQUIRED_ALIAS not equals to specified in the databases.conf! + # + assert fname_in_dbconf + + test_sql = f""" + set list on; + create database '{REQUIRED_ALIAS}' user {act.db.user} password '{act.db.password}' page_size {PAGE_SIZE}; + commit; + recreate sequence g; + recreate table test(id int, b blob); + + set autoterm on; + execute block as + declare n int; + begin + insert into test(id, b) values(gen_id(g,1), gen_uuid()); + insert into test(id, b) + values( + gen_id(g,1) + ,(select list(gen_uuid()) as s from rdb$types,rdb$types) + ); + + insert into test(id, b) + values( + gen_id(g,1) + ,(select list(gen_uuid()) as s from (select 1 x from rdb$types,rdb$types,rdb$types rows 800000)) + ); + end + ; + commit; + select mon$database_name, mon$page_buffers from mon$database; + """ + + test_fdb_file = 'UNDEFINED' + act.isql(switches=['-q'], input = test_sql, connect_db = False, credentials = False, combine_output = True, io_enc = locale.getpreferredencoding()) + for line in act.stdout.splitlines(): + if line.startswith('mon$database_name'.upper()): + test_fdb_file = line.split()[1] + if line.startswith('mon$page_buffers'.upper()): + test_fdb_buffers = int(line.split()[1]) + + assert test_fdb_buffers == 128 + act.reset() + + act.gbak(switches=['-b', act.db.dsn, str(tmp_fbk)], combine_output = True, io_enc = locale.getpreferredencoding()) + assert act.clean_stdout == '' + act.reset() + + act.gbak(switches=['-rep', '-v', '-st', 'w', str(tmp_fbk), REQUIRED_ALIAS], combine_output = True, io_enc = locale.getpreferredencoding()) + + # gbak: NNNN total statistics + gbak_write_total_stat_ptn = re.compile( r'gbak(:)?\s+\d+\s+total\s+statistics', re.IGNORECASE ) + + EXPECTED_MSG = 'EXPECTED: gbak total statistics for writes NOT LESS than ratio DB_FILE_SIZE / PAGE_SIZE' + for line in act.stdout.splitlines(): + if gbak_write_total_stat_ptn.search(line): + gbak_total_writes = int(line.split()[1]) + restored_file_size = os.stat(test_fdb_file).st_size + if gbak_total_writes >= restored_file_size / PAGE_SIZE: + print(EXPECTED_MSG) + else: + print(f'UNEXPECTED: {gbak_total_writes=} -- LESS than {restored_file_size/PAGE_SIZE=}') + + act.expected_stdout = EXPECTED_MSG + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8394_test.py b/tests/bugs/gh_8394_test.py new file mode 100644 index 00000000..11fe2335 --- /dev/null +++ b/tests/bugs/gh_8394_test.py @@ -0,0 +1,101 @@ +#coding:utf-8 + +""" +ID: issue-8394 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8394 +TITLE: Make Trace use HEX representation for parameter values of types [VAR]CHAR CHARACTER SET OCTETS and [VAR]BINARY +DESCRIPTION: +NOTES: + [22.01.2025] pzotov + Checked on 5.0.2.1601, 6.0.0.594 +""" +import re + +import pytest +from firebird.qa import * + +db = db_factory(page_size = 8192) + +substitutions = [ + ( r'Blobs:\s+\d+,\s+total\s+length:\s+\d+,\s+blob\s+pages:\s+\d+', 'Blobs: N, total length: M, blob pages: P') + ,( r'\d+,\s+total\s+length: \d+,\s+blob\s+pages:\s+\d+', 'X, total length: M, blob pages: P') + ,( r'Table\s+size:\s+\d+\s+bytes', 'Table size: N bytes') +] +act = python_act('db', substitutions = substitutions) + +test_sql = """ + recreate sequence g; + recreate table test(id int, b blob); + + -- https://firebirdsql.org/file/documentation/chunk/en/refdocs/fblangref30/fblangref30-datatypes-bnrytypes.html + -- 3.7.2. BLOB Specifics / BLOB Storage + -- * By default, a regular record is created for each BLOB and it is stored on a data page that is allocated for it. + -- If the entire BLOB fits onto this page, it is called a level 0 BLOB. + -- * The number of this special record is stored in the table record and occupies 8 bytes. + -- If a BLOB does not fit onto one data page, its contents are put onto separate pages allocated exclusively to it (blob pages), + -- while the numbers of these pages are stored into the BLOB record. This is a level 1 BLOB. + -- * If the array of page numbers containing the BLOB data does not fit onto a data page, the array is put on separate blob pages, + -- while the numbers of these pages are put into the BLOB record. This is a level 2 BLOB. + + set term ^; + execute block as + declare n int; + begin + insert into test(id, b) values(gen_id(g,1), gen_uuid()); + insert into test(id, b) + values( + gen_id(g,1) + ,(select list(gen_uuid()) as s from rdb$types,rdb$types) + ); + + insert into test(id, b) + values( + gen_id(g,1) + ,(select list(gen_uuid()) as s from (select 1 x from rdb$types,rdb$types,rdb$types rows 800000)) + ); + end + ^ + set term ;^ + commit; +""" + +@pytest.mark.version('>=5.0.2') +def test_1(act: Action, capsys): + + act.isql(switches = ['-q'], input = test_sql, combine_output = True) + + # Pipe of command to ISQL before 6.x leads to appearing of following 'noise info': + # Database: localhost:..., User: SYSDBA + # SQL> SQL> SQL> SQL> SQL> SQL> SQL> ... + # We have to use 'clean_stdout' in order to ignore this: + assert act.clean_stdout == '' + act.reset() + + #--------------------------------------------------------------------------------- + + blob_overall_info_ptn = re.compile( r'Blobs:\s+\d+,\s+total\s+length:\s+\d+,\s+blob\s+pages', re.IGNORECASE ) + blob_level_info_ptn = re.compile( r'Level\s+\d+: \d+,\s+total\s+length: \d+,\s+blob\s+pages', re.IGNORECASE ) + table_size_ptn = re.compile( r'Table\s+size:\s+\d+\s+bytes', re.IGNORECASE ) + + act.gstat(switches=['-r']) + blob_overall_found = False + for line in act.stdout.splitlines(): + if blob_overall_info_ptn.search(line): + blob_overall_found = True + print(line) + if blob_overall_found: + if blob_level_info_ptn.search(line): + print(line) + if table_size_ptn.search(line): + print(line) + + + act.expected_stdout = """ + Blobs: N, total length: M, blob pages: P + Level 0: X, total length: M, blob pages: P + Level 1: X, total length: M, blob pages: P + Level 2: X, total length: M, blob pages: P + Table size: N bytes + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8409_test.py b/tests/bugs/gh_8409_test.py new file mode 100644 index 00000000..f226ab91 --- /dev/null +++ b/tests/bugs/gh_8409_test.py @@ -0,0 +1,62 @@ +#coding:utf-8 + +""" +ID: issue-8409 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8409 +TITLE: Error message "SQL -104 / Unexpected end of command" appears in a trace log when 'SET AUTOTERM ON;' is used +DESCRIPTION: Test checks that trace will not contain error messages caused by DDL prepare. Expected output is empty. +NOTES: + [23.01.2025] pzotov + Checked on 6.0.0.595-2c5b146 (intermediate snapshot, UTC 20250123 01:49). +""" +import re + +import pytest +from firebird.qa import * + +db = db_factory() +act = python_act('db') + +test_sql = """ +set autoterm on; +create procedure sp_test1 as + declare n smallint; +begin + n = 1; +end;create procedure sp_test2 as declare n smallint;begin n = 2;end + +; +""" + +trace_events_lst = \ + [ 'time_threshold = 0' + ,'log_errors = true' + ,'log_statement_prepare = true' + ,'log_initfini = false' + ] + +@pytest.mark.trace +@pytest.mark.version('>=6.0') +def test_1(act: Action, capsys): + + with act.trace(db_events = trace_events_lst): + act.reset() + act.isql(switches = ['-q'], input = test_sql, combine_output = True) + + allowed_patterns = [ + 'ERROR AT JStatement::prepare' + ,'335544569 : Dynamic SQL Error' + ,'335544436 : SQL error code = -104' + ,'335544851 : Unexpected end of command' + ] + allowed_patterns = [re.compile(p, re. IGNORECASE) for p in allowed_patterns] + + + for line in act.trace_log: + for p in allowed_patterns: + if p.search(line): + print('UNEXPECTED: '+line) + + act.expected_stdout = "" + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8421_test.py b/tests/bugs/gh_8421_test.py new file mode 100644 index 00000000..6ac151f7 --- /dev/null +++ b/tests/bugs/gh_8421_test.py @@ -0,0 +1,332 @@ +#coding:utf-8 + +""" +ID: issue-8421 +ISSUE: 8421 +TITLE: Add pointers tree to TempSpace class +DESCRIPTION: + Test uses example provided in the ticket, but with different set of N values. + Following results were achieved for set of N values provided in the ticket: + --------------------------------------------------- + BEFORE FIX AFTER FIX + N time_ms ratio N time_ms ratio + --------------------------------------------------- + 1000 218 | 1000 213 + 2000 627 2.88 | 2000 503 2.36 + 4000 1493 2.38 | 4000 997 1.98 + 8000 3835 2.57 | 8000 2157 2.16 + 16000 14922 3.89 | 16000 4271 1.98 + 32000 67525 4.53 | 32000 8697 2.04 + 64000 253474 3.75 | 64000 17013 1.96 + 128000 1007229 3.97 | 128000 35285 2.07 + --------------------------------------------------- + + For N = 375; 750; 1500; 3000; 6000; 12000; 24000; 48000: + * BEFORE fix: + poor_ratios = [2.324675324675325, 2.2960893854748603, 2.411192214111922, 2.4339051463168517, 3.16832504145937, 4.301753467678618, 4.9132749285149355] + stdev(poor_ratios) = 1.0723186770194812 + + * AFTER fix: + good_ratios = [2.0357142857142856, 2.08187134502924, 1.9438202247191012, 2.066473988439306, 1.9713286713286713, 2.0503724725079815, 2.0278546712802767] + stdev(good_ratios) = 0.05031744060704646 +NOTES: + [25.01.2025] pzotov + Commits that fixed problem: + 6.x: https://github.com/FirebirdSQL/firebird/commit/8d6c46e0e1f8eec374008e5ded6be119264ed3a6 + 5.x: https://github.com/FirebirdSQL/firebird/commit/00b699cc085278c26734bcfb4329d89a49d5d1e8 + + Test executes for ~30s. + Confirmed problem on 6.0.0.647-9fccb55. + Checked on intermediate snapshots: 6.0.0.652-58633c8 (24.02.2025); 5.0.3.1624-00b699c (25.02.2025) +""" +import os +from statistics import stdev +import time +import datetime as py_dt + +import pytest +from firebird.qa import * + +db = db_factory() +act = python_act('db') + +@pytest.mark.version('>=5.0.3') +def test_1(act: Action, capsys): + + ############################################# + MAX_STD_DEV = 0.5 if os.name == 'nt' else 0.8 + ############################################# + + + ddl_lst = [] + ddl_lst.extend( + ( + """ + recreate table log_table( + log_id bigint generated by default as identity constraint pk_log_table primary key + ,field_name varchar(31) not null + ,old_value blob + ) + """ + ,""" + recreate table test_table ( + id bigint not null, + fld_01 varchar(40), + fld_02 date, + fld_03 varchar(40), + fld_04 date, + fld_05 varchar(1000), + fld_06 varchar(1000), + fld_07 numeric(15,2), + fld_08 date, + fld_09 varchar(95), + fld_10 bigint, + fld_11 bigint, + fld_12 bigint, + fld_13 varchar(250), + fld_14 bigint, + fld_15 date, + fld_16 integer, + fld_17 bigint, + fld_18 date, + fld_19 bigint, + fld_20 varchar(95), + fld_21 date, + fld_22 bigint, + fld_23 numeric(16,0), + fld_24 smallint, + fld_25 bigint, + fld_26 bigint, + fld_27 smallint default 0, + fld_28 date, + fld_29 smallint, + fld_30 date, + fld_31 date, + fld_32 date, + fld_33 date, + fld_34 date, + fld_35 date, + fld_36 date, + fld_37 date, + fld_38 date, + fld_39 date, + fld_40 smallint, + fld_41 smallint, + fld_42 smallint, + fld_43 bigint, + fld_44 bigint, + fld_45 bigint, + fld_46 smallint, + fld_47 varchar(1000), + fld_48 bigint, + fld_49 varchar(4000), + fld_50 bigint + ) + """ + ,'create generator audit' + ,'alter table test_table add constraint pk_test_table primary key (id) using descending index pk_test_table' + ,""" + create or alter trigger h$test_table for test_table + active after update position 0 + as + declare id bigint; + declare o blob sub_type 1 segment size 80; + begin + id = gen_id(audit, 1); + o = old.fld_01; if (o is distinct from new.fld_01) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_01', :o); end + o = old.fld_02; if (o is distinct from new.fld_02) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_02', :o); end + o = old.fld_03; if (o is distinct from new.fld_03) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_03', :o); end + o = old.fld_04; if (o is distinct from new.fld_04) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_04', :o); end + o = old.fld_05; if (o is distinct from new.fld_05) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_05', :o); end + o = old.fld_06; if (o is distinct from new.fld_06) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_06', :o); end + o = old.fld_07; if (o is distinct from new.fld_07) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_07', :o); end + o = old.fld_08; if (o is distinct from new.fld_08) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_08', :o); end + o = old.fld_09; if (o is distinct from new.fld_09) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_09', :o); end + o = old.fld_10; if (o is distinct from new.fld_10) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_10', :o); end + + + o = old.fld_11; if (o is distinct from new.fld_11) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_11', :o); end + o = old.fld_12; if (o is distinct from new.fld_12) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_12', :o); end + o = old.fld_13; if (o is distinct from new.fld_13) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_13', :o); end + o = old.fld_14; if (o is distinct from new.fld_14) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_14', :o); end + o = old.fld_15; if (o is distinct from new.fld_15) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_15', :o); end + o = old.fld_16; if (o is distinct from new.fld_16) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_16', :o); end + o = old.fld_17; if (o is distinct from new.fld_17) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_17', :o); end + o = old.fld_18; if (o is distinct from new.fld_18) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_18', :o); end + o = old.fld_19; if (o is distinct from new.fld_19) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_19', :o); end + o = old.fld_20; if (o is distinct from new.fld_20) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_20', :o); end + + + o = old.fld_21; if (o is distinct from new.fld_21) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_21', :o); end + o = old.fld_22; if (o is distinct from new.fld_22) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_22', :o); end + o = old.fld_23; if (o is distinct from new.fld_23) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_23', :o); end + o = old.fld_24; if (o is distinct from new.fld_24) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_24', :o); end + o = old.fld_25; if (o is distinct from new.fld_25) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_25', :o); end + o = old.fld_26; if (o is distinct from new.fld_26) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_26', :o); end + o = old.fld_27; if (o is distinct from new.fld_27) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_27', :o); end + o = old.fld_28; if (o is distinct from new.fld_28) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_28', :o); end + o = old.fld_29; if (o is distinct from new.fld_29) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_29', :o); end + o = old.fld_30; if (o is distinct from new.fld_30) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_30', :o); end + + + o = old.fld_31; if (o is distinct from new.fld_31) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_31', :o); end + o = old.fld_32; if (o is distinct from new.fld_32) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_32', :o); end + o = old.fld_33; if (o is distinct from new.fld_33) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_33', :o); end + o = old.fld_34; if (o is distinct from new.fld_34) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_34', :o); end + o = old.fld_35; if (o is distinct from new.fld_35) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_35', :o); end + o = old.fld_36; if (o is distinct from new.fld_36) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_36', :o); end + o = old.fld_37; if (o is distinct from new.fld_37) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_37', :o); end + o = old.fld_38; if (o is distinct from new.fld_38) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_38', :o); end + o = old.fld_39; if (o is distinct from new.fld_39) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_39', :o); end + o = old.fld_40; if (o is distinct from new.fld_40) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_40', :o); end + + + + o = old.fld_41; if (o is distinct from new.fld_41) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_41', :o); end + o = old.fld_42; if (o is distinct from new.fld_42) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_42', :o); end + o = old.fld_43; if (o is distinct from new.fld_43) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_43', :o); end + o = old.fld_44; if (o is distinct from new.fld_44) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_44', :o); end + o = old.fld_45; if (o is distinct from new.fld_45) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_45', :o); end + o = old.fld_46; if (o is distinct from new.fld_46) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_46', :o); end + o = old.fld_47; if (o is distinct from new.fld_47) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_47', :o); end + o = old.fld_48; if (o is distinct from new.fld_48) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_48', :o); end + o = old.fld_49; if (o is distinct from new.fld_49) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_49', :o); end + o = old.fld_50; if (o is distinct from new.fld_50) then begin insert into log_table(log_id, field_name, old_value) values(:id, 'fld_50', :o); end + end + """ + ,""" + execute block + as + declare id bigint; + begin + id = 0; + while (id < 128000) do + begin + insert into test_table (id,fld_01,fld_02,fld_03,fld_04,fld_05,fld_06,fld_07,fld_08,fld_09,fld_10,fld_11,fld_12,fld_13,fld_14,fld_15,fld_16,fld_17,fld_18,fld_19,fld_20,fld_21,fld_22,fld_23,fld_24,fld_25,fld_26,fld_27,fld_28,fld_29,fld_30,fld_31,fld_32,fld_33,fld_34,fld_35,fld_36,fld_37,fld_38,fld_39,fld_40,fld_41,fld_42,fld_43,fld_44,fld_45,fld_46,fld_47,fld_48,fld_49,fld_50) + values ( + :id, + 'Identifier', + '2025-01-23', + '1234567890123456789', + '2025-01-23', + 'Test data', + 'Test another data', + 500, + '2025-01-24', + 'Test Test Test', + 3, + 12345678901234, + 12, + 'Test', + 12345678901234, + '2025-01-30', + 0, + NULL, + '2025-01-31', + 12345679801234, + 'Test Test data', + NULL, + 1234, + 1234567, + 2025, + NULL, + NULL, + 0, + '2025-02-01', + 0, + NULL, + NULL, + NULL, + NULL, + '2025-01-15', + NULL, + NULL, + NULL, + NULL, + NULL, + 1, + 0, + 0, + NULL, + 12346579801234, + 12345678901234, + NULL, + 'Long test data for varchar(1000)', + 12345678901234, + 'Very long test data for varchar(4000)... Very long test data for varchar(4000)... Very long test data for varchar(4000)... Very long test data for varchar(4000)... Very long test data for varchar(4000)...', + 0 + ); + + id = id + 1; + end + end + """ + ,""" + create or alter procedure run_test(a int) + returns (t_cnt int, t_diff bigint) + as + declare id bigint; + declare t_begin timestamp; + declare t_end timestamp; + begin + t_cnt = a; + t_begin = 'now'; + for + select id + from test_table + where + id >= 0 and id < :t_cnt + into :id do + update test_table set fld_50 = :t_cnt where id = :id; + t_end = 'now'; + t_diff = datediff(millisecond from :t_begin to :t_end); + suspend; + end + """ + ) + ) + + with act.db.connect() as con: + for x in ddl_lst: + con.execute_immediate(x) + con.commit() + + #-------------------------------------- + + cur = con.cursor() + rs = None + ps = cur.prepare('select t.* from run_test(?) t') + elapsed_time_ratios = [] + ms_prev = -1 + for n in (0.375, 0.75, 1.5, 3, 6, 12, 24, 48): + # d1 = py_dt.timedelta(0) + rs = cur.execute(ps, ( int(n * 1000),)) + t1 = py_dt.datetime.now() + cur.fetchall() + t2 = py_dt.datetime.now() + con.commit() + d1 = t2-t1 + ms_curr = d1.seconds*1000 + d1.microseconds//1000 + + if ms_prev > 0: + elapsed_time_ratios.append(ms_curr / ms_prev) + ms_prev = ms_curr + + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() + + expected_msg = 'Expected: data dispersion is low.' + std_deviation = stdev(elapsed_time_ratios) + if std_deviation <= MAX_STD_DEV: + print(expected_msg) + else: + print(f'UNEXPECTED: standard deviation of elapsed time ratios is {std_deviation} - greater than {MAX_STD_DEV=}') + print(f'Elapsed time ratios: {elapsed_time_ratios}') + + act.expected_stdout = f""" + {expected_msg} + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8427_test.py b/tests/bugs/gh_8427_test.py new file mode 100644 index 00000000..d90b73e9 --- /dev/null +++ b/tests/bugs/gh_8427_test.py @@ -0,0 +1,173 @@ +#coding:utf-8 + +""" +ID: issue-8427 +ISSUE: https://github.com/FirebirdSQL/firebird/pull/8427 +TITLE: Allow specifying exactly what type of database string is expected in the database section for trace configuration +DESCRIPTION: + We create five temporary files which will be overwritten by test DB. + Two of them ('tmp_fdb1a' and 'tmp_fdb1b') have curvy braces in name in order to check that parsing properly handles such names. + Another two files ('tmp_fdb2a' and 'tmp_fdb2b') have digital suffix containing thee digits in order to check that they will be + properly taken in account by '[[:DIGIT:]]{3,}' that will be used in 'databaseRegex' section. + And 5th file ('tmp_fdb2c') has name that must NOT occur in the trace log. + + Trace config will have following structure: + ============ + database + { + enabled = false + } + + # FIRST occurence of 'databaseName': + databaseName = + { + enabled = true + log_initfini = false + log_connections = true + } + + # SECOND occurence of 'databaseName': + databaseName = + { + enabled = true + log_initfini = false + log_connections = true + } + + databaseRegex = + { + enabled = true + log_initfini = false + log_connections = true + } + ============ + + Then we make .sql script that only has 'CONNECT' command to each of these databases. + If we launch trace and execute this .sql then: + * events 'ATTACH_DATABASE' and 'DETACH_DATABASE' must be met one time in the trace log for every databases EXCEPT ; + * for no events must be in the trace log. + We can consider test as passed if and only if above mentioned conditions are met. +NOTES: + [03.04.2025] pzotov + Checked on 6.0.0.715-08cb3f9 SS/CS (Windows). +""" + +import locale +import re +from pathlib import Path +import shutil + +import pytest +from firebird.qa import * + +db = db_factory() + +act = python_act('db') + +tmp_fdb1a = temp_file('tmp_8427_{1}.fdb') +tmp_fdb1b = temp_file('tmp_8427_{2}.fdb') +tmp_fdb2a = temp_file('tmp_8427_301.fdb') +tmp_fdb2b = temp_file('tmp_8427_302.fdb') +tmp_fdb2c = temp_file('tmp_8427_qwe.fdb') # <<< this must NOT be traced, see blow +tmp_conf = temp_file('tmp_8427_trace.conf') +tmp_sql = temp_file('tmp_8427_check.sql') + +@pytest.mark.trace +@pytest.mark.version('>=6.0') +def test_1(act: Action, tmp_fdb1a: Path, tmp_fdb1b: Path, tmp_fdb2a: Path, tmp_fdb2b: Path, tmp_fdb2c: Path, tmp_conf: Path, tmp_sql: Path, capsys): + + # NOTE. We have to replace each '{' and '}' with duplicated one: + db_name1 = str(tmp_fdb1a).replace('{', '{{').replace('}', '}}') + db_name2 = str(tmp_fdb1b).replace('{', '{{').replace('}', '}}') + + db_patt = '(%[\\\\/](tmp_8427_[[:DIGIT:]]{{3,}}).fdb)' + + trace_conf = f""" + database + {{ + enabled = false + }} + + databaseName = {db_name1} + {{ + enabled = true + log_initfini = false + log_connections = true + }} + + databaseName = {db_name2} + {{ + enabled = true + log_initfini = false + log_connections = true + }} + + databaseRegex = {db_patt} + {{ + enabled = true + log_initfini = false + log_connections = true + }} + """ + + # for debug only: + # tmp_conf.write_text(trace_conf) + + chk_sql_lines = [] + for db_i in (tmp_fdb1a, tmp_fdb1b, tmp_fdb2a, tmp_fdb2b, tmp_fdb2c): + shutil.copy2(act.db.db_path, db_i) + chk_sql_lines.append(f"connect 'inet://{db_i}' user {act.db.user} password {act.db.password};") + chk_sql_lines.append('commit;') + + tmp_sql.write_text('\n'.join(chk_sql_lines)) + + with act.trace(config = [x for x in trace_conf.splitlines() if x.strip()], encoding='utf8', encoding_errors='utf8'): + act.isql(switches = ['-q'], input_file = tmp_sql, connect_db = False, combine_output = True, io_enc = locale.getpreferredencoding()) + isql_retcode = act.return_code + isql_output = act.clean_stdout + + assert isql_retcode == 0 and isql_output == '', 'Script to check trace output FAILED, check its output.' + act.reset() + + traced_db_count = {} # K = db_name; V = (attach_count, detach_count) + trace_out = [] + for line in act.trace_log: + if (x := line.rstrip()): + trace_out.append(x) + + attach_detach_ptn = re.compile( r'\)\s+(ATTACH_|DETACH_)DATABASE' ) + attach_id_pattern = re.compile( r'\s+\(ATT_\d+,\s+' + act.db.user.upper() ) + + for i,line in enumerate(trace_out): + if attach_detach_ptn.search(line): + next_line = trace_out[i+1] + if (p := attach_id_pattern.search(next_line)): + handled_db = Path(next_line[ : p.span()[0] ].strip().lower()) + attach_cnt, detach_cnt = traced_db_count.get(handled_db, [0,0]) + if 'ATTACH_DATABASE' in line: + attach_cnt += 1 + elif 'DETACH_DATABASE' in line: + detach_cnt += 1 + traced_db_count[handled_db] = (attach_cnt, detach_cnt) + + # Expected content of traced_db_count: + # /tmp_8427_{1}.fdb (1, 1) + # /tmp_8427_{2}.fdb (1, 1) + # /tmp_8427_301.fdb (1, 1) + # /tmp_8427_302.fdb (1, 1) + + EXPECTED_MSG = 'Expected: found *exact* set of ATTACH and DETACH events.' + + if list(traced_db_count.keys()) == [tmp_fdb1a, tmp_fdb1b, tmp_fdb2a, tmp_fdb2b] and set(traced_db_count.values()) == set([(1,1)]): + print(EXPECTED_MSG) + else: + print('Trace either has no events for some DB or excessive databases / events present:') + for k,v in traced_db_count.items(): + print(str(k), f'attach_count: {v[0]}, detach_count: {v[1]}') + + act.expected_stdout = f""" + {EXPECTED_MSG} + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() diff --git a/tests/bugs/gh_8429_test.py b/tests/bugs/gh_8429_test.py new file mode 100644 index 00000000..8f04394d --- /dev/null +++ b/tests/bugs/gh_8429_test.py @@ -0,0 +1,312 @@ +#coding:utf-8 + +""" +ID: issue-8429 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8429 +TITLE: Segfault when already destroyed callback interface is used +DESCRIPTION: + Test uses THREE preliminary created aliases with KeyHolderPlugin that points to special configuration with name 'KH2'. + This configuration makes crypt plugin accept key ONLY from client app (in contrary to other encryption-related tests). + Three non-privileged users are created for this test. +NOTES: + [24.02.2025] pzotov + 1) firebird.conf must have KeyHolderPlugin = fbSampleKeyHolder and plugins/fbSampleKeyHolder.conf must have Auto = true. + 2) aliases gh_8429_alias_a, gh_8429_alias_b, gh_8429_alias_c that are used in this test must have (in databases.conf) + KeyHolderPlugin = KH2 and plugins.conf must contain: + --------------- + Plugin = KH2 { + Module = $(dir_plugins)/fbSampleKeyHolder + RegisterName = fbSampleKeyHolder + Config = KH2 + } + Config = KH2 { + Auto = false + } + --------------- + + 3) Third database (defined by 'gh_8429_alias_c') must be encrypted in FB-6.x in order to reproduce bug. + 4) All databases are created here by explicit 'create_database()' call and their deletion does NOT perform. + This was done intentionally in order to suppress appearance of any encryption-related errors that do not matter. + 5) Method 'create_database()' uses LOCAL protocol if $QA_ROOT/firebir-driver.conf has no section [firebird.server.defaults] + It can be empty but it MUST present if we want database to be created using prefix 'inet://' for its DSN. + Because of that, encryption of db_file_c will be done at exclusive connection, see reduced indent for 'run_encr_decr()' call. + + Great thanks to Alex for provided scenario for this test (letter 10-feb-2025 20:43) ans lot of suggestions. + + Confirmed bug only for Super, snapshots: 6.0.0.600-188de60; 5.0.2.1606-a92f352; 4.0.6.3181-00b648f. + Classic not affected. + Checked on 6.0.0.607-1985b88; 5.0.2.1610-5e63ad0; 4.0.6.3185-9cac45a - all fine. +""" +import os +import time +import datetime as py_dt +import re +from pathlib import Path + +import pytest +from firebird.qa import * +from firebird.driver import create_database, ShutdownMode, ShutdownMethod + +########################### +### S E T T I N G S ### +########################### + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +enc_settings = QA_GLOBALS['encryption'] + +# ACHTUNG: this must be carefully tuned on every new host: +# +MAX_WAITING_ENCR_FINISH = int(enc_settings['MAX_WAIT_FOR_ENCR_FINISH_WIN' if os.name == 'nt' else 'MAX_WAIT_FOR_ENCR_FINISH_NIX']) +assert MAX_WAITING_ENCR_FINISH > 0 + +ENCRYPTION_PLUGIN = enc_settings['encryption_plugin'] # fbSampleDbCrypt + +db = db_factory() +usr_x = user_factory('db', name = 'tmp_8429_x', password = 'tmp_8429_x', plugin = 'Srp') +usr_y = user_factory('db', name = 'tmp_8429_y', password = 'tmp_8429_y', plugin = 'Srp') +usr_z = user_factory('db', name = 'tmp_8429_z', password = 'tmp_8429_z', plugin = 'Srp') + +db_a = db_factory(filename = '#gh_8429_alias_a', do_not_create = True, do_not_drop = True) +db_b = db_factory(filename = '#gh_8429_alias_b', do_not_create = True, do_not_drop = True) +db_c = db_factory(filename = '#gh_8429_alias_c', do_not_create = True, do_not_drop = True) + +act_a = python_act('db_a') +act_b = python_act('db_b') +act_c = python_act('db_c') + +#---------------------------------------------------- + +def get_filename_by_alias(act: Action, alias_from_dbconf = None): + + if not alias_from_dbconf: + alias_from_dbconf = act.db.db_path + + # Scan line-by-line through databases.conf, find line starting with and extract name of file that + # must be created in the $(dir_sampleDb)/qa/ folder. This name will be used further as target database (tmp_fdb). + # NOTE: we have to SKIP lines which are commented out, i.e. if they starts with '#': + p_required_alias_ptn = re.compile( '^(?!#)((^|\\s+)' + alias_from_dbconf + ')\\s*=\\s*\\$\\(dir_sampleDb\\)/qa/', re.IGNORECASE ) + fname_in_dbconf = None + + with open(act.home_dir/'databases.conf', 'r') as f: + for line in f: + if p_required_alias_ptn.search(line): + # If databases.conf contains line like this: + # tmp_6147_alias = $(dir_sampleDb)/qa/tmp_core_6147.fdb + # - then we extract filename: 'tmp_core_6147.fdb' (see below): + fname_in_dbconf = Path(line.split('=')[1].strip()).name + break + + # if 'fname_in_dbconf' remains undefined here then propably not equals to specified in the databases.conf! + # + assert fname_in_dbconf + + #------------------------------------------------------------------ + # Full path + filename of database to which we will try to connect: + # + tmp_fdb = Path( act.vars['sample_dir'], 'qa', fname_in_dbconf ) + + return tmp_fdb + +#----------------------------------------------------------------------- + +def run_encr_decr(act: Action, mode, max_wait_encr_thread_finish, capsys): + if mode == 'encrypt': + # See letter from Alex, 15.12.2023 16:16 demo-plugin can not transfer named key over network. + # Because of that, we have to use 'ALTER DATABASE ENCRYPT WITH ' _WITHOUT_ adding 'key "{ENCRYPTION_KEY}"'. + # ::: NB ::: One need to be sure that $FB_HOME/plugins.conf contains following lines: + # Plugin = KH2 { + # Module = $(dir_plugins)/fbSampleKeyHolder + # RegisterName = fbSampleKeyHolder + # Config = KH2 + # } + # Config = KH2 { + # Auto = false + # } + # Otherwise error will raise: + # unsuccessful metadata update + # -ALTER DATABASE failed + # -Missing database encryption key for your attachment + # -Plugin fbSampleDbCrypt: + # -Crypt key not set + # + alter_db_sttm = f'alter database encrypt with "{ENCRYPTION_PLUGIN}"' # <<< ::: NB ::: DO NOT add '... key "{ENCRYPTION_KEY}"' here! + wait_for_state = 'Database encrypted' + elif mode == 'decrypt': + alter_db_sttm = 'alter database decrypt' + wait_for_state = 'Database not encrypted' + + + e_thread_finished = False + + # 0 = non crypted; + # 1 = has been encrypted; + # 2 = is DEcrypting; + # 3 = is Encrypting; + # + REQUIRED_CRYPT_STATE = 1 if mode == 'encrypt' else 0 + current_crypt_state = -1 + d1 = py_dt.timedelta(0) + with act.db.connect() as con: + cur = con.cursor() + ps, rs = None, None + try: + ps = cur.prepare('select mon$crypt_state from mon$database') + t1=py_dt.datetime.now() + d1 = t1-t1 + con.execute_immediate(alter_db_sttm) + con.commit() + while True: + t2=py_dt.datetime.now() + d1=t2-t1 + if d1.seconds*1000 + d1.microseconds//1000 > max_wait_encr_thread_finish: + break + + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + rs = cur.execute(ps) + for r in rs: + ###################################################### + ### C H E C K M O N $ C R Y P T _ S T A T E ### + ###################################################### + current_crypt_state = r[0] + con.commit() + if current_crypt_state == REQUIRED_CRYPT_STATE: + e_thread_finished = True + break + else: + time.sleep(0.5) + except DatabaseError as e: + print( e.__str__() ) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() + + + assert e_thread_finished, f'TIMEOUT EXPIRATION. Mode="{mode}" took {d1.seconds*1000 + d1.microseconds//1000} ms which exceeds limit = {max_wait_encr_thread_finish} ms; current_crypt_state={current_crypt_state}' + + +#----------------------------------------------------------------------- + +@pytest.mark.es_eds +@pytest.mark.encryption +@pytest.mark.version('>=4.0.6') +def test_1(act_a: Action, act_b: Action, act_c: Action, usr_x: User, usr_y: User, usr_z: User, capsys): + + # ::: NB ::: + # Encryption required for db_file_c in order to reproduce problem on FB 6.x + # Because of that, this DB can be inaccessibe something was wrong in previous run of this test. + # Thus we have to create all databases now (rather than use previously created): + # + dbfile_a = get_filename_by_alias(act_a) + dbfile_b = get_filename_by_alias(act_b) + dbfile_c = get_filename_by_alias(act_c) + + dbfile_a.unlink(missing_ok = True) + dbfile_b.unlink(missing_ok = True) + dbfile_c.unlink(missing_ok = True) + + # ::: NB ::: + # 'create_database()' will use LOCAL protocol if $QA_ROOT/firebir-driver.conf has no section [firebird.server.defaults] + # It can be empty but it MUST present if we want database to be created using prefix 'inet://' for its DSN. + # + with create_database(act_a.db.db_path, user = act_a.db.user, password = act_a.db.password) as con_a, \ + create_database(act_b.db.db_path, user = act_b.db.user, password = act_b.db.password) as con_b, \ + create_database(act_c.db.db_path, user = act_c.db.user, password = act_c.db.password) as con_c: + + sql = f""" + create procedure sp_a (a_who varchar(31)) returns (o_info varchar(512)) as + begin + -- e1-meta.sql: + execute statement 'select * from sp_b(''' || a_who || ''')' as user '{usr_x.name}' password '{usr_x.password}' on external 'gh_8429_alias_b' into o_info; + suspend; + end + ^ + grant execute on procedure sp_a to public + ^ + """ + for x in sql.split('^'): + if (s := x.strip()): + con_a.execute_immediate(s) + con_a.commit() + + # .................................................. + + sql = f""" + create procedure sp_b (a_who varchar(31)) returns (o_info varchar(512)) as + begin + execute statement 'select * from sp_c' as user a_who password a_who on external 'gh_8429_alias_c' into o_info; + suspend; + end + ^ + grant execute on procedure sp_b to public + ^ + """ + for x in sql.split('^'): + if (s := x.strip()): + con_b.execute_immediate(s) + con_b.commit() + + # .................................................. + + sql = f""" + create procedure sp_c returns (o_info varchar(512)) as + begin + o_info = lower(current_user); + -- o_info = lower(rdb$get_context('SYSTEM', 'DB_NAME')) || ':' || lower(current_user); + suspend; + end + ^ + grant execute on procedure sp_c to public + ^ + """ + for x in sql.split('^'): + if (s := x.strip()): + con_c.execute_immediate(s) + con_c.commit() + # < close all connections + + ############################################ + ### E N C R Y P T D A T A B A S E ### + ############################################ + # Run encryption. No concurrent connection must be here to db_file_c: + run_encr_decr(act_c, 'encrypt', MAX_WAITING_ENCR_FINISH, capsys) + + with act_a.db.connect() as con_a: + cur = con_a.cursor() + cur.execute(f"select o_info from sp_a('{usr_x.name.lower()}')") + for r in cur: + print(r[0]) + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # this caused crash before fix: + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + with act_a.db.connect() as con_a: + cur = con_a.cursor() + cur.execute(f"select o_info from sp_a('{usr_y.name.lower()}')") + for r in cur: + print(r[0]) + + ######### + # CLEANUP + ######### + for a in (act_a, act_b, act_c): + with a.db.connect() as con: + con.execute_immediate('ALTER EXTERNAL CONNECTIONS POOL CLEAR ALL') + con.commit() + con.drop_database() + + act_a.expected_stdout = f""" + {usr_x.name.lower()} + {usr_y.name.lower()} + """ + act_a.stdout = capsys.readouterr().out + assert act_a.clean_stdout == act_a.clean_expected_stdout + act_a.reset() diff --git a/tests/bugs/gh_8434_test.py b/tests/bugs/gh_8434_test.py new file mode 100644 index 00000000..328f8c56 --- /dev/null +++ b/tests/bugs/gh_8434_test.py @@ -0,0 +1,371 @@ +#coding:utf-8 + +""" +ID: issue-8434 +ISSUE: https://github.com/FirebirdSQL/firebird/pull/8434 +TITLE: Fix implicitly used collations after ALTER DOMAIN +DESCRIPTION: + Test creates two tables ('test1' and 'test2'), both are used to check non-ascii characters; results for Natural and Indexed Reads are verified. + First table is used to check output when 'common' indices are used, second - for check partial indices (and both tables have asc and desc indices). + Every table has text field based on UTF8-domain, initially no collation is specified for it. + Every table has four records with same character: 1) lowercased, with accent; 2) uppercased, with accent; 3) lowercased, "normal"; 4) uppercased, "normal". + First table stores character 'd', second - character 'l'. + We run query against each table with WHERE-expression like ' = ' -- where is in lowercased, "normal" form (i.e. 'd' or 'l'). + + When tables are in 'initial state' (i.e. before any domain altering) then such query must show only ONE record. + Then we change domain definition three times: + 1) 'alter domain ... collate unicode_ci'. + This must cause TWO records to be shown: 'lowercased, "normal"' and 'uppercased, "normal"'. Rows with accented chartacters must NOT be shown. + 2) 'alter domain ... collate unicode_ci_ai'. + This must cause ALL records to be shown, i.e. in "normal" form and with accent. + 3) 'alter domain', i.e. WITHOUT any 'collate' mention. + This must return output to what it was in 'initial state': only one record must be shown. +NOTES: + [13.02.2025] pzotov + ::: NB ::: + Currently only UTF8 charset is checked. + We can NOT check single-byte character sets because some of them have incompleted definitions of ACCENT-/CASE- INSENSITIVE rules. + + For example, in src/intl/collations/pw1251cyrr.h we can see than 'e' with accent characters are defined as a standalone primary code, + not with the same primary code as 'e' without accent. + This causes different comparison result of characters used in French, Russian and Ukrainian alphabets, namely: + 1) 'e' vs 'é' vs 'É' // U+0065; U+00E9; U+00C9 + 2) 'е' vs 'ё' vs 'Ё' // U+0435; U+0451; U+0401 + 3) 'г' vs 'ґ' vs 'Ґ' // U+0433; U+0491; U+0490 + Result for "1" will be true in all cases for ISO8859_1, but result for "2" and "3" will be true only when we compare accent_lower vs accent_upper. + + Explained by Adriano, letter 13.02.2025 13:44. + + Checked on 6.0.0.336. +""" +import sys +import pytest +from firebird.qa import * +from firebird.driver import DatabaseError + +sys.stdout.reconfigure(encoding='utf-8') + +init_sql = """ + create domain dm_utf8 varchar(1) character set utf8; + + create table test1 ( + id int + ,what varchar(50) + ,f_utf8 dm_utf8 + ); + + create table test2 ( + id int + ,what varchar(50) + ,f_utf8 dm_utf8 + ); + + ---------------------- + insert into test1 ( + id + ,what + ,f_utf8 + ) + select + 1, 'lowered "D" w/accent', 'ð' from rdb$database union all select + 2, 'UPPERED "D" w/accent', 'Ð' from rdb$database union all select + 3, 'lowered "D" normal', 'd' from rdb$database union all select + 4, 'UPPERED "D" normal', 'D' from rdb$database + ; + + insert into test2 ( + id + ,what + ,f_utf8 + ) + select + 1, 'lowered "L" w/accent', 'ł' from rdb$database union all select + 2, 'UPPERED "L" w/accent', 'Ł' from rdb$database union all select + 3, 'lowered "L" normal', 'l' from rdb$database union all select + 4, 'UPPERED "L" normal', 'L' from rdb$database + ; + commit; + + -- ........................... + create index test1_asc on test1(f_utf8); + create descending index test1_dec on test1(f_utf8); + + create index test2_partial_asc on test2(f_utf8) where f_utf8 = 'l'; + create index test2_partial_dec on test2(f_utf8) where f_utf8 = 'l'; + commit; + + create view v1_chk_nr as select id, what, f_utf8 from test1 where f_utf8 || '' = 'd' order by id; + create view v1_chk_ir_asc as select id, what, f_utf8 from test1 where f_utf8 = 'd' order by f_utf8; + create view v1_chk_ir_dec as select id, what, f_utf8 from test1 where f_utf8 = 'd' order by f_utf8 desc; + + create view v2_chk_nr as select id, what, f_utf8 from test2 where f_utf8 || '' = 'l' order by id; + create view v2_chk_ir_asc as select id, what, f_utf8 from test2 where f_utf8 = 'l' order by f_utf8; + create view v2_chk_ir_dec as select id, what, f_utf8 from test2 where f_utf8 = 'l' order by f_utf8 desc; + commit; +""" + +db = db_factory(init = init_sql) +act = python_act('db', substitutions = [(r'record length: \d+, key length: \d+', 'record length: NN, key length: MM')]) + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- + +@pytest.mark.version('>=6') +def test_1(act: Action, capsys): + qry_map = { + 1 : 'select * from v1_chk_nr' + ,2 : 'select * from v1_chk_ir_asc' + ,3 : 'select * from v1_chk_ir_dec' + ,4 : 'select * from v2_chk_nr' + ,5 : 'select * from v2_chk_ir_asc' + ,6 : 'select * from v2_chk_ir_dec' + } + + alter_lst = ( + '' + ,'alter domain dm_utf8 type varchar(1) character set utf8 collate unicode_ci' + ,'alter domain dm_utf8 type varchar(1) character set utf8 collate unicode_ci_ai' + ,'alter domain dm_utf8 type varchar(1) character set utf8' + ) + + with act.db.connect(charset = 'utf8') as con: + cur = con.cursor() + for alter_i in alter_lst: + if alter_i.strip(): + con.execute_immediate(alter_i) + con.commit() + print(f'\nAfter {alter_i}:') + else: + print('Initial state:') + + for k, v in qry_map.items(): + ps, rs = None, None + try: + ps = cur.prepare(v) + + print('Query:', v) + # Print explained plan with padding eash line by dots in order to see indentations: + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + print('') + + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + rs = cur.execute(ps) + for r in rs: + print(r[0], r[1]) + except DatabaseError as e: + print(e.__str__()) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() + + + expected_stdout = """ + Initial state: + + Query: select * from v1_chk_nr + Select Expression + ....-> Sort (record length: NN, key length: MM) + ........-> Filter + ............-> Table "TEST1" as "V1_CHK_NR TEST1" Full Scan + 3 lowered "D" normal + Query: select * from v1_chk_ir_asc + Select Expression + ....-> Filter + ........-> Table "TEST1" as "V1_CHK_IR_ASC TEST1" Access By ID + ............-> Index "TEST1_ASC" Range Scan (full match) + 3 lowered "D" normal + Query: select * from v1_chk_ir_dec + Select Expression + ....-> Filter + ........-> Table "TEST1" as "V1_CHK_IR_DEC TEST1" Access By ID + ............-> Index "TEST1_DEC" Range Scan (full match) + 3 lowered "D" normal + Query: select * from v2_chk_nr + Select Expression + ....-> Sort (record length: NN, key length: MM) + ........-> Filter + ............-> Table "TEST2" as "V2_CHK_NR TEST2" Full Scan + 3 lowered "L" normal + Query: select * from v2_chk_ir_asc + Select Expression + ....-> Filter + ........-> Table "TEST2" as "V2_CHK_IR_ASC TEST2" Access By ID + ............-> Index "TEST2_PARTIAL_ASC" Full Scan + 3 lowered "L" normal + Query: select * from v2_chk_ir_dec + Select Expression + ....-> Sort (record length: NN, key length: MM) + ........-> Filter + ............-> Table "TEST2" as "V2_CHK_IR_DEC TEST2" Access By ID + ................-> Bitmap + ....................-> Index "TEST2_PARTIAL_ASC" Full Scan + 3 lowered "L" normal + + + After alter domain dm_utf8 type varchar(1) character set utf8 collate unicode_ci: + + Query: select * from v1_chk_nr + Select Expression + ....-> Sort (record length: NN, key length: MM) + ........-> Filter + ............-> Table "TEST1" as "V1_CHK_NR TEST1" Full Scan + 3 lowered "D" normal + 4 UPPERED "D" normal + Query: select * from v1_chk_ir_asc + Select Expression + ....-> Filter + ........-> Table "TEST1" as "V1_CHK_IR_ASC TEST1" Access By ID + ............-> Index "TEST1_ASC" Range Scan (full match) + 3 lowered "D" normal + 4 UPPERED "D" normal + Query: select * from v1_chk_ir_dec + Select Expression + ....-> Filter + ........-> Table "TEST1" as "V1_CHK_IR_DEC TEST1" Access By ID + ............-> Index "TEST1_DEC" Range Scan (full match) + 4 UPPERED "D" normal + 3 lowered "D" normal + Query: select * from v2_chk_nr + Select Expression + ....-> Sort (record length: NN, key length: MM) + ........-> Filter + ............-> Table "TEST2" as "V2_CHK_NR TEST2" Full Scan + 3 lowered "L" normal + 4 UPPERED "L" normal + Query: select * from v2_chk_ir_asc + Select Expression + ....-> Filter + ........-> Table "TEST2" as "V2_CHK_IR_ASC TEST2" Access By ID + ............-> Index "TEST2_PARTIAL_ASC" Full Scan + 3 lowered "L" normal + 4 UPPERED "L" normal + Query: select * from v2_chk_ir_dec + Select Expression + ....-> Sort (record length: NN, key length: MM) + ........-> Filter + ............-> Table "TEST2" as "V2_CHK_IR_DEC TEST2" Access By ID + ................-> Bitmap + ....................-> Index "TEST2_PARTIAL_ASC" Full Scan + 4 UPPERED "L" normal + 3 lowered "L" normal + + + After alter domain dm_utf8 type varchar(1) character set utf8 collate unicode_ci_ai: + + Query: select * from v1_chk_nr + Select Expression + ....-> Sort (record length: NN, key length: MM) + ........-> Filter + ............-> Table "TEST1" as "V1_CHK_NR TEST1" Full Scan + 1 lowered "D" w/accent + 2 UPPERED "D" w/accent + 3 lowered "D" normal + 4 UPPERED "D" normal + Query: select * from v1_chk_ir_asc + Select Expression + ....-> Filter + ........-> Table "TEST1" as "V1_CHK_IR_ASC TEST1" Access By ID + ............-> Index "TEST1_ASC" Range Scan (full match) + 3 lowered "D" normal + 4 UPPERED "D" normal + 1 lowered "D" w/accent + 2 UPPERED "D" w/accent + Query: select * from v1_chk_ir_dec + Select Expression + ....-> Filter + ........-> Table "TEST1" as "V1_CHK_IR_DEC TEST1" Access By ID + ............-> Index "TEST1_DEC" Range Scan (full match) + 2 UPPERED "D" w/accent + 1 lowered "D" w/accent + 4 UPPERED "D" normal + 3 lowered "D" normal + Query: select * from v2_chk_nr + Select Expression + ....-> Sort (record length: NN, key length: MM) + ........-> Filter + ............-> Table "TEST2" as "V2_CHK_NR TEST2" Full Scan + 1 lowered "L" w/accent + 2 UPPERED "L" w/accent + 3 lowered "L" normal + 4 UPPERED "L" normal + Query: select * from v2_chk_ir_asc + Select Expression + ....-> Filter + ........-> Table "TEST2" as "V2_CHK_IR_ASC TEST2" Access By ID + ............-> Index "TEST2_PARTIAL_ASC" Full Scan + 3 lowered "L" normal + 4 UPPERED "L" normal + 1 lowered "L" w/accent + 2 UPPERED "L" w/accent + Query: select * from v2_chk_ir_dec + Select Expression + ....-> Sort (record length: NN, key length: MM) + ........-> Filter + ............-> Table "TEST2" as "V2_CHK_IR_DEC TEST2" Access By ID + ................-> Bitmap + ....................-> Index "TEST2_PARTIAL_ASC" Full Scan + 2 UPPERED "L" w/accent + 1 lowered "L" w/accent + 4 UPPERED "L" normal + 3 lowered "L" normal + + + After alter domain dm_utf8 type varchar(1) character set utf8: + + Query: select * from v1_chk_nr + Select Expression + ....-> Sort (record length: NN, key length: MM) + ........-> Filter + ............-> Table "TEST1" as "V1_CHK_NR TEST1" Full Scan + 3 lowered "D" normal + Query: select * from v1_chk_ir_asc + Select Expression + ....-> Filter + ........-> Table "TEST1" as "V1_CHK_IR_ASC TEST1" Access By ID + ............-> Index "TEST1_ASC" Range Scan (full match) + 3 lowered "D" normal + Query: select * from v1_chk_ir_dec + Select Expression + ....-> Filter + ........-> Table "TEST1" as "V1_CHK_IR_DEC TEST1" Access By ID + ............-> Index "TEST1_DEC" Range Scan (full match) + 3 lowered "D" normal + Query: select * from v2_chk_nr + Select Expression + ....-> Sort (record length: NN, key length: MM) + ........-> Filter + ............-> Table "TEST2" as "V2_CHK_NR TEST2" Full Scan + 3 lowered "L" normal + Query: select * from v2_chk_ir_asc + Select Expression + ....-> Filter + ........-> Table "TEST2" as "V2_CHK_IR_ASC TEST2" Access By ID + ............-> Index "TEST2_PARTIAL_ASC" Full Scan + 3 lowered "L" normal + Query: select * from v2_chk_ir_dec + Select Expression + ....-> Sort (record length: NN, key length: MM) + ........-> Filter + ............-> Table "TEST2" as "V2_CHK_IR_DEC TEST2" Access By ID + ................-> Bitmap + ....................-> Index "TEST2_PARTIAL_ASC" Full Scan + 3 lowered "L" normal + """ + + act.expected_stdout = expected_stdout + + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8435_test.py b/tests/bugs/gh_8435_test.py new file mode 100644 index 00000000..6e0a1e54 --- /dev/null +++ b/tests/bugs/gh_8435_test.py @@ -0,0 +1,37 @@ +#coding:utf-8 + +""" +ID: issue-8435 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8435 +TITLE: Error "Can not transliterate character" with text concatention utf8 + win1250 +DESCRIPTION: +NOTES: + [31.05.2025] pzotov + Confirmed bug on 6.0.0.702. + Checked on intermediate snapshot 6.0.0.707-487f423 + Thanks to Adriano for provided example to reproduce bug. +""" +import pytest +from firebird.qa import * + +db = db_factory() +CHECKED_TEXT = "Kwota faktury płatna wyłącznie na rachunek ING Commercial Finance Polska S.A., ul. Malczewskiego 45, 02-622 Warszawa, www.ingcomfin.pl Bank: ING Bank Śląski rachunek nr: PL33 1050 0099 5381 0000 1000 9471 któremu zbyliśmy nasze wierzytelności łącznie z niniejszą." + +test_script = f""" + set blob all; + set list on; + select _utf8 'u' || cast(_utf8 '{CHECKED_TEXT}' as blob character set win1250) blob_id from rdb$database; +""" + +act = isql_act('db', test_script, substitutions=[ ('BLOB_ID .*', ''), ('[ \\t]+', ' ') ]) + +@pytest.mark.intl +@pytest.mark.version('>=6.0') +def test_1(act: Action): + + expected_stdout = f""" + u{CHECKED_TEXT} + """ + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8437_test.py b/tests/bugs/gh_8437_test.py new file mode 100644 index 00000000..457b7b11 --- /dev/null +++ b/tests/bugs/gh_8437_test.py @@ -0,0 +1,43 @@ +#coding:utf-8 + +""" +ID: issue-8437 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8437 +TITLE: Segmentation fault when running query with partition by and subquery +DESCRIPTION: +NOTES: + [24.02.2025] pzotov + Confirmed bug on 6.0.0.647-9fccb55; 5.0.3.1619-81c5f17; 4.0.6.3188-8ee1ca8 + Checked on 6.0.0.652-226622f; 5.0.3.1622-c1a518f; 4.0.6.3189-3fb0bbf - all fine. + + [18.03.2025] pzotov + Confirmed crash on 3.0.13.33798; checked on 3.0.13.33804 after fix #e02eaffb. + Reduced min_version from 4.0.6 to 3.0.13. + + [25.04.2024] pzotov + Added temporary mark 'disabled_in_forks' to SKIP this test when QA runs agains *fork* of standard FB. +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set heading off; + select row_number()over(partition by (select 1 from rdb$database)) from rdb$database; +""" + +act = isql_act('db', test_script) + +expected_stdout = """ + 1 +""" + +@pytest.mark.disabled_in_forks +@pytest.mark.version('>=3.0.13') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout + diff --git a/tests/bugs/gh_8444_test.py b/tests/bugs/gh_8444_test.py new file mode 100644 index 00000000..5bac906c --- /dev/null +++ b/tests/bugs/gh_8444_test.py @@ -0,0 +1,65 @@ +#coding:utf-8 + +""" +ID: issue-8444 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8444 +TITLE: GBAK: GDS error batch_big_seg2 when restoring a table with many BLOBs +DESCRIPTION: +NOTES: + [28.02.2025] pzotov + 1) restore must use remote protocol, i.e. 'localhost:' must be specified in DSN of target FDB. + 2) in case of success test executes for ~60 seconds. + Confirmed problem on 6.0.0.655-6e3e059, 5.0.3.1624-00b699c: restore terminates prematurely, gbak crashes (with retcode=3221225477). + Checked on 6.0.0.656-25fb454 +""" +from pathlib import Path +import zipfile +import locale +import re +import pytest +from firebird.qa import * + +db = db_factory() +act = python_act('db') +tmp_fbk = temp_file('gh_8444.tmp.fbk') +tmp_fdb = temp_file('gh_8444.tmp.fdb') + +expected_stdout = """ + gbak:finishing, closing, and going home + gbak:adjusting the ONLINE and FORCED WRITES flags +""" + +@pytest.mark.version('>=6.0') +def test_1(act: Action, tmp_fbk: Path, tmp_fdb: Path, capsys): + zipped_fbk_file = zipfile.Path(act.files_dir / 'gh_8444.zip', at = 'gh_8444.fbk') + tmp_fbk.write_bytes(zipped_fbk_file.read_bytes()) + + watching_patterns = \ + ( + 'gbak:finishing, closing, and going home' + ,'gbak:adjusting the ONLINE and FORCED WRITES flags' + ) + watching_patterns = [ re.compile(p, re.IGNORECASE) for p in watching_patterns ] + + # restore _WITHOUT_ building indices: + act.gbak(switches=['-rep', '-i', '-verbint', '1000', str(tmp_fbk), 'localhost:' + str(tmp_fdb) ], combine_output = True, io_enc = locale.getpreferredencoding()) + print(f'Restore finished with retcode = {act.return_code}') + if act.return_code == 0: + # Print only interesting lines from gbak tail: + for line in act.clean_stdout.splitlines(): + for p in watching_patterns: + if p.search(line): + print(line) + else: + # If retcode !=0 then we can print the whole output of failed gbak: + for line in act.clean_stdout.splitlines(): + print(line) + act.reset() + + act.expected_stdout = """ + Restore finished with retcode = 0 + gbak:finishing, closing, and going home + gbak:adjusting the ONLINE and FORCED WRITES flags + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8452_test.py b/tests/bugs/gh_8452_test.py new file mode 100644 index 00000000..7bdafae7 --- /dev/null +++ b/tests/bugs/gh_8452_test.py @@ -0,0 +1,157 @@ +#coding:utf-8 + +""" +ID: issue-8452 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8452 +TITLE: Disallow creation of object names with only spaces +DESCRIPTION: +NOTES: + [03.04.2025] pzotov + 1. Test verifies ability to create DB with name = that belongs + to following unicode ranges: + * [SPACE CHARACTER] // ascii-code = 32 + * https://www.compart.com/en/unicode/category/Cc : BEL, TAB, LF, CR + * https://www.compart.com/en/unicode/category/Cf : 0xAD ("soft hyhen") + * https://www.compart.com/en/unicode/category/Zl : 0x2028, Line Separator + * https://www.compart.com/en/unicode/category/Zp : 0x2029, Paragraph Separator + + 2. Currently FB *allows* to create databases with single characters from these range, + except space (ascii 32) -- this is prohibited after fix. + Characters from other ranges are NOT prohibited to be used as DB name! + See comment by Adriano: + https://github.com/FirebirdSQL/firebird/pull/8473#issuecomment-2738617159 + + Following unicode category currently SKIPPED: + * https://www.compart.com/en/unicode/category/Cs : (0xD800, 0xDB7F) High Surrogates + (got: SyntaxError: (unicode error) 'utf-8' codec can't decode byte 0xed in position 2061: invalid continuation byte) + + 3. Ability to use space-only names in PSQL currently not checked. + + Database which name ' ' (single space char) could be created up to 6.0.0.677. + Checked on 6.0.0.710 - "spaces-only" DB name no more allowed. +""" +import pytest +from firebird.qa import * + +db = db_factory() + +act = python_act('db', substitutions = [('-At line \\d+, column \\d+', '')]) + +@pytest.mark.version('>=6.0') +def test_1(act: Action): + + + test_sql = """ + -- from https://github.com/FirebirdSQL/firebird/issues/8452: + -- A is any character of the source language + -- character set other than a that is not included in the + -- Unicode General Categories “Cc”, “Cf”, “Cn”, “Cs”, “Zl”, or “Zp”. + + -- https://www.compart.com/en/unicode/category/Cc : BEL, TAB, LF, CR + -- https://www.compart.com/en/unicode/category/Cf : 0xAD ("soft hyhen") + -- https://www.compart.com/en/unicode/category/Cs : (0xD800, 0xDB7F) High Surrogates + -- https://www.compart.com/en/unicode/category/Zl : 0x2028, Line Separator + -- https://www.compart.com/en/unicode/category/Zp : 0x2029, Paragraph Separator + + set bail off; + + -- SPACE character only: + recreate table " " (id int); + + -- ############################################ + + -- Cc category: + + -- "t" + CR + LF: + recreate table "t + " (id int); + comment on table "t + " is '"t" + CR + LF'; + + -- CR + LF: + recreate table " + " (id int); + comment on table " + " is 'CR + LF'; + + -- LF only: + recreate table " + " (id int); + comment on table " + " is 'Cc: LF only'; + + -- CR only: + recreate table " + " (id int); + comment on table " + " is 'Cc: CR only'; + + -- TAB character: + recreate table " " (id int); + comment on table " " is 'Cc: TAB character'; + + -- BEL character: + recreate table "" (id int); + comment on table "" is 'Cc: BEL character'; + + -- ############################################ + + -- Cf category: + -- `SHY;` aka SoftHyphen + recreate table "­" (id int); + comment on table "­" is 'Cf: SoftHyphen'; + + -- ############################################ + + -- Zl category: + recreate table "
" (id int); -- 0x2028 + comment on table "
" is 'Zl: Line separator'; + + -- ############################################ + + -- Zp category: + recreate table "
" (id int); -- 0x2029 + comment on table "
" is 'Zp: Paragraph Separator'; + + commit; + + set count on; + set list on; + select unicode_val(rdb$relation_name) as rel_name_uval, cast(rdb$description as varchar(1024)) as rel_comment from rdb$relations + where rdb$system_flag is distinct from 1 + order by rdb$relation_id + ; + set count off; + """ + + expected_stdout = f""" + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Zero length identifiers are not allowed + + REL_NAME_UVAL 116 + REL_COMMENT "t" + CR + LF + + REL_NAME_UVAL 10 + REL_COMMENT Cc: CR only + + REL_NAME_UVAL 9 + REL_COMMENT Cc: TAB character + + REL_NAME_UVAL 7 + REL_COMMENT Cc: BEL character + + REL_NAME_UVAL 173 + REL_COMMENT Cf: SoftHyphen + + REL_NAME_UVAL 8232 + REL_COMMENT Zl: Line separator + + REL_NAME_UVAL 8233 + REL_COMMENT Zp: Paragraph Separator + Records affected: 7 + """ + act.expected_stdout = expected_stdout + act.isql(input = test_sql, combine_output=True, charset = 'utf8') + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8454_test.py b/tests/bugs/gh_8454_test.py new file mode 100644 index 00000000..5a6f8e2a --- /dev/null +++ b/tests/bugs/gh_8454_test.py @@ -0,0 +1,41 @@ +#coding:utf-8 + +""" +ID: issue-8437 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8437 +TITLE: incorrect behavior of FF1...FF4 patterns in CAST FORMAT for string to datetime conversion +DESCRIPTION: + +NOTES: + [27.03.2025] pzotov + Original ticket title: "Input FORMAT of second fractions" + Confirmed bug on 6.0.0.656-25fb454. + Checked fix on 6.0.0.698-6c21404. +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + select + timestamp '1-1-1 1:1:1.1' as source_timestamp_literal, + cast('1-1-1 1:1:1.1' as timestamp format 'yyyy-mm-dd hh24:mi:ss.ff4') string_as_ts_with_format + from rdb$database; +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +expected_stdout = """ + SOURCE_TIMESTAMP_LITERAL 2001-01-01 01:01:01.1000 + STRING_AS_TS_WITH_FORMAT 0001-01-01 01:01:01.1000 +""" + +@pytest.mark.version('>=6.0') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout + diff --git a/tests/bugs/gh_8462_test.py b/tests/bugs/gh_8462_test.py new file mode 100644 index 00000000..f81daae7 --- /dev/null +++ b/tests/bugs/gh_8462_test.py @@ -0,0 +1,66 @@ +#coding:utf-8 + +""" +ID: issue-8462 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8462 +TITLE: A user with "GRANT_REVOKE_ON_ANY_OBJECT" privilege can't revoke a role from himself if he is not a grantor +DESCRIPTION: +NOTES: + [07.03.2025] pzotov + Confirmed issue on 6.0.0.658. + Checked on 6.0.0.660-6cbd3aa; 5.0.3.1630-81e0de9; 4.0.6.3190 -- all OK. +""" + +import pytest +from firebird.qa import * + +db = db_factory() +tmp_user = user_factory('db', name = 'tmp$8462', password = '123') +tmp_role = role_factory('db', name = 'role_8462') + +act = python_act('db') + +@pytest.mark.version('>=4.0.6') +def test_1(act: Action, tmp_user: User, tmp_role: Role): + + test_sql = f""" + set list on; + set term ^; + execute block as + begin + execute statement 'drop role {tmp_role.name}'; + when any do + begin + --- nop --- + end + end + ^ + set term ;^ + commit; + + grant RDB$ADMIN to {tmp_user.name}; + + commit; + connect '{act.db.dsn}' user {tmp_user.name} password '{tmp_user.password}' role rdb$admin; + + create role {tmp_role.name}; + grant {tmp_role.name} to {act.db.user}; + + commit; + connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; + + set count on; + + --select * from rdb$user_privileges p where p.rdb$relation_name = upper('{tmp_role.name}'); + revoke {tmp_role.name} from {act.db.user}; + commit; + select * from rdb$user_privileges p where p.rdb$relation_name = upper('{tmp_role.name}'); + """ + + expected_stdout = """ + Records affected: 0 + """ + act.expected_stdout = expected_stdout + act.isql(input = test_sql, combine_output=True) + assert act.clean_stdout == act.clean_expected_stdout + diff --git a/tests/bugs/gh_8469_test.py b/tests/bugs/gh_8469_test.py new file mode 100644 index 00000000..de677cc7 --- /dev/null +++ b/tests/bugs/gh_8469_test.py @@ -0,0 +1,50 @@ +#coding:utf-8 + +""" +ID: issue-8437 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8437 +TITLE: Throw exception for non-existing date in string to datetime conversion +DESCRIPTION: + +NOTES: + [27.03.2025] pzotov + Confirmed bug on 6.0.0.656-25fb454: wrong date was silently ignored and 'next day' was shown: + 0001-10-01 01:01:01.0001 + 9999-03-01 00:00:00.0000 + Checked fix on 6.0.0.698-6c21404. +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + select + cast('1-09-31 1:1:1.1' as timestamp format 'yyyy-mm-dd hh24:mi:ss.ff4') + from rdb$database + ; + + select + cast('9999-2-29' as timestamp format 'yyyy-mm-dd') + from rdb$database + ; +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +expected_stdout = """ + Statement failed, SQLSTATE = 22018 + conversion error from string "1-09-31 1:1:1.1" + + Statement failed, SQLSTATE = 22018 + conversion error from string "9999-2-29" +""" + +@pytest.mark.version('>=6.0') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout + diff --git a/tests/bugs/gh_8475_test.py b/tests/bugs/gh_8475_test.py new file mode 100644 index 00000000..13222fa9 --- /dev/null +++ b/tests/bugs/gh_8475_test.py @@ -0,0 +1,72 @@ +#coding:utf-8 + +""" +ID: issue-8475 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8475 +TITLE: Wrong message about invalid time zone in CAST FORMAT +DESCRIPTION: +NOTES: + [02.04.2025] pzotov + 1. See also: + https://github.com/FirebirdSQL/firebird/issues/2388 + 2. An issue found, see: + https://github.com/FirebirdSQL/firebird/issues/8475#issuecomment-2772324636 + (appropriate statement was commented for now) + + Confirmed problem on 6.0.0.687-730aa8f (22-mar-2025). + Checked on 6.0.0.710-40651f6. +""" +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = f""" + set bail OFF; + set heading off; + select cast ('2005-03-16 01:02:03.1234 +01:00'as timestamp with time zone format 'YYYY-MM-DD HH24:MI:SS.FF4 TZR') from rdb$database; + select cast ('12:30 2:30' as time with time zone format 'HH24:MI TZR') from rdb$database; + select cast ('12:30 -2:30' as time with time zone format 'HH24:MI TZR') from rdb$database; + select cast ('2:30 0:30 A.M.' as time with time zone format 'HH:MI TZR A.M.') from rdb$database; + select cast ('00:60' as time with time zone format 'TZH:TZM') from rdb$database; + select cast ('-9:-99' as time with time zone format 'TZH:TZM') from rdb$database; + select cast ('-14:01' as time with time zone format 'TZH:TZM') from rdb$database; + select cast ('12 12' as time with time zone format 'HH24 HH24') from rdb$database; + select cast ('2025-02-30' as date format 'YYYY-MM-DD') from rdb$database; + -- COMMENTED TEMPORARY (?), SEE + -- https://github.com/FirebirdSQL/firebird/issues/8475#issuecomment-2772324636 + -- select cast ('00:2147483648' as time with time zone format 'TZH:TZM') from rdb$database; +""" + +act = isql_act('db', test_script) + +@pytest.mark.version('>=6.0') +def test_1(act: Action): + + expected_stdout = f""" + 2005-03-16 01:02:03.1234 +01:00 + + 12:30:00.0000 +02:30 + + 12:30:00.0000 -02:30 + + 02:30:00.0000 +00:30 + + Statement failed, SQLSTATE = HY000 + Value for TZM pattern is out of range [0, 59] + + Statement failed, SQLSTATE = HY000 + Value for TZM pattern is out of range [0, 59] + + Statement failed, SQLSTATE = 22009 + Invalid time zone offset: -14:01 - must use format +/-hours:minutes and be between -14:00 and +14:00 + + Statement failed, SQLSTATE = HY000 + Cannot use the same pattern twice: HH24 + + Statement failed, SQLSTATE = 22018 + conversion error from string "2025-02-30" + """ + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8477_test.py b/tests/bugs/gh_8477_test.py new file mode 100644 index 00000000..43f4afa0 --- /dev/null +++ b/tests/bugs/gh_8477_test.py @@ -0,0 +1,83 @@ +#coding:utf-8 + +""" +ID: issue-8477 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8477 +TITLE: Inheritance of WINDOW does not work +DESCRIPTION: +NOTES: + [19.03.2025] pzotov + Confirmed bug (wrong data in 'error_sum' field) on 6.0.0.680-90d2983 (18-mar-2025 20:23). + Checked on intermediate snapshot 6.0.0.680-9178ee6 (19-mar-2025 15:23) -- all fine. + + [20.03.2025] pzotov + Checked on 5.0.3.1633-25a0817, 4.0.6.3192-91e3c11; reduced min_version to '4.0.6'. +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + recreate table test(i int, j int); + insert into test values(1, 10); + insert into test values(2, 20); + insert into test values(3, 30); + insert into test values(4, 40); + insert into test values(5, 50); + + select i, j, + sum(j) over (w2) as error_sum, -- looks like no partition + sum(j) over (w1 order by j) as correct1, + sum(j) over (partition by i order by j) as correct2, + sum(j) over (order by j) as like_error_sum + from test + window + w1 as (partition by i), + w2 as (w1 order by j) + ; +""" + +act = isql_act('db', test_script, substitutions = [('[ \t]+', ' ')]) + +expected_stdout = """ + I 1 + J 10 + ERROR_SUM 10 + CORRECT1 10 + CORRECT2 10 + LIKE_ERROR_SUM 10 + I 2 + J 20 + ERROR_SUM 20 + CORRECT1 20 + CORRECT2 20 + LIKE_ERROR_SUM 30 + I 3 + J 30 + ERROR_SUM 30 + CORRECT1 30 + CORRECT2 30 + LIKE_ERROR_SUM 60 + I 4 + J 40 + ERROR_SUM 40 + CORRECT1 40 + CORRECT2 40 + LIKE_ERROR_SUM 100 + I 5 + J 50 + ERROR_SUM 50 + CORRECT1 50 + CORRECT2 50 + LIKE_ERROR_SUM 150 +""" + +@pytest.mark.version('>=4.0.6') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout + diff --git a/tests/bugs/gh_8485_test.py b/tests/bugs/gh_8485_test.py new file mode 100644 index 00000000..d72492fc --- /dev/null +++ b/tests/bugs/gh_8485_test.py @@ -0,0 +1,62 @@ +#coding:utf-8 + +""" +ID: issue-8485 +ISSUE: https://github.com/FirebirdSQL/firebird/pull/8485 +TITLE: Segfault/AV on incorrect databases.conf starting with subconfig (without line alias=database_path) +DESCRIPTION: + Test makes temporary copy of $FB_HOME/databases.conf (assuming that QA account has access to this folder in order to overwrite content of this file). + Then we save invalid content in the databases.conf and try to establish connection to test DB using isql. + Before fix isql crashed, after fix "SQLSTATE = XX000 / databases.conf: illegal line" must be issued (twise). +NOTES: + [03.04.2025] pzotov + ### NOTE ### QA must run with access rights to $FB_HOME folder because databases.conf will be temporary overwritten. + + Confirmed bug (isql crashes): on 6.0.0.693; 5.0.3.1633; 4.0.6.3192; 3.0.13.33804 + Checked on 6.0.0.710; 5.0.3.1639; 4.0.6.3194; 3.0.13.33806 +""" +from pathlib import Path +import shutil +import time +import pytest +from firebird.qa import * + +db = db_factory() +act = python_act('db') +tmp_file = temp_file('databases-conf.copy') + +WRONG_DBCONF = """ +#security.db = $(dir_secDb)/security3.fdb +{ + RemoteAccess = false + DefaultDbCachePages = 320 + LockMemSize = 2M +} +""" + +@pytest.mark.skip("INVESTIGATION NEEDED. Weird affecting on functional/syspriv/test_user_management_in_selfsec_db.py") +@pytest.mark.version('>=3.0.13') +def test_1(act: Action, tmp_file: Path, store_config: ConfigManager, capsys): + + store_config.replace('databases.conf', WRONG_DBCONF) + try: + act.isql(switches = ['-q'], input = f'connect {act.db.dsn};', combine_output = True) + except Error as e: + # Despite crash, no messages were issued here before fix. + print(e) + + for line in act.stdout.splitlines(): + if (pos := line.lower().find('databases.conf')) > 0: + print(line.lower()[pos:]) + else: + print(line) + + act.expected_stdout = f""" + Statement failed, SQLSTATE = XX000 + databases.conf: illegal line + Statement failed, SQLSTATE = XX000 + databases.conf: illegal line + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() diff --git a/tests/bugs/gh_8491_test.py b/tests/bugs/gh_8491_test.py new file mode 100644 index 00000000..f0704f70 --- /dev/null +++ b/tests/bugs/gh_8491_test.py @@ -0,0 +1,58 @@ +#coding:utf-8 + +""" +ID: issue-8491 +ISSUE: https://github.com/FirebirdSQL/firebird/pull/8491 +TITLE: Fix reset of db guid in fixup +DESCRIPTION: +NOTES: + [02.04.2025] pzotov + Confirmed regression starting from 6.0.0.647-9fccb55 (21.02.2025); on 6.0.0.640-9b8ac53 result was Ok. + Confirmed bug on 6.0.0.708-cb06990 (31.03.2025) + Checked on 6.0.0.710-40651f6 -- all OK. +""" +from pathlib import Path +import locale + +import pytest +from firebird.qa import * + +db = db_factory() +act = python_act('db') + +@pytest.mark.version('>=6.0') +def test_1(act: Action, capsys): + + with act.db.connect() as con: + cur = con.cursor() + cur.execute("select rdb$get_context('SYSTEM', 'DB_GUID') from rdb$database") + db_guid_ini = cur.fetchall()[0][0] + + act.nbackup(switches=['-L', act.db.db_path], io_enc = locale.getpreferredencoding(), combine_output = True) + assert act.return_code == 0,f'Attempt to lock DB failed:\n{act.clean_stdout}' + act.reset() + + act.nbackup(switches=['-F', act.db.db_path], io_enc = locale.getpreferredencoding(), combine_output = True) + assert act.return_code == 0,f'Attempt to fixup DB failed:\n{act.clean_stdout}' + act.reset() + + with act.db.connect() as con: + cur = con.cursor() + cur.execute("select rdb$get_context('SYSTEM', 'DB_GUID') from rdb$database") + db_guid_cur = cur.fetchall()[0][0] + + act.reset() + EXPECTED_MSG = 'Expected: database GUID has changed after fixup.' + if db_guid_ini != db_guid_cur: + print(EXPECTED_MSG) + else: + print('Database GUID did NOT change:') + print('Initial:',db_guid_ini) + print('Current:',db_guid_ini) + + act.expected_stdout = f""" + {EXPECTED_MSG} + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() diff --git a/tests/bugs/gh_8498_test.py b/tests/bugs/gh_8498_test.py new file mode 100644 index 00000000..0faa5853 --- /dev/null +++ b/tests/bugs/gh_8498_test.py @@ -0,0 +1,272 @@ +#coding:utf-8 + +""" +ID: issue-8498 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8498 +TITLE: Range-based FOR statement +DESCRIPTION: + Test check issues from doc/sql.extensions/README.range_based_for.md: + * ability to pass parameter for ; + * ability to use values near bounds for appropriate datatype and raised error; + * error in case if non-exact numeric types are used for ``, ``, `` and ``; + * error if range-based FOR BY argument not positive (and not null); + * caching of ``, `` and `` + * etc + Basic test see in functional/gtcs/test_fb_sql_for_range.py +NOTES: + [08.04.2025] pzotov + Checked on 6.0.0.717-f5b6b0c +""" +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = f""" + set list on; + create sequence g_init; + create sequence g_fini; + create sequence g_step; + create table test(id int128 primary key); + commit; + set term ^; + + ---------------------------------------- + -- check doc: + -- FOR = TO ... + -- ... + -- "`` also accepts parameters" + execute block as + declare init_value int = 11; + begin + execute statement + ( + q'# + execute block (a_param int = ?) as + declare x smallint; + begin + for a_param = a_param downto a_param - 2 do insert into test(id) values( 2 * :a_param - 1); + end + #' + ) (init_value) + ; + end + ^ + -- must issue: ID_1 = 21; 19; 17 + select t.id as id_1 from test t + ^ + rollback + ^ + ---------------------------------------- + + -- check ability to use boundary values and error in case if they exceeed appropriate datatype limit + -- must raise (in STDOUT): + -- counter_1 = 32767 + -- raised_gds = 335544321 // arithmetic exception, numeric overflow, or string truncation + execute block returns(counter_1 smallint, raised_gds int) as + begin + begin + raised_gds = null; + for counter_1 = 32767 to 170141183460469231731687303715884105727 do + begin + -- nop -- + end + when any do + begin + raised_gds = gdscode; + end + end + suspend; + end + ^ + + -- ``, ``, `` and `` + -- must be expressions of exact numeric types. + -- must raise compile-time error: + -- Statement failed, SQLSTATE = 42000 + -- Dynamic SQL Error + -- -Arguments for range-based FOR must be exact numeric types + -- (stdout remains empty) + execute block returns(counter_2 smallint, raised_gds int) as + begin + begin + raised_gds = null; + for counter_2 = 3 to 4 by 1e0 do + begin + -- nop -- + end + when any do + begin + raised_gds = gdscode; + end + end + suspend; + end + ^ + + -- `` ... If it is zero or negative, an error is raised. + -- Statement failed, SQLSTATE = 42000 + -- Range-based FOR BY argument must be positive + -- Raises to STDOUT: + -- COUNTER_3 4 + -- RAISED_GDS 335545314 // isc_range_for_by_should_be_positive (see src/include/gen/Firebird.pas) + execute block returns(counter_3 smallint, raised_gds int) as + declare x smallint = 0; + begin + begin + raised_gds = null; + for counter_3 = 4 to 3 by x do + begin + -- nop -- + end + when any do + begin + raised_gds = gdscode; + end + end + suspend; + end + ^ + + -- `` is evaluated and assigned to ``. + -- If it is `NULL`, the loop is not executed. + -- Issues to STDOUT: counter_4 + execute block returns(counter_4 smallint) as + begin + begin + for counter_4 = 1 to 3 do + begin + counter_4 = null; + suspend; + end + end + end + ^ + + -- must be cached, so its assigning to null must not stop loop. + -- STDOUT must contain: counter_5 = 1; 2; 3 + execute block returns(counter_5 smallint) as + declare n smallint = 3; + begin + begin + for counter_5 = 1 to n do + begin + n = null; + suspend; + end + end + end + ^ + + -- must be cached, so its assigning to null must not stop loop. + -- STDOUT must contain: counter_5 = 1; 2; 3 + execute block returns(counter_6 smallint) as + declare x smallint = 1; + begin + begin + for counter_6 = 1 to 3 by x do + begin + x = null; + suspend; + end + end + end + ^ + + -- must issue: 4; 10; 22 + execute block returns (counter_7 integer) + as + begin + for counter_7 = 2 to 16 do + begin + counter_7 = counter_7 * 2; + suspend; + end + + end + ^ + + -- must issue: 8; 3; 1 + execute block returns (counter_8 integer) + as + begin + for counter_8 = 16 downto 2 do + begin + counter_8 = counter_8 / 2; + suspend; + end + end + ^ + + -- must issue: 1; 2; 3; 4. + -- generators must have values: g_init = 1; g_fini = 4; g_step = 1 (because of caching , , ) + execute block returns (counter_9 integer) + as + begin + for counter_9 = gen_id(g_init,1) to gen_id(g_fini,4) by gen_id(g_step,1) do + begin + suspend; + end + end + ^ + + select + gen_id(g_init,0) as g_init_value + ,gen_id(g_fini,0) as g_fini_value + ,gen_id(g_step,0) as g_step_value + from rdb$database + ^ + +""" + +act = isql_act('db', test_script, substitutions=[ ('BLOB_ID .*', ''), ('[ \\t]+', ' ') ]) + +@pytest.mark.intl +@pytest.mark.version('>=6.0') +def test_1(act: Action): + + expected_stdout = f""" + ID_1 21 + ID_1 19 + ID_1 17 + + COUNTER_1 32767 + RAISED_GDS 335544321 + + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -Arguments for range-based FOR must be exact numeric types + + COUNTER_3 4 + RAISED_GDS 335545314 + + COUNTER_4 + + COUNTER_5 1 + COUNTER_5 2 + COUNTER_5 3 + + COUNTER_6 1 + COUNTER_6 2 + COUNTER_6 3 + + COUNTER_7 4 + COUNTER_7 10 + COUNTER_7 22 + + COUNTER_8 8 + COUNTER_8 3 + COUNTER_8 1 + + COUNTER_9 1 + COUNTER_9 2 + COUNTER_9 3 + COUNTER_9 4 + + G_INIT_VALUE 1 + G_FINI_VALUE 4 + G_STEP_VALUE 1 + """ + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8501_test.py b/tests/bugs/gh_8501_test.py new file mode 100644 index 00000000..a42d5f97 --- /dev/null +++ b/tests/bugs/gh_8501_test.py @@ -0,0 +1,218 @@ +#coding:utf-8 + +""" +ID: issue-8501 +ISSUE: https://github.com/FirebirdSQL/firebird/pull/8501 +TITLE: fix(cast-format): Throw exception when value cannot be found for specific pattern in string to datetime conversion +DESCRIPTION: + Commit: + https://github.com/FirebirdSQL/firebird/commit/90e0f49acac2f317757b9ebecf8935692a2b46df + Examples taken from src/common/tests/CvtTest.cpp: + https://github.com/FirebirdSQL/firebird/pull/8501/files#diff-4c41a1e71a7a12168ddce05a6e570ee08d64e36c981fca8e7274ebac4c41edb1 + Also (for "Handle integer overflow when converting string to int") see: + https://github.com/FirebirdSQL/firebird/issues/8475#issuecomment-2772324636 +NOTES: + [11.04.2025] pzotov + + Following queries display THE SAME error messages before and after fix: + * select cast('00:60' as time with time zone format 'TZR') from rdb$database + * select cast('15:00' as time format 'TZH:TZM') from rdb$database + * select cast('15:00' as time format 'TZR') from rdb$database + * select cast('-15:00' as time format 'TZH:TZM') from rdb$database + * select cast('-15:00' as time format 'TZR') from rdb$database + It is unclear for what reason they were added into src/common/tests/CvtTest.cpp + + Confirmed wrong output (w/o errors or with different messages) on 6.0.0.722. + Checked on 6.0.0.725 +""" +from firebird.driver import DatabaseError +import pytest +from firebird.qa import * + +db = db_factory() + +# NB: all following expression must issue errors: +# ################# +query_map = { + + # These must issue: + # Cannot find value in input string for "..." pattern + 1000 : ( "select cast('Apr' as date format 'Y MON') from rdb$database" ) + ,1010 : ( "select cast('Apr' as date format 'YY MON') from rdb$database" ) + ,1020 : ( "select cast('Apr' as date format 'YYY MON') from rdb$database" ) + ,1030 : ( "select cast('Apr' as date format 'YYYY MON') from rdb$database" ) + ,1040 : ( "select cast('Apr' as date format 'YEAR MON') from rdb$database" ) + ,1050 : ( "select cast('Apr' as date format 'RR MON') from rdb$database" ) + ,1060 : ( "select cast('Apr' as date format 'RRRR MON') from rdb$database" ) + ,1070 : ( "select cast('Apr' as date format 'MM MON') from rdb$database" ) + ,1080 : ( "select cast('Apr' as date format 'DD MON') from rdb$database" ) + ,1090 : ( "select cast('Apr' as date format 'J MON') from rdb$database" ) + + # These must issue: + # * Invalid time zone offset: 2147483647 - must use format +/-hours:minutes and be between -14:00 and +14:00 + # OR + # * Value for TZR pattern is out of range [0, 59] + # See also: https://github.com/FirebirdSQL/firebird/issues/8475#issuecomment-2772324636: + ,2000 : ( "select cast('9999999999999999999999999999999999999:00' as time with time zone format 'TZH:TZM') from rdb$database" ) + ,2010 : ( "select cast('9999999999999999999999999999999999999:00' as time with time zone format 'TZR') from rdb$database" ) + ,2020 : ( "select cast('-9999999999999999999999999999999999999:00' as time with time zone format 'TZH:TZM') from rdb$database" ) + ,2030 : ( "select cast('-9999999999999999999999999999999999999:00' as time with time zone format 'TZR') from rdb$database" ) + ,2040 : ( "select cast('00:9999999999999999999999999999999999999' as time with time zone format 'TZH:TZM') from rdb$database" ) + ,2050 : ( "select cast('00:9999999999999999999999999999999999999' as time with time zone format 'TZR') from rdb$database" ) + ,2060 : ( "select cast('00:-9999999999999999999999999999999999999' as time with time zone format 'TZH:TZM') from rdb$database;" ) + ,2070 : ( "select cast('00:-9999999999999999999999999999999999999' as time with time zone format 'TZR') from rdb$database" ) + + # These issue the same as in previous snapshot: + # * Value for TZR pattern is out of range [0, 59] + # OR + # * Cannot use format with current date type + ,3000 : ( "select cast('00:60' as time with time zone format 'TZR') from rdb$database" ) + ,3010 : ( "select cast('15:00' as time format 'TZH:TZM') from rdb$database" ) + ,3020 : ( "select cast('15:00' as time format 'TZR') from rdb$database" ) + ,3030 : ( "select cast('-15:00' as time format 'TZH:TZM') from rdb$database" ) + ,3040 : ( "select cast('-15:00' as time format 'TZR') from rdb$database" ) +} + + +############################################################################### + +act = python_act('db') + +#----------------------------------------------------------- + +@pytest.mark.version('>=6.0') +def test_1(act: Action, capsys): + with act.db.connect() as con: + + cur = con.cursor() + for idx, test_sql in query_map.items(): + print(idx) + print(test_sql) + try: + cur.execute(test_sql) + for r in cur: + print(r[0]) + except DatabaseError as e: + print(e.__str__()) + for x in e.gds_codes: + print(x) + + expected_out = f""" + 1000 + {query_map[1000]} + Cannot find value in input string for "Y" pattern + 335545315 + + 1010 + {query_map[1010]} + Cannot find value in input string for "YY" pattern + 335545315 + + 1020 + {query_map[1020]} + Cannot find value in input string for "YYY" pattern + 335545315 + + 1030 + {query_map[1030]} + Cannot find value in input string for "YYYY" pattern + 335545315 + + 1040 + {query_map[1040]} + Cannot find value in input string for "YEAR" pattern + 335545315 + + 1050 + {query_map[1050]} + Cannot find value in input string for "RR" pattern + 335545315 + + 1060 + {query_map[1060]} + Cannot find value in input string for "RRRR" pattern + 335545315 + + 1070 + {query_map[1070]} + Cannot find value in input string for "MM" pattern + 335545315 + + 1080 + {query_map[1080]} + Cannot find value in input string for "DD" pattern + 335545315 + + 1090 + {query_map[1090]} + Cannot find value in input string for "J" pattern + 335545315 + + 2000 + {query_map[2000]} + Invalid time zone offset: 2147483647 - must use format +/-hours:minutes and be between -14:00 and +14:00 + 335545213 + + 2010 + {query_map[2010]} + Value for TZR pattern is out of range [0, 59] + 335545297 + + 2020 + {query_map[2020]} + Invalid time zone offset: 2147483647 - must use format +/-hours:minutes and be between -14:00 and +14:00 + 335545213 + + 2030 + {query_map[2030]} + Value for TZR pattern is out of range [0, 59] + 335545297 + + 2040 + {query_map[2040]} + Value for TZM pattern is out of range [0, 59] + 335545297 + + 2050 + {query_map[2050]} + Value for TZR pattern is out of range [0, 59] + 335545297 + + 2060 + {query_map[2060]} + Value for TZM pattern is out of range [0, 59] + 335545297 + + 2070 + {query_map[2070]} + Value for TZR pattern is out of range [0, 59] + 335545297 + + 3000 + {query_map[3000]} + Value for TZR pattern is out of range [0, 59] + 335545297 + + 3010 + {query_map[3010]} + Cannot use "TZH" format with current date type + 335545296 + + 3020 + {query_map[3020]} + Cannot use "TZR" format with current date type + 335545296 + + 3030 + {query_map[3030]} + Cannot use "TZH" format with current date type + 335545296 + + 3040 + {query_map[3040]} + Cannot use "TZR" format with current date type + 335545296 + """ + act.expected_stdout = expected_out + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8508_test.py b/tests/bugs/gh_8508_test.py new file mode 100644 index 00000000..70d52d9b --- /dev/null +++ b/tests/bugs/gh_8508_test.py @@ -0,0 +1,40 @@ +#coding:utf-8 + +""" +ID: issue-8508 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8508 +TITLE: Conversion Error with old.field in UPDATE OR INSERT +DESCRIPTION: +NOTES: + [09.04.2025] pzotov + Confirmed bug on 6.0.0.717; 5.0.3.1639. + Checked on 6.0.0.722; 5.0.3.1641; 4.0.6.3194; 3.0.13.33806. +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + create table test (id int primary key); + insert into test(id) values(999); + + update or insert into test (id) values (999) + matching (id) + returning old.id as old_id, new.id as new_id; +""" + +act = isql_act('db', test_script, substitutions=[ ('[ \\t]+', ' ') ]) + +expected_stdout = """ + OLD_ID 999 + NEW_ID 999 +""" + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8513_test.py b/tests/bugs/gh_8513_test.py new file mode 100644 index 00000000..3415f60d --- /dev/null +++ b/tests/bugs/gh_8513_test.py @@ -0,0 +1,75 @@ +#coding:utf-8 + +""" +ID: issue-8513 +ISSUE: https://github.com/FirebirdSQL/firebird/pull/8513 +TITLE: Makes MON$COMPILED_STATEMENTS and MON$STATEMENTS share blobs with text and plan content of the same statement +DESCRIPTION: + Test runs ISQL with query that joins mon$statements and mon$compiled_statements on mon$compiled_statement_id value. + ISQL must produce same BLOB_ID value for both these tables (we parse ISQL output and filter only interesting lines of it). +NOTES: + [14.04.2025] pzotov + Thanks to Vlad for suggestion about this test implementation. + + Confirmed different BLOB_ID values on 6.0.0.722. + Reduced min_version to 5.0.3 (17.04.2025). + Checked on 6.0.0.734, 5.0.3.1647-475d999 (intermediate snapshot). +""" + +import os +import re +import time + +import pytest +from firebird.qa import * + +db = db_factory() +act = python_act('db') + +msg_prefix = 'Total unique BLOB_ID values:' +#----------------------------------------------------------- + +@pytest.mark.version('>=5.0.3') +def test_1(act: Action, capsys): + test_sql = f""" + set list on; + set blob all; + select + s.mon$sql_text as blob_id_mon_statements + ,c.mon$sql_text as blob_id_mon_compiled_s + from mon$statements s + join mon$compiled_statements c using(mon$compiled_statement_id) + where mon$attachment_id = current_connection and s.mon$sql_text is not null + ; + """ + + act.isql(switches = ['-q'], input = test_sql, combine_output = True) + + blob_id_pattern = re.compile('^blob_id_mon_', re.IGNORECASE); + + blob_ids_map = {} + if act.return_code == 0: + # Print only interesting lines from ISQl output tail: + for line in act.clean_stdout.splitlines(): + if (blob_id_pattern.search(line)): + blob_ids_map[ line.split()[0] ] = line.split()[1] + if len(set(blob_ids_map.values())) == 1: + print(f'{msg_prefix} {len(set(blob_ids_map.values()))}') + else: + print('UNEXPECTED: number of unique BLOB_ID values is different from 1:') + for k,v in blob_ids_map.items(): + print(k,v) + else: + # If retcode !=0 then we can print the whole output of failed gbak: + print('ISQL failed, check output:') + for line in act.clean_stdout.splitlines(): + print(line) + act.reset() + + expected_stdout = f""" + {msg_prefix} 1 + """ + + act.expected_stdout = expected_stdout + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8520_test.py b/tests/bugs/gh_8520_test.py new file mode 100644 index 00000000..f59e3988 --- /dev/null +++ b/tests/bugs/gh_8520_test.py @@ -0,0 +1,72 @@ +#coding:utf-8 + +""" +ID: issue-8520 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8520 +TITLE: Error in iTransaction.getInfo() on embedded connection +DESCRIPTION: +NOTES: + [17.04.2025] pzotov + Confirmed problem on 6.0.0.686 (20-mar-2025). + Checked on 6.0.0.737-cf1d367, intermediate snapshot. + + [18.04.2025] pzotov + Reduced min_version after check on iontermediate snapshots: + 5.0.3.1648-ca2f3e7; 4.0.6.3199-0503997; 3.0.13.33807-5d3394e7 +""" + +import os +import re +import time + +import pytest +from firebird.qa import * +from firebird.driver import connect, driver_config, NetProtocol, DatabaseError + +db = db_factory() +act = python_act('db') + +#----------------------------------------------------------- + +@pytest.mark.version('>=3.0.13') +def test_1(act: Action, capsys): + + srv_config = driver_config.register_server(name = 'test_8520_srv', config = '') + db_cfg_object = driver_config.register_database(name = 'test_8520_cfg') + db_cfg_object.database.value = str(act.db.db_path) + db_cfg_object.server.value = srv_config.name + + sql_sttm = 'select mon$remote_protocol from mon$attachments where mon$attachment_id = current_connection' + + protocols_list = [ None, NetProtocol.INET, ] # None - for local/embedded connection. + if act.platform == 'Windows': + protocols_list.append(NetProtocol.XNET) + if act.is_version('<5'): + protocols_list.append(NetProtocol.WNET) + + expected_out_map = {} + for p in protocols_list: + db_cfg_object.protocol.value = p + with connect(db_cfg_object.name, user = act.db.user, password = act.db.password) as con: + protocol_name = 'UNKNOWN' + with con.cursor() as cur: + for r in cur.execute(sql_sttm): + protocol_name = 'NONE' if p == None else p.name.upper() + try: + with con.main_transaction as tr: + expected_out_map[ protocol_name ] = tr.info.database + except DatabaseError as e: + print(f'Error encountered for {protocol_name=}:') + print(e.__str__()) + print(e.gds_codes) + + # Construct expected output + print actual result for comparison with expected one: + expected_out_lst = [] + for k,v in expected_out_map.items(): + print(k.lower(), v.lower()) + expected_out_lst.append( (k + ' ' + ('' if k == 'NONE' else k +'://') + str(act.db.db_path)).lower() ) + + expected_stdout = '\n'.join(expected_out_lst) + act.expected_stdout = expected_stdout + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8524_test.py b/tests/bugs/gh_8524_test.py new file mode 100644 index 00000000..ce44d182 --- /dev/null +++ b/tests/bugs/gh_8524_test.py @@ -0,0 +1,310 @@ +#coding:utf-8 + +""" +ID: issue-8524 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8524 +TITLE: ISQL will truncate lines longer than 255 when pasting +DESCRIPTION: + Test creates temporary batch file with requirement to perform following commands: + ========== + chcp 65001 + {act.vars['isql']} -q + exit + ========== + This batch further is called as CHILD process via: 'cmd.exe /c start ' which will launch ISQL. + ISQL process will have console window (which will appear on screen) and we will try to paste some text in it. + + Text may contain non-printable / non-readable characters, so we have to check not the result of pasting but + the value of CHAR_LENGTH(). This is done by creating a table and using INSERT command: + ========= + insert into test(s_utf8) values( + '' + ) returning char_length(s_utf8); + ========= + Result of this command is redirected to log. + Finally, we open this log and parse it in order to see there: 'CHAR_LENGTH {checked_char_len}' + test can be considered as successful if we actually found this line. +NOTES: + [27.05.2025] pzotov + 0. Package 'pywin32' must be installed for this test (pip install pywin32). + 1. Problem could be reproduced only on Windows. Emulation of PASTE required (PIPE will not show any problem). + 2. Test was added just to be able to check result of pasting long text in existing ISQL console. + This test will not able to create console window if python was launched by scheduler and + appropriate task has not set to: "Run only when user is logged on". + Because of that, this test has '@pytest.mark.skip' marker. + ################################################## + REMOVE OR COMMENT OUT THIS MARKER TO RUN THIS TEST + ################################################## + + 3. String that we want to be pasted into console must NOT have two adjacent characters, e.g. 'commit'. + Otherwise only first of them will be actually pasted but others will be 'swallowed' for unknown reason. + 4. Every line of (i.e. text to be pasted in console of ISQL) must be prefixed with TWO characters: ascii_char(13) and ascii_char(10). + It is NOT enough to use as prefix only CHR_10 at this case! + 5. Command processor ('cmd.exe') must change code page to 65001 before running ISQL, thus we launch BATCH rather than just ISQL (as child process). + + Checked on 6.0.0.789. +""" + +import os +import random +import subprocess +import time +from pathlib import Path + +import pytest +from firebird.qa import * + +db = db_factory(charset = 'utf8') + +#----------------------------------------------------------- + +def get_random_unicode(length): + + # https://jrgraphix.net/r/Unicode/ + UNICODE_RANGES_MAP = { + (0x0028, 0x007E) : 'Basic Latin WITHOUT apostrophe and control character , 0x7F', + (0x00A0, 0x00FF) : 'Latin-1 Supplement', + (0x0100, 0x017F) : 'Latin Extended-A', + (0x0180, 0x024F) : 'Latin Extended-B', + (0x0400, 0x04FF) : 'Cyrillic', + (0x0500, 0x052F) : 'Cyrillic Supplementary', + (0x0300, 0x036F) : 'Combining Diacritical Marks', + (0x0250, 0x02AF) : 'IPA Extensions', + (0x0370, 0x03FF) : 'Greek and Coptic', + (0x0530, 0x058F) : 'Armenian', + (0x02B0, 0x02FF) : 'Spacing Modifier Letters', + (0x0590, 0x05FF) : 'Hebrew', + (0x0600, 0x06FF) : 'Arabic', + (0x0700, 0x074F) : 'Syriac', + (0x0780, 0x07BF) : 'Thaana', + (0x0900, 0x097F) : 'Devanagari', + (0x0980, 0x09FF) : 'Bengali', + (0x0A00, 0x0A7F) : 'Gurmukhi', + (0x0A80, 0x0AFF) : 'Gujarati', + (0x0B00, 0x0B7F) : 'Oriya', + (0x0B80, 0x0BFF) : 'Tamil', + (0x0C00, 0x0C7F) : 'Telugu', + (0x0C80, 0x0CFF) : 'Kannada', + (0x0D00, 0x0D7F) : 'Malayalam', + (0x0D80, 0x0DFF) : 'Sinhala', + (0x0E00, 0x0E7F) : 'Thai', + (0x0E80, 0x0EFF) : 'Lao', + (0x0F00, 0x0FFF) : 'Tibetan', + (0x1000, 0x109F) : 'Myanmar', + (0x10A0, 0x10FF) : 'Georgian', + (0x1100, 0x11FF) : 'Hangul Jamo', + (0x1200, 0x137F) : 'Ethiopic', + (0x13A0, 0x13FF) : 'Cherokee', + (0x1400, 0x167F) : 'Unified Canadian Aboriginal Syllabics', + (0x1680, 0x169F) : 'Ogham', + (0x16A0, 0x16FF) : 'Runic', + (0x1700, 0x171F) : 'Tagalog', + (0x1720, 0x173F) : 'Hanunoo', + (0x1740, 0x175F) : 'Buhid', + (0x1760, 0x177F) : 'Tagbanwa', + (0x1780, 0x17FF) : 'Khmer', + (0x1800, 0x18AF) : 'Mongolian', + (0x1900, 0x194F) : 'Limbu', + (0x1950, 0x197F) : 'Tai Le', + (0x19E0, 0x19FF) : 'Khmer Symbols', + (0x1D00, 0x1D7F) : 'Phonetic Extensions', + (0x1E00, 0x1EFF) : 'Latin Extended Additional', + (0x1F00, 0x1FFF) : 'Greek Extended', + (0x2000, 0x206F) : 'General Punctuation', + (0x2070, 0x209F) : 'Superscripts and Subscripts', + (0x20A0, 0x20CF) : 'Currency Symbols', + (0x20D0, 0x20FF) : 'Combining Diacritical Marks for Symbols', + (0x2100, 0x214F) : 'Letterlike Symbols', + (0x2150, 0x218F) : 'Number Forms', + (0x2190, 0x21FF) : 'Arrows', + (0x2200, 0x22FF) : 'Mathematical Operators', + (0x2300, 0x23FF) : 'Miscellaneous Technical', + (0x2400, 0x243F) : 'Control Pictures', + (0x2440, 0x245F) : 'Optical Character Recognition', + (0x2460, 0x24FF) : 'Enclosed Alphanumerics', + (0x2500, 0x257F) : 'Box Drawing', + (0x2580, 0x259F) : 'Block Elements', + (0x25A0, 0x25FF) : 'Geometric Shapes', + (0x2600, 0x26FF) : 'Miscellaneous Symbols', + (0x2700, 0x27BF) : 'Dingbats', + (0x27C0, 0x27EF) : 'Miscellaneous Mathematical Symbols-A', + (0x27F0, 0x27FF) : 'Supplemental Arrows-A', + (0x2800, 0x28FF) : 'Braille Patterns', + (0x2900, 0x297F) : 'Supplemental Arrows-B', + (0x2980, 0x29FF) : 'Miscellaneous Mathematical Symbols-B', + (0x2A00, 0x2AFF) : 'Supplemental Mathematical Operators', + (0x2B00, 0x2BFF) : 'Miscellaneous Symbols and Arrows', + (0x2E80, 0x2EFF) : 'CJK Radicals Supplement', + (0x2F00, 0x2FDF) : 'Kangxi Radicals', + (0x2FF0, 0x2FFF) : 'Ideographic Description Characters', + (0x3000, 0x303F) : 'CJK Symbols and Punctuation', + (0x3040, 0x309F) : 'Hiragana', + (0x30A0, 0x30FF) : 'Katakana', + (0x3100, 0x312F) : 'Bopomofo', + (0x3130, 0x318F) : 'Hangul Compatibility Jamo', + (0x3190, 0x319F) : 'Kanbun', + (0x31A0, 0x31BF) : 'Bopomofo Extended', + (0x31F0, 0x31FF) : 'Katakana Phonetic Extensions', + (0x3200, 0x32FF) : 'Enclosed CJK Letters and Months', + (0x3300, 0x33FF) : 'CJK Compatibility', + (0x3400, 0x4DBF) : 'CJK Unified Ideographs Extension A', + (0x4DC0, 0x4DFF) : 'Yijing Hexagram Symbols', + (0x4E00, 0x9FFF) : 'CJK Unified Ideographs', + (0xA000, 0xA48F) : 'Yi Syllables', + (0xA490, 0xA4CF) : 'Yi Radicals', + (0xAC00, 0xD7AF) : 'Hangul Syllables', + (0xE000, 0xF8FF) : 'Private Use Area', + (0xF900, 0xFAFF) : 'CJK Compatibility Ideographs', + (0xFB00, 0xFB4F) : 'Alphabetic Presentation Forms', + (0xFB50, 0xFDFF) : 'Arabic Presentation Forms-A', + (0xFE00, 0xFE0F) : 'Variation Selectors', + (0xFE20, 0xFE2F) : 'Combining Half Marks', + (0xFE30, 0xFE4F) : 'CJK Compatibility Forms', + (0xFE50, 0xFE6F) : 'Small Form Variants', + (0xFE70, 0xFEFF) : 'Arabic Presentation Forms-B', + (0xFF00, 0xFFEF) : 'Halfwidth and Fullwidth Forms', + (0xFFF0, 0xFFFF) : 'Specials', + (0x10000, 0x1007F) : 'Linear B Syllabary', + (0x10080, 0x100FF) : 'Linear B Ideograms', + (0x10100, 0x1013F) : 'Aegean Numbers', + (0x10300, 0x1032F) : 'Old Italic', + (0x10330, 0x1034F) : 'Gothic', + (0x10380, 0x1039F) : 'Ugaritic', + (0x10400, 0x1044F) : 'Deseret', + (0x10450, 0x1047F) : 'Shavian', + (0x10480, 0x104AF) : 'Osmanya', + (0x10800, 0x1083F) : 'Cypriot Syllabary', + (0x1D000, 0x1D0FF) : 'Byzantine Musical Symbols', + (0x1D100, 0x1D1FF) : 'Musical Symbols', + (0x1D300, 0x1D35F) : 'Tai Xuan Jing Symbols', + (0x1D400, 0x1D7FF) : 'Mathematical Alphanumeric Symbols', + (0x20000, 0x2A6DF) : 'CJK Unified Ideographs Extension B', + (0x2F800, 0x2FA1F) : 'CJK Compatibility Ideographs Supplement', + (0xE0000, 0xE007F) : 'Tags', + } + + alphabet = [ + chr(code_point) for current_range in UNICODE_RANGES_MAP.keys() + for code_point in range(current_range[0], current_range[1] + 1) + ] + return ''.join(random.choice(alphabet) for i in range(length)) + + +#----------------------------------------------------------- +def send_keys_to_window(window_title, keys_to_send): + + import win32gui + import win32con + + window_id = win32gui.FindWindow(None, window_title) + assert window_id > 0, f"Window with title '{window_title}' not found. Script must run only when user is logged on." + + win32gui.SetForegroundWindow(window_id) + time.sleep(0.1) # Give the window time to activate + + for key in keys_to_send: + if key.isupper(): + win32gui.SendMessage(window_id, win32con.WM_KEYDOWN, win32con.VK_SHIFT, 0) + win32gui.SendMessage(window_id, win32con.WM_CHAR, ord(key), 0) + win32gui.SendMessage(window_id, win32con.WM_KEYUP, win32con.VK_SHIFT, 0) + elif key == '\n': + win32gui.SendMessage(window_id, win32con.WM_KEYDOWN, win32con.VK_RETURN, 0) + win32gui.SendMessage(window_id, win32con.WM_KEYUP, win32con.VK_RETURN, 0) + else: + win32gui.SendMessage(window_id, win32con.WM_CHAR, ord(key), 0) +#----------------------------------------------------------- + +MAX_WAIT_FOR_ISQL_FINISH_S = 20 + +test_script = """ +""" + +act = python_act('db', substitutions=[ ('[ \\t]+', ' ') ]) + +isql_out = temp_file('tmp_8524.log') +isql_bat = temp_file('tmp_8524.bat') +utf8_dat = temp_file('tmp_8524.dat') # for debug only + +expected_stdout = """ +""" + +@pytest.mark.version('>=4.0.6') +@pytest.mark.platform('Windows') +@pytest.mark.skip("Can not run when user is logged out. Child process must run in console window.") + +def test_1(act: Action, isql_out: Path, utf8_dat: Path, isql_bat: Path, capsys): + + # ::: ACHTUNG ::: + # The whole data that will be pasted in ISQL console must not have two same adjacent characters. + # For example, 'commit' will be 'comit' --> token unknown etc. + # The same for unicode string: second character (of two adjacent ones) will be 'swallowed'! + # The reason of this weird behaviour remained unknown. + # + #long_utf8_data = ''.join(set(get_random_unicode(8190))) + + long_utf8_data = '∑∏Ω' * (8191//3) + checked_char_len = len(long_utf8_data) + send_sql = f""" + set names utf8; + set list on; + connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; + show version; + recreate table test( s_utf8 varchar(8191) ); + out {str(isql_out)}; + insert into test(s_utf8) values( + '{long_utf8_data}' + ) returning char_length(s_utf8); + out; + exit; + + """ + + with open(utf8_dat, 'w', encoding = 'utf8') as f: + f.write(send_sql) + + # Child processes cmd.exe or isql.exe will use code page = 866. + # We have to set BEFOREHAND codepage = 65001 otherwise 'malformed string' will raise in ISQL console, even when use charset = utf8. + # To to this, we have to launch as child process BATCH file rather than just cmd.exe because there is no way to start sopmewhat like + # 'cmd.exe /cp 65001' with set needed codepage at one command. + # + launch_commands = f""" + cls + chcp 65001 + {act.vars['isql']} -q + exit + """ + with open(isql_bat, 'w') as f: + f.write(launch_commands) + + + # WRONG (can lead to 'malformed string' because we did not set codepage to 65001): + #p_child = subprocess.run( [ 'cmd.exe', '/c', 'start', act.vars['isql'], '-q' ] ) # , stdout = f, stderr = subprocess.STDOUT) + + p_child = subprocess.run( [ 'cmd.exe', '/c', 'start', str(isql_bat) ] ) + time.sleep(1) + + # ----------------------------------------------------------- + # ::: NB ::: + # Every line of (i.e. text to be pasted in console of ISQL) must be prefixed with TWO characters: ascii_char(13) and ascii_char(10). + # It is NOT enough to use as prefix only CHR_10 at this case! + # ----------------------------------------------------------- + + #send_keys_to_window( str(act.vars['isql']), '\r\n'.join( [x.lstrip() for x in send_sql.splitlines()] ) ) + send_keys_to_window( os.getenv('SystemRoot') + '\\system32\\cmd.exe - ' + str(isql_bat), '\r\n'.join( [x.lstrip() for x in send_sql.splitlines()] ) ) + + time.sleep(1) + try: + p_child.wait(MAX_WAIT_FOR_ISQL_FINISH_S) + p_child.terminate() + except AttributeError: # 'CompletedProcess' object has no attribute 'wait' + pass + + with open(isql_out, mode='r') as f: + for line in f: + print(line) + + act.expected_stdout = f""" + CHAR_LENGTH {checked_char_len} + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8532_test.py b/tests/bugs/gh_8532_test.py new file mode 100644 index 00000000..77431569 --- /dev/null +++ b/tests/bugs/gh_8532_test.py @@ -0,0 +1,125 @@ +#coding:utf-8 + +""" +ID: issue-8532 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8532 +TITLE: GREATEST and LEAST (SQL:2023 - T054) +DESCRIPTION: +NOTES: + [30.04.2025] pzotov + Checked on 6.0.0.755-9d191e8 (intermediate snapshot) +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + + select least(654, 987, 123, 741) as least_01 from rdb$database; + + select least(1.976262583364986e-323, 4.940656458412465e-324, 9.881312916824931e-324) as least_02 from rdb$database; + + select least(0e0, -0e0) as least_03 from rdb$database; + + select least(true, false, false) as least_04 from rdb$database; + + select least(timestamp '01.01.0001 00:00:00.100', timestamp '31.12.9999 23:59:59.999', timestamp '01.01.1970 00:00:00.000') as least_05 from rdb$database; + + select least(-170141183460469231731687303715884105728, 170141183460469231731687303715884105727) as least_06 from rdb$database; + + select least(df0, df1, df2) as least_07 + from ( + select + exp( cast( -14221.4586815117860898045324562520948 as decfloat) ) as df0 + ,exp( cast( -14221.4586815117860898045324562520949 as decfloat) ) as df1 + ,exp( cast( -14221.4586815117860898045324562520950 as decfloat) ) as df2 + from rdb$database + ); + + + select least(null, null, 191, null, null, -213, null, null) as least_08 from rdb$database; + + select least( + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 --50 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 --100 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 --150 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 --200 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 --250 + ,1,1,1,1,1 -- the last element (# 255) where result is OK + ,1 -- adding this element leads to error message + ) as least_09 + from rdb$database; + + --------------------------------------------------------------------- + + select greatest(654, 987, 123, 741) as greatest_01 from rdb$database; + + select greatest(1.976262583364986e-323, 4.940656458412465e-324, 9.881312916824931e-324) as greatest_02 from rdb$database; + + select greatest(0e0, -0e0) as greatest_03 from rdb$database; + + select greatest(true, false, false) as greatest_04 from rdb$database; + + select greatest(timestamp '01.01.0001 00:00:00.100', timestamp '31.12.9999 23:59:59.999', timestamp '01.01.1970 00:00:00.000') as greatest_05 from rdb$database; + + select greatest(-170141183460469231731687303715884105728, 170141183460469231731687303715884105727) as greatest_06 from rdb$database; + + select greatest(df0, df1, df2) as greatest_07 + from ( + select + exp( cast( -14221.4586815117860898045324562520948 as decfloat) ) as df0 + ,exp( cast( -14221.4586815117860898045324562520949 as decfloat) ) as df1 + ,exp( cast( -14221.4586815117860898045324562520950 as decfloat) ) as df2 + from rdb$database + ); + + + select greatest(null, null, 191, null, null, -213, null, null) as greatest_08 from rdb$database; + + select greatest( + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 --50 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 --100 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 --150 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 --200 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 --250 + ,1,1,1,1,1 -- the last element (# 255) where result is OK + ,1 -- adding this element leads to error message + ) as greatest_09 + from rdb$database; +""" + +act = isql_act('db', test_script, substitutions = [ ('[ \t]+',' ') ]) + +expected_stdout = """ + LEAST_01 123 + LEAST_02 4.940656458412465E-324 + LEAST_03 0.000000000000000 + LEAST_04 + LEAST_05 0001-01-01 00:00:00.1000 + LEAST_06 -170141183460469231731687303715884105728 + LEAST_07 0E-6176 + LEAST_08 + Statement failed, SQLSTATE = 42000 + Maximum (255) number of arguments exceeded for function LEAST + + GREATEST_01 987 + GREATEST_02 1.976262583364986E-323 + GREATEST_03 0.000000000000000 + GREATEST_04 + GREATEST_05 9999-12-31 23:59:59.9990 + GREATEST_06 170141183460469231731687303715884105727 + GREATEST_07 1E-6176 + GREATEST_08 + Statement failed, SQLSTATE = 42000 + Maximum (255) number of arguments exceeded for function GREATEST +""" + +@pytest.mark.version('>=6.0') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout + diff --git a/tests/bugs/gh_8579_test.py b/tests/bugs/gh_8579_test.py new file mode 100644 index 00000000..9fc7a450 --- /dev/null +++ b/tests/bugs/gh_8579_test.py @@ -0,0 +1,172 @@ +#coding:utf-8 + +""" +ID: issue-8579 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8579 +TITLE: Add sub routines info in the BLR debug blob filter +DESCRIPTION: + Test creates procedure (sp_main) and function (fn_main) and each contains declarations + of inner procedures and functions. + Also, package with two units is created: procedure 'packaged_proc' and function 'packaged_func', and each + of them have declarations of inner sub-units (procedures and functions). + Then we run query 'select rdb$debug_info from rdb$...' and check that line line + occurs: + * one time for each 'main-level' unit (i.e. sp_main; fn_pain; packaged_proc and packaged_func; + * exactly times after each kind sub-routine name. + Expected output is build 'on-the-fly' during generation of appropriate DDL expressions, see 'expected_lst'. + One need to keep in mind that rdb$debug_info shows inner functions *before* inner procedures. +NOTES: + [31.05.2025] pzotov + If we try to declare inner procedures 19 times then line 'BLR to Source mapping:' appears to be "broken" + for last (19th) sub-routine. Because of this, test currently must use no more than 18 declarations. + See: https://github.com/FirebirdSQL/firebird/issues/8590 + + Problem did exist up to 6.0.0.799-c82c9cf and has been fixed 11-jun-2025: + https://github.com/FirebirdSQL/firebird/commit/c867f34da84fbc0e1843757c386f955c014d8d41 + After fix, test execution time is ~6s. + + Checked on 6.0.0.800-96dd669e +""" +import pytest +from firebird.qa import * +import time + +SUBROUTINES_COUNT = 2000 # 31.05.2025: 19 - broken line 'BLR to Source mapping:', see #8590. +BLR_TO_SOURCE_TXT = 'BLR to Source mapping:' + +db = db_factory() +substitutions = [ ('^((?!' + BLR_TO_SOURCE_TXT + '|Sub function|Sub procedure).)*$', '') ] +act = python_act('db', substitutions = substitutions) + +expected_lst = [] + +ddl_sp = [] +ddl_fn = [] +ddl_pkg = [] +ddl_pbd = [] + +# ................................................... +# ... f u n c t i o n, s t a n d a l o n e ... +# ................................................... +expected_lst.append(BLR_TO_SOURCE_TXT) +ddl_fn = [ 'create function fn_main returns int as' ] +for i in range(SUBROUTINES_COUNT): + # Inner functions: + ddl_fn.extend( [f' declare function fn_main_sub_func_{i:05} returns int as', ' begin', f' return {i};', ' end'] ) + +for i in range(SUBROUTINES_COUNT): + # Inner procedures: + ddl_fn.extend( [f' declare procedure fn_main_sub_proc_{i:05} as', ' declare n int;', ' begin', f' n = {i};', ' end'] ) +ddl_fn.extend( ['begin', ' return 1;', 'end'] ) + +# Add lines that must be in expected output. NOTE: inner functions are shown always BEFORE procedures! +for suffix in ('sub_func', 'sub_proc'): + sub_routine_type = 'function' if suffix == 'sub_func' else 'procedure' + for i in range(SUBROUTINES_COUNT): + expected_lst.extend([f'Sub {sub_routine_type} FN_MAIN_{suffix.upper()}_{i:05}:', BLR_TO_SOURCE_TXT]) + + +# ..................................................... +# ... p r o c e d u r e, s t a n d a l o n e ... +# ..................................................... +expected_lst.append(BLR_TO_SOURCE_TXT) +ddl_sp = [ 'create procedure sp_main as' ] +for i in range(SUBROUTINES_COUNT): + # Inner functions: + ddl_sp.extend( [f' declare function sp_main_sub_func_{i:05} returns int as', ' begin', f' return {i};', ' end'] ) + +for i in range(SUBROUTINES_COUNT): + # Inner procedures: + ddl_sp.extend( [f' declare procedure sp_main_sub_proc_{i:05} as', ' declare n int;', ' begin', f' n = {i};', ' end'] ) +ddl_sp.extend( ['begin', 'end'] ) + +# Add lines that must be in expected output. NOTE: inner functions are shown always BEFORE procedures! +for suffix in ('sub_func', 'sub_proc'): + sub_routine_type = 'function' if suffix == 'sub_func' else 'procedure' + for i in range(SUBROUTINES_COUNT): + expected_lst.extend([f'Sub {sub_routine_type} SP_MAIN_{suffix.upper()}_{i:05}:', BLR_TO_SOURCE_TXT]) + + +# ............................................................. +# ... p a c k a g e w i t h f u n c a n d p r o c +# ............................................................. +ddl_pkg = [ 'create package pg_test as', 'begin' ] +ddl_pkg.extend( [' function packaged_func returns int;'] ) +ddl_pkg.extend( [' procedure packaged_proc;'] ) +ddl_pkg.extend( ['end'] ) # end of package header + +ddl_pbd = [ 'create package body pg_test as', 'begin' ] + +# packaged function with inner units: +# ------------------------------------------------------- +ddl_pbd.extend( [' function packaged_func returns int as' ] ) +for i in range(SUBROUTINES_COUNT): + ddl_pbd.extend( [f' declare function pg_func_sub_func_{i:05} returns int as', ' begin', f' return 1;', ' end'] ) + ddl_pbd.extend( [f' declare procedure pg_func_sub_proc_{i:05} as', ' declare n int;', ' begin', f' n = 1;', ' end'] ) +ddl_pbd.extend( [' begin', ' end'] ) + + +# packaged procedure with inner units: +# -------------------------------------------------------- +ddl_pbd.extend( [' procedure packaged_proc as' ] ) +for i in range(SUBROUTINES_COUNT): + ddl_pbd.extend( [f' declare function pg_proc_sub_func_{i:05} returns int as', ' begin', f' return 1;', ' end'] ) + ddl_pbd.extend( [f' declare procedure pg_proc_sub_proc_{i:05} as', ' declare n int;', ' begin', f' n = 1;', ' end'] ) + +ddl_pbd.extend( [' begin', ' end'] ) + +ddl_pbd.extend( ['end'] ) # end of package body + +# Add lines that must be in expected output. NOTE: inner functions are shown always BEFORE procedures! +for prefix in ('pg_func', 'pg_proc'): + expected_lst.append(BLR_TO_SOURCE_TXT) + for i in range(SUBROUTINES_COUNT): + expected_lst.extend([f'Sub function {prefix.upper()}_SUB_FUNC_{i:05}:', BLR_TO_SOURCE_TXT]) + + for i in range(SUBROUTINES_COUNT): + expected_lst.extend([f'Sub procedure {prefix.upper()}_SUB_PROC_{i:05}:', BLR_TO_SOURCE_TXT]) + +##################################################################################################### + +@pytest.mark.version('>=6.0') +def test_1(act: Action, capsys): + + with act.db.connect(charset = 'utf8') as con: + + for ddl_cmd_lst in (ddl_sp, ddl_fn, ddl_pkg, ddl_pbd): + if ddl_cmd_lst: + ddl_cmd_txt = '\n'.join([x.strip() for x in ddl_cmd_lst]) + #print(len(ddl_cmd_txt)) + con.execute_immediate( ddl_cmd_txt ) + con.commit() + + rdb_query_data = ( + ('rdb$functions', 'rdb$function_name', 'fn_main', None) + ,('rdb$procedures', 'rdb$procedure_name', 'sp_main', None) + ,('rdb$functions', 'rdb$function_name', 'packaged_func', 'pg_test') + ,('rdb$procedures', 'rdb$procedure_name', 'packaged_proc', 'pg_test') + ) + + for p in rdb_query_data: + + rdb_table_name, rdb_field_name, rdb_field_value, rdb_package_name = p + + check_sql = f""" + set heading off; + set blob all; + select p.rdb$debug_info as blob_debug_info + from {rdb_table_name} p + where p.{rdb_field_name} = '{rdb_field_value.upper()}' + """ + if rdb_package_name: + check_sql += f" and p.rdb$package_name = '{rdb_package_name.upper()}';" + else: + check_sql += f" and p.rdb$package_name is null;" + + act.isql(switches = ['-q'], input = check_sql, combine_output = True) + print(act.stdout) + act.reset() + + act.expected_stdout = '\n'.join(expected_lst) + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8589_test.py b/tests/bugs/gh_8589_test.py new file mode 100644 index 00000000..d4c8bdb9 --- /dev/null +++ b/tests/bugs/gh_8589_test.py @@ -0,0 +1,83 @@ +#coding:utf-8 + +""" +ID: issue-8589 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8589 +TITLE: PERCENT_RANK may return NaN instead of 0 +DESCRIPTION: +NOTES: + [07.06.2025] pzotov + Confirmed bug on 6.0.0.797-0-303e8d4 + Checked on 6.0.0.797-bc305e6; 5.0.3.1369-fe53465; 4.0.6.3206-9580691 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + + set list on; + recreate table test( + id integer not null, + the_int integer, + primary key (id) + ); + + insert into test(the_int, id) values (5, 1); + insert into test(the_int, id) values (6, 2); + insert into test(the_int, id) values (7, 3); + insert into test(the_int, id) values (13, 4); + insert into test(the_int, id) values (5, 5); + + select + id, + the_int, + row_number() over(partition by t.the_int order by t.id), + percent_rank() over(partition by t.the_int order by t.id), + cume_dist() over(partition by t.the_int order by t.id) + from test t + order by 1; +""" + +act = isql_act('db', test_script, substitutions = [('[\t ]+', ' ')]) + +expected_stdout = """ + ID 1 + THE_INT 5 + ROW_NUMBER 1 + PERCENT_RANK 0.000000000000000 + CUME_DIST 0.5000000000000000 + + ID 2 + THE_INT 6 + ROW_NUMBER 1 + PERCENT_RANK 0.000000000000000 + CUME_DIST 1.000000000000000 + + ID 3 + THE_INT 7 + ROW_NUMBER 1 + PERCENT_RANK 0.000000000000000 + CUME_DIST 1.000000000000000 + + ID 4 + THE_INT 13 + ROW_NUMBER 1 + PERCENT_RANK 0.000000000000000 + CUME_DIST 1.000000000000000 + + ID 5 + THE_INT 5 + ROW_NUMBER 2 + PERCENT_RANK 1.000000000000000 + CUME_DIST 1.000000000000000 +""" + +@pytest.mark.version('>=4.0.6') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout + diff --git a/tests/bugs/gh_8592_test.py b/tests/bugs/gh_8592_test.py new file mode 100644 index 00000000..fbc68b9f --- /dev/null +++ b/tests/bugs/gh_8592_test.py @@ -0,0 +1,40 @@ +#coding:utf-8 + +""" +ID: issue-8592 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8592 +TITLE: Presence of 'ROWS ' causes garbage in error message when string conversion problem raises +DESCRIPTION: + See https://www.sqlite.org/src/tktview/de7db14784 +NOTES: + [10.06.2025] pzotov + Checked on 6.0.0.799-c82c9cf; 5.0.3.1660-0-d0d870a; 4.0.6.3207-4a300e7 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set heading off; + create table test_a(f01 varchar(5) primary key, f02 varchar(5)); + create table test_b(f01 varchar(5), f02 int); + insert into test_a values('one', 'i'); + insert into test_b values('one', 1); + select a.f01 from test_a a where exists (select 1 from test_b b where a.f01 = b.f02 rows 1); +""" + +act = isql_act('db', test_script, substitutions = [('[\t ]+', ' ')]) + +expected_stdout = """ + Statement failed, SQLSTATE = 22018 + conversion error from string "one" +""" + +@pytest.mark.version('>=4.0.6') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout + diff --git a/tests/bugs/gh_8595_test.py b/tests/bugs/gh_8595_test.py new file mode 100644 index 00000000..687eb1ca --- /dev/null +++ b/tests/bugs/gh_8595_test.py @@ -0,0 +1,66 @@ +#coding:utf-8 + +""" +ID: issue-8595 +ISSUE: 8595 +TITLE: Unable to restore database to Firebird 6.0 (with schemas) from ODS 13.1 if database contains views with system tables used in subqueries +DESCRIPTION: + Test uses backup of database that was created in ODS 13.1 (FB 5.0.3.x) and contain view V_TEST with column defined as subquery to mon$attachments table: + create view v_test as + select (select count(*) from mon$attachments) as att_cnt + from rdb$database; + We extract this .fbk from .zip and try to restore from it using services API. No errors must occur + Finally, we run query to the V_TEST. It must run w/o errors and return one row. +NOTES: + [13.06.2025] pzotov + Confirmed bug on 6.0.0.800 (got 'table "PUBLIC"."MON$ATTACHMENTS" is not defined'). + Checked on 6.0.0.834-a9a0f28. +""" + +import pytest +import zipfile +import locale +from pathlib import Path +from firebird.qa import * +from firebird.driver import SrvRestoreFlag, DatabaseError +import time + +db = db_factory() + +act = python_act('db', substitutions=[('[ \t]+', ' ')]) + +tmp_fbk = temp_file('gh_8595-ods13_1.fbk') +tmp_fdb = temp_file('tmp_restore_8595.fdb') + +#------------------------------------------------------------------ +# Callback function: capture output of ERROR messages in restore (output must be empty): +def print_log(line: str) -> None: + if ( s:= line.strip()): + if 'ERROR' in s: + print(s) +#------------------------------------------------------------------ + +@pytest.mark.version('>=6.0') +def test_1(act: Action, tmp_fbk: Path, tmp_fdb: Path, capsys): + + zipped_fbk_file = zipfile.Path(act.files_dir / 'gh_8595-ods13_1.zip', at = 'gh_8595-ods13_1.fbk') + tmp_fbk.write_bytes(zipped_fbk_file.read_bytes()) + # + restore_log = [] + with act.connect_server(encoding_errors = locale.getpreferredencoding()) as srv: + try: + srv.database.restore(backup=tmp_fbk, database=tmp_fdb, flags=SrvRestoreFlag.REPLACE, verbose=True, callback = print_log) + act.isql( switches=[ str(tmp_fdb), '-q' ], connect_db = False, input = 'set list on; select sign(att_cnt) as att_cnt_sign from v_test;', combine_output = True ) + print(act.stdout) + except DatabaseError as e: + print(f'Restore failed:') + print(e.__str__()) + print(e.gds_codes) + except Exception as x: + print(x) + + act.expected_stdout = """ + ATT_CNT_SIGN 1 + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8597_test.py b/tests/bugs/gh_8597_test.py new file mode 100644 index 00000000..191b0217 --- /dev/null +++ b/tests/bugs/gh_8597_test.py @@ -0,0 +1,80 @@ +#coding:utf-8 + +""" +ID: issue-8597 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8597 +TITLE: Unable to restore database to Firebird 6.0 (with schemas) from ODS 13.1 if database contains views with system tables used in subqueries +DESCRIPTION: + Test uses backup of database that was created in ODS 13.1 (FB 5.0.3.x) and contains + * standalone procedure + function + * package with procedure and function. + Every unit has IN-parameters defined as type of columns from some rdb$tables, and OUT parameters referencing to rdb$ domains. + We extract this .fbk from .zip and try to restore from it using services API. No errors must occur + Finally, we run every proc / func / packaged units - no error must raise. +NOTES: + [13.06.2025] pzotov + Confirmed bug on 6.0.0.835-3da8317: + Error while parsing procedure "PUBLIC"."SP_TEST"'s BLR + -column "RDB$RELATION_NAME" does not exist in table/view "PUBLIC"."RDB$RELATIONS" + Checked on 6.0.0.835-0-b1fd7d8 +""" + +import pytest +import zipfile +import locale +from pathlib import Path +from firebird.qa import * +from firebird.driver import SrvRestoreFlag, DatabaseError +import time + +db = db_factory() + +act = python_act('db', substitutions=[('[ \t]+', ' ')]) + +tmp_fbk = temp_file('gh_8597-ods13_1.fbk') +tmp_fdb = temp_file('tmp_restore_8597.fdb') + +#------------------------------------------------------------------ +# Callback function: capture output of ERROR messages in restore (output must be empty): +def print_log(line: str) -> None: + if ( s:= line.strip()): + if 'ERROR' in s: + print(s) +#------------------------------------------------------------------ + +@pytest.mark.version('>=6.0') +def test_1(act: Action, tmp_fbk: Path, tmp_fdb: Path, capsys): + + zipped_fbk_file = zipfile.Path(act.files_dir / 'gh_8597-ods13_1.zip', at = 'gh_8597-ods13_1.fbk') + tmp_fbk.write_bytes(zipped_fbk_file.read_bytes()) + # + restore_log = [] + test_sql = """ + set list on; + set blob all; + select p.o_id as proc_outcome from sp_test('rdb$database') p; + select fn_test('rdb$database') as func_outcome from rdb$database; + select p.o_id as pg_proc_outcome from pg_test.pg_sp_test('rdb$database') p; + select pg_test.pg_fn_test('rdb$database') as pg_func_outcome from rdb$database; + """ + with act.connect_server(encoding_errors = locale.getpreferredencoding()) as srv: + try: + srv.database.restore(backup=tmp_fbk, database=tmp_fdb, flags=SrvRestoreFlag.REPLACE, verbose=True, callback = print_log) + + act.isql( switches=[ str(tmp_fdb), '-q' ], connect_db = False, input = test_sql, combine_output = True ) + print(act.stdout) + except DatabaseError as e: + print(f'Restore failed:') + print(e.__str__()) + print(e.gds_codes) + except Exception as x: + print(x) + + act.expected_stdout = """ + PROC_OUTCOME 1 + FUNC_OUTCOME + PG_PROC_OUTCOME 2 + PG_FUNC_OUTCOME + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8601_test.py b/tests/bugs/gh_8601_test.py new file mode 100644 index 00000000..83427c7c --- /dev/null +++ b/tests/bugs/gh_8601_test.py @@ -0,0 +1,44 @@ +#coding:utf-8 + +""" +ID: issue-8601 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8601 +TITLE: Charset and collation are not found in the search path when altering a table (FB 6.0 SQL schemas) +DESCRIPTION: +NOTES: + [14.06.2025] pzotov + Confirmed bug on 6.0.0.835, got: -Data type unknown / -CHARACTER SET "PUBLIC"."UTF8" is not defined + Checked on 6.0.0.838-0-0b49fa8. +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set bail on; + set list on; + recreate table t1 ( + id bigint not null primary key, + name varchar(10) character set utf8 + ); + + alter table t1 + alter name type varchar(10) character set utf8 collate unicode_ci_ai; + insert into t1(id, name) values(1, 'äÄöÖõÕšŠžŽ'); + insert into t1(id, name) values(2, 'AaOoOOSsZZ'); + select count(*) from t1 where name like 'AA%Zz'; +""" + +act = isql_act('db', test_script, substitutions=[ ('[ \\t]+', ' ') ]) + +expected_stdout = """ + COUNT 2 +""" + +@pytest.mark.version('>=6.0') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/bugs/gh_8619_test.py b/tests/bugs/gh_8619_test.py new file mode 100644 index 00000000..e08962fa --- /dev/null +++ b/tests/bugs/gh_8619_test.py @@ -0,0 +1,51 @@ +#coding:utf-8 + +""" +ID: issue-8619 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8619 +TITLE: Regression in 6.0.0.653 ("Stack overflow. ... requirements of the runtime stack have exceeded the memory"). +DESCRIPTION: +NOTES: + [22.06.2025] pzotov + Checked on 6.0.0.853-c1954c4 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + recreate table test(id1 int, id2 int, id3 int, unique(id1, id2, id3)); + + -- ####################### + -- INSERT USING SELECT ... + -- ####################### + insert into test(id1, id2, id3) + with + a as ( + select 1000 as v from rdb$database union all + select 1000 as v from rdb$database union all + select null as v from rdb$database + ) + select distinct a1.v, a2.v, a3.v + from a a1 + cross join a a2 + cross join a a3 + ; + + select 1 from mon$database; +""" + +act = isql_act('db', test_script, substitutions = [('[\t ]+', ' ')]) + +expected_stdout = """ + CONSTANT 1 +""" + +@pytest.mark.version('>=6.0') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/arno/optimizer/test_opt_inner_join_05.py b/tests/functional/arno/optimizer/test_opt_inner_join_05.py index f08ea254..b6e2fc91 100644 --- a/tests/functional/arno/optimizer/test_opt_inner_join_05.py +++ b/tests/functional/arno/optimizer/test_opt_inner_join_05.py @@ -13,6 +13,21 @@ https://github.com/FirebirdSQL/firebird/commit/ae427762d5a3e740b69c7239acb9e2383bc9ca83 // FB 5.x ("More realistic cardinality adjustments for unmatchable booleans, this should also fix #7904: FB5 bad plan for query") Letter from dimitr, 15.12.2023 10:05. + + [24.09.2024] pzotov + Changed substitutions: one need to suppress '(keys: N, total key length: M)' in FB 6.x (and ONLY there), + otherwise actual and expected output become differ. + Commit: https://github.com/FirebirdSQL/firebird/commit/c50b0aa652014ce3610a1890017c9dd436388c43 + ("Add key info to the hash join plan output", 23.09.2024 18:26) + Discussed with dimitr. + Checked on 6.0.0.467-cc183f5, 5.0.2.1513 + + [31.10.2024] pzotov + Adjusted expected_out discuss with dimitr: explained plan for FB 6.x became identical to FB 5.x and earlier after + https://github.com/FirebirdSQL/firebird/commit/e7e9e01fa9d7c13d8513fcadca102d23ad7c5e2a + ("Rework fix for #8290: Unique scan is incorrectly reported in the explained plan for unique index and IS NULL predicate") + + Checked on 3.0.13.33794, 4.0.6.3165, 5.0.2.1551, 6.0.0.515 """ import pytest @@ -308,12 +323,17 @@ db = db_factory(init=init_script) -substitutions = [('record length:\\s+\\d+', 'record length'), ('key length:\\s+\\d+', 'key length') ] +substitutions = \ + [ + ( r'\(record length: \d+, key length: \d+\)', '' ) # (record length: 132, key length: 16) + ,( r'\(keys: \d+, total key length: \d+\)', '' ) # (keys: 1, total key length: 2) + ] + act = python_act('db', substitutions = substitutions) #---------------------------------------------------------- -def replace_leading(source, char="#"): +def replace_leading(source, char="."): stripped = source.lstrip() return char * (len(source) - len(stripped)) + stripped @@ -342,29 +362,29 @@ def test_1(act: Action, capsys): expected_stdout_4x = """ Select Expression - ####-> Sort (record length: 150, key length: 44) - ########-> Nested Loop Join (inner) - ############-> Filter - ################-> Table "COUNTRIES" as "C" Access By ID - ####################-> Bitmap - ########################-> Index "I_COUNTRYNAME" Unique Scan - ############-> Filter - ################-> Table "RELATIONS" as "R" Access By ID - ####################-> Bitmap - ########################-> Index "FK_RELATIONS_COUNTRIES" Range Scan (full match) + ....-> Sort (record length: 150, key length: 44) + ........-> Nested Loop Join (inner) + ............-> Filter + ................-> Table "COUNTRIES" as "C" Access By ID + ....................-> Bitmap + ........................-> Index "I_COUNTRYNAME" Unique Scan + ............-> Filter + ................-> Table "RELATIONS" as "R" Access By ID + ....................-> Bitmap + ........................-> Index "FK_RELATIONS_COUNTRIES" Range Scan (full match) """ expected_stdout_5x = """ Select Expression - ####-> Sort (record length: 150, key length: 44) - ########-> Filter - ############-> Hash Join (inner) - ################-> Table "RELATIONS" as "R" Full Scan - ################-> Record Buffer (record length: 81) - ####################-> Filter - ########################-> Table "COUNTRIES" as "C" Access By ID - ############################-> Bitmap - ################################-> Index "I_COUNTRYNAME" Unique Scan + ....-> Sort (record length: 150, key length: 44) + ........-> Filter + ............-> Hash Join (inner) + ................-> Table "RELATIONS" as "R" Full Scan + ................-> Record Buffer (record length: 81) + ....................-> Filter + ........................-> Table "COUNTRIES" as "C" Access By ID + ............................-> Bitmap + ................................-> Index "I_COUNTRYNAME" Range Scan (full match) """ act.expected_stdout = expected_stdout_4x if act.is_version('<5') else expected_stdout_5x diff --git a/tests/functional/arno/optimizer/test_opt_sort_by_index_18.py b/tests/functional/arno/optimizer/test_opt_sort_by_index_18.py index bd1e8d8c..befc5a5e 100644 --- a/tests/functional/arno/optimizer/test_opt_sort_by_index_18.py +++ b/tests/functional/arno/optimizer/test_opt_sort_by_index_18.py @@ -4,89 +4,123 @@ ID: optimizer.sort-by-index-18 TITLE: ORDER BY ASC using index (single) and WHERE clause DESCRIPTION: - WHERE X = 1 ORDER BY Y - Index for both X and Y should be used when available. + WHERE X = 1 ORDER BY Y + Index for both X and Y should be used when available. FBTEST: functional.arno.optimizer.opt_sort_by_index_18 +NOTES: + [17.11.2024] pzotov + Query text was replaced after https://github.com/FirebirdSQL/firebird/commit/26e64e9c08f635d55ac7a111469498b3f0c7fe81 + ( Cost-based decision between ORDER and SORT plans (#8316) ): 'OPTIMIZE FOR FIRST ROWS' is used for 6.x + Suggested by dimitr, letter 16.11.2024 15:15 + + Checked on 6.0.0.532; 5.0.2.1567; 4.0.6.3168; 3.0.13.33794. """ import pytest from firebird.qa import * -init_script = """CREATE TABLE Table_53 ( - ID1 INTEGER, - ID2 INTEGER -); - -SET TERM ^^ ; -CREATE PROCEDURE PR_FillTable_53 -AS -DECLARE VARIABLE FillID INTEGER; -DECLARE VARIABLE FillID1 INTEGER; -BEGIN - FillID = 1; - WHILE (FillID <= 50) DO - BEGIN - FillID1 = (FillID / 10) * 10; - INSERT INTO Table_53 - (ID1, ID2) - VALUES - (:FillID1, :FillID - :FillID1); - FillID = FillID + 1; - END - INSERT INTO Table_53 (ID1, ID2) VALUES (0, NULL); - INSERT INTO Table_53 (ID1, ID2) VALUES (NULL, 0); - INSERT INTO Table_53 (ID1, ID2) VALUES (NULL, NULL); -END -^^ -SET TERM ; ^^ - -COMMIT; - -EXECUTE PROCEDURE PR_FillTable_53; - -COMMIT; - -CREATE ASC INDEX I_Table_53_ID1_ASC ON Table_53 (ID1); -CREATE DESC INDEX I_Table_53_ID1_DESC ON Table_53 (ID1); -CREATE ASC INDEX I_Table_53_ID2_ASC ON Table_53 (ID2); -CREATE DESC INDEX I_Table_53_ID2_DESC ON Table_53 (ID2); - -COMMIT; +init_sql = """ + recreate table table_53 ( + id1 integer, + id2 integer + ); + + set term ^ ; + create procedure pr_filltable_53 + as + declare k integer; + declare i integer; + begin + k = 1; + while (k <= 50) do + begin + i = (k / 10) * 10; + insert into table_53 (id1, id2) values (:i, :k - :i); + k = k + 1; + end + insert into table_53 (id1, id2) values (0, null); + insert into table_53 (id1, id2) values (null, 0); + insert into table_53 (id1, id2) values (null, null); + end + ^ + set term ;^ + commit; + + execute procedure pr_filltable_53; + commit; + + create asc index i_table_53_id1_asc on table_53 (id1); + create desc index i_table_53_id1_desc on table_53 (id1); + create asc index i_table_53_id2_asc on table_53 (id2); + create desc index i_table_53_id2_desc on table_53 (id2); + commit; """ -db = db_factory(init=init_script) - -test_script = """SET PLAN ON; -SELECT - t53.ID2, - t53.ID1 -FROM - Table_53 t53 -WHERE - t53.ID1 = 30 -ORDER BY -t53.ID2 ASC;""" - -act = isql_act('db', test_script) - -expected_stdout = """PLAN (T53 ORDER I_TABLE_53_ID2_ASC INDEX (I_TABLE_53_ID1_ASC)) - - ID2 ID1 -============ ============ - - 0 30 - 1 30 - 2 30 - 3 30 - 4 30 - 5 30 - 6 30 - 7 30 - 8 30 -9 30""" +db = db_factory(init = init_sql) + +act = python_act('db', substitutions = [(r'record length: \d+, key length: \d+', 'record length: NN, key length: MM')]) + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- @pytest.mark.version('>=3') -def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() +def test_1(act: Action, capsys): + + OPT_CLAUSE = '' if act.is_version('<6') else 'optimize for first rows' + + test_sql = f""" + select + t53.id2, + t53.id1 + from table_53 t53 + where + t53.id1 = 30 + order by + t53.id2 asc + {OPT_CLAUSE} + """ + + with act.db.connect() as con: + cur = con.cursor() + ps = None + try: + ps = cur.prepare(test_sql) + + # Print explained plan with padding eash line by dots in order to see indentations: + # print(test_sql) + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + print('') + except DatabaseError as e: + print(e.__str__()) + print(e.gds_codes) + finally: + if ps: + ps.free() + + expected_stdout_5x = """ + Select Expression + ....-> Filter + ........-> Table "TABLE_53" as "T53" Access By ID + ............-> Index "I_TABLE_53_ID2_ASC" Full Scan + ................-> Bitmap + ....................-> Index "I_TABLE_53_ID1_ASC" Range Scan (full match) + """ + + expected_stdout_6x = """ + Select Expression + ....-> Filter + ........-> Table "TABLE_53" as "T53" Access By ID + ............-> Index "I_TABLE_53_ID2_ASC" Full Scan + ................-> Bitmap + ....................-> Index "I_TABLE_53_ID1_ASC" Range Scan (full match) + """ + + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + + act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/basic/isql/test_00.py b/tests/functional/basic/isql/test_00.py index 095e1441..ae5c047c 100644 --- a/tests/functional/basic/isql/test_00.py +++ b/tests/functional/basic/isql/test_00.py @@ -17,7 +17,7 @@ help set; """ -act = isql_act('db', test_script) +act = isql_act('db', test_script, substitutions = [ ('[ \t]+', ' '), ]) fb3x_checked_stdout = """ Frontend commands: @@ -77,7 +77,7 @@ SHOW [] -- display system information = CHECK, COLLATION, DATABASE, DOMAIN, EXCEPTION, FILTER, FUNCTION, GENERATOR, GRANT, INDEX, PACKAGE, PROCEDURE, ROLE, SQL DIALECT, - SYSTEM, TABLE, TRIGGER, VERSION, USERS, VIEW + SYSTEM, TABLE, TRIGGER, VERSION, USERS, VIEW, WIRE_STATISTICS EXIT -- exit and commit changes QUIT -- exit and roll back changes All commands may be abbreviated to letters in CAPitals @@ -103,6 +103,7 @@ SET TIME -- toggle display of timestamp with DATE values SET TERM -- change statement terminator string SET WIDTH [] -- set/unset print width to for column + SET WIRE_stats -- toggle display of wire (network) statistics All commands may be abbreviated to letters in CAPitals """ @@ -122,7 +123,7 @@ SHOW [] -- display system information = CHECK, COLLATION, DATABASE, DOMAIN, EXCEPTION, FILTER, FUNCTION, GENERATOR, GRANT, INDEX, PACKAGE, PROCEDURE, ROLE, SQL DIALECT, - SYSTEM, TABLE, TRIGGER, VERSION, USERS, VIEW + SYSTEM, TABLE, TRIGGER, VERSION, USERS, VIEW, WIRE_STATISTICS EXIT -- exit and commit changes QUIT -- exit and roll back changes All commands may be abbreviated to letters in CAPitals @@ -149,6 +150,7 @@ SET TIME -- toggle display of timestamp with DATE values SET TERM -- change statement terminator string SET WIDTH [] -- set/unset print width to for column + SET WIRE_stats -- toggle display of wire (network) statistics All commands may be abbreviated to letters in CAPitals """ diff --git a/tests/functional/basic/isql/test_01.py b/tests/functional/basic/isql/test_01.py index 5a99bd40..623184c6 100644 --- a/tests/functional/basic/isql/test_01.py +++ b/tests/functional/basic/isql/test_01.py @@ -18,6 +18,7 @@ ('Number of DB pages free.*', 'Number of DB pages free'), ('Sweep.*', 'Sweep'), ('Forced Writes.*', 'Forced Writes'), ('Transaction -.*', ''), ('ODS.*', 'ODS'), + ('DB =.*', ''), ('Host = .*', ''), ('Wire crypt plugin.*', 'Wire crypt plugin'), ('Creation date.*', 'Creation date'), ('Protocol version.*', 'Protocol version'), diff --git a/tests/functional/basic/isql/test_autoterm_01.py b/tests/functional/basic/isql/test_autoterm_01.py index 8b383452..02d01df8 100644 --- a/tests/functional/basic/isql/test_autoterm_01.py +++ b/tests/functional/basic/isql/test_autoterm_01.py @@ -7,6 +7,7 @@ DESCRIPTION: NOTES: Checked on 6.0.0.139 (intermediate snapshot of 23-nov-2023). + Adjusted expected output after check on 6.0.0.530. """ import pytest @@ -57,6 +58,7 @@ def test_1(act: Action): ! Print statistics: OFF Print per-table stats: OFF + Print wire stats: OFF Echo commands: OFF List format: ON Show Row Count: OFF diff --git a/tests/functional/blob/test_blob_over_4gb.py b/tests/functional/blob/test_blob_over_4gb.py new file mode 100644 index 00000000..898a9401 --- /dev/null +++ b/tests/functional/blob/test_blob_over_4gb.py @@ -0,0 +1,231 @@ +#coding:utf-8 + +""" +ID: functional.blob.test_blob_over_4gb.py +TITLE: Check ability to operate with blob with size greater than 4Gb +DESCRIPTION: + Test generated binary file with random data and size = 4Gb + 1 bytes (see 'BLOB_DATA_LEN'). + In order to reduce memory consumption, generated data is splitted onto CHUNKS + with size = 4Gb / 1000 (see 'DATA_CHUNK_LEN'), and writing to the file is done by such small portions. + During generation, hashes are evaluated for each chunk; they are added to the array (see 'chunk_hashes_lst'). + + Then we do several operations with DB in order to check their results: + * run 'nbackup -L' (return_code must be 0); + * open generated file as blob_stream and insert its content to the view v_data; + * read just inserted blob (also as blob stream) and compare its hash with hash of original data. + We read this blob by small portions (chunks) and evaluate hash for each gathered part. + All such hashes are accumulated in array (see 'check_hashes_lst') and then lists are compared. + Comparison of two lists must have result = True, size of obtained data must equal to BLOB_DATA_LEN + * run 'nbackup -N' (return_code must be 0); + * run 'gstat -r' and parse some lines of its output (rows with blob info and table size): + gstat output must have: + ** two lines with info about blob, like these: + Blobs: 1, total length: 4294967297, blob pages: 526345 + Level 2: 1, total length: 4294967297, blob pages: 526345 + (and 'total length' must be equal BLOB_DATA_LEN); + ** one line with info about table size: + Table size: 4311842816 bytes + (and size must be greater than BLOB_DATA_LEN). +NOTES: + [18.03.2025] pzotov + 1. ### ACHTUNG ### At least 8.1 Gb of free storage required for generated data and test DB used by this test. + CHECK folder that is specified in '--basetemp' switch of pytest command! + 2. Usual backup and restore *was* checked but duration of test lasts too long in this case (about 10 minutes). + Because of this, b/r currently is DISABLED. + 3. Also, it was checked wire statistics when blob data was obtained in ISQL: + Wire logical statistics: + send packets = 262186 + recv packets = 262184 + send bytes = 4195080 + recv bytes = 4303881656 + Wire physical statistics: + send packets = 262180 + recv packets = 786533 + send bytes = 4195080 + recv bytes = 4303881656 + roundtrips = 262179 + But this call also was DISABLED because it takes too much time. + + Discussed with dimitr, letters since 15-mar-2025 02:16. + Test execution time: about 5 minutes. + Checked on 6.0.0.677 SS/CS. +""" + +import os +import hashlib +import re +import locale +from pathlib import Path +import time + +import pytest +from firebird.qa import * +from firebird.driver import SrvRestoreFlag + +init_ddl = """ + set term ^; + recreate view v_data as select 1 x from rdb$database + ^ + recreate view v_vchr as select 1 x from rdb$database + ^ + recreate view v_blob as select 1 x from rdb$database + ^ + recreate table test_vchr(id int primary key using index test_vchr_pk, text_fld varchar(1)) + ^ + recreate table test_blob(id int primary key using index test_blob_pk, blob_fld blob) + ^ + recreate sequence gen_test + ^ + recreate view v_vchr as select id, text_fld from test_vchr + ^ + recreate view v_blob as select id, blob_fld from test_blob + ^ + recreate view v_data as select a.id, a.text_fld, b.blob_fld from test_vchr a left join test_blob b on a.id = b.id + ^ + create or alter trigger v_data_biud for v_data active before insert or update or delete position 0 + as + declare v_id int; + begin + if (inserting) then + begin + insert into v_vchr(id, text_fld) values( coalesce( new.id, gen_id(gen_test,1) ), new.text_fld ) + returning id into v_id; + insert into v_blob(id, blob_fld) values( :v_id, new.blob_fld ); + end + else if (updating) then + begin + update v_vchr set id = coalesce(new.id, old.id), text_fld = :new.text_fld where id = old.id; + update v_blob set id = coalesce(new.id, old.id), blob_fld = :new.blob_fld where id = old.id; + end + else + begin + delete from v_vchr where id = old.id; + delete from v_blob where id = old.id; + end + end + ^ + commit + ^ +""" + +db = db_factory(init = init_ddl) +act = python_act('db', substitutions = [(r'blob pages: \d+', 'blob pages'), (r'Level(:)?\s+\d+:\s+\d+, ', 'Level, ')]) + +BLOB_DATA_LEN = 2**32+1 +DATA_CHUNK_LEN = BLOB_DATA_LEN // 1000 + +tmp_dat = temp_file('tmp_blob_4gb.dat') +tmp_fbk = temp_file('tmp_blob_4gb.fbk') + +@pytest.mark.version('>=6.0') +def test_1(act: Action, tmp_dat: Path, tmp_fbk: Path, capsys): + + chunk_hashes_lst = [] + with open(tmp_dat, 'wb+') as f_blob_data: + n = 0 + while n < BLOB_DATA_LEN: + rnd_chunk = os.urandom(min(DATA_CHUNK_LEN, BLOB_DATA_LEN-n)) + chunk_hashes_lst.append( hashlib.sha1(rnd_chunk).hexdigest() ) + f_blob_data.write( rnd_chunk ) + n += DATA_CHUNK_LEN + + fdb_delta = act.db.db_path.with_suffix('.delta') + fdb_delta.unlink(missing_ok = True) + + #----------------------------------------- + act.nbackup(switches=['-L', act.db.dsn], combine_output = True, io_enc = locale.getpreferredencoding()) + assert act.return_code == 0, f'nbackup -L failed: {act.clean_stdout}' + act.reset() + #----------------------------------------- + + with act.db.connect() as con: + cur = con.cursor() + with open(tmp_dat, 'rb') as f_blob_data: + cur.execute("insert into v_data(text_fld, blob_fld) values (?, ?)", ('a', f_blob_data)) + con.commit() + tmp_dat.unlink() + #------------------------------------- + cur.stream_blobs.append('BLOB_FLD') + cur.execute('select blob_fld, octet_length(blob_fld) from v_data order by id rows 1') + b_reader, b_length = cur.fetchone() + n = 0 + check_hashes_lst = [] + while n < BLOB_DATA_LEN: + b_data_chunk = b_reader.read( min(DATA_CHUNK_LEN, BLOB_DATA_LEN-n) ) + check_hashes_lst.append( hashlib.sha1(b_data_chunk).hexdigest() ) + n += DATA_CHUNK_LEN + print(f'Fetch finished, {b_length=}. Equality check:', chunk_hashes_lst == check_hashes_lst) + + act.expected_stdout = f""" + Fetch finished, {b_length=}. Equality check: True + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() + + #--------------------------------------------------- + act.nbackup(switches=['-N', act.db.dsn], combine_output = True, io_enc = locale.getpreferredencoding()) + assert act.return_code == 0, f'nbackup -N failed: {act.clean_stdout}' + act.reset() + #--------------------------------------------------- + + act.gstat(switches=['-r', '-t', 'TEST_BLOB']) + + blob_page_ptn = re.compile(r'total length(:)?\s+\d+, blob pages(:)?\s+\d+') + table_size_ptn = re.compile(r'Table size(:)?\s+\d+\s+bytes') + table_size_parsing_result = '' + expected_table_size = f'Table size: NOT LESS than {BLOB_DATA_LEN}' + for line in act.stdout.splitlines(): + if blob_page_ptn.search(line): + print(line) + if table_size_ptn.search(line): + table_size_value = int(line.split()[2]) + if table_size_value > BLOB_DATA_LEN: + table_size_parsing_result = expected_table_size + else: + table_size_parsing_result = f'Table size: INCORRECT, {table_size_value} - less than {BLOB_DATA_LEN}' + print(table_size_parsing_result) + + act.stdout = capsys.readouterr().out + act.expected_stdout = f""" + Blobs: 1, total length: {BLOB_DATA_LEN}, blob pages + Level, total length: {BLOB_DATA_LEN}, blob pages + {expected_table_size} + """ + assert act.clean_stdout == act.clean_expected_stdout + act.reset() + + ''' + D I S A B L E D: T O O L O N G T I M E + #---------------------------------------------------- + expected_backup_out = 'Backup completed OK.' + expected_restore_out = 'Restore completed OK.' + successful_backup_pattern = re.compile(r'gbak:closing file, committing, and finishing. \\d+ bytes written', re.IGNORECASE) + successful_restore_pattern = re.compile( r'gbak:finishing, closing, and going home', re.IGNORECASE ) + + act.gbak(switches = ['-b', '-verbint', str(2**31-1), act.db.dsn, tmp_fbk]) + for line in act.stdout.splitlines(): + if successful_backup_pattern.search(line): + print(expected_backup_out) + break + + act.expected_stdout = f""" + {expected_backup_out} + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() + #---------------------------------------------------- + act.gbak(switches = ['-rep', '-verbint', str(2**31-1), tmp_fbk, act.db.dsn]) + for line in act.stdout.splitlines(): + if successful_restore_pattern.search(line): + print(expected_restore_out) + break + + act.expected_stdout = f""" + {expected_restore_out} + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() + ''' diff --git a/tests/functional/blob/test_read_blob_in_multiple_cursors.py b/tests/functional/blob/test_read_blob_in_multiple_cursors.py new file mode 100644 index 00000000..b155198d --- /dev/null +++ b/tests/functional/blob/test_read_blob_in_multiple_cursors.py @@ -0,0 +1,117 @@ +#coding:utf-8 + +""" +ID: functional.blob.test_access_to_blob_in_multiple_cursors.py +TITLE: Ability to read same blob_id using multiple cursors +DESCRIPTION: + Source ticket: + https://github.com/FirebirdSQL/firebird/pull/8318 + + Test creates small file and writes random data in it. + Then it opens streamed blob, stores file content in DB and attempts to access this blob using two cursors + by creating blob_readers, call read() and comparing fetched data. + Closing of second blob_reader must PASS w/o error (it raises 'invalid BLOB handle' before fix). + Above mentioned actions are done for several sizes, see DATA_LEN_LIST. + + If all fine then only 'dummy' message is printed about success. + Otherwise we use capsys.readouterr().out that accumulated print() output and show all detailed info. +NOTES: + [29.03.2025] pzotov + Problem with access to same blob using two cursors was fixed in: + https://github.com/FirebirdSQL/firebird/commit/48ef37d826454eff70057c17b414c19e0f00f8df + + Confirmed bug on 6.0.0.698-f5c7e57: got 'invalid BLOB handle' when trying to close second blob_reader. + Checked on 6.0.0.702-16ef06e -- all OK. + + [02.04.2025] pzotov + Checked on 5.0.3.1639-f47fcd9 after commit 54c61058 ("Backported seek for cached blob (missed part of #8318)"). + Reduced min_version to 5.0.3. +""" +import os +from pathlib import Path +import random +import traceback +import string + +import pytest +from firebird.qa import * +from firebird.driver import DbInfoCode, DatabaseError + +EXPECTED_MSG = 'Passed.' +init_ddl = """ + recreate global temporary table bdata(blob_fld blob) on commit preserve rows; + commit; +""" + +db = db_factory(init = init_ddl) +act = python_act('db') + +tmp_blob_file = temp_file('tmp_small_blob.dat') + +@pytest.mark.version('>=5.0.3') +def test_1(act: Action, tmp_blob_file: Path, capsys): + + DATA_LEN_LIST = \ + ( 0, 1, 2, 3, 4, + 32764,32765,32766,32767,32768, + 65532,65533,65534,65535,65536 + ) + + with act.db.connect() as con: + cur1 = con.cursor() + cur2 = con.cursor() + cur1.execute("select rdb$get_context('SYSTEM', 'CLIENT_VERSION') as client_version from rdb$database") + hdr=cur1.description + for r in cur1: + for i in range(0,len(hdr)): + print( hdr[i][0],':', r[i] ) + print(f'{con.info.version=}, {con.info.get_info(DbInfoCode.PROTOCOL_VERSION)=}') + failed_cnt = 0 + for b_gen_size in DATA_LEN_LIST: + print(f'\n\nStart of loop for {b_gen_size=} in DATA_LEN_LIST') + try: + b_data_1, b_data_2 = '', '' + with open(tmp_blob_file, 'wb') as f_binary_data: + # f_binary_data.write( os.urandom(b_gen_size) ) + f_binary_data.write( bytearray( ''.join(random.choices(string.ascii_letters, k = b_gen_size)).encode('ascii') ) ) + + with open(tmp_blob_file, 'rb') as f_binary_data: + cur1.execute("delete from bdata") + cur1.execute("insert into bdata(blob_fld) values (?)", (f_binary_data,)) + con.commit() + + cur1.stream_blobs.append('BLOB_FLD') + cur1.execute('select blob_fld from bdata') + blob_reader_1 = cur1.fetchone()[0] + blob_reader_1.seek(0, os.SEEK_SET) + b_data_1 = blob_reader_1.read(b_gen_size) + + cur2.stream_blobs.append('BLOB_FLD') + cur2.execute('select blob_fld from bdata') + blob_reader_2 = cur2.fetchone()[0] + blob_reader_2.seek(-b_gen_size, os.SEEK_END) + b_data_2 = blob_reader_2.read(b_gen_size) + + blob_reader_1.close() + + # before fix this caused 'invalid BLOB handle' (gdscode = 335544328): + blob_reader_2.close() + + if b_gen_size > 0 and (not b_data_1 or not b_data_2) or b_data_1 != b_data_2: + print(f'{b_gen_size=}. Data fetched in different cursors are not equal: {b_data_1=}, {b_data_2=}.') + failed_cnt += 1 + else: + print(f'{b_gen_size=}. Data fetched in different cursors EQUALS: {b_data_1=}, {b_data_2=}.') + except DatabaseError as e: + print(e) + print(e.gds_codes) + for x in traceback.format_exc().split('\n'): + print(' ',x) + + + if failed_cnt == 0: + act.stdout = EXPECTED_MSG + else: + act.stdout = 'CHECK NOT PASSED:\n\n' + capsys.readouterr().out + + assert act.clean_stdout == EXPECTED_MSG diff --git a/tests/functional/blob/test_small_blob_caching.py b/tests/functional/blob/test_small_blob_caching.py new file mode 100644 index 00000000..79d842ce --- /dev/null +++ b/tests/functional/blob/test_small_blob_caching.py @@ -0,0 +1,183 @@ +#coding:utf-8 + +""" +ID: functional.blob.test_small_blob_caching.py +TITLE: Operations with inline streamed blobs when boundary values are used for seek() and read() methods. +DESCRIPTION: + Source ticket: + https://github.com/FirebirdSQL/firebird/pull/8318 + + Test verifies that handling of data stored in file and in database blob field leads to same result. + This is done for several scopes of size which are close to 0, 1, 8, 16, 32 and 64K (see 'CHECKED_LEN_RANGES'). + For each scope of sizes following actions are performed: + * start outer loop for size, from low to high bound (see: 'for b_gen_size in range(..., ...)'); + * create random string of size and store it in the file and blob field (using stream API); + * let scope boundary values be: 'pos_beg', 'pos_end' (they will be used in evaluation of seek() arg.); + * start inner loop: 'for i_pos in range(pos_beg, pos_end+1)' and use counter (i_pos) as argument for seek(); + * adjust position of file pointer using seek(); + * calculate number of bytes to be gathered after seek(): b_read_cnt = pos_end+1 - i_pos; + * read bytes from file with data; + * do the same for blob that is stored in DB; + * compare content of bytearrays that was gathered from FILE vs BLOB. They must be equal. + These actions are done for all values of 'whence' argument for seek() call: os.SEEK_SET, os.SEEK_CUR, os.SEEK_END + If checks passed for all iterations then only 'dummy' message is printed about success. + Otherwise we use capsys.readouterr().out that accumulated print() output and show all detailed info. +NOTES: + [27.03.2025] pzotov + 0. ### ACHTUNG ### + Source code of Python firebird-driver has to be checked/adjusted in order this test runs correctly! + In class Cursor we have to check that its method '_unpack_output()' contains in if-else branch which checks + 'datatype == SQLDataType.BLOB' following line: + blob = self._connection._att.open_blob(self._transaction._tra, blobid) // note: last argument is 'blobid' + ~~~~~~ + Originally this line looked like this: + blob = self._connection._att.open_blob(self._transaction._tra, blobid, _bpb_stream) + ~~~~~~~~~~~ + This must be considered as BUG because kind of blob (stream vs materialized) can be set only when blob is CREATED + but NOT when it is to be fetched. + Opening blob with BPB (i.e. with argument '_bpb_stream') forces client to remove previously cached inline blob + (because it was opened without BPB). This, in turn, causes seek() to work with NON-cached blob and test in such case + will not check what we need. + + This issue was found by Vlad, letter 25.03.2025 00:13. + + 1. Test tries to read content both within data and beyond its end! + For example, following occurs when use b_gen_size = 4 and call seek() with requirement to set file position + before first byte of file/blob: + data: |qwer| + i_pos = 0: *12345 // seek(0, whence = os.SEEK_BEG); read(5) --> result must be bytearray with len = 4 + i_pos = 1: |*12345 // seek(1, whence = os.SEEK_BEG); read(5) --> result must be bytearray with len = 3 + i_pos = 2: | *12345 // seek(2, whence = os.SEEK_BEG); read(5) --> result must be bytearray with len = 2 + i_pos = 3: | *12345 // seek(3, whence = os.SEEK_BEG); read(5) --> result must be bytearray with len = 1 + i_pos = 4: | *12345 // seek(4, whence = os.SEEK_BEG); read(5) --> result must be bytearray with len = 0 + Legend: + | = boundaries (before start and after end of data) + * = position for seek + 12345 = subsequent count of bytes that we try to gather into bytearray + After all values of i_pos are checked, we switch whence from os.SEEK_BEG to os.SEEK_CUR, i.e. use relative offsets. + In this case we start from moving pointer back from current position to the beginning of file/blob, and repeat the same. + After all values of i_pos for os.SEEK_CUR, we check the same for os.SEEK_END, but one need to take in account that + we can not use negative values of b_read_pos with ABS() greater than b_gen_size (e.g. b_read_pos=-5 when b_gen_size=4), + because it causes Python exception. + 2. During test implementation, it was discovered that comparison of FILE content vs DB blob field can fail if size of + generated blobs are about 32K and blob is stored in GLOBAL TEMPORARY TABLE. It was fixed by commit: + https://github.com/FirebirdSQL/firebird/commit/51b72178c29331ccb33e38f0c36f4d0cee902a7c + ("Don't rely on wrong assumption that stream blob always have maximum possible segment size"). + Because of that, test intentionally uses GTT instead of permanent table to store blobs. + + Checked on intermediate snapshot 6.0.0.698-f5c7e57. + + [02.04.2025] pzotov + Checked on 5.0.3.1639-f47fcd9 after commit 54c61058 ("Backported seek for cached blob (missed part of #8318)"). + Reduced min_version to 5.0.3. +""" +import os +from pathlib import Path +import random +import string + +import pytest +from firebird.qa import * +from firebird.driver import DbInfoCode + +CHECKED_LEN_RANGES = { + 'about_00k' : (0, 5) + ,'about_01k' : ( 1024-4, 1024+1) + ,'about_08k' : ( 8*1024-4, 8*1024+1) + ,'about_16k' : (16*1024-4, 16*1024+1) + ,'about_32k' : (32*1024-4, 32*1024+1) + ,'about_64k' : (64*1024-4, 64*1024+1) +} + +EXPECTED_MSG = 'All checks passed.' +init_ddl = """ + recreate global temporary table bdata(blob_fld blob) on commit preserve rows; + -- recreate table bdata(blob_fld blob); + recreate view v_bdata as select blob_fld as v_blob_fld from bdata; + commit; +""" + +db = db_factory(init = init_ddl) +act = python_act('db') + +tmp_blob_file = temp_file('tmp_small_blob.dat') + +@pytest.mark.version('>=5.0.3') +def test_1(act: Action, tmp_blob_file: Path, capsys): + with act.db.connect() as con: + cur1 = con.cursor() + cur1.execute("select rdb$get_context('SYSTEM', 'CLIENT_VERSION') as client_version from rdb$database") + hdr=cur1.description + for r in cur1: + for i in range(0,len(hdr)): + print( hdr[i][0],':', r[i] ) + print(f'{con.info.version=}, {con.info.get_info(DbInfoCode.PROTOCOL_VERSION)=}') + + failed_count = 0 + for b_range_mnemona, b_len_checked_range in CHECKED_LEN_RANGES.items(): + print(f'Preparing data, {b_range_mnemona=}, {b_len_checked_range=}') + for b_gen_size in range(b_len_checked_range[0], b_len_checked_range[1]+1): + print(f'\nGenerate file with data to be stored further in blob field, {b_len_checked_range=}, {b_gen_size=}') + with open(tmp_blob_file, 'wb') as f_binary_data: + f_binary_data.write( bytearray( ''.join(random.choices(string.ascii_letters + string.digits, k = b_gen_size)).encode('ascii') ) ) + + with open(tmp_blob_file, 'rb') as f_binary_data: + cur1.execute("delete from bdata") + cur1.execute("insert into bdata(blob_fld) values (?)", (f_binary_data,)) + con.commit() + + cur1.stream_blobs.append('BLOB_FLD') + cur1.execute('select blob_fld from bdata') + blob_reader_1 = cur1.fetchone()[0] + + pos_beg, pos_end = b_len_checked_range[:2] + b_read_cnt = 0 + for i_pos in range(pos_beg, pos_end+1): + print(f'\n{i_pos=}', ' - *NOTE* WORK BEYOND EOF:' if i_pos > b_gen_size else '') + for whence in (os.SEEK_SET, os.SEEK_CUR, os.SEEK_END): + print(f'\n Start loop for whence in (os.SEEK_SET, os.SEEK_CUR, os.SEEK_END): {f_binary_data.tell()=}, {blob_reader_1.tell()=}') + if whence == os.SEEK_CUR: + f_binary_data.seek( -min(b_gen_size, (i_pos + b_read_cnt)), os.SEEK_CUR) + blob_reader_1.seek( -min(b_gen_size, (i_pos + b_read_cnt)), os.SEEK_CUR) + print(f' whence == os.SEEK_CUR --> move back for {-min(b_gen_size, (i_pos + b_read_cnt))=}. Result: {f_binary_data.tell()=}, {blob_reader_1.tell()=}') + elif whence == os.SEEK_END: + b_read_pos = -(pos_end+1 - i_pos) + print(f' whence == os.SEEK_END: {b_read_pos=}') + if -b_read_pos > b_gen_size: + print(f' Can not use negative {b_read_pos=} for {whence=} and {b_gen_size=}') + continue + else: + b_read_pos = i_pos + print(f' whence == os.SEEK_SET: {b_read_pos=}') + + print(f' FILE: f_binary_data.seek({b_read_pos}, {whence=})') + f_binary_data.seek(b_read_pos, whence) + b_read_cnt = pos_end+1 - i_pos + print(f' FILE: {f_binary_data.tell()=}, attempt to read {b_read_cnt=} bytes starting from {i_pos=} using {whence=}') + b_data_in_file = f_binary_data.read(b_read_cnt) + print(f' FILE: completed read {b_read_cnt=} bytes starting from {i_pos=} using {whence=}. Result: {f_binary_data.tell()=}, {len(b_data_in_file)=}') + + # Now do the same against DB (stream blob): + print(f' BLOB: blob_reader_1.seek({b_read_pos}, {whence=})') + blob_reader_1.seek(b_read_pos, whence) + print(f' BLOB: {blob_reader_1.tell()=}, attempt to read {b_read_cnt=} bytes starting from {i_pos=} using {whence=}') + b_data_from_db = blob_reader_1.read(b_read_cnt) + print(f' BLOB: completed read {b_read_cnt=} bytes starting from {i_pos=} using {whence=}. Result: {blob_reader_1.tell()=}, {len(b_data_from_db)=}') + + if not b_data_in_file == b_data_from_db: + print(f'### ERROR ### Data gathered from FILE differs from BLOB: b_data_in_file: >{b_data_in_file}<, b_data_from_db: >{b_data_from_db}<') + failed_count += 1 + else: + print(f'+++ PASSED +++ Data gathered from FILE and BLOB equals: b_data_in_file: >{b_data_in_file}<, b_data_from_db: >{b_data_from_db}<') + + blob_reader_1.close() + + #< for b_gen_size in range(b_len_checked_range[0], b_len_checked_range[1]+1) + #< for b_range_mnemona, b_len_checked_range in CHECKED_LEN_RANGES.items() + + if failed_count == 0: + act.stdout = EXPECTED_MSG + else: + act.stdout = 'CHECK NOT PASSED:\n\n' + capsys.readouterr().out + + assert act.clean_stdout == EXPECTED_MSG diff --git a/tests/functional/database/alter/test_01.py b/tests/functional/database/alter/test_01.py index 1fdb86d3..f35bdc81 100644 --- a/tests/functional/database/alter/test_01.py +++ b/tests/functional/database/alter/test_01.py @@ -4,7 +4,11 @@ ID: alter-database-01 TITLE: Alter database: adding a secondary file DESCRIPTION: Adding a secondary file to the database -FBTEST: functional.database.alter.01 +NOTES: + [29.12.2024] pzotov + Added restriction for FB 6.x: this test now must be skipped, see: + https://github.com/FirebirdSQL/firebird/commit/f0740d2a3282ed92a87b8e0547139ba8efe61173 + ("Wipe out multi-file database support (#8047)") """ import pytest @@ -20,7 +24,7 @@ C:\\JOB\\QA\\FBTEST\\TMP\\TEST.G00 1 10000 0 """ -@pytest.mark.version('>=3.0') +@pytest.mark.version('>=3.0,<6') def test_1(act: Action, capsys): with act.db.connect() as con: with con.cursor() as c: diff --git a/tests/functional/database/alter/test_02.py b/tests/functional/database/alter/test_02.py index 642a6ee7..1bedd276 100644 --- a/tests/functional/database/alter/test_02.py +++ b/tests/functional/database/alter/test_02.py @@ -4,7 +4,11 @@ ID: alter-database-02 TITLE: Alter database: adding secondary file with alternate keyword DESCRIPTION: Adding secondary file with alternate keyword for database. -FBTEST: functional.database.alter.02 +NOTES: + [29.12.2024] pzotov + Added restriction for FB 6.x: this test now must be skipped, see: + https://github.com/FirebirdSQL/firebird/commit/f0740d2a3282ed92a87b8e0547139ba8efe61173 + ("Wipe out multi-file database support (#8047)") """ import pytest @@ -19,7 +23,7 @@ C:\\JOB\\QA\\FBTEST\\TMP\\TEST.G00 1 10000 0 """ -@pytest.mark.version('>=3.0') +@pytest.mark.version('>=3.0,<6') def test_1(act: Action, capsys): with act.db.connect() as con: with con.cursor() as c: diff --git a/tests/functional/database/alter/test_03.py b/tests/functional/database/alter/test_03.py index 952c87c3..b86701aa 100644 --- a/tests/functional/database/alter/test_03.py +++ b/tests/functional/database/alter/test_03.py @@ -5,14 +5,18 @@ TITLE: Alter database: add file with name of this database or previously added files must fail DESCRIPTION: Add same file twice must fail NOTES: - [08.02.2022] pcisar - Fails on Windows with 3.0.8: + [08.02.2022] pcisar + Fails on Windows with 3.0.8: Regex pattern '.*Cannot add file with the same name as the database or added files.*' does not match 'unsuccessful metadata update\n-ALTER DATABASE failed\n-unknown ISC error 336068774'. - [08.04.2022] pzotov - Test PASSES on FB 3.0.8 Rls, 4.0.1 RLs and 5.0.0.467. -FBTEST: functional.database.alter.03 + [08.04.2022] pzotov + Test PASSES on FB 3.0.8 Rls, 4.0.1 RLs and 5.0.0.467. + + [29.12.2024] pzotov + Added restriction for FB 6.x: this test now must be skipped, see: + https://github.com/FirebirdSQL/firebird/commit/f0740d2a3282ed92a87b8e0547139ba8efe61173 + ("Wipe out multi-file database support (#8047)") """ import pytest @@ -24,7 +28,7 @@ act = python_act('db') -@pytest.mark.version('>=3.0') +@pytest.mark.version('>=3.0,<6') def test_1(act: Action, capsys): with act.db.connect() as con: with con.cursor() as c: diff --git a/tests/functional/database/create/test_01.py b/tests/functional/database/create/test_01.py index 92d2c6f0..5eb76318 100644 --- a/tests/functional/database/create/test_01.py +++ b/tests/functional/database/create/test_01.py @@ -25,14 +25,14 @@ @pytest.mark.version('>=3.0') def test_1(act: Action): script = f""" - create database '{act.db.dsn}' user '{act.db.user}' - password '{act.db.password}' set names 'win1251' default character set utf8 ; - set list on ; - select c.rdb$character_set_name as client_char_set, r.rdb$character_set_name as db_char_set - from mon$attachments a join rdb$character_sets c on a.mon$character_set_id = c.rdb$character_set_id - cross join rdb$database r - where a.mon$attachment_id = current_connection ; + create database '{act.db.dsn}' user '{act.db.user}' + password '{act.db.password}' set names 'win1251' default character set utf8 ; + set list on ; + select c.rdb$character_set_name as client_char_set, r.rdb$character_set_name as db_char_set + from mon$attachments a join rdb$character_sets c on a.mon$character_set_id = c.rdb$character_set_id + cross join rdb$database r + where a.mon$attachment_id = current_connection ; """ act.expected_stdout = expected_stdout - act.isql(switches=[], input=script, connect_db=False) + act.isql(switches = ['-q'], input=script, connect_db=False, combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/database/create/test_02.py b/tests/functional/database/create/test_02.py index 7170abf1..907183cb 100644 --- a/tests/functional/database/create/test_02.py +++ b/tests/functional/database/create/test_02.py @@ -5,60 +5,67 @@ TITLE: Create database: non sysdba user DESCRIPTION: FBTEST: functional.database.create.02 +NOTES: + [13.01.2025] pzotov + Added (temporary ?) 'credentials = False' to prevent ISQL from using '-USER ... -PASS ...'. + This is needed since 6.0.0.570, otherwise we get (on attempting to create DB): + Statement failed, SQLSTATE = 28000 + Your user name and password are not defined. Ask your database administrator to set up a Firebird login. + -Different logins in connect and attach packets - client library error + (IMO, this is bug; see https://github.com/FirebirdSQL/firebird/issues/8385) """ +import locale +from pathlib import Path import pytest from firebird.qa import * db = db_factory() +act = python_act('db', substitutions = [('[ \t]+', ' ')]) -test_script = """ - set wng off; - set bail on; - create or alter user ozzy password 'osb' revoke admin role; - commit; - revoke all on all from ozzy; - commit; +tmp_user = user_factory('db', name='tmp$boss', password='123') +test_db = temp_file('tmp4test.fdb') - -- ::: NB ::: do NOT miss specification of 'USER' or 'ROLE' clause in - -- GRANT | REVOKE CREATE DATABASE, between `to` and login! Otherwise: - -- Statement failed, SQLSTATE = 0A000 - -- unsuccessful metadata update - -- -GRANT failed - -- -feature is not supported - -- -Only grants to USER or ROLE are supported for CREATE DATABASE - grant create database to USER ozzy; - -- ^^^^ - grant drop database to USER ozzy; - -- ^^^^ - commit; - - create database 'localhost:$(DATABASE_LOCATION)tmp.ozzy$db$987456321.tmp' user 'OZZY' password 'osb'; - - set list on; - select - a.mon$user "Who am I ?" - ,iif( m.mon$database_name containing 'tmp.ozzy$db$987456321.tmp' , 'YES', 'NO! ' || m.mon$database_name) "Am I on just created DB ?" - from mon$database m, mon$attachments a where a.mon$attachment_id = current_connection; - commit; +@pytest.mark.version('>=3.0') +def test_1(act: Action, tmp_user: User, test_db: Path): - drop database; - connect '$(DSN)' user 'SYSDBA' password 'masterkey'; - revoke create database from user ozzy; - revoke drop database from user ozzy; - drop user ozzy; - commit; -""" + test_script = f""" + set wng off; + set bail on; + connect '{act.db.dsn}' user {act.db.user} password '{act.db.password}'; + revoke all on all from {tmp_user.name}; + commit; -act = isql_act('db', test_script) + -- ::: NB ::: do NOT miss specification of 'USER' or 'ROLE' clause in + -- GRANT | REVOKE CREATE DATABASE, between `to` and login! Otherwise: + -- Statement failed, SQLSTATE = 0A000 + -- unsuccessful metadata update + -- -GRANT failed + -- -feature is not supported + -- -Only grants to USER or ROLE are supported for CREATE DATABASE + grant create database to USER {tmp_user.name}; + -- ^^^^ + grant drop database to USER {tmp_user.name}; + -- ^^^^ + commit; + create database 'localhost:{test_db}' user {tmp_user.name} password '{tmp_user.password}'; -expected_stdout = """ - Who am I ? OZZY - Am I on just created DB ? YES -""" + set list on; + select + a.mon$user "Who am I ?" + ,iif( m.mon$database_name containing '{test_db}' , 'YES', 'NO! ' || m.mon$database_name) "Am I on just created DB ?" + from mon$database m cross join mon$attachments a + where a.mon$attachment_id = current_connection; + commit; + drop database; + set echo on; + -- Done. + """ -@pytest.mark.version('>=3.0') -def test_1(act: Action): - act.expected_stdout = expected_stdout - act.execute() + act.expected_stdout = f""" + Who am I ? {tmp_user.name.upper()} + Am I on just created DB ? YES + -- Done. + """ + act.isql(switches=['-q'], input=test_script, connect_db=False, combine_output = True, credentials = False, io_enc = locale.getpreferredencoding()) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/database/create/test_08.py b/tests/functional/database/create/test_08.py index 38767e64..2ace3a43 100644 --- a/tests/functional/database/create/test_08.py +++ b/tests/functional/database/create/test_08.py @@ -4,7 +4,11 @@ ID: create-database-08 TITLE: Create database: Multi file DB DESCRIPTION: Create database with two files. -FBTEST: functional.database.create.08 +NOTES: + [29.12.2024] pzotov + Added restriction for FB 6.x: this test now must be skipped, see: + https://github.com/FirebirdSQL/firebird/commit/f0740d2a3282ed92a87b8e0547139ba8efe61173 + ("Wipe out multi-file database support (#8047)") """ import pytest @@ -22,7 +26,7 @@ RDB$FILE_LENGTH 300 """ -@pytest.mark.version('>=3') +@pytest.mark.version('>=3,<6') def test_1(act: Action): script = f""" create database '{act.db.dsn}' user '{act.db.user}' @@ -38,5 +42,5 @@ def test_1(act: Action): from rdb$files ; """ act.expected_stdout = expected_stdout - act.isql(switches=[], input=script, connect_db=False) + act.isql(switches = ['-q'], input=script, connect_db=False, combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/database/create/test_09.py b/tests/functional/database/create/test_09.py index 4d2b0d3e..d309e2f1 100644 --- a/tests/functional/database/create/test_09.py +++ b/tests/functional/database/create/test_09.py @@ -4,7 +4,11 @@ ID: create-database-09 TITLE: Create database: Multi file DB DESCRIPTION: Create database with four files. -FBTEST: functional.database.create.09 +NOTES: + [29.12.2024] pzotov + Added restriction for FB 6.x: this test now must be skipped, see: + https://github.com/FirebirdSQL/firebird/commit/f0740d2a3282ed92a87b8e0547139ba8efe61173 + ("Wipe out multi-file database support (#8047)") """ import pytest @@ -30,7 +34,7 @@ RDB$FILE_LENGTH 300 """ -@pytest.mark.version('>=3') +@pytest.mark.version('>=3,<6') def test_1(act: Action): script = f""" create database '{act.db.dsn}' user '{act.db.user}' @@ -48,5 +52,5 @@ def test_1(act: Action): from rdb$files ; """ act.expected_stdout = expected_stdout - act.isql(switches=[], input=script, connect_db=False) + act.isql(switches = ['-q'], input=script, connect_db=False, combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/database/create/test_10.py b/tests/functional/database/create/test_10.py index 003ed6a5..a1ec4e4e 100644 --- a/tests/functional/database/create/test_10.py +++ b/tests/functional/database/create/test_10.py @@ -4,7 +4,11 @@ ID: create-database-10 TITLE: Create database: Multi file DB - starting DESCRIPTION: Database with four files. Additional files specified by STARTING AT. -FBTEST: functional.database.create.10 +NOTES: + [29.12.2024] pzotov + Added restriction for FB 6.x: this test now must be skipped, see: + https://github.com/FirebirdSQL/firebird/commit/f0740d2a3282ed92a87b8e0547139ba8efe61173 + ("Wipe out multi-file database support (#8047)") """ import pytest @@ -29,7 +33,7 @@ RDB$FILE_LENGTH 0 """ -@pytest.mark.version('>=3.0') +@pytest.mark.version('>=3.0,<6') def test_1(act: Action): script = f""" create database '{act.db.dsn}' user '{act.db.user}' @@ -47,5 +51,5 @@ def test_1(act: Action): from rdb$files ; """ act.expected_stdout = expected_stdout - act.isql(switches=[], input=script, connect_db=False) + act.isql(switches = ['-q'], input=script, connect_db=False, combine_output = True) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/datatypes/test_double_min_distinguish.py b/tests/functional/datatypes/test_double_min_distinguish.py new file mode 100644 index 00000000..8d939412 --- /dev/null +++ b/tests/functional/datatypes/test_double_min_distinguish.py @@ -0,0 +1,595 @@ +#coding:utf-8 + +""" +ID: double.min-distinguish +TITLE: List of all values starting from 1.0 divided by 2, until previous and current become equal +DESCRIPTION: +NOTES: + [16.04.2025] pzotov + Discussed with hvlad and dimitr, letters since 11-apr-2025 19:12. + On Windows FB 3.x has lot of differences; FB 4.x also has differences, albeit very few. + On Linux FB 4.x ... 6.x have no differences from each other (and from Windows for Fb 5.x ... 6.x). + Because of that, min_version is set to 5.0. + + Checked on Linux: 6.0.0.726-d79c643; 5.0.3.1642-9dc399f; Windows: 6.0.0.726-90e0f49; 5.0.3.1641-da493b5 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set heading off; + set term ^; + execute block returns(i int, x double precision, y double precision) as + begin + i = 0; + x = cast(1.0 as double precision); + y = cast(0.5 as double precision); + while ( i < 20000 ) do + begin + suspend; + i = i+1; + x = y/2; + y = y/4; + if ( x<= y ) then + begin + suspend; + leave; + end + end + end + ^ + set term ;^ +""" + +act = isql_act('db', test_script, substitutions=[('[\\s]+', ' ')]) + +expected_stdout = """ + 0 1.000000000000000 0.5000000000000000 + 1 0.2500000000000000 0.1250000000000000 + 2 0.06250000000000000 0.03125000000000000 + 3 0.01562500000000000 0.007812500000000000 + 4 0.003906250000000000 0.001953125000000000 + 5 0.0009765625000000000 0.0004882812500000000 + 6 0.0002441406250000000 0.0001220703125000000 + 7 6.103515625000000e-05 3.051757812500000e-05 + 8 1.525878906250000e-05 7.629394531250000e-06 + 9 3.814697265625000e-06 1.907348632812500e-06 + 10 9.536743164062500e-07 4.768371582031250e-07 + 11 2.384185791015625e-07 1.192092895507812e-07 + 12 5.960464477539062e-08 2.980232238769531e-08 + 13 1.490116119384766e-08 7.450580596923828e-09 + 14 3.725290298461914e-09 1.862645149230957e-09 + 15 9.313225746154785e-10 4.656612873077393e-10 + 16 2.328306436538696e-10 1.164153218269348e-10 + 17 5.820766091346741e-11 2.910383045673370e-11 + 18 1.455191522836685e-11 7.275957614183426e-12 + 19 3.637978807091713e-12 1.818989403545856e-12 + 20 9.094947017729282e-13 4.547473508864641e-13 + 21 2.273736754432321e-13 1.136868377216160e-13 + 22 5.684341886080801e-14 2.842170943040401e-14 + 23 1.421085471520200e-14 7.105427357601002e-15 + 24 3.552713678800501e-15 1.776356839400250e-15 + 25 8.881784197001252e-16 4.440892098500626e-16 + 26 2.220446049250313e-16 1.110223024625157e-16 + 27 5.551115123125783e-17 2.775557561562891e-17 + 28 1.387778780781446e-17 6.938893903907228e-18 + 29 3.469446951953614e-18 1.734723475976807e-18 + 30 8.673617379884035e-19 4.336808689942018e-19 + 31 2.168404344971009e-19 1.084202172485504e-19 + 32 5.421010862427522e-20 2.710505431213761e-20 + 33 1.355252715606881e-20 6.776263578034403e-21 + 34 3.388131789017201e-21 1.694065894508601e-21 + 35 8.470329472543003e-22 4.235164736271502e-22 + 36 2.117582368135751e-22 1.058791184067875e-22 + 37 5.293955920339377e-23 2.646977960169689e-23 + 38 1.323488980084844e-23 6.617444900424221e-24 + 39 3.308722450212111e-24 1.654361225106055e-24 + 40 8.271806125530277e-25 4.135903062765138e-25 + 41 2.067951531382569e-25 1.033975765691285e-25 + 42 5.169878828456423e-26 2.584939414228211e-26 + 43 1.292469707114106e-26 6.462348535570529e-27 + 44 3.231174267785264e-27 1.615587133892632e-27 + 45 8.077935669463161e-28 4.038967834731580e-28 + 46 2.019483917365790e-28 1.009741958682895e-28 + 47 5.048709793414476e-29 2.524354896707238e-29 + 48 1.262177448353619e-29 6.310887241768094e-30 + 49 3.155443620884047e-30 1.577721810442024e-30 + 50 7.888609052210118e-31 3.944304526105059e-31 + 51 1.972152263052530e-31 9.860761315262648e-32 + 52 4.930380657631324e-32 2.465190328815662e-32 + 53 1.232595164407831e-32 6.162975822039155e-33 + 54 3.081487911019577e-33 1.540743955509789e-33 + 55 7.703719777548943e-34 3.851859888774472e-34 + 56 1.925929944387236e-34 9.629649721936179e-35 + 57 4.814824860968090e-35 2.407412430484045e-35 + 58 1.203706215242022e-35 6.018531076210112e-36 + 59 3.009265538105056e-36 1.504632769052528e-36 + 60 7.523163845262640e-37 3.761581922631320e-37 + 61 1.880790961315660e-37 9.403954806578300e-38 + 62 4.701977403289150e-38 2.350988701644575e-38 + 63 1.175494350822288e-38 5.877471754111438e-39 + 64 2.938735877055719e-39 1.469367938527859e-39 + 65 7.346839692639297e-40 3.673419846319648e-40 + 66 1.836709923159824e-40 9.183549615799121e-41 + 67 4.591774807899561e-41 2.295887403949780e-41 + 68 1.147943701974890e-41 5.739718509874451e-42 + 69 2.869859254937225e-42 1.434929627468613e-42 + 70 7.174648137343063e-43 3.587324068671532e-43 + 71 1.793662034335766e-43 8.968310171678829e-44 + 72 4.484155085839415e-44 2.242077542919707e-44 + 73 1.121038771459854e-44 5.605193857299268e-45 + 74 2.802596928649634e-45 1.401298464324817e-45 + 75 7.006492321624085e-46 3.503246160812043e-46 + 76 1.751623080406021e-46 8.758115402030107e-47 + 77 4.379057701015053e-47 2.189528850507527e-47 + 78 1.094764425253763e-47 5.473822126268817e-48 + 79 2.736911063134408e-48 1.368455531567204e-48 + 80 6.842277657836021e-49 3.421138828918010e-49 + 81 1.710569414459005e-49 8.552847072295026e-50 + 82 4.276423536147513e-50 2.138211768073757e-50 + 83 1.069105884036878e-50 5.345529420184391e-51 + 84 2.672764710092196e-51 1.336382355046098e-51 + 85 6.681911775230489e-52 3.340955887615245e-52 + 86 1.670477943807622e-52 8.352389719038111e-53 + 87 4.176194859519056e-53 2.088097429759528e-53 + 88 1.044048714879764e-53 5.220243574398820e-54 + 89 2.610121787199410e-54 1.305060893599705e-54 + 90 6.525304467998525e-55 3.262652233999262e-55 + 91 1.631326116999631e-55 8.156630584998156e-56 + 92 4.078315292499078e-56 2.039157646249539e-56 + 93 1.019578823124769e-56 5.097894115623847e-57 + 94 2.548947057811924e-57 1.274473528905962e-57 + 95 6.372367644529809e-58 3.186183822264905e-58 + 96 1.593091911132452e-58 7.965459555662261e-59 + 97 3.982729777831131e-59 1.991364888915565e-59 + 98 9.956824444577827e-60 4.978412222288913e-60 + 99 2.489206111144457e-60 1.244603055572228e-60 + 100 6.223015277861142e-61 3.111507638930571e-61 + 101 1.555753819465285e-61 7.778769097326427e-62 + 102 3.889384548663214e-62 1.944692274331607e-62 + 103 9.723461371658034e-63 4.861730685829017e-63 + 104 2.430865342914508e-63 1.215432671457254e-63 + 105 6.077163357286271e-64 3.038581678643136e-64 + 106 1.519290839321568e-64 7.596454196607839e-65 + 107 3.798227098303919e-65 1.899113549151960e-65 + 108 9.495567745759799e-66 4.747783872879899e-66 + 109 2.373891936439950e-66 1.186945968219975e-66 + 110 5.934729841099874e-67 2.967364920549937e-67 + 111 1.483682460274969e-67 7.418412301374843e-68 + 112 3.709206150687421e-68 1.854603075343711e-68 + 113 9.273015376718553e-69 4.636507688359277e-69 + 114 2.318253844179638e-69 1.159126922089819e-69 + 115 5.795634610449096e-70 2.897817305224548e-70 + 116 1.448908652612274e-70 7.244543263061370e-71 + 117 3.622271631530685e-71 1.811135815765342e-71 + 118 9.055679078826712e-72 4.527839539413356e-72 + 119 2.263919769706678e-72 1.131959884853339e-72 + 120 5.659799424266695e-73 2.829899712133348e-73 + 121 1.414949856066674e-73 7.074749280333369e-74 + 122 3.537374640166685e-74 1.768687320083342e-74 + 123 8.843436600416711e-75 4.421718300208356e-75 + 124 2.210859150104178e-75 1.105429575052089e-75 + 125 5.527147875260445e-76 2.763573937630222e-76 + 126 1.381786968815111e-76 6.908934844075556e-77 + 127 3.454467422037778e-77 1.727233711018889e-77 + 128 8.636168555094445e-78 4.318084277547222e-78 + 129 2.159042138773611e-78 1.079521069386806e-78 + 130 5.397605346934028e-79 2.698802673467014e-79 + 131 1.349401336733507e-79 6.747006683667535e-80 + 132 3.373503341833767e-80 1.686751670916884e-80 + 133 8.433758354584419e-81 4.216879177292209e-81 + 134 2.108439588646105e-81 1.054219794323052e-81 + 135 5.271098971615262e-82 2.635549485807631e-82 + 136 1.317774742903815e-82 6.588873714519077e-83 + 137 3.294436857259539e-83 1.647218428629769e-83 + 138 8.236092143148846e-84 4.118046071574423e-84 + 139 2.059023035787212e-84 1.029511517893606e-84 + 140 5.147557589468029e-85 2.573778794734014e-85 + 141 1.286889397367007e-85 6.434446986835036e-86 + 142 3.217223493417518e-86 1.608611746708759e-86 + 143 8.043058733543795e-87 4.021529366771898e-87 + 144 2.010764683385949e-87 1.005382341692974e-87 + 145 5.026911708464872e-88 2.513455854232436e-88 + 146 1.256727927116218e-88 6.283639635581090e-89 + 147 3.141819817790545e-89 1.570909908895272e-89 + 148 7.854549544476362e-90 3.927274772238181e-90 + 149 1.963637386119091e-90 9.818186930595453e-91 + 150 4.909093465297727e-91 2.454546732648863e-91 + 151 1.227273366324432e-91 6.136366831622158e-92 + 152 3.068183415811079e-92 1.534091707905540e-92 + 153 7.670458539527698e-93 3.835229269763849e-93 + 154 1.917614634881924e-93 9.588073174409622e-94 + 155 4.794036587204811e-94 2.397018293602406e-94 + 156 1.198509146801203e-94 5.992545734006014e-95 + 157 2.996272867003007e-95 1.498136433501503e-95 + 158 7.490682167507517e-96 3.745341083753759e-96 + 159 1.872670541876879e-96 9.363352709384397e-97 + 160 4.681676354692198e-97 2.340838177346099e-97 + 161 1.170419088673050e-97 5.852095443365248e-98 + 162 2.926047721682624e-98 1.463023860841312e-98 + 163 7.315119304206560e-99 3.657559652103280e-99 + 164 1.828779826051640e-99 9.143899130258200e-100 + 165 4.571949565129100e-100 2.285974782564550e-100 + 166 1.142987391282275e-100 5.714936956411375e-101 + 167 2.857468478205687e-101 1.428734239102844e-101 + 168 7.143671195514219e-102 3.571835597757109e-102 + 169 1.785917798878555e-102 8.929588994392773e-103 + 170 4.464794497196387e-103 2.232397248598193e-103 + 171 1.116198624299097e-103 5.580993121495483e-104 + 172 2.790496560747742e-104 1.395248280373871e-104 + 173 6.976241401869354e-105 3.488120700934677e-105 + 174 1.744060350467339e-105 8.720301752336693e-106 + 175 4.360150876168346e-106 2.180075438084173e-106 + 176 1.090037719042087e-106 5.450188595210433e-107 + 177 2.725094297605216e-107 1.362547148802608e-107 + 178 6.812735744013041e-108 3.406367872006521e-108 + 179 1.703183936003260e-108 8.515919680016301e-109 + 180 4.257959840008151e-109 2.128979920004075e-109 + 181 1.064489960002038e-109 5.322449800010188e-110 + 182 2.661224900005094e-110 1.330612450002547e-110 + 183 6.653062250012735e-111 3.326531125006368e-111 + 184 1.663265562503184e-111 8.316327812515919e-112 + 185 4.158163906257960e-112 2.079081953128980e-112 + 186 1.039540976564490e-112 5.197704882822450e-113 + 187 2.598852441411225e-113 1.299426220705612e-113 + 188 6.497131103528062e-114 3.248565551764031e-114 + 189 1.624282775882016e-114 8.121413879410078e-115 + 190 4.060706939705039e-115 2.030353469852519e-115 + 191 1.015176734926260e-115 5.075883674631298e-116 + 192 2.537941837315649e-116 1.268970918657825e-116 + 193 6.344854593289123e-117 3.172427296644562e-117 + 194 1.586213648322281e-117 7.931068241611404e-118 + 195 3.965534120805702e-118 1.982767060402851e-118 + 196 9.913835302014255e-119 4.956917651007127e-119 + 197 2.478458825503564e-119 1.239229412751782e-119 + 198 6.196147063758909e-120 3.098073531879455e-120 + 199 1.549036765939727e-120 7.745183829698637e-121 + 200 3.872591914849318e-121 1.936295957424659e-121 + 201 9.681479787123296e-122 4.840739893561648e-122 + 202 2.420369946780824e-122 1.210184973390412e-122 + 203 6.050924866952060e-123 3.025462433476030e-123 + 204 1.512731216738015e-123 7.563656083690075e-124 + 205 3.781828041845037e-124 1.890914020922519e-124 + 206 9.454570104612593e-125 4.727285052306297e-125 + 207 2.363642526153148e-125 1.181821263076574e-125 + 208 5.909106315382871e-126 2.954553157691435e-126 + 209 1.477276578845718e-126 7.386382894228589e-127 + 210 3.693191447114294e-127 1.846595723557147e-127 + 211 9.232978617785736e-128 4.616489308892868e-128 + 212 2.308244654446434e-128 1.154122327223217e-128 + 213 5.770611636116085e-129 2.885305818058042e-129 + 214 1.442652909029021e-129 7.213264545145106e-130 + 215 3.606632272572553e-130 1.803316136286277e-130 + 216 9.016580681431383e-131 4.508290340715691e-131 + 217 2.254145170357846e-131 1.127072585178923e-131 + 218 5.635362925894614e-132 2.817681462947307e-132 + 219 1.408840731473654e-132 7.044203657368268e-133 + 220 3.522101828684134e-133 1.761050914342067e-133 + 221 8.805254571710335e-134 4.402627285855167e-134 + 222 2.201313642927584e-134 1.100656821463792e-134 + 223 5.503284107318959e-135 2.751642053659480e-135 + 224 1.375821026829740e-135 6.879105134148699e-136 + 225 3.439552567074349e-136 1.719776283537175e-136 + 226 8.598881417685874e-137 4.299440708842937e-137 + 227 2.149720354421468e-137 1.074860177210734e-137 + 228 5.374300886053671e-138 2.687150443026836e-138 + 229 1.343575221513418e-138 6.717876107567089e-139 + 230 3.358938053783544e-139 1.679469026891772e-139 + 231 8.397345134458861e-140 4.198672567229430e-140 + 232 2.099336283614715e-140 1.049668141807358e-140 + 233 5.248340709036788e-141 2.624170354518394e-141 + 234 1.312085177259197e-141 6.560425886295985e-142 + 235 3.280212943147993e-142 1.640106471573996e-142 + 236 8.200532357869981e-143 4.100266178934991e-143 + 237 2.050133089467495e-143 1.025066544733748e-143 + 238 5.125332723668738e-144 2.562666361834369e-144 + 239 1.281333180917185e-144 6.406665904585923e-145 + 240 3.203332952292961e-145 1.601666476146481e-145 + 241 8.008332380732404e-146 4.004166190366202e-146 + 242 2.002083095183101e-146 1.001041547591550e-146 + 243 5.005207737957752e-147 2.502603868978876e-147 + 244 1.251301934489438e-147 6.256509672447190e-148 + 245 3.128254836223595e-148 1.564127418111798e-148 + 246 7.820637090558988e-149 3.910318545279494e-149 + 247 1.955159272639747e-149 9.775796363198735e-150 + 248 4.887898181599367e-150 2.443949090799684e-150 + 249 1.221974545399842e-150 6.109872726999209e-151 + 250 3.054936363499605e-151 1.527468181749802e-151 + 251 7.637340908749012e-152 3.818670454374506e-152 + 252 1.909335227187253e-152 9.546676135936265e-153 + 253 4.773338067968132e-153 2.386669033984066e-153 + 254 1.193334516992033e-153 5.966672584960165e-154 + 255 2.983336292480083e-154 1.491668146240041e-154 + 256 7.458340731200207e-155 3.729170365600103e-155 + 257 1.864585182800052e-155 9.322925914000258e-156 + 258 4.661462957000129e-156 2.330731478500065e-156 + 259 1.165365739250032e-156 5.826828696250162e-157 + 260 2.913414348125081e-157 1.456707174062540e-157 + 261 7.283535870312702e-158 3.641767935156351e-158 + 262 1.820883967578175e-158 9.104419837890877e-159 + 263 4.552209918945439e-159 2.276104959472719e-159 + 264 1.138052479736360e-159 5.690262398681798e-160 + 265 2.845131199340899e-160 1.422565599670450e-160 + 266 7.112827998352248e-161 3.556413999176124e-161 + 267 1.778206999588062e-161 8.891034997940310e-162 + 268 4.445517498970155e-162 2.222758749485077e-162 + 269 1.111379374742539e-162 5.556896873712694e-163 + 270 2.778448436856347e-163 1.389224218428173e-163 + 271 6.946121092140867e-164 3.473060546070434e-164 + 272 1.736530273035217e-164 8.682651365176084e-165 + 273 4.341325682588042e-165 2.170662841294021e-165 + 274 1.085331420647010e-165 5.426657103235052e-166 + 275 2.713328551617526e-166 1.356664275808763e-166 + 276 6.783321379043816e-167 3.391660689521908e-167 + 277 1.695830344760954e-167 8.479151723804769e-168 + 278 4.239575861902385e-168 2.119787930951192e-168 + 279 1.059893965475596e-168 5.299469827377981e-169 + 280 2.649734913688990e-169 1.324867456844495e-169 + 281 6.624337284222476e-170 3.312168642111238e-170 + 282 1.656084321055619e-170 8.280421605278095e-171 + 283 4.140210802639048e-171 2.070105401319524e-171 + 284 1.035052700659762e-171 5.175263503298809e-172 + 285 2.587631751649405e-172 1.293815875824702e-172 + 286 6.469079379123512e-173 3.234539689561756e-173 + 287 1.617269844780878e-173 8.086349223904390e-174 + 288 4.043174611952195e-174 2.021587305976097e-174 + 289 1.010793652988049e-174 5.053968264940244e-175 + 290 2.526984132470122e-175 1.263492066235061e-175 + 291 6.317460331175305e-176 3.158730165587652e-176 + 292 1.579365082793826e-176 7.896825413969131e-177 + 293 3.948412706984565e-177 1.974206353492283e-177 + 294 9.871031767461413e-178 4.935515883730707e-178 + 295 2.467757941865353e-178 1.233878970932677e-178 + 296 6.169394854663383e-179 3.084697427331692e-179 + 297 1.542348713665846e-179 7.711743568329229e-180 + 298 3.855871784164615e-180 1.927935892082307e-180 + 299 9.639679460411536e-181 4.819839730205768e-181 + 300 2.409919865102884e-181 1.204959932551442e-181 + 301 6.024799662757210e-182 3.012399831378605e-182 + 302 1.506199915689303e-182 7.530999578446513e-183 + 303 3.765499789223256e-183 1.882749894611628e-183 + 304 9.413749473058141e-184 4.706874736529071e-184 + 305 2.353437368264535e-184 1.176718684132268e-184 + 306 5.883593420661338e-185 2.941796710330669e-185 + 307 1.470898355165335e-185 7.354491775826673e-186 + 308 3.677245887913336e-186 1.838622943956668e-186 + 309 9.193114719783341e-187 4.596557359891670e-187 + 310 2.298278679945835e-187 1.149139339972918e-187 + 311 5.745696699864588e-188 2.872848349932294e-188 + 312 1.436424174966147e-188 7.182120874830735e-189 + 313 3.591060437415368e-189 1.795530218707684e-189 + 314 8.977651093538419e-190 4.488825546769209e-190 + 315 2.244412773384605e-190 1.122206386692302e-190 + 316 5.611031933461512e-191 2.805515966730756e-191 + 317 1.402757983365378e-191 7.013789916826890e-192 + 318 3.506894958413445e-192 1.753447479206722e-192 + 319 8.767237396033612e-193 4.383618698016806e-193 + 320 2.191809349008403e-193 1.095904674504202e-193 + 321 5.479523372521008e-194 2.739761686260504e-194 + 322 1.369880843130252e-194 6.849404215651259e-195 + 323 3.424702107825630e-195 1.712351053912815e-195 + 324 8.561755269564074e-196 4.280877634782037e-196 + 325 2.140438817391019e-196 1.070219408695509e-196 + 326 5.351097043477546e-197 2.675548521738773e-197 + 327 1.337774260869387e-197 6.688871304346933e-198 + 328 3.344435652173467e-198 1.672217826086733e-198 + 329 8.361089130433666e-199 4.180544565216833e-199 + 330 2.090272282608417e-199 1.045136141304208e-199 + 331 5.225680706521041e-200 2.612840353260521e-200 + 332 1.306420176630260e-200 6.532100883151302e-201 + 333 3.266050441575651e-201 1.633025220787825e-201 + 334 8.165126103939127e-202 4.082563051969564e-202 + 335 2.041281525984782e-202 1.020640762992391e-202 + 336 5.103203814961955e-203 2.551601907480977e-203 + 337 1.275800953740489e-203 6.379004768702443e-204 + 338 3.189502384351222e-204 1.594751192175611e-204 + 339 7.973755960878054e-205 3.986877980439027e-205 + 340 1.993438990219514e-205 9.967194951097568e-206 + 341 4.983597475548784e-206 2.491798737774392e-206 + 342 1.245899368887196e-206 6.229496844435980e-207 + 343 3.114748422217990e-207 1.557374211108995e-207 + 344 7.786871055544975e-208 3.893435527772487e-208 + 345 1.946717763886244e-208 9.733588819431218e-209 + 346 4.866794409715609e-209 2.433397204857805e-209 + 347 1.216698602428902e-209 6.083493012144511e-210 + 348 3.041746506072256e-210 1.520873253036128e-210 + 349 7.604366265180639e-211 3.802183132590320e-211 + 350 1.901091566295160e-211 9.505457831475799e-212 + 351 4.752728915737900e-212 2.376364457868950e-212 + 352 1.188182228934475e-212 5.940911144672374e-213 + 353 2.970455572336187e-213 1.485227786168094e-213 + 354 7.426138930840468e-214 3.713069465420234e-214 + 355 1.856534732710117e-214 9.282673663550585e-215 + 356 4.641336831775293e-215 2.320668415887646e-215 + 357 1.160334207943823e-215 5.801671039719116e-216 + 358 2.900835519859558e-216 1.450417759929779e-216 + 359 7.252088799648895e-217 3.626044399824447e-217 + 360 1.813022199912224e-217 9.065110999561118e-218 + 361 4.532555499780559e-218 2.266277749890280e-218 + 362 1.133138874945140e-218 5.665694374725699e-219 + 363 2.832847187362849e-219 1.416423593681425e-219 + 364 7.082117968407124e-220 3.541058984203562e-220 + 365 1.770529492101781e-220 8.852647460508905e-221 + 366 4.426323730254452e-221 2.213161865127226e-221 + 367 1.106580932563613e-221 5.532904662818065e-222 + 368 2.766452331409033e-222 1.383226165704516e-222 + 369 6.916130828522582e-223 3.458065414261291e-223 + 370 1.729032707130645e-223 8.645163535653227e-224 + 371 4.322581767826614e-224 2.161290883913307e-224 + 372 1.080645441956653e-224 5.403227209783267e-225 + 373 2.701613604891633e-225 1.350806802445817e-225 + 374 6.754034012229084e-226 3.377017006114542e-226 + 375 1.688508503057271e-226 8.442542515286355e-227 + 376 4.221271257643177e-227 2.110635628821589e-227 + 377 1.055317814410794e-227 5.276589072053972e-228 + 378 2.638294536026986e-228 1.319147268013493e-228 + 379 6.595736340067465e-229 3.297868170033732e-229 + 380 1.648934085016866e-229 8.244670425084331e-230 + 381 4.122335212542165e-230 2.061167606271083e-230 + 382 1.030583803135541e-230 5.152919015677707e-231 + 383 2.576459507838853e-231 1.288229753919427e-231 + 384 6.441148769597133e-232 3.220574384798567e-232 + 385 1.610287192399283e-232 8.051435961996417e-233 + 386 4.025717980998208e-233 2.012858990499104e-233 + 387 1.006429495249552e-233 5.032147476247760e-234 + 388 2.516073738123880e-234 1.258036869061940e-234 + 389 6.290184345309700e-235 3.145092172654850e-235 + 390 1.572546086327425e-235 7.862730431637126e-236 + 391 3.931365215818563e-236 1.965682607909281e-236 + 392 9.828413039546407e-237 4.914206519773204e-237 + 393 2.457103259886602e-237 1.228551629943301e-237 + 394 6.142758149716504e-238 3.071379074858252e-238 + 395 1.535689537429126e-238 7.678447687145630e-239 + 396 3.839223843572815e-239 1.919611921786408e-239 + 397 9.598059608932038e-240 4.799029804466019e-240 + 398 2.399514902233010e-240 1.199757451116505e-240 + 399 5.998787255582524e-241 2.999393627791262e-241 + 400 1.499696813895631e-241 7.498484069478155e-242 + 401 3.749242034739077e-242 1.874621017369539e-242 + 402 9.373105086847693e-243 4.686552543423847e-243 + 403 2.343276271711923e-243 1.171638135855962e-243 + 404 5.858190679279808e-244 2.929095339639904e-244 + 405 1.464547669819952e-244 7.322738349099761e-245 + 406 3.661369174549880e-245 1.830684587274940e-245 + 407 9.153422936374701e-246 4.576711468187350e-246 + 408 2.288355734093675e-246 1.144177867046838e-246 + 409 5.720889335234188e-247 2.860444667617094e-247 + 410 1.430222333808547e-247 7.151111669042735e-248 + 411 3.575555834521367e-248 1.787777917260684e-248 + 412 8.938889586303419e-249 4.469444793151709e-249 + 413 2.234722396575855e-249 1.117361198287927e-249 + 414 5.586805991439637e-250 2.793402995719818e-250 + 415 1.396701497859909e-250 6.983507489299546e-251 + 416 3.491753744649773e-251 1.745876872324886e-251 + 417 8.729384361624432e-252 4.364692180812216e-252 + 418 2.182346090406108e-252 1.091173045203054e-252 + 419 5.455865226015270e-253 2.727932613007635e-253 + 420 1.363966306503818e-253 6.819831532519088e-254 + 421 3.409915766259544e-254 1.704957883129772e-254 + 422 8.524789415648860e-255 4.262394707824430e-255 + 423 2.131197353912215e-255 1.065598676956107e-255 + 424 5.327993384780537e-256 2.663996692390269e-256 + 425 1.331998346195134e-256 6.659991730975672e-257 + 426 3.329995865487836e-257 1.664997932743918e-257 + 427 8.324989663719589e-258 4.162494831859795e-258 + 428 2.081247415929897e-258 1.040623707964949e-258 + 429 5.203118539824743e-259 2.601559269912372e-259 + 430 1.300779634956186e-259 6.503898174780929e-260 + 431 3.251949087390465e-260 1.625974543695232e-260 + 432 8.129872718476162e-261 4.064936359238081e-261 + 433 2.032468179619040e-261 1.016234089809520e-261 + 434 5.081170449047601e-262 2.540585224523800e-262 + 435 1.270292612261900e-262 6.351463061309501e-263 + 436 3.175731530654751e-263 1.587865765327375e-263 + 437 7.939328826636877e-264 3.969664413318438e-264 + 438 1.984832206659219e-264 9.924161033296096e-265 + 439 4.962080516648048e-265 2.481040258324024e-265 + 440 1.240520129162012e-265 6.202600645810060e-266 + 441 3.101300322905030e-266 1.550650161452515e-266 + 442 7.753250807262575e-267 3.876625403631287e-267 + 443 1.938312701815644e-267 9.691563509078218e-268 + 444 4.845781754539109e-268 2.422890877269555e-268 + 445 1.211445438634777e-268 6.057227193173887e-269 + 446 3.028613596586943e-269 1.514306798293472e-269 + 447 7.571533991467358e-270 3.785766995733679e-270 + 448 1.892883497866840e-270 9.464417489334198e-271 + 449 4.732208744667099e-271 2.366104372333549e-271 + 450 1.183052186166775e-271 5.915260930833874e-272 + 451 2.957630465416937e-272 1.478815232708468e-272 + 452 7.394076163542342e-273 3.697038081771171e-273 + 453 1.848519040885585e-273 9.242595204427927e-274 + 454 4.621297602213964e-274 2.310648801106982e-274 + 455 1.155324400553491e-274 5.776622002767455e-275 + 456 2.888311001383727e-275 1.444155500691864e-275 + 457 7.220777503459318e-276 3.610388751729659e-276 + 458 1.805194375864830e-276 9.025971879324148e-277 + 459 4.512985939662074e-277 2.256492969831037e-277 + 460 1.128246484915518e-277 5.641232424577592e-278 + 461 2.820616212288796e-278 1.410308106144398e-278 + 462 7.051540530721991e-279 3.525770265360995e-279 + 463 1.762885132680498e-279 8.814425663402488e-280 + 464 4.407212831701244e-280 2.203606415850622e-280 + 465 1.101803207925311e-280 5.509016039626555e-281 + 466 2.754508019813278e-281 1.377254009906639e-281 + 467 6.886270049533194e-282 3.443135024766597e-282 + 468 1.721567512383298e-282 8.607837561916492e-283 + 469 4.303918780958246e-283 2.151959390479123e-283 + 470 1.075979695239562e-283 5.379898476197808e-284 + 471 2.689949238098904e-284 1.344974619049452e-284 + 472 6.724873095247260e-285 3.362436547623630e-285 + 473 1.681218273811815e-285 8.406091369059075e-286 + 474 4.203045684529537e-286 2.101522842264769e-286 + 475 1.050761421132384e-286 5.253807105661922e-287 + 476 2.626903552830961e-287 1.313451776415480e-287 + 477 6.567258882077402e-288 3.283629441038701e-288 + 478 1.641814720519350e-288 8.209073602596752e-289 + 479 4.104536801298376e-289 2.052268400649188e-289 + 480 1.026134200324594e-289 5.130671001622970e-290 + 481 2.565335500811485e-290 1.282667750405743e-290 + 482 6.413338752028713e-291 3.206669376014356e-291 + 483 1.603334688007178e-291 8.016673440035891e-292 + 484 4.008336720017946e-292 2.004168360008973e-292 + 485 1.002084180004486e-292 5.010420900022432e-293 + 486 2.505210450011216e-293 1.252605225005608e-293 + 487 6.263026125028040e-294 3.131513062514020e-294 + 488 1.565756531257010e-294 7.828782656285050e-295 + 489 3.914391328142525e-295 1.957195664071262e-295 + 490 9.785978320356312e-296 4.892989160178156e-296 + 491 2.446494580089078e-296 1.223247290044539e-296 + 492 6.116236450222695e-297 3.058118225111348e-297 + 493 1.529059112555674e-297 7.645295562778369e-298 + 494 3.822647781389185e-298 1.911323890694592e-298 + 495 9.556619453472961e-299 4.778309726736481e-299 + 496 2.389154863368240e-299 1.194577431684120e-299 + 497 5.972887158420601e-300 2.986443579210300e-300 + 498 1.493221789605150e-300 7.466108948025751e-301 + 499 3.733054474012876e-301 1.866527237006438e-301 + 500 9.332636185032189e-302 4.666318092516094e-302 + 501 2.333159046258047e-302 1.166579523129024e-302 + 502 5.832897615645118e-303 2.916448807822559e-303 + 503 1.458224403911279e-303 7.291122019556397e-304 + 504 3.645561009778199e-304 1.822780504889099e-304 + 505 9.113902524445497e-305 4.556951262222748e-305 + 506 2.278475631111374e-305 1.139237815555687e-305 + 507 5.696189077778436e-306 2.848094538889218e-306 + 508 1.424047269444609e-306 7.120236347223044e-307 + 509 3.560118173611522e-307 1.780059086805761e-307 + 510 8.900295434028806e-308 4.450147717014403e-308 + 511 2.225073858507201e-308 1.112536929253601e-308 + 512 5.562684646268003e-309 2.781342323134002e-309 + 513 1.390671161567001e-309 6.953355807835004e-310 + 514 3.476677903917502e-310 1.738338951958751e-310 + 515 8.691694759793755e-311 4.345847379896878e-311 + 516 2.172923689948439e-311 1.086461844974219e-311 + 517 5.432309224871097e-312 2.716154612435549e-312 + 518 1.358077306217774e-312 6.790386531088871e-313 + 519 3.395193265544436e-313 1.697596632772218e-313 + 520 8.487983163861089e-314 4.243991581930545e-314 + 521 2.121995790965272e-314 1.060997895482636e-314 + 522 5.304989477413181e-315 2.652494738706590e-315 + 523 1.326247369353295e-315 6.631236846766476e-316 + 524 3.315618423383238e-316 1.657809211691619e-316 + 525 8.289046058458095e-317 4.144523029229047e-317 + 526 2.072261514614524e-317 1.036130757307262e-317 + 527 5.180653786536309e-318 2.590326893268155e-318 + 528 1.295163446634077e-318 6.475817233170387e-319 + 529 3.237908616585193e-319 1.618954308292597e-319 + 530 8.094771541462983e-320 4.047385770731492e-320 + 531 2.023692885365746e-320 1.011846442682873e-320 + 532 5.059232213414365e-321 2.529616106707182e-321 + 533 1.264808053353591e-321 6.324040266767956e-322 + 534 3.162020133383978e-322 1.581010066691989e-322 + 535 7.905050333459945e-323 3.952525166729972e-323 + 536 1.976262583364986e-323 9.881312916824931e-324 + 537 4.940656458412465e-324 0.000000000000000 + 538 0.000000000000000 0.000000000000000 +""" + +@pytest.mark.version('>=5.0') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute() + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/dml/skip_locked/test_skip_locked_basic.py b/tests/functional/dml/skip_locked/test_skip_locked_basic.py new file mode 100644 index 00000000..0549090f --- /dev/null +++ b/tests/functional/dml/skip_locked/test_skip_locked_basic.py @@ -0,0 +1,106 @@ +#coding:utf-8 + +""" +ID: dml.skip_locked +TITLE: Basic check of SKIP LOCKED +DESCRIPTION: + Trivial test to check SKIP LOCKED functionality on all kinds of transaction isolation level. + More complex cases see in gh_7350_test.py. +NOTES: + [26.02.2025] pzotov + Commit that introduced this feature (5.0.0.811, 29-oct-2022): + https://github.com/FirebirdSQL/firebird/commit/5cc8a8f7fd27d72d5ca6f19eb691e93f2404ddd1 +""" +from firebird.driver import tpb, Isolation, DatabaseError + +import pytest +from firebird.qa import * + +init_script = \ +f''' + set bail on; + recreate table test(id int primary key, f01 int); + commit; + insert into test(id, f01) select row_number()over(), 0 from rdb$types rows 10; + commit; +''' + +db = db_factory(init = init_script ) +act = python_act('db') + +@pytest.mark.version('>=5.0') +def test_1(act: Action, capsys): + + tx_isol_lst = [ + Isolation.SERIALIZABLE, + Isolation.SNAPSHOT, + Isolation.READ_COMMITTED_NO_RECORD_VERSION, + Isolation.READ_COMMITTED_RECORD_VERSION, + ] + if act.is_version('>=4'): + tx_isol_lst.append(Isolation.READ_COMMITTED_READ_CONSISTENCY) + + with act.db.connect() as con_locker, act.db.connect() as con_worker: + con_locker.execute_immediate('update test set f01 = 1 where id in (1,5,9)') + for x_isol in tx_isol_lst: + custom_tpb = tpb(isolation = x_isol, lock_timeout = 0) + print(x_isol.name) + tx_worker = con_worker.transaction_manager(custom_tpb) + tx_worker.begin() + cur = tx_worker.cursor() + cur.execute('select id from test with lock skip locked') + try: + for r in cur: + print('TIL:',x_isol.name, 'ID:', r[0]) + except DatabaseError as e: + print(e) + # E firebird.driver.types.DatabaseError: lock conflict on no wait transaction + # E -Acquire lock for relation (TEST) failed + + cur.close() + tx_worker.rollback() + + act.expected_stdout = """ + SERIALIZABLE + lock conflict on no wait transaction + -Acquire lock for relation (TEST) failed + + SNAPSHOT + TIL: SNAPSHOT ID: 2 + TIL: SNAPSHOT ID: 3 + TIL: SNAPSHOT ID: 4 + TIL: SNAPSHOT ID: 6 + TIL: SNAPSHOT ID: 7 + TIL: SNAPSHOT ID: 8 + TIL: SNAPSHOT ID: 10 + + READ_COMMITTED_NO_RECORD_VERSION + TIL: READ_COMMITTED_NO_RECORD_VERSION ID: 2 + TIL: READ_COMMITTED_NO_RECORD_VERSION ID: 3 + TIL: READ_COMMITTED_NO_RECORD_VERSION ID: 4 + TIL: READ_COMMITTED_NO_RECORD_VERSION ID: 6 + TIL: READ_COMMITTED_NO_RECORD_VERSION ID: 7 + TIL: READ_COMMITTED_NO_RECORD_VERSION ID: 8 + TIL: READ_COMMITTED_NO_RECORD_VERSION ID: 10 + + READ_COMMITTED_RECORD_VERSION + TIL: READ_COMMITTED_RECORD_VERSION ID: 2 + TIL: READ_COMMITTED_RECORD_VERSION ID: 3 + TIL: READ_COMMITTED_RECORD_VERSION ID: 4 + TIL: READ_COMMITTED_RECORD_VERSION ID: 6 + TIL: READ_COMMITTED_RECORD_VERSION ID: 7 + TIL: READ_COMMITTED_RECORD_VERSION ID: 8 + TIL: READ_COMMITTED_RECORD_VERSION ID: 10 + + READ_COMMITTED_READ_CONSISTENCY + TIL: READ_COMMITTED_READ_CONSISTENCY ID: 2 + TIL: READ_COMMITTED_READ_CONSISTENCY ID: 3 + TIL: READ_COMMITTED_READ_CONSISTENCY ID: 4 + TIL: READ_COMMITTED_READ_CONSISTENCY ID: 6 + TIL: READ_COMMITTED_READ_CONSISTENCY ID: 7 + TIL: READ_COMMITTED_READ_CONSISTENCY ID: 8 + TIL: READ_COMMITTED_READ_CONSISTENCY ID: 10 + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() diff --git a/tests/functional/exception/test_handling_name_and_message.py b/tests/functional/exception/test_handling_name_and_message.py index e737cb9d..c3e3ace3 100644 --- a/tests/functional/exception/test_handling_name_and_message.py +++ b/tests/functional/exception/test_handling_name_and_message.py @@ -157,6 +157,7 @@ Records affected: 1 """ +@pytest.mark.intl @pytest.mark.version('>=4.0') def test_1(act: Action): act.expected_stdout = expected_stdout diff --git a/tests/functional/fkey/cascade/__init__.py b/tests/functional/fkey/cascade/__init__.py new file mode 100644 index 00000000..4adc9840 --- /dev/null +++ b/tests/functional/fkey/cascade/__init__.py @@ -0,0 +1 @@ +# Python module diff --git a/tests/functional/fkey/cascade/test_cascade_recursive_actions.py b/tests/functional/fkey/cascade/test_cascade_recursive_actions.py new file mode 100644 index 00000000..9955b630 --- /dev/null +++ b/tests/functional/fkey/cascade/test_cascade_recursive_actions.py @@ -0,0 +1,191 @@ +#coding:utf-8 + +""" +ID: n/a +TITLE: Cyclic references between PK and FK (declared with 'ON ... CASCADE') must not cause infinite loop when records are deleted or updated in any of involved tables. +DESCRIPTION: + Test verifies RI mechanism when ON DELETE/UPDATE CASCADE option is used. + Three tables are created which have PK and FK, and refer to each other as follows: + T1 (id, fk3) <-- T2 (id, fk1) // "t1.id <-- t2.fk1" + T2 (id, fk1) <-- T3 (id, fk2) // "t2.id <-- t3.fk2" + T3 (id, fk2) <-- T1 (id, fk3) // "t3.id <-- t1.fk3" + One record with values = (1,1) is added to each table. + + Deletion of this record from t1 (or any other table) must finish w/o errors and all table must become empty. + + But "update t1 set id = id + 1;" will cause infinite loop and engine has to stop it. + One may see 'progress' of this loop by querying value of sequence 'g'. + ::: ACHTUNG ::: + Only FB 3.x detects this problem when GEN_ID = 1003 (i.e. after iteration N 1000) and raises: + Statement failed, SQLSTATE = 54001 + Too many concurrent executions of the same request + -At trigger 'CHECK_3' + At trigger 'CHECK_2' + At trigger 'CHECK_1' + At trigger 'CHECK_3' + ... + Other major versions (FB 4.x ... 6.x) will *hang*, without any CPU usage. + Interrupting of ISQL using Ctrl-Break will not release DB, one need to restart FB. + Values of GEN_ID will be different: + 4.0.6.3213: Generator G, current value: 2859 + 5.0.3.1666: Generator G, current value: 2334 + 6.0.0.838: Generator PUBLIC.G, current value: 2335 + Because of this, it was decided to limit number of iterations for update, see 'LIMIT_ITERATIONS_FOR_UPDATE' + (discussed with Vlad, letter 21.06.2025 15:01, subj: "#8598: how to check it ?") + + Only single segmented UK/FK are checked. + Work within a single transaction. +NOTES: + [21.06.2025] pzotov + ::: NB ::: + SQL schema name (6.x+), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Discussed with Vlad, letters 16.06.2025 13:54 (subj: "#8598: ...") + Checked on 6.0.0.838; 3.0.13.33813. +""" + +import pytest +from firebird.qa import * + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' ')] +for p in addi_subst_tokens.split(): + substitutions.append( (p, '') ) + +db = db_factory() +act = python_act('db', substitutions = substitutions) + +# ::: ACHTUNG ::: +# Only FB3.x can detect infinite loop and interrupt it with raising +# "SQLSTATE = 54001 / Too many concurrent executions of the same request" +# FB 4.x ... 6.x will hang on update after ~2330 iterations. +# We have to limit number of iterations! +################################## +LIMIT_ITERATIONS_FOR_UPDATE = 2000 +################################## + +expected_stdout = """ + Records affected: 1 + Records affected: 0 + Records affected: 0 + Records affected: 0 + + Records affected: 1 + + ID 2003 + FK3 2005 + Records affected: 1 + + ID 2004 + FK1 2003 + Records affected: 1 + + ID 2005 + FK2 2004 + Records affected: 1 + + GEN_ID 2003 +""" + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + + test_sql = f""" + -- ################# + -- ON DELETE CASCADE + -- ################# + set bail off; + set list on; + create table t1 (id int unique, fk3 int default null); + create table t2 (id int unique, fk1 int references t1 (id) on delete cascade); + create table t3 (id int unique, fk2 int references t2 (id) on delete cascade); + commit; + + insert into t1(id, fk3) values(1, 1); + insert into t2(id, fk1) values(1, 1); + insert into t3(id, fk2) values(1, 1); + commit; + + alter table t1 add constraint t1_fk foreign key(fk3) references t3(id) on delete cascade; + commit; + + set bail off; + + set list on; + set count on; + + delete from t1; + select * from t1; + select * from t2; + select * from t3; + set count off; + commit; + alter table t1 drop constraint t1_fk; + drop table t3; + drop table t2; + drop table t1; + + -------------------------------------------- + -- ################# + -- ON UPDATE CASCADE + -- ################# + set bail on; + create sequence g; + create table t1 (id int unique, fk3 int default null); + create table t2 (id int unique, fk1 int references t1 (id) on update cascade); + create table t3 (id int unique, fk2 int references t2 (id) on update cascade); + commit; + + insert into t1(id, fk3) values(1, 1); + insert into t2(id, fk1) values(1, 1); + insert into t3(id, fk2) values(1, 1); + commit; + + alter table t1 add constraint t1_fk foreign key(fk3) references t3(id) on update cascade; + set term ^; + create trigger t1_bu for t1 before update as + begin + if (gen_id(g,0) > {LIMIT_ITERATIONS_FOR_UPDATE}) then + exit; + if (old.fk3 <> new.fk3) then + new.id = new.fk3 + 1 + 0*gen_id(g,1); + end + ^ + create trigger t2_bu for t2 before update as + begin + new.id = new.fk1 + 1 + 0*gen_id(g,1); + end + ^ + create trigger t3_bu for t3 before update as + begin + new.id = new.fk2 + 1 + 0*gen_id(g,1); + end + ^ + set term ;^ + commit; + set bail off; + set count on; + update t1 set id = id + 1; + select * from t1; + select * from t2; + select * from t3; + set count off; + select gen_id(g,0) from rdb$database; + commit; + alter table t1 drop constraint t1_fk; + drop table t3; + drop table t2; + drop table t1; + """ + + act.expected_stdout = expected_stdout + act.isql(switches=['-q'], combine_output = True, input = test_sql) + + assert act.clean_stdout == act.clean_expected_stdout + \ No newline at end of file diff --git a/tests/functional/fkey/cascade/test_master_pk_delete_if_no_cascade.py b/tests/functional/fkey/cascade/test_master_pk_delete_if_no_cascade.py new file mode 100644 index 00000000..3321b082 --- /dev/null +++ b/tests/functional/fkey/cascade/test_master_pk_delete_if_no_cascade.py @@ -0,0 +1,99 @@ +#coding:utf-8 + +""" +ID: n/a +TITLE: Attempt to delete record in master must fail if there is FK record in detail and FK was declared without CASCADE option +DESCRIPTION: + Test verifies RI mechanism when CASCADE option missed: record in master may be deleted + only if there is no appropriate record in detail (with value in FK equal to PK value from master). + Single- and multi-segmented PK/FK are checked. + Work within a single transaction. +NOTES: + [21.06.2025] pzotov + ::: NB ::: + SQL schema name (6.x+), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Discussed with Vlad, letters 16.06.2025 13:54 (subj: "#8598: ...") + Checked on 6.0.0.838; 3.0.13.33813. +""" + +import pytest +from firebird.qa import * + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' ')] +for p in addi_subst_tokens.split(): + substitutions.append( (p, '') ) + +db = db_factory() +act = python_act('db', substitutions = substitutions) + +expected_stdout = """ + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_FK on table TDETL + -Foreign key references are present for the record + -Problematic key value is (ID = 1) + + REMAINED_MASTER_ID 1 + REMAINED_DETAIL_ID 100 + REMAINED_DETAIL_ID 101 + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_FK on table TDETL + -Foreign key references are present for the record + -Problematic key value is (ID1 = 1, ID2 = 1, ID3 = 1) + REMAINED_MASTER_ID1 1 + REMAINED_MASTER_ID2 1 + REMAINED_MASTER_ID3 1 + REMAINED_DETAIL_ID 100 + REMAINED_DETAIL_ID 101 +""" + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + + test_sql = """ + set bail OFF; + set list on; + + -- case-1: single segment PK: + recreate table tmain(id int generated by default as identity primary key using index tmain_pk); + recreate table tdetl(id int generated by default as identity primary key, pid int, constraint tdetl_fk foreign key(pid) references tmain(id)); + insert into tmain(id) values(1); + insert into tmain(id) values(2); + insert into tdetl(id, pid) values(100, 1); + insert into tdetl(id, pid) values(101, null); + delete from tmain where id = 1; -- must FAIL + delete from tmain where id = 2; -- must PASS + select id as remained_master_id from tmain order by id; + select id as remained_detail_id from tdetl order by id; + commit; + drop table tdetl; + drop table tmain; + -------------------------------------------- + -- case-2: multi-segment PK: + recreate table tmain(id1 int, id2 int, id3 int, primary key(id1, id2, id3) using index tmain_pk); + recreate table tdetl(id int primary key, pid1 int, pid2 int, pid3 int, constraint tdetl_fk foreign key(pid1, pid2, pid3) references tmain); + insert into tmain(id1, id2, id3) values(1,1,1); + insert into tmain(id1, id2, id3) values(1,1,2); + insert into tdetl(id, pid1, pid2, pid3) values(100, 1, 1, 1); + insert into tdetl(id, pid1, pid2, pid3) values(101, 1, 1, null); + delete from tmain where id1 = 1 and id2 = 1 and id3 = 1; -- must FAIL + delete from tmain where id1 = 1 and id2 = 1 and id3 = 2; -- must PASS + select id1 as remained_master_id1, id2 as remained_master_id2, id3 as remained_master_id3 from tmain order by id1, id2, id3; + select id as remained_detail_id from tdetl order by id; + commit; + drop table tdetl; + drop table tmain; + """ + + act.expected_stdout = expected_stdout + act.isql(switches=['-q'], combine_output = True, input = test_sql) + + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/fkey/cascade/test_master_pk_on_delete_cascade.py b/tests/functional/fkey/cascade/test_master_pk_on_delete_cascade.py new file mode 100644 index 00000000..2c1fbc07 --- /dev/null +++ b/tests/functional/fkey/cascade/test_master_pk_on_delete_cascade.py @@ -0,0 +1,92 @@ +#coding:utf-8 + +""" +ID: n/a +TITLE: Deleting record in master must cause deletions in appropriate detail if FK references to PK and 'ON DELETE CASCADE' option is used. +DESCRIPTION: + Test verifies RI mechanism when ON DELETE CASCADE option is used. + Parent table has PRIMARY KEY constraint. + Child table has column(s) on which FK is declared, with option ON DELETE CASCADE. + Single- and multi-segmented PK/FK are checked. + Work within a single transaction. +NOTES: + [21.06.2025] pzotov + ::: NB ::: + SQL schema name (6.x+), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Discussed with Vlad, letters 16.06.2025 13:54 (subj: "#8598: ...") + Checked on 6.0.0.838; 3.0.13.33813. +""" + +import pytest +from firebird.qa import * + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' ')] +for p in addi_subst_tokens.split(): + substitutions.append( (p, '') ) + +db = db_factory() +act = python_act('db', substitutions = substitutions) + +expected_stdout = """ + REMAINED_IN_TDETL_CASE_1 0 + REMAINED_IN_TDETL_CASE_2 0 +""" + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + + test_sql = """ + set bail OFF; + set list on; + + -- case-1: single segment PK: + recreate table tmain(id int generated by default as identity primary key using index tmain_pk); + recreate table tdetl( + id int generated by default as identity primary key + ,pid int + ,constraint tdetl_fk foreign key(pid) references tmain + ON DELETE CASCADE + ); + insert into tmain(id) values(1); + insert into tdetl(id, pid) values(100, 1); + insert into tdetl(id, pid) values(101, 1); + delete from tmain where id = 1; -- must PASS and cause deletion of all records in tdetl + select count(*) as remained_in_tdetl_case_1 from tdetl; + commit; + drop table tdetl; + drop table tmain; + + -------------------------------------------- + + -- case-2: multi-segment PK: + recreate table tmain(id1 int, id2 int, id3 int, primary key(id1, id2, id3) using index tmain_pk); + recreate table tdetl( + id int primary key + ,pid1 int + ,pid2 int + ,pid3 int + ,constraint tdetl_fk foreign key(pid1, pid2, pid3) references tmain + ON DELETE CASCADE + ); + insert into tmain(id1, id2, id3) values(1,1,1); + insert into tdetl(id, pid1, pid2, pid3) values(200, 1, 1, 1); + insert into tdetl(id, pid1, pid2, pid3) values(201, 1, 1, 1); + delete from tmain where id1 = 1 and id2 = 1 and id3 = 1; -- must PASS and cause deletion of all records in tdetl + select count(*) as remained_in_tdetl_case_2 from tdetl; + commit; + drop table tdetl; + drop table tmain; + """ + + act.expected_stdout = expected_stdout + act.isql(switches=['-q'], combine_output = True, input = test_sql) + + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/fkey/cascade/test_master_pk_on_update_cascade.py b/tests/functional/fkey/cascade/test_master_pk_on_update_cascade.py new file mode 100644 index 00000000..61d17f98 --- /dev/null +++ b/tests/functional/fkey/cascade/test_master_pk_on_update_cascade.py @@ -0,0 +1,105 @@ +#coding:utf-8 + +""" +ID: n/a +TITLE: Updating PK column(s) in master must cause changes in appropriate detail column(s) if FK references to PK and 'ON UPDATE CASCADE' option is used +DESCRIPTION: + Test verifies RI mechanism when ON UPDATE CASCADE option is used: updating record in master should cause appropriate updates in detail. + Parent table has PRIMARY KEY constraint. + Child table has column(s) on which FK is declared, with option ON UPDATE CASCADE. + Single- and multi-segmented PK/FK are checked. + Work within a single transaction. +NOTES: + [21.06.2025] pzotov + ::: NB ::: + SQL schema name (6.x+), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Discussed with Vlad, letters 16.06.2025 13:54 (subj: "#8598: ...") + Checked on 6.0.0.838; 3.0.13.33813. +""" + +import pytest +from firebird.qa import * + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' ')] +for p in addi_subst_tokens.split(): + substitutions.append( (p, '') ) + +db = db_factory() +act = python_act('db', substitutions = substitutions) + +expected_stdout = """ + DETAIL_ID 100 + DETAIL_PID -1 + + DETAIL_ID 101 + DETAIL_PID -1 + + DETAIL_ID 200 + DETAIL_PID1 -1 + DETAIL_PID2 -1 + DETAIL_PID3 -3 + + DETAIL_ID 201 + DETAIL_PID1 -1 + DETAIL_PID2 -1 + DETAIL_PID3 -3 +""" + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + + test_sql = """ + set bail OFF; + set list on; + + -- case-1: single segment PK: + recreate table tmain(id int generated by default as identity primary key using index tmain_pk); + recreate table tdetl( + id int generated by default as identity primary key + ,pid int + ,constraint tdetl_fk foreign key(pid) references tmain + ON UPDATE CASCADE + ); + insert into tmain(id) values(1); + insert into tdetl(id, pid) values(100, 1); + insert into tdetl(id, pid) values(101, 1); + update tmain set id = -id where id = 1; -- must PASS and cause update in tdetl + select id as detail_id, pid as detail_pid from tdetl order by id; + commit; + drop table tdetl; + drop table tmain; + + -------------------------------------------- + + -- case-2: multi-segment PK: + recreate table tmain(id1 int, id2 int, id3 int, primary key(id1, id2, id3) using index tmain_pk); + recreate table tdetl( + id int primary key + ,pid1 int + ,pid2 int + ,pid3 int + ,constraint tdetl_fk foreign key(pid1, pid2, pid3) references tmain + ON UPDATE CASCADE + ); + insert into tmain(id1, id2, id3) values(1,1,1); + insert into tdetl(id, pid1, pid2, pid3) values(200, 1, 1, 1); + insert into tdetl(id, pid1, pid2, pid3) values(201, 1, 1, 1); + update tmain set id1 = -id1, id2 = -id2, id3 = -3 where id1 = 1 and id2 = 1 and id3 = 1; -- must PASS and cause update in tdetl + select id as detail_id, pid1 as detail_pid1, pid2 as detail_pid2, pid3 as detail_pid3 from tdetl order by id; + commit; + drop table tdetl; + drop table tmain; + """ + + act.expected_stdout = expected_stdout + act.isql(switches=['-q'], combine_output = True, input = test_sql) + + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/fkey/cascade/test_master_pk_update_if_no_cascade.py b/tests/functional/fkey/cascade/test_master_pk_update_if_no_cascade.py new file mode 100644 index 00000000..a38ef6c7 --- /dev/null +++ b/tests/functional/fkey/cascade/test_master_pk_update_if_no_cascade.py @@ -0,0 +1,104 @@ +#coding:utf-8 + +""" +ID: n/a +TITLE: Attempt to update PK column(s) in master must fail if there is FK record in detail and FK was declared without CASCADE option +DESCRIPTION: + Test verifies RI mechanism when CASCADE option missed: PK-column(s) in master may be updated + only if there is no appropriate record in detail, i.e. with value in FK column(s) equal to PK value(s) from master. + Single- and multi-segmented PK/FK are checked. + Work within a single transaction. +NOTES: + [21.06.2025] pzotov + ::: NB ::: + SQL schema name (6.x+), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Discussed with Vlad, letters 16.06.2025 13:54 (subj: "#8598: ...") + Checked on 6.0.0.838; 3.0.13.33813. +""" + +import pytest +from firebird.qa import * + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' ')] +for p in addi_subst_tokens.split(): + substitutions.append( (p, '') ) + +db = db_factory() +act = python_act('db', substitutions = substitutions) + +expected_stdout = """ + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_FK on table TDETL + -Foreign key references are present for the record + -Problematic key value is (ID = 1) + REMAINED_MASTER_ID -2 + REMAINED_MASTER_ID 1 + REMAINED_DETAIL_ID 100 + REMAINED_DETAIL_ID 101 + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_FK on table TDETL + -Foreign key references are present for the record + -Problematic key value is (ID1 = 1, ID2 = 1, ID3 = 1) + REMAINED_MASTER_ID1 -2 + REMAINED_MASTER_ID2 -2 + REMAINED_MASTER_ID3 -2 + REMAINED_MASTER_ID1 1 + REMAINED_MASTER_ID2 1 + REMAINED_MASTER_ID3 1 + REMAINED_DETAIL_ID 100 + REMAINED_DETAIL_ID 101 +""" + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + + test_sql = """ + set bail OFF; + set list on; + + -- case-1: single segment PK: + recreate table tmain(id int generated by default as identity primary key using index tmain_pk); + recreate table tdetl(id int generated by default as identity primary key, pid int, constraint tdetl_fk foreign key(pid) references tmain(id)); + insert into tmain(id) values(1); + insert into tmain(id) values(2); + insert into tdetl(id, pid) values(100, 1); + insert into tdetl(id, pid) values(101, null); + update tmain set id = -id where id = 1; -- must FAIL + update tmain set id = -id where id = 2; -- must PASS + select id as remained_master_id from tmain order by id; + select id as remained_detail_id from tdetl order by id; + commit; + drop table tdetl; + drop table tmain; + -------------------------------------------- + + + -- case-2: multi-segment PK: + recreate table tmain(id1 int, id2 int, id3 int, primary key(id1, id2, id3) using index tmain_pk); + recreate table tdetl(id int primary key, pid1 int, pid2 int, pid3 int, constraint tdetl_fk foreign key(pid1, pid2, pid3) references tmain); + insert into tmain(id1, id2, id3) values(1,1,1); + insert into tmain(id1, id2, id3) values(1,1,2); + insert into tdetl(id, pid1, pid2, pid3) values(100, 1, 1, 1); + insert into tdetl(id, pid1, pid2, pid3) values(101, 1, 1, null); + update tmain set id1 = -1, id2 = -1, id3 = -1 where id1 = 1 and id2 = 1 and id3 = 1; -- must FAIL + update tmain set id1 = -2, id2 = -2, id3 = -2 where id1 = 1 and id2 = 1 and id3 = 2; -- must PASS + select id1 as remained_master_id1, id2 as remained_master_id2, id3 as remained_master_id3 from tmain order by id1, id2, id3; + select id as remained_detail_id from tdetl order by id; + commit; + drop table tdetl; + drop table tmain; + """ + + act.expected_stdout = expected_stdout + act.isql(switches=['-q'], combine_output = True, input = test_sql) + + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/fkey/cascade/test_master_uk_delete_if_no_cascade.py b/tests/functional/fkey/cascade/test_master_uk_delete_if_no_cascade.py new file mode 100644 index 00000000..0fe0d9fa --- /dev/null +++ b/tests/functional/fkey/cascade/test_master_uk_delete_if_no_cascade.py @@ -0,0 +1,152 @@ +#coding:utf-8 + +""" +ID: n/a +TITLE: Attempt to delete record in master with UK must fail if there is FK record in detail and FK was declared without CASCADE option +DESCRIPTION: + Test verifies RI mechanism when CASCADE option missed: record in master with UNIQUE constraint may be deleted + only if there is no appropriate record in detail (with value in FK equal to UK value from master) or all key fields are NULL. + Single- and multi-segmented PK/FK are checked. + Work within a single transaction. +NOTES: + [21.06.2025] pzotov + ::: NB ::: + SQL schema name (6.x+), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Discussed with Vlad, letters 16.06.2025 13:54 (subj: "#8598: ...") + Checked on 6.0.0.838; 3.0.13.33813. +""" + +import pytest +from firebird.qa import * + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' ')] +for p in addi_subst_tokens.split(): + substitutions.append( (p, '') ) + +db = db_factory() +act = python_act('db', substitutions = substitutions) + +expected_stdout = """ + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_FK on table TDETL + -Foreign key references are present for the record + -Problematic key value is (ID = 1000) + REMAINED_MASTER_ID 1000 + REMAINED_DETAIL_ID 100 + REMAINED_DETAIL_ID 101 + + MON$VARIABLE_NAME FK_VIOLATION_335544466 + MON$VARIABLE_VALUE 7 +""" + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + + test_sql = """ + set bail OFF; + set list on; + + -- case-1: single segment UK: + recreate table tmain(id int unique using index tmain_uk); + recreate table tdetl(id int primary key, pid int, constraint tdetl_fk foreign key(pid) references tmain(id)); + insert into tmain(id) values(null); + insert into tmain(id) values(1000); + insert into tmain(id) values(2000); + insert into tdetl(id, pid) values(100, 1000); + insert into tdetl(id, pid) values(101, null); + delete from tmain where id = 1000; -- must FAIL + delete from tmain where id = 2000; -- must PASS + delete from tmain where id is null; -- must PASS + select id as remained_master_id from tmain order by id; + select id as remained_detail_id from tdetl order by id; + commit; + drop table tdetl; + drop table tmain; + + --###################################################### + + -- case-2: multi-segment UK: + recreate table tmain(id1 int, id2 int, id3 int, unique(id1, id2, id3) using index tmain_uk); + recreate table tdetl(id int primary key, pid1 int, pid2 int, pid3 int, constraint tdetl_fk foreign key(pid1, pid2, pid3) references tmain(id1, id2, id3)); + + insert into tmain(id1, id2, id3) + with + a as ( + select 1000 as v from rdb$database union all + select 1000 as v from rdb$database union all + select null as v from rdb$database + ) + select distinct a1.v, a2.v, a3.v + from a a1 + cross join a a2 + cross join a a3 + ; + insert into tdetl(id, pid1, pid2, pid3) + select row_number()over(), id1, id2, id3 + from tmain + order by id1, id2, id3; + + -- tmain: + -- ID1 ID2 ID3 + -- ============ ============ ============ + -- + -- 1000 + -- 1000 + -- 1000 1000 + -- 1000 + -- 1000 1000 + -- 1000 1000 + -- 1000 1000 1000 + + -- tdetl: + -- ID PID1 PID2 PID3 + -- ============ ============ ============ ============ + -- 1 + -- 2 1000 + -- 3 1000 + -- 4 1000 1000 + -- 5 1000 + -- 6 1000 1000 + -- 7 1000 1000 + -- 8 1000 1000 1000 + + + delete from tmain where coalesce(id1, id2, id3) is null; -- must PASS because all key fields are null + + -- Any records in tmain with at least one NOT null value among ID1 ... ID3 must not be deleted in following code. + -- We try to delete every row with accumulating count of errors (for appropriate gdscode which must be the same: 335544466). + -- Finally, we check content of mon$context_variables table: number of accumulated errors must be 7. + set term ^; + execute block as + declare v_ctx_name varchar(255); + begin + for select id1, id2, id3 from tmain where coalesce(id1, id2, id3) is NOT null as cursor c + do begin + -- 335544466 : violation of FOREIGN KEY constraint "TDETL_FK" on table "PUBLIC"."TDETL" + delete from tmain where current of c; + when any do + begin + v_ctx_name = 'FK_VIOLATION_' || gdscode; + rdb$set_context('USER_SESSION', v_ctx_name, coalesce(cast( rdb$get_context('USER_SESSION', v_ctx_name) as int), 0) + 1); + end + end + end ^ + set term ;^ + select mon$variable_name, mon$variable_value from mon$context_variables where mon$variable_name starting with 'FK_VIOLATION_'; + commit; + drop table tdetl; + drop table tmain; + """ + + act.expected_stdout = expected_stdout + act.isql(switches=['-q'], combine_output = True, input = test_sql) + + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/fkey/cascade/test_master_uk_on_update_cascade.py b/tests/functional/fkey/cascade/test_master_uk_on_update_cascade.py new file mode 100644 index 00000000..985b269b --- /dev/null +++ b/tests/functional/fkey/cascade/test_master_uk_on_update_cascade.py @@ -0,0 +1,122 @@ +#coding:utf-8 + +""" +ID: n/a +TITLE: Updating PK column(s) in master must cause changes in appropriate detail column(s) if 'ON UPDATE CASCADE' option is used +DESCRIPTION: + Test verifies RI mechanism when ON UPDATE CASCADE option is used: updating record in master should cause appropriate updates in detail. + Single- and multi-segmented PK/FK are checked. + Work within a single transaction. +NOTES: + [17.06.2025] pzotov + 1. Extended 'subsitutions' list is used here to suppress "PUBLIC" schema prefix and remove single/double quotes from all object names. Need since 6.0.0.834. + ::: NB ::: + File act.files_dir/'test_config.ini' must contain section: + [schema_n_quotes_suppress] + addi_subst="PUBLIC". " ' + (this file is used in qa/plugin.py, see QA_GLOBALS dictionary). + + Value of parameter 'addi_subst' is splitted on tokens using space character and we add every token to 'substitutions' list which + eventually will be like this: + substitutions = [ ( , ('"PUBLIC".', ''), ('"', ''), ("'", '') ] + 2. Adjusted expected output: removed single quotes from DB object name(s). + + Discussed with Vlad, letters 16.06.2025 13:54 (subj: "#8598: ...") + Checked on 6.0.0.838; 3.0.13.33813. +""" + +import pytest +from firebird.qa import * + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' ')] +for p in addi_subst_tokens.split(): + substitutions.append( (p, '') ) + +db = db_factory() +act = python_act('db', substitutions = substitutions) + +expected_stdout = """ + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_UK on table TDETL + -Foreign key references are present for the record + -Problematic key value is (ID = -1) + + DETAIL_ID 100 + DETAIL_PID -1 + DETAIL_ID 101 + DETAIL_PID -1 + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_FK on table TDETL + -Foreign key references are present for the record + -Problematic key value is (ID1 = -1, ID2 = -1, ID3 = -1) + + DETAIL_ID 200 + DETAIL_PID1 -1 + DETAIL_PID2 -1 + DETAIL_PID3 -1 + DETAIL_ID 201 + DETAIL_PID1 -1 + DETAIL_PID2 -1 + DETAIL_PID3 -1 +""" + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + + test_sql = """ + set bail OFF; + set list on; + + -- case-1: single segment UK: + recreate table tmain(id int unique using index tmain_uk); + recreate table tdetl( + id int primary key + ,pid int + ,constraint tdetl_uk foreign key(pid) references tmain(id) + ON UPDATE CASCADE + ); + insert into tmain(id) values(1); + insert into tdetl(id, pid) values(100, 1); + insert into tdetl(id, pid) values(101, 1); + update tmain set id = -1 where id = 1; -- must PASS and cause update in tdetl + update tmain set id = null where id = -1; -- must FAIL: 'on update SET NULL' must be for this case! + select id as detail_id, pid as detail_pid from tdetl order by id; + commit; + drop table tdetl; + drop table tmain; + + -------------------------------------------- + + -- case-2: multi-segment UK: + recreate table tmain(id1 int, id2 int, id3 int, unique(id1, id2, id3) using index tmain_uk); + recreate table tdetl( + id int primary key + ,pid1 int + ,pid2 int + ,pid3 int + ,constraint tdetl_fk foreign key(pid1, pid2, pid3) references tmain(id1, id2, id3) + ON UPDATE CASCADE + ); + insert into tmain(id1, id2, id3) values(1,1,1); + insert into tdetl(id, pid1, pid2, pid3) values(200, 1, 1, 1); + insert into tdetl(id, pid1, pid2, pid3) values(201, 1, 1, 1); + update tmain set id1 = -id1, id2 = -id2, id3 = -1 where id1 = 1 and id2 = 1 and id3 = 1; -- must PASS and cause update in tdetl + update tmain set id3 = null where id3 = -1; + select id as detail_id, pid1 as detail_pid1, pid2 as detail_pid2, pid3 as detail_pid3 from tdetl order by id; + commit; + drop table tdetl; + drop table tmain; + + """ + + act.expected_stdout = expected_stdout + act.isql(switches=['-q'], combine_output = True, input = test_sql) + + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/fkey/cascade/test_master_uk_update_if_no_cascade.py b/tests/functional/fkey/cascade/test_master_uk_update_if_no_cascade.py new file mode 100644 index 00000000..7c095de9 --- /dev/null +++ b/tests/functional/fkey/cascade/test_master_uk_update_if_no_cascade.py @@ -0,0 +1,155 @@ +#coding:utf-8 + +""" +ID: n/a +TITLE: Attempt to update UK column(s) in master must fail if there is FK record in detail and FK was declared without CASCADE option +DESCRIPTION: + Test verifies RI mechanism when CASCADE option missed: key columns in master with UNIQUE constraint may be updated + only if there is no appropriate record in detail (with value in FK equal to UK value from master) or all key fields are NULL. + Single- and multi-segmented PK/FK are checked. + Work within a single transaction. +NOTES: + [21.06.2025] pzotov + ::: NB ::: + SQL schema name (6.x+), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Discussed with Vlad, letters 16.06.2025 13:54 (subj: "#8598: ...") + Checked on 6.0.0.838; 3.0.13.33813. +""" + +import pytest +from firebird.qa import * + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' ')] +for p in addi_subst_tokens.split(): + substitutions.append( (p, '') ) + +db = db_factory() +act = python_act('db', substitutions = substitutions) + +expected_stdout = """ + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_FK on table TDETL + -Foreign key references are present for the record + -Problematic key value is (ID = 1000) + REMAINED_MASTER_ID -2000 + REMAINED_MASTER_ID 1000 + REMAINED_MASTER_ID 3000 + REMAINED_DETAIL_ID 100 + REMAINED_DETAIL_ID 101 + + MON$VARIABLE_NAME FK_VIOLATION_335544466 + MON$VARIABLE_VALUE 7 +""" + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + + test_sql = """ + set bail OFF; + set list on; + + -- case-1: single segment UK: + recreate table tmain(id int unique using index tmain_uk); + recreate table tdetl(id int primary key, pid int, constraint tdetl_fk foreign key(pid) references tmain(id)); + insert into tmain(id) values(null); + insert into tmain(id) values(1000); + insert into tmain(id) values(2000); + insert into tdetl(id, pid) values(100, 1000); + insert into tdetl(id, pid) values(101, null); + update tmain set id = -id where id = 1000; -- must FAIL + update tmain set id = -id where id = 2000; -- must PASS + update tmain set id = 3000 where id is null; -- must PASS + select id as remained_master_id from tmain order by id; + select id as remained_detail_id from tdetl order by id; + commit; + drop table tdetl; + drop table tmain; + + --###################################################### + + -- case-2: multi-segment UK: + recreate table tmain(id1 int, id2 int, id3 int, unique(id1, id2, id3) using index tmain_uk); + recreate table tdetl(id int primary key, pid1 int, pid2 int, pid3 int, constraint tdetl_fk foreign key(pid1, pid2, pid3) references tmain(id1, id2, id3)); + + insert into tmain(id1, id2, id3) + with + a as ( + select 1000 as v from rdb$database union all + select 1000 as v from rdb$database union all + select null as v from rdb$database + ) + select distinct a1.v, a2.v, a3.v + from a a1 + cross join a a2 + cross join a a3 + ; + insert into tdetl(id, pid1, pid2, pid3) + select row_number()over(), id1, id2, id3 + from tmain + order by id1, id2, id3; + + -- tmain: + -- ID1 ID2 ID3 + -- ============ ============ ============ + -- + -- 1000 + -- 1000 + -- 1000 1000 + -- 1000 + -- 1000 1000 + -- 1000 1000 + -- 1000 1000 1000 + + -- tdetl: + -- ID PID1 PID2 PID3 + -- ============ ============ ============ ============ + -- 1 + -- 2 1000 + -- 3 1000 + -- 4 1000 1000 + -- 5 1000 + -- 6 1000 1000 + -- 7 1000 1000 + -- 8 1000 1000 1000 + + + -- must PASS because no detail can refer to the record in master which contain nulls in all key columns: + update tmain set id1 = 9999, id2 = 9999, id3 = 9999 where coalesce(id1, id2, id3) is null; + + -- Any records in tmain with at least one NOT null value among ID1 ... ID3 must not be updated in following code. + -- We try to update every row with accumulating count of errors (for appropriate gdscode which must be the same: 335544466). + -- Finally, we check content of mon$context_variables table: number of accumulated errors must be 7. + set term ^; + execute block as + declare v_ctx_name varchar(255); + begin + for select id1, id2, id3 from tmain where coalesce(id1, id2, id3) is NOT null as cursor c + do begin + -- 335544466 : violation of FOREIGN KEY constraint "TDETL_FK" on table "PUBLIC"."TDETL" + update tmain set id1 = null, id2 = null, id3 = null where current of c; + when any do + begin + v_ctx_name = 'FK_VIOLATION_' || gdscode; + rdb$set_context('USER_SESSION', v_ctx_name, coalesce(cast( rdb$get_context('USER_SESSION', v_ctx_name) as int), 0) + 1); + end + end + end ^ + set term ;^ + select mon$variable_name, mon$variable_value from mon$context_variables where mon$variable_name starting with 'FK_VIOLATION_'; + commit; + drop table tdetl; + drop table tmain; + """ + + act.expected_stdout = expected_stdout + act.isql(switches=['-q'], combine_output = True, input = test_sql) + + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/fkey/common/test_dml_in_detail_if_no_parent_exists.py b/tests/functional/fkey/common/test_dml_in_detail_if_no_parent_exists.py new file mode 100644 index 00000000..fdc34753 --- /dev/null +++ b/tests/functional/fkey/common/test_dml_in_detail_if_no_parent_exists.py @@ -0,0 +1,181 @@ +#coding:utf-8 + +""" +ID: n/a +TITLE: FK-columns columns in the child table must either all be equal to appropriate columns in the parent or some/all of them must be null +DESCRIPTION: + Single- and multi-segmented FK are checked. + Work within a single transaction. +NOTES: + [21.06.2025] pzotov + ::: NB ::: + SQL schema name (6.x+), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Discussed with Vlad, letters 16.06.2025 13:54 (subj: "#8598: ...") + Checked on 6.0.0.838; 3.0.13.33813. +""" + +import pytest +from firebird.qa import * + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' ')] +for p in addi_subst_tokens.split(): + substitutions.append( (p, '') ) + +db = db_factory() +act = python_act('db', substitutions = substitutions) + +expected_stdout = """ + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_FK on table TDETL + -Foreign key reference target does not exist + -Problematic key value is (PID = 2) + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_FK on table TDETL + -Foreign key reference target does not exist + -Problematic key value is (PID = 2) + + TDETL_ID 100 + TDETL_PID + TDETL_ID 101 + TDETL_PID 1 + Records affected: 2 + + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_MULTI_FK on table TDETL_MULTI_FK + -Foreign key reference target does not exist + -Problematic key value is (FK_ID1 = 3333, FK_ID2 = 4444) + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_MULTI_FK on table TDETL_MULTI_FK + -Foreign key reference target does not exist + -Problematic key value is (FK_ID1 = 3333, FK_ID2 = 2222) + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_MULTI_FK on table TDETL_MULTI_FK + -Foreign key reference target does not exist + -Problematic key value is (FK_ID1 = 1111, FK_ID2 = 3333) + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_MULTI_FK on table TDETL_MULTI_FK + -Foreign key reference target does not exist + -Problematic key value is (FK_ID1 = 3333, FK_ID2 = 1111) + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_MULTI_FK on table TDETL_MULTI_FK + -Foreign key reference target does not exist + -Problematic key value is (FK_ID1 = 3333, FK_ID2 = 3333) + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_MULTI_FK on table TDETL_MULTI_FK + -Foreign key reference target does not exist + -Problematic key value is (FK_ID1 = 9999, FK_ID2 = 3333) + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_MULTI_FK on table TDETL_MULTI_FK + -Foreign key reference target does not exist + -Problematic key value is (FK_ID1 = 3333, FK_ID2 = 9999) + + TDETL_MULTI_ID 1 + TDETL_MULTI_FK1 + TDETL_MULTI_FK2 2222 + TDETL_MULTI_ID 2 + TDETL_MULTI_FK1 + TDETL_MULTI_FK2 2222 + TDETL_MULTI_ID 3 + TDETL_MULTI_FK1 1111 + TDETL_MULTI_FK2 + TDETL_MULTI_ID 4 + TDETL_MULTI_FK1 1111 + TDETL_MULTI_FK2 2222 + TDETL_MULTI_ID 5 + TDETL_MULTI_FK1 + TDETL_MULTI_FK2 7777 + TDETL_MULTI_ID 6 + TDETL_MULTI_FK1 8888 + TDETL_MULTI_FK2 + Records affected: 6 + +""" + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + + test_sql = """ + set bail OFF; + set list on; + + -- case-1: single segment UK/FK: + -- +++++++++++++++++++++++++++++ + recreate table tmain(id int unique using index tmain_uk); + recreate table tdetl(id int unique using index tdetl_uk, pid int, constraint tdetl_fk foreign key(pid) references tmain(id)); + insert into tmain(id) values(1); + insert into tdetl(id, pid) values(100, 1); -- must PASS + insert into tdetl(id, pid) values(101, null); -- must PASS + insert into tdetl(id, pid) values(102, 2); -- must FAIL // no record in tmain with ID = 2 + update tdetl set pid = 2 where id = 101; -- must FAIL // no record in tmain with ID = 2 + update tdetl set pid = 1 where id = 101; -- must PASS + update tdetl set pid = null where id = 100; -- must PASS + set count on; + select d.id as tdetl_id, d.pid as tdetl_pid from tdetl d order by d.id; + set count off; + commit; + drop table tdetl; + drop table tmain; + -------------------------------------------- + + -- case-2: multi segment UK/FK: + -- ++++++++++++++++++++++++++++ + recreate table tmain_multi_uk(id1 int, id2 int, unique(id1, id2) using index tmain_uk); + recreate table tdetl_multi_fk( + id int generated by default as identity + ,fk_id1 int + ,fk_id2 int + ,constraint tdetl_multi_fk foreign key(fk_id1, fk_id2) references tmain_multi_uk(id1, id2) + ); + commit; + insert into tmain_multi_uk(id1, id2) values(1111, 2222); + -- following six statements must PASS. First because of full match FK to parent UK, others because of nulls: + insert into tdetl_multi_fk(id, fk_id1, fk_id2) values(1,1111, 2222); + insert into tdetl_multi_fk(id, fk_id1, fk_id2) values(2,1111, null); + insert into tdetl_multi_fk(id, fk_id1, fk_id2) values(3,null, 1111); + insert into tdetl_multi_fk(id, fk_id1, fk_id2) values(4,null, null); + insert into tdetl_multi_fk(id, fk_id1, fk_id2) values(5,9999, null); + insert into tdetl_multi_fk(id, fk_id1, fk_id2) values(6,null, 9999); + + insert into tdetl_multi_fk(id,fk_id1, fk_id2) values(7,3333, 4444); -- must FAIL // no record in parent with (id1,id2) = (3333, 4444) + + -- Following must FAIL // no record in parent with appropriate UK which have not-null values + update tdetl_multi_fk set fk_id1 = 3333 where id = 1; + update tdetl_multi_fk set fk_id2 = 3333 where id = 2; + update tdetl_multi_fk set fk_id1 = 3333 where id = 3; + update tdetl_multi_fk set fk_id1 = 3333, fk_id2 = 3333 where id = 4; + update tdetl_multi_fk set fk_id2 = 3333 where id = 5; + update tdetl_multi_fk set fk_id1 = 3333 where id = 6; + + -- must PASS: we use null in at least one column values: + update tdetl_multi_fk set fk_id1 = null where id = 1; + update tdetl_multi_fk set fk_id1 = null, fk_id2 = 2222 where id = 2; + update tdetl_multi_fk set fk_id1 = 1111, fk_id2 = null where id = 3; + update tdetl_multi_fk set fk_id1 = 1111, fk_id2 = 2222 where id = 4; -- match to parent key + update tdetl_multi_fk set fk_id1 = null, fk_id2 = 7777 where id = 5; + update tdetl_multi_fk set fk_id1 = 8888, fk_id2 = null where id = 6; + set count on; + select d.id as tdetl_multi_id, d.fk_id1 as tdetl_multi_fk1, d.fk_id2 as tdetl_multi_fk2 from tdetl_multi_fk d order by d.id; + set count off; + commit; + """ + + act.expected_stdout = expected_stdout + act.isql(switches=['-q'], combine_output = True, input = test_sql) + + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/fkey/common/test_non_priv_user_access_to_child_for_cascade.py b/tests/functional/fkey/common/test_non_priv_user_access_to_child_for_cascade.py new file mode 100644 index 00000000..d9cccce6 --- /dev/null +++ b/tests/functional/fkey/common/test_non_priv_user_access_to_child_for_cascade.py @@ -0,0 +1,119 @@ +#coding:utf-8 + +""" +ID: n/a +TITLE: Child table must be updated even if current user has no privileges on it but 'CASCADE' present in FK declaration. +DESCRIPTION: + Child table has column(s) on which FK is declared, with options: ON UPDATE CASCADE and ON DELETE CASCADE. + Work within a single transaction. +NOTES: + [21.06.2025] pzotov + ::: NB ::: + SQL schema name (6.x+), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Discussed with Vlad, letters 16.06.2025 13:54 (subj: "#8598: ...") + Checked on 6.0.0.838; 3.0.13.33813. +""" + +import pytest +from firebird.qa import * + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' '), ('(-)?At trigger\\s+\\S+', 'At trigger')] +for p in addi_subst_tokens.split(): + substitutions.append( (p, '') ) + +init_sql = """ + recreate table tmain(id int unique using index tmain_uk); + recreate table tdetl( + id int generated by default as identity + ,pid int + ,constraint tdetl_fk foreign key(pid) references tmain(id) + ON DELETE CASCADE + ON UPDATE CASCADE + ); + commit; + insert into tmain(id) values(1111); + insert into tdetl(pid) values(1111); + commit; +""" +db = db_factory(init = init_sql) +act = python_act('db', substitutions = substitutions) + +tmp_user = user_factory('db', name='tmp_parent_only_accessor', password='123') + +@pytest.mark.version('>=3.0') +def test_1(act: Action, tmp_user: User): + + test_sql = f""" + set bail OFF; + set wng off; + set list on; + -- set echo on; + revoke all on all from {tmp_user.name}; + grant select, insert, update, delete on tmain to {tmp_user.name}; + commit; + + connect '{act.db.dsn}' user '{tmp_user.name}' password '{tmp_user.password}'; + select current_user as whoami from rdb$database; + + -- Update existsing record in parent which have references from detail. + -- NB: detail table WILL BE CHANGED despite that current user has no any privilege on this table. + -- New value in tdetl.pid will be -9999: + update tmain set id = -9999; + commit; + + connect '{act.db.dsn}' user '{act.db.user}' password '{act.db.password}'; + select 'point-1' as msg from rdb$database; + set count on; + select m.id as tmain_pk from tmain m; + select d.id as tdetl_pk, d.pid as tdetl_pid from tdetl d; + set count off; + + delete from tdetl; + delete from tmain; + insert into tmain(id) values(1111); + insert into tdetl(pid) values(1111); + commit; + + connect '{act.db.dsn}' user '{tmp_user.name}' password '{tmp_user.password}'; + + -- NB: detail table WILL BE CHANGED despite that current user has no any privilege on this table. + -- Record in tdetl.pid will be DELETED: + delete from tmain where id = 1111; + commit; + + connect '{act.db.dsn}' user '{act.db.user}' password '{act.db.password}'; + select 'point-2' as msg from rdb$database; + set count on; + select m.id as tmain_pk from tmain m; + select d.id as tdetl_pk, d.pid as tdetl_pid from tdetl d; + set count off; + """ + + expected_stdout = f""" + WHOAMI {tmp_user.name.upper()} + + MSG point-1 + TMAIN_PK -9999 + Records affected: 1 + + TDETL_PK 1 + TDETL_PID -9999 + Records affected: 1 + + MSG point-2 + Records affected: 0 + Records affected: 0 + """ + + act.expected_stdout = expected_stdout + act.isql(switches=['-q'], combine_output = True, input = test_sql) + + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/fkey/common/test_non_priv_user_access_to_child_for_set_default.py b/tests/functional/fkey/common/test_non_priv_user_access_to_child_for_set_default.py new file mode 100644 index 00000000..9784cc74 --- /dev/null +++ b/tests/functional/fkey/common/test_non_priv_user_access_to_child_for_set_default.py @@ -0,0 +1,113 @@ +#coding:utf-8 + +""" +ID: n/a +TITLE: Child table must be updated even if current user has no privileges on it but 'SET DEFAULT' present in FK declaration. +DESCRIPTION: + Child table has column(s) on which FK is declared, with options: ON UPDATE SET DEFAULT and ON DELETE SET DEFAULT. + Work within a single transaction. +NOTES: + [21.06.2025] pzotov + ::: NB ::: + SQL schema name (6.x+), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Discussed with Vlad, letters 16.06.2025 13:54 (subj: "#8598: ...") + Checked on 6.0.0.838; 3.0.13.33813. +""" + +import pytest +from firebird.qa import * + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' '), ('(-)?At trigger\\s+\\S+', 'At trigger')] +for p in addi_subst_tokens.split(): + substitutions.append( (p, '') ) + +init_sql = """ + recreate table tmain(id int unique using index tmain_uk); + recreate table tdetl( + id int generated by default as identity + ,pid int default -9999 + ,constraint tdetl_fk foreign key(pid) references tmain(id) + ON DELETE SET DEFAULT + ON UPDATE SET DEFAULT + ); + commit; + insert into tmain(id) values(1111); + insert into tdetl(pid) values(1111); + commit; +""" +db = db_factory(init = init_sql) +act = python_act('db', substitutions = substitutions) + +tmp_user = user_factory('db', name='tmp_parent_only_accessor', password='123') + +@pytest.mark.version('>=3.0') +def test_1(act: Action, tmp_user: User): + + test_sql = f""" + set bail OFF; + set wng off; + set list on; + -- set echo on; + revoke all on all from {tmp_user.name}; + grant select, insert, update, delete on tmain to {tmp_user.name}; + commit; + + connect '{act.db.dsn}' user '{tmp_user.name}' password '{tmp_user.password}'; + select current_user as whoami from rdb$database; + + -- Update existsing record in parent which have references from detail. + -- Must PASS because new value of ID equals to DEFAULT value for domain on which FK column is based. + -- NB: detail table WILL BE CHANGED despite that current user has no any privilege on this table. + update tmain set id = -9999; + commit; + + connect '{act.db.dsn}' user '{act.db.user}' password '{act.db.password}'; + select 'point-1' as msg, m.id as tmain_pk, d.id as tdetl_pk, d.pid as tdetl_pid from tmain m left join tdetl d on m.id = d.pid; + + delete from tdetl; + delete from tmain; + insert into tmain(id) values(1111); + insert into tdetl(pid) values(1111); + commit; + + connect '{act.db.dsn}' user '{tmp_user.name}' password '{tmp_user.password}'; + + -- Add record so that both 1111 and -9999 now exists in the parent table: + insert into tmain(id) values(-9999); + + -- must PASS because default value now exists in just inserted record. + -- New value tdetl.pid must become -9999. + -- NB: detail table WILL BE CHANGED despite that current user has no any privilege on this table. + delete from tmain where id = 1111; + commit; + + connect '{act.db.dsn}' user '{act.db.user}' password '{act.db.password}'; + select 'point-2' as msg, m.id as tmain_pk, d.id as tdetl_pk, d.pid as tdetl_pid from tmain m left join tdetl d on m.id = d.pid; + """ + + expected_stdout = f""" + WHOAMI {tmp_user.name.upper()} + + MSG point-1 + TMAIN_PK -9999 + TDETL_PK 1 + TDETL_PID -9999 + + MSG point-2 + TMAIN_PK -9999 + TDETL_PK 2 + TDETL_PID -9999 + """ + + act.expected_stdout = expected_stdout + act.isql(switches=['-q'], combine_output = True, input = test_sql) + + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/fkey/common/test_non_priv_user_access_to_child_for_set_null.py b/tests/functional/fkey/common/test_non_priv_user_access_to_child_for_set_null.py new file mode 100644 index 00000000..0d79d695 --- /dev/null +++ b/tests/functional/fkey/common/test_non_priv_user_access_to_child_for_set_null.py @@ -0,0 +1,123 @@ +#coding:utf-8 + +""" +ID: n/a +TITLE: Child table must be updated even if current user has no privileges on it but 'SET NULL' present in FK declaration. +DESCRIPTION: + Child table has column(s) on which FK is declared, with options: ON UPDATE SET NULL and ON DELETE SET NULL. + Work within a single transaction. +NOTES: + [21.06.2025] pzotov + ::: NB ::: + SQL schema name (6.x+), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Discussed with Vlad, letters 16.06.2025 13:54 (subj: "#8598: ...") + Checked on 6.0.0.838; 3.0.13.33813. +""" + +import pytest +from firebird.qa import * + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' '), ('(-)?At trigger\\s+\\S+', 'At trigger')] +for p in addi_subst_tokens.split(): + substitutions.append( (p, '') ) + +init_sql = """ + recreate table tmain(id int unique using index tmain_uk); + recreate table tdetl( + id int generated by default as identity + ,pid int + ,constraint tdetl_fk foreign key(pid) references tmain(id) + ON DELETE SET NULL + ON UPDATE SET NULL + ); + commit; + insert into tmain(id) values(1111); + insert into tdetl(pid) values(1111); + commit; +""" +db = db_factory(init = init_sql) +act = python_act('db', substitutions = substitutions) + +tmp_user = user_factory('db', name='tmp_parent_only_accessor', password='123') + +@pytest.mark.version('>=3.0') +def test_1(act: Action, tmp_user: User): + + test_sql = f""" + set bail OFF; + set wng off; + set list on; + -- set echo on; + revoke all on all from {tmp_user.name}; + grant select, insert, update, delete on tmain to {tmp_user.name}; + commit; + + connect '{act.db.dsn}' user '{tmp_user.name}' password '{tmp_user.password}'; + select current_user as whoami from rdb$database; + + -- Update existsing record in parent which have references from detail. + -- NB: detail table WILL BE CHANGED despite that current user has no any privilege on this table. + -- New value in tdetl.pid will be NULL: + update tmain set id = -9999; + commit; + + connect '{act.db.dsn}' user '{act.db.user}' password '{act.db.password}'; + select 'point-1' as msg from rdb$database; + set count on; + select m.id as tmain_pk from tmain m; + select d.id as tdetl_pk, d.pid as tdetl_pid from tdetl d; + set count off; + + delete from tdetl; + delete from tmain; + insert into tmain(id) values(1111); + insert into tdetl(pid) values(1111); + commit; + + connect '{act.db.dsn}' user '{tmp_user.name}' password '{tmp_user.password}'; + + -- NB: detail table WILL BE CHANGED despite that current user has no any privilege on this table. + -- New value in tdetl.pid will be NULL: + delete from tmain where id = 1111; + commit; + + connect '{act.db.dsn}' user '{act.db.user}' password '{act.db.password}'; + select 'point-2' as msg from rdb$database; + set count on; + select m.id as tmain_pk from tmain m; + select d.id as tdetl_pk, d.pid as tdetl_pid from tdetl d; + set count off; + """ + + expected_stdout = f""" + WHOAMI {tmp_user.name.upper()} + MSG point-1 + + TMAIN_PK -9999 + Records affected: 1 + + TDETL_PK 1 + TDETL_PID + Records affected: 1 + + MSG point-2 + + Records affected: 0 + + TDETL_PK 2 + TDETL_PID + Records affected: 1 + """ + + act.expected_stdout = expected_stdout + act.isql(switches=['-q'], combine_output = True, input = test_sql) + + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/fkey/common/test_prohibit_change_of_key_column_types.py b/tests/functional/fkey/common/test_prohibit_change_of_key_column_types.py new file mode 100644 index 00000000..cad11736 --- /dev/null +++ b/tests/functional/fkey/common/test_prohibit_change_of_key_column_types.py @@ -0,0 +1,186 @@ +#coding:utf-8 + +""" +ID: n/a +TITLE: Attempt to change type of any column involved in PK/FK constraints must fail. +DESCRIPTION: + Single- and multi-segmented PK/UK are checked. + Work within a single transaction. +NOTES: + [21.06.2025] pzotov + ::: NB ::: + SQL schema name (6.x+), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Discussed with Vlad, letters 16.06.2025 13:54 (subj: "#8598: ...") + Checked on 6.0.0.838; 3.0.13.33813. +""" + +import pytest +from firebird.qa import * + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' ')] +for p in addi_subst_tokens.split(): + substitutions.append( (p, '') ) + +db = db_factory() +act = python_act('db', substitutions = substitutions) + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + + test_sql = """ + set bail OFF; + set autoddl off; + set list on; + + -- all following statements shoudl FAIL: + + -- case-1a: single segment PK + -- ------------------------- + recreate table tmain_single_pk(id_pk int primary key using index tmain_pk); + recreate table tdetl_single_pk(id_pk int primary key, pid2pk int, constraint tdetl_fk foreign key(pid2pk) references tmain_single_pk(id_pk)); + commit; + alter table tmain_single_pk alter column id_pk type bigint; + alter table tdetl_single_pk alter column pid2pk type bigint; + commit; + drop table tdetl_single_pk; + drop table tmain_single_pk; + + -- case-1b: single segment UK + -- ------------------------- + recreate table tmain_single_uk(id_uk int unique using index tmain_pk); + recreate table tdetl_single_uk(id int primary key, pid2uk int, constraint tdetl_fk foreign key(pid2uk) references tmain_single_uk(id_uk)); + commit; + alter table tmain_single_uk alter column id_uk type bigint; + alter table tdetl_single_uk alter column pid2uk type bigint; + commit; + drop table tdetl_single_uk; + drop table tmain_single_uk; + + -- case-2a: multi segment PK + -- ------------------------- + recreate table tmain_multi_pk(pk_id1 int, pk_id2 int, primary key(pk_id1, pk_id2) using index tmain_pk); + recreate table tdetl_multi_pk(id_pk int primary key, fk_id1 int, fk_id2 int, constraint tdetl_fk foreign key(fk_id1, fk_id2) references tmain_multi_pk(pk_id1, pk_id2)); + commit; + alter table tmain_multi_pk alter column pk_id2 type bigint; + alter table tdetl_multi_pk alter column fk_id2 type bigint; + commit; + drop table tdetl_multi_pk; + drop table tmain_multi_pk; + + -- case-2b: multi segment UK + -- ------------------------- + recreate table tmain_multi_uk(pk_id1 int, pk_id2 int, unique(pk_id1, pk_id2) using index tmain_uk); + recreate table tdetl_multi_uk(id_pk int primary key, fk_id1 int, fk_id2 int, constraint tdetl_fk foreign key(fk_id1, fk_id2) references tmain_multi_uk(pk_id1, pk_id2)); + commit; + alter table tmain_multi_uk alter column pk_id2 type bigint; + alter table tdetl_multi_uk alter column fk_id2 type bigint; + commit; + drop table tdetl_multi_uk; + drop table tmain_multi_uk; + """ + + expected_stdout_3x = """ + Statement failed, SQLSTATE = 27000 + unsuccessful metadata update + -ALTER TABLE TMAIN_SINGLE_PK failed + -action cancelled by trigger (1) to preserve data integrity + -Cannot update index segment used by an Integrity Constraint + + Statement failed, SQLSTATE = 27000 + unsuccessful metadata update + -ALTER TABLE TDETL_SINGLE_PK failed + -action cancelled by trigger (1) to preserve data integrity + -Cannot update index segment used by an Integrity Constraint + + Statement failed, SQLSTATE = 27000 + unsuccessful metadata update + -ALTER TABLE TMAIN_SINGLE_UK failed + -action cancelled by trigger (1) to preserve data integrity + -Cannot update index segment used by an Integrity Constraint + + Statement failed, SQLSTATE = 27000 + unsuccessful metadata update + -ALTER TABLE TDETL_SINGLE_UK failed + -action cancelled by trigger (1) to preserve data integrity + -Cannot update index segment used by an Integrity Constraint + + Statement failed, SQLSTATE = 27000 + unsuccessful metadata update + -ALTER TABLE TMAIN_MULTI_PK failed + -action cancelled by trigger (1) to preserve data integrity + -Cannot update index segment used by an Integrity Constraint + + Statement failed, SQLSTATE = 27000 + unsuccessful metadata update + -ALTER TABLE TDETL_MULTI_PK failed + -action cancelled by trigger (1) to preserve data integrity + -Cannot update index segment used by an Integrity Constraint + + Statement failed, SQLSTATE = 27000 + unsuccessful metadata update + -ALTER TABLE TMAIN_MULTI_UK failed + -action cancelled by trigger (1) to preserve data integrity + -Cannot update index segment used by an Integrity Constraint + + Statement failed, SQLSTATE = 27000 + unsuccessful metadata update + -ALTER TABLE TDETL_MULTI_UK failed + -action cancelled by trigger (1) to preserve data integrity + -Cannot update index segment used by an Integrity Constraint + """ + + + expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -ALTER TABLE TMAIN_SINGLE_PK failed + -Cannot update index segment used by an Integrity Constraint + + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -ALTER TABLE TDETL_SINGLE_PK failed + -Cannot update index segment used by an Integrity Constraint + + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -ALTER TABLE TMAIN_SINGLE_UK failed + -Cannot update index segment used by an Integrity Constraint + + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -ALTER TABLE TDETL_SINGLE_UK failed + -Cannot update index segment used by an Integrity Constraint + + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -ALTER TABLE TMAIN_MULTI_PK failed + -Cannot update index segment used by an Integrity Constraint + + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -ALTER TABLE TDETL_MULTI_PK failed + -Cannot update index segment used by an Integrity Constraint + + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -ALTER TABLE TMAIN_MULTI_UK failed + -Cannot update index segment used by an Integrity Constraint + + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -ALTER TABLE TDETL_MULTI_UK failed + -Cannot update index segment used by an Integrity Constraint + """ + + act.expected_stdout = expected_stdout_3x if act.is_version('<6') else expected_stdout_6x + act.isql(switches=['-q'], combine_output = True, input = test_sql) + + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/fkey/common/test_restrictions_for_default_fk_fields.py b/tests/functional/fkey/common/test_restrictions_for_default_fk_fields.py new file mode 100644 index 00000000..64d77545 --- /dev/null +++ b/tests/functional/fkey/common/test_restrictions_for_default_fk_fields.py @@ -0,0 +1,210 @@ +#coding:utf-8 + +""" +ID: n/a +TITLE: FK definition: SET DEFAULT clause should not lead FK-columns to have their default values if parent record not yet exists or was removed +DESCRIPTION: + Parent table has UNIQUE constraint declared for NULLABLE column(s). + Child table has column(s) on which FK is declared, with options: ON UPDATE SET DEFAULT and ON DELETE SET DEFAULT. + Single- and multi-segmented unique keys are checked. + Work within a single transaction. +NOTES: + [21.06.2025] pzotov + ::: NB ::: + SQL schema name (6.x+), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Discussed with Vlad, letters 16.06.2025 13:54 (subj: "#8598: ...") + Checked on 6.0.0.838; 3.0.13.33813. +""" + +import pytest +from firebird.qa import * + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' '), ('(-)?At trigger\\s+\\S+', 'At trigger')] +for p in addi_subst_tokens.split(): + substitutions.append( (p, '') ) + +db = db_factory() +act = python_act('db', substitutions = substitutions) + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + + test_sql = """ + set bail OFF; + set autoddl off; + set list on; + + create domain dm_int_default int default -9999; + + -- case-1: single segment UK + -- ------------------------- + recreate table tmain_single_uk(id int unique using index tmain_pk); + recreate table tdetl_single_uk( + id int generated by default as identity + ,fk_id dm_int_default + ,constraint tdetl_fk foreign key(fk_id) references tmain_single_uk(id) + ON UPDATE SET DEFAULT + ON DELETE SET DEFAULT + ); + commit; + insert into tmain_single_uk(id) values(1111); + insert into tdetl_single_uk(fk_id) values(1111); + commit; + + -- Update existsing record in parent which have references from detail. + -- Must PASS because new value of ID equals to DEFAULT value for domain on which FK column is based: + update tmain_single_uk set id = -9999; + select * from tdetl_single_uk; + rollback; + + -- Add record so that both 1111 and -9999 now exists in the parent table: + insert into tmain_single_uk(id) values(-9999); + -- must PASS because default value now exists in just inserted record. + -- New value tdetl_single_uk.fk_id must become -9999: + delete from tmain_single_uk where id = 1111; + select * from tdetl_single_uk; + rollback; + + -- Must fail because 'set default' cause FK column to have value = -9999 that not exists in the parent table: + -- Statement failed, SQLSTATE = 23000 + -- violation of FOREIGN KEY constraint TDETL_FK on table TDETL_SINGLE_UK + -- -Foreign key reference target does not exist + -- -Problematic key value is (FK_ID = -9999) + update tmain_single_uk set id = -8888; + rollback; + + -- Must fail because 'set default' cause FK column to have value = -9999 that not exists in the parent table: + -- Statement failed, SQLSTATE = 23000 + -- violation of FOREIGN KEY constraint TDETL_FK on table TDETL_SINGLE_UK + -- -Foreign key reference target does not exist + -- -Problematic key value is (FK_ID = -9999) + delete from tmain_single_uk; + commit; + drop table tdetl_single_uk; + drop table tmain_single_uk; + + --############################## + + -- case-2: multi segment UK + -- ------------------------- + recreate table tmain_multi_uk(id1 int, id2 int, unique(id1, id2) using index tmain_uk); + recreate table tdetl_multi_uk( + id int generated by default as identity + ,fk_id1 dm_int_default + ,fk_id2 dm_int_default + ,constraint tdetl_multi_fk foreign key(fk_id1, fk_id2) references tmain_multi_uk(id1, id2) + ON UPDATE SET DEFAULT + ON DELETE SET DEFAULT + ); + commit; + insert into tmain_multi_uk(id1, id2) values(1111, 2222); + insert into tdetl_multi_uk(fk_id1, fk_id2) values(1111, 2222); + commit; + + + -- Update existsing record in parent which have references from detail. + -- Must PASS because new value for ID1 and ID2 equals to DEFAULT value for domain on which FK columns are based: + update tmain_multi_uk set id1 = -9999, id2 = -9999; + select * from tdetl_multi_uk; + rollback; + + + insert into tmain_multi_uk(id1, id2) values(-9999, -9999); + delete from tmain_multi_uk where id1 = 1111 and id2 = 2222; + select * from tdetl_multi_uk; + rollback; + + --+++++++++++++++++++++++++++++++++++++++++++++++++++ + -- Following three statements fail with same messages (all with SQLSTATE = 23000): + -- violation of FOREIGN KEY constraint TDETL_MULTI_FK on table TDETL_MULTI_UK + -- -Foreign key references are >>> present <<< for the record + -- -Problematic key value is (ID1 = 1111, ID2 = 2222) + update tmain_multi_uk set id2 = null; + update tmain_multi_uk set id1 = null; + update tmain_multi_uk set id1 = null, id2 = null; + + + -- Following three statements fail with same messages (all with SQLSTATE = 23000): + -- violation of FOREIGN KEY constraint TDETL_MULTI_FK on table TDETL_MULTI_UK + -- -Foreign key reference >>> target <<< does not exist + -- -Problematic key value is (FK_ID1 = -9999, FK_ID2 = -9999) + -- -At trigger CHECK_* + update tmain_multi_uk set id1 = 3333, id2 = null; + update tmain_multi_uk set id1 = null, id2 = 4444; + update tmain_multi_uk set id1 = 2222, id2 = 1111; + """ + + expected_stdout = """ + ID 1 + FK_ID -9999 + + ID 1 + FK_ID -9999 + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_FK on table TDETL_SINGLE_UK + -Foreign key reference target does not exist + -Problematic key value is (FK_ID = -9999) + -At trigger CHECK_1 + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_FK on table TDETL_SINGLE_UK + -Foreign key reference target does not exist + -Problematic key value is (FK_ID = -9999) + -At trigger CHECK_2 + + + ID 1 + FK_ID1 -9999 + FK_ID2 -9999 + + ID 1 + FK_ID1 -9999 + FK_ID2 -9999 + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_MULTI_FK on table TDETL_MULTI_UK + -Foreign key references are present for the record + -Problematic key value is (ID1 = 1111, ID2 = 2222) + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_MULTI_FK on table TDETL_MULTI_UK + -Foreign key references are present for the record + -Problematic key value is (ID1 = 1111, ID2 = 2222) + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_MULTI_FK on table TDETL_MULTI_UK + -Foreign key references are present for the record + -Problematic key value is (ID1 = 1111, ID2 = 2222) + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_MULTI_FK on table TDETL_MULTI_UK + -Foreign key reference target does not exist + -Problematic key value is (FK_ID1 = -9999, FK_ID2 = -9999) + -At trigger CHECK_3 + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_MULTI_FK on table TDETL_MULTI_UK + -Foreign key reference target does not exist + -Problematic key value is (FK_ID1 = -9999, FK_ID2 = -9999) + -At trigger CHECK_3 + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_MULTI_FK on table TDETL_MULTI_UK + -Foreign key reference target does not exist + -Problematic key value is (FK_ID1 = -9999, FK_ID2 = -9999) + -At trigger CHECK_3 + """ + + act.expected_stdout = expected_stdout + act.isql(switches=['-q'], combine_output = True, input = test_sql) + + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/fkey/common/test_restrictions_for_diff_table_types.py b/tests/functional/fkey/common/test_restrictions_for_diff_table_types.py new file mode 100644 index 00000000..06fb24b4 --- /dev/null +++ b/tests/functional/fkey/common/test_restrictions_for_diff_table_types.py @@ -0,0 +1,129 @@ +#coding:utf-8 + +""" +ID: n/a +TITLE: Tables involved in referential integrity have to meet restrictions when their types differ (persistent vs GTT; connection-bound vs transaction-bound). +DESCRIPTION: + Test verifies that: + * GTTs and regular ("permanent") tables cannot reference one another. + * A connection-bound ("PRESERVE ROWS") GTT cannot reference a transaction-bound ("DELETE ROWS") GTT + Work within a single attachment. +NOTES: + [21.06.2025] pzotov + 1. ::: NB ::: + SQL schema name (6.x+), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + 2. See doc: + https://firebirdsql.org/file/documentation/html/en/refdocs/fblangref50/firebird-50-language-reference.html#fblangref50-ddl-tbl-gtt-restrictions + + Discussed with Vlad, letters 16.06.2025 13:54 (subj: "#8598: ...") + Checked on 6.0.0.838; 3.0.13.33813. +""" + +import pytest +from firebird.qa import * + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' '), ('(-)?At line \\d+.*', '')] +for p in addi_subst_tokens.split(): + substitutions.append( (p, '') ) + +db = db_factory() +act = python_act('db', substitutions = substitutions) + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + + test_sql = """ + set bail OFF; + set list on; + set count on; + + recreate table tmain_fixed (id int primary key using index tmain_fixed_pk); + + -- case-1a: GTT of type 'on commit PRESERVE rows' can not refer to persistent table: + recreate global temporary table tdetl_gtt_keep_rows(id int primary key using index tdetl_pk, pid int references tmain_fixed(id) using index tdetl_keep_fk) on commit preserve rows; + select * from rdb$relations where rdb$relation_name = upper('tdetl_gtt_keep_rows'); + + -- case-1b: GTT of type 'on commit DELETE rows' can not refer to persistent table: + recreate global temporary table tdetl_gtt_kill_rows(id int primary key using index tdetl_pk, pid int references tmain_fixed(id) using index tdetl_kill_fk) on commit delete rows; + select * from rdb$relations where rdb$relation_name = upper('tdetl_gtt_kill_rows'); + commit; + drop table tmain_fixed; + + + -- case-1c: persistent table can not refer to GTT of type 'on commit PRESERVE rows': + recreate global temporary table tmain_gtt_keep_rows(id int primary key using index tmain_keep_pk) on commit preserve rows; + recreate table tdetl_fixed(id int primary key using index tdetl_fixed_pk, pid int references tmain_gtt_keep_rows(id) using index tdetl_fixed_fk); + select * from rdb$relations where rdb$relation_name = upper('tdetl_fixed'); + commit; + drop table tmain_gtt_keep_rows; + + -- case-1d: persistent table can not refer to GTT of type 'on commit DELETE rows': + recreate global temporary table tmain_gtt_kill_rows(id int primary key using index tmain_kill_pk) on commit delete rows; + recreate table tdetl_fixed(id int primary key using index tdetl_fixed_pk, pid int references tmain_gtt_kill_rows(id) using index tdetl_fixed_fk); + select * from rdb$relations where rdb$relation_name = upper('tdetl_fixed'); + commit; + drop table tmain_gtt_kill_rows; + + + -- case-2a: GTT of type 'on commit PRESERVE rows' cannot refer a GTT of type 'on commit DELETE rows' + recreate global temporary table tmain_gtt_kill_rows(id int primary key using index tmain_kill_pk) on commit delete rows; + recreate global temporary table tdetl_gtt_keep_rows(id int primary key using index tdetl_keep_pk, pid int references tmain_gtt_kill_rows(id) using index tdetl_keep_fk) on commit preserve rows; + select * from rdb$relations where rdb$relation_name = upper('tdetl_gtt_keep_rows'); + commit; + drop table tmain_gtt_kill_rows; + + + -- case-2b: GTT of type 'on commit DELETE rows' *** CAN ** refer a GTT of type 'on commit PRESERVE rows' + recreate global temporary table tmain_gtt_keep_rows(id int primary key using index tmain_keep_pk) on commit preserve rows; + recreate global temporary table tdetl_gtt_kill_rows(id int primary key using index tdetl_kill_pk, pid int references tmain_gtt_keep_rows(id) using index tdetl_kill_fk) on commit delete rows; + commit; + set count off; + select count(*) from rdb$relations where rdb$relation_name = upper('tdetl_gtt_kill_rows'); + commit; + """ + + expected_stdout = """ + Statement failed, SQLSTATE = HY000 + unsuccessful metadata update + -RECREATE TABLE TDETL_GTT_KEEP_ROWS failed + -global temporary table TDETL_GTT_KEEP_ROWS of type ON COMMIT PRESERVE ROWS cannot reference persistent table TMAIN_FIXED + Records affected: 0 + + Statement failed, SQLSTATE = HY000 + unsuccessful metadata update + -RECREATE TABLE TDETL_GTT_KILL_ROWS failed + -global temporary table TDETL_GTT_KILL_ROWS of type ON COMMIT DELETE ROWS cannot reference persistent table TMAIN_FIXED + Records affected: 0 + + Statement failed, SQLSTATE = HY000 + unsuccessful metadata update + -RECREATE TABLE TDETL_FIXED failed + -persistent table TDETL_FIXED cannot reference global temporary table TMAIN_GTT_KEEP_ROWS of type ON COMMIT PRESERVE ROWS + Records affected: 0 + + Statement failed, SQLSTATE = HY000 + unsuccessful metadata update + -RECREATE TABLE TDETL_FIXED failed + -persistent table TDETL_FIXED cannot reference global temporary table TMAIN_GTT_KILL_ROWS of type ON COMMIT DELETE ROWS + Records affected: 0 + + Statement failed, SQLSTATE = HY000 + unsuccessful metadata update + -RECREATE TABLE TDETL_GTT_KEEP_ROWS failed + -global temporary table TDETL_GTT_KEEP_ROWS of type ON COMMIT PRESERVE ROWS cannot reference global temporary table TMAIN_GTT_KILL_ROWS of type ON COMMIT DELETE ROWS + Records affected: 0 + + COUNT 1 + """ + + act.expected_stdout = expected_stdout + act.isql(switches=['-q'], combine_output = True, input = test_sql) + + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/fkey/common/test_restrictions_for_not_null_fk_fields.py b/tests/functional/fkey/common/test_restrictions_for_not_null_fk_fields.py new file mode 100644 index 00000000..ba2230cc --- /dev/null +++ b/tests/functional/fkey/common/test_restrictions_for_not_null_fk_fields.py @@ -0,0 +1,197 @@ +#coding:utf-8 + +""" +ID: n/a +TITLE: FK definition: SET NULL clause should not lead FK-columns to have NULLs. +DESCRIPTION: + Parent table has UNIQUE constraint declared for NULLABLE column(s). + Child table has column(s) on which FK is declared, with options: ON UPDATE SET NULL and ON DELETE SET NULL. + Single- and multi-segmented PK/UK are checked. + Work within a single transaction. +NOTES: + [21.06.2025] pzotov + ::: NB ::: + SQL schema name (6.x+), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Discussed with Vlad, letters 16.06.2025 13:54 (subj: "#8598: ...") + Checked on 6.0.0.838; 3.0.13.33813. +""" + +import pytest +from firebird.qa import * + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +substitutions = [('[ \t]+', ' '), ('(-)?At trigger\\s+\\S+', 'At trigger')] +for p in addi_subst_tokens.split(): + substitutions.append( (p, '') ) + +db = db_factory() +act = python_act('db', substitutions = substitutions) + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + + test_sql = """ + set bail OFF; + set autoddl off; + set list on; + + create domain dm_int_not_null int NOT NULL; + + -- case-1: single segment UK + -- ------------------------- + recreate table tmain_single_uk(id int unique using index tmain_uk); + recreate table tdetl_single_uk( + id int generated by default as identity + ,fk_id dm_int_not_null + ,constraint tdetl_fk foreign key(fk_id) references tmain_single_uk(id) + ON UPDATE SET NULL + ON DELETE SET NULL + ); + commit; + insert into tmain_single_uk(id) values(1111); + insert into tdetl_single_uk(fk_id) values(1111); + commit; + + -- Fails with: + -- SQLSTATE = 23000 / violation of FOREIGN KEY ... / Foreign key references are present / Problematic key value is (ID = 1111) + -- ::: NB ::: + -- Behaviour here same as "ON UPDATE NO ACTION". Check for 'not null' (defined in domain) not occurs. + update tmain_single_uk set id = null; + rollback; + + insert into tmain_single_uk(id) values(null); + -- Must fail with: + -- Statement failed, SQLSTATE = 23000 + -- validation error for column TDETL_SINGLE_UK.FK_ID, value *** null *** + -- At trigger CHECK_* + delete from tmain_single_uk where id = 1111; + rollback; + + -- Fails with: + -- Statement failed, SQLSTATE = 23000 + -- validation error for column TDETL_SINGLE_UK.FK_ID, value *** null *** + -- -At trigger CHECK_* + delete from tmain_single_uk; + commit; + drop table tdetl_single_uk; + drop table tmain_single_uk; + + --############################## + + -- case-2: multi segment UK + -- ------------------------- + recreate table tmain_multi_uk(id1 int, id2 int, unique(id1, id2) using index tmain_uk); + recreate table tdetl_multi_uk( + id int generated by default as identity + ,fk_id1 dm_int_not_null + ,fk_id2 dm_int_not_null + ,constraint tdetl_multi_fk foreign key(fk_id1, fk_id2) references tmain_multi_uk(id1, id2) + ON UPDATE SET NULL + ON DELETE SET NULL + ); + commit; + insert into tmain_multi_uk(id1, id2) values(1111, 2222); + insert into tdetl_multi_uk(fk_id1, fk_id2) values(1111, 2222); + commit; + + --+++++++++++++++++++++++++++++++++++++++++++++++++++ + -- Following three statements fail with same messages (all with SQLSTATE = 23000): + -- violation of FOREIGN KEY constraint TDETL_MULTI_FK on table TDETL_MULTI_UK + -- -Foreign key references are present for the record + -- -Problematic key value is (ID1 = 1111, ID2 = 2222) + -- ::: NB ::: + -- Behaviour here same as "ON UPDATE NO ACTION". Check for 'not null' (defined in domain) not occurs. + update tmain_multi_uk set id2 = null; + update tmain_multi_uk set id1 = null; + update tmain_multi_uk set id1 = null, id2 = null; + --+++++++++++++++++++++++++++++++++++++++++++++++++++ + -- Following three statements fail with same messages (all with SQLSTATE = 23000): + -- Statement failed, SQLSTATE = 23000 + -- validation error for column TDETL_MULTI_UK.FK_ID1, value *** null *** + -- -At trigger CHECK_* + update tmain_multi_uk set id1 = 3333, id2 = null; + update tmain_multi_uk set id1 = null, id2 = 4444; + update tmain_multi_uk set id1 = 2222, id2 = 1111; + + rollback; + + -- Add record such that its columns not distinct from the pair of default values (defined for FK). + insert into tmain_multi_uk(id1, id2) values(null, null); + -- Now we try to remove record which currently is parent (id1 = 1111, id2 = 2222): + -- Following statement must cause violation of domain check expression and fail with SQLSTATE = 23000: + -- validation error for column TDETL_MULTI_UK.FK_ID1, value *** null *** + -- -At trigger CHECK_* + delete from tmain_multi_uk where id1 = 1111 and id2 = 2222; + rollback; + + -- Following statement must cause violation of domain check expression and fail with SQLSTATE = 23000: + -- validation error for column TDETL_MULTI_UK.FK_ID1, value *** null *** + -- -At trigger CHECK_* + delete from tmain_multi_uk; + commit; + drop table tdetl_multi_uk; + drop table tmain_multi_uk; + """ + + expected_stdout = """ + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_FK on table TDETL_SINGLE_UK + -Foreign key references are present for the record + -Problematic key value is (ID = 1111) + + Statement failed, SQLSTATE = 23000 + validation error for column TDETL_SINGLE_UK.FK_ID, value *** null *** + -At trigger CHECK_2 + + Statement failed, SQLSTATE = 23000 + validation error for column TDETL_SINGLE_UK.FK_ID, value *** null *** + -At trigger CHECK_2 + + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_MULTI_FK on table TDETL_MULTI_UK + -Foreign key references are present for the record + -Problematic key value is (ID1 = 1111, ID2 = 2222) + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_MULTI_FK on table TDETL_MULTI_UK + -Foreign key references are present for the record + -Problematic key value is (ID1 = 1111, ID2 = 2222) + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint TDETL_MULTI_FK on table TDETL_MULTI_UK + -Foreign key references are present for the record + -Problematic key value is (ID1 = 1111, ID2 = 2222) + + Statement failed, SQLSTATE = 23000 + validation error for column TDETL_MULTI_UK.FK_ID1, value *** null *** + -At trigger CHECK_3 + + Statement failed, SQLSTATE = 23000 + validation error for column TDETL_MULTI_UK.FK_ID1, value *** null *** + -At trigger CHECK_3 + + Statement failed, SQLSTATE = 23000 + validation error for column TDETL_MULTI_UK.FK_ID1, value *** null *** + -At trigger CHECK_3 + + Statement failed, SQLSTATE = 23000 + validation error for column TDETL_MULTI_UK.FK_ID1, value *** null *** + -At trigger CHECK_4 + + Statement failed, SQLSTATE = 23000 + validation error for column TDETL_MULTI_UK.FK_ID1, value *** null *** + -At trigger CHECK_4 + """ + + act.expected_stdout = expected_stdout + act.isql(switches=['-q'], combine_output = True, input = test_sql) + + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/gtcs/test_cast_datatypes.py b/tests/functional/gtcs/test_cast_datatypes.py index 8fe6ba6b..cb13dc6b 100644 --- a/tests/functional/gtcs/test_cast_datatypes.py +++ b/tests/functional/gtcs/test_cast_datatypes.py @@ -2,9 +2,30 @@ """ ID: gtcs.cast-datatypes -TITLE: GTCS/tests/PROC_CAST1_ISQL.script ... PROC_CAST10_ISQL.script +TITLE: Check result or CAST() for misc agruments // from GTCS. DESCRIPTION: -FBTEST: functional.gtcs.cast-datatypes + Original tests see in: + https://github.com/FirebirdSQL/fbtcs/blob/master/GTCS/tests/PROC_CAST1_ISQL.script + https://github.com/FirebirdSQL/fbtcs/blob/master/GTCS/tests/PROC_CAST2_ISQL.script + ... + https://github.com/FirebirdSQL/fbtcs/blob/master/GTCS/tests/PROC_CAST10_ISQL.script + + This test uses pre-created script ( /files/gtcs-cast-gen-ddl.sql ) which produces + syntactically correct SQL code for further applying against test DB. This auto-generated + SQL code contains of lot of 'CREATE PROCEDURE' statements which create stored procedures + with accepting single input parameter and making cast of this argument to other type and + returning it to caller. All possible combinations of datatypes are covered in such manner. + Cast is performed mostly IMPLICITLY, with exception for DATE/TIME literals such as 'TODAY', + 'TOMORROW' and 'YESTERDAY'. + Names of auto-generated stored procedures have form '_', e.g.: + create or alter procedure "bigint_char(10)" ( a bigint ) + returns ( b char(10) ) as + begin + b = 1608.90*a/100.00; + suspend; + end +NOTES: + Checked on 4.0.1.2692. """ import pytest @@ -272,7 +293,7 @@ varchar(30)_float 80.445 varchar(30)_int -2147483648 varchar(30)_nchar(30) 81985529216487135 - varchar(30)_numeric(5,2) 80.45 + varchar(30)_numeric(5,2) 80.45 varchar(30)_smallint 32767 varchar(30)_time 01:02:03.4560 """ @@ -280,28 +301,13 @@ @pytest.mark.version('>=3.0') def test_1(act: Action, capsys): - sql_gen_ddl = act.files_dir / 'gtcs-cast-gen-ddl.sql' - - act.expected_stderr = ' ' # Need to add a space symbol as an expected error to prevent raising of the exception "ISQL execution failed" - - act.isql(switches=['-q'], input_file=sql_gen_ddl) + act.isql(switches=['-q'], input_file = act.files_dir / 'gtcs-cast-gen-ddl.sql') init_script = act.stdout init_err = act.stderr - + assert init_err == '' act.reset() act.expected_stdout = expected_stdout - act.expected_stderr = ' ' # Need to add a space symbol as an expected error to prevent raising of the exception "ISQL execution failed" - - act.isql(switches=['-q'], input=init_script) - cast_err = act.stderr - + act.isql(switches=['-q'], input = init_script, combine_output= True) assert act.clean_stdout == act.clean_expected_stdout - - for err in ((init_err, 'init'), (cast_err, 'cast_err')): - for line in err[0].split('\n'): - if line.split(): - print('UNEXPECTED OUTPUT in ' + err[1] + ': ' + line, file=sys.stderr) - - act.stderr = capsys.readouterr().err - assert act.clean_stderr == act.clean_expected_stderr + act.reset() diff --git a/tests/functional/gtcs/test_dsql_domain_22.py b/tests/functional/gtcs/test_dsql_domain_22.py index 3cd7cd77..8496467a 100644 --- a/tests/functional/gtcs/test_dsql_domain_22.py +++ b/tests/functional/gtcs/test_dsql_domain_22.py @@ -6,53 +6,78 @@ TITLE: Verify result of ALTER DOMAIN with changing NOT NULL flag and CHECK constraints when a table exists with field based on this domain DESCRIPTION: - Original test see in: - https://github.com/FirebirdSQL/fbtcs/blob/master/GTCS/tests/DSQL_DOMAIN_22.script + Original test see in: + https://github.com/FirebirdSQL/fbtcs/blob/master/GTCS/tests/DSQL_DOMAIN_22.script - Comment in GTCS: - test for error conditions when using the alter domain statement on domains - that are already in use in table definitions, - with domain defaults and check constraints. + Comment in GTCS: + test for error conditions when using the alter domain statement on domains + that are already in use in table definitions, + with domain defaults and check constraints. - Test creates domain with DEFAULT value and CHECK constraint. - Initially domain definition: - 1) allows insertion of NULLs; - 2) have DEFAULT value which meets CHECK requirements. + Test creates domain with DEFAULT value and CHECK constraint. + Initially domain definition: + 1) allows insertion of NULLs; + 2) have DEFAULT value which meets CHECK requirements. - Then we create table and insert one record with DEFAULT value (it must pass) and second record with NULL. + Then we create table and insert one record with DEFAULT value (it must pass) and second record with NULL. - After this we try to change domain definition by adding NOT NULL clause - and it must - fail because of existing record with null. Finally, we replace CHECK constraint so that - its new expression will opposite to previous one, and try again to insert record with DEFAULT value. - It must fail because of new domain CHECK violation. + After this we try to change domain definition by adding NOT NULL clause - and it must + fail because of existing record with null. Finally, we replace CHECK constraint so that + its new expression will opposite to previous one, and try again to insert record with DEFAULT value. + It must fail because of new domain CHECK violation. - This is performed separately for each datatype (smallint, int, ...). + This is performed separately for each datatype (smallint, int, ...). - ::: NB-1 ::: - Test uses datatypes that did appear only in FB 4.0: INT128, DECFLOAT and - TIME[STAMP] WITH TIME ZONE. For this reason only FB 4.0+ can be tested. - - ::: NB-2 ::: - Domain CHECK constraint *can* be changed so that existing data will not satisfy new expression. - Only NOT NULL is verified against data that were inserted in the table. + ::: NB-1 ::: + Test uses datatypes that did appear only in FB 4.0: INT128, DECFLOAT and + TIME[STAMP] WITH TIME ZONE. For this reason only FB 4.0+ can be tested. + ::: NB-2 ::: + Domain CHECK constraint *can* be changed so that existing data will not satisfy new expression. + Only NOT NULL is verified against data that were inserted in the table. NOTES: -[19.04.2022] pzotov - Manipulations with domain 'dom22_08' were changed: removed usage of EXP() to get value that is minimal - distinguish from zero (used before: exp(-745.NNNNN)). Reason: result is hardware-dependent (Intel vs AMD). + [19.04.2022] pzotov + Manipulations with domain 'dom22_08' were changed: removed usage of EXP() to get value that is minimal + distinguish from zero (used before: exp(-745.NNNNN)). Reason: result is hardware-dependent (Intel vs AMD). + + [23.06.2025] pzotov + Fixed wrong value of charset that was used to connect: "utf-8". This caused crash of isql in recent 6.x. + https://github.com/FirebirdSQL/firebird/commit/5b41342b169e0d79d63b8d2fdbc033061323fa1b + Thanks to Vlad for solved problem. + ::: NB ::: + SQL schema name (introduced since 6.0.0.834), single and double quotes are suppressed in the output. + See $QA_HOME/README.substitutions.md or https://github.com/FirebirdSQL/firebird-qa/blob/master/README.substitutions.md + + Checked on 6.0.0.853; 6.0.3.1668; 4.0.6.3214; 3.0.13.33813. """ import pytest from firebird.qa import * from pathlib import Path -substitutions = [('After line.*', ''), ('X_BLOB_20.*', ''), ('X_BLOB_21.*', ''), - ('X_BLOB_22.*', ''), ('DM_FVALID.*', ''), ('DM_FDEFAULT.*', ''), - ('0.0000000000000000', '0.000000000000000'), - ('X_DATE 20.*', 'X_DATE 20'), - ('validation error for column "TEST"."X_DATE", value .*', - 'validation error for column "TEST"."X_DATE"')] +substitutions = [ + ('After line.*', ''), + ('X_BLOB_20.*', ''), + ('X_BLOB_21.*', ''), + ('X_BLOB_22.*', ''), + ('DM_FVALID.*', ''), + ('DM_FDEFAULT.*', ''), + ('0.0000000000000000', '0.000000000000000'), + ('DATE_FROM_DOMAIN_DEFAULT .*', 'DATE_FROM_DOMAIN_DEFAULT'), + ('"TEST"."X_DATE", value .*', 'TEST.X_DATE'), + ] + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +# +addi_subst_settings = QA_GLOBALS['schema_n_quotes_suppress'] +addi_subst_tokens = addi_subst_settings['addi_subst'] + +for p in addi_subst_tokens.split(' '): + substitutions.append( (p, '') ) + +act = python_act('db', substitutions = substitutions) db = db_factory() @@ -598,7 +623,7 @@ X_SML 0 X_INT 500 - X_DATE 2021-04-20 + DATE_FROM_DOMAIN_DEFAULT 2021-04-20 X_CHAR Wisła X_VCHR Norrström X_NUM -327.68 @@ -651,6 +676,8 @@ create domain dom22_01 as smallint default 0 check (value >= 0 and value < 100); create domain dom22_02 as integer default 500 check (value >= 500); + + -- NB: concrete value of date will be suppressed, see subst.: create domain dom22_03 as date default 'TODAY' check (value >= 'today'); -- CHECK-expression of this domain will be changed to @@ -754,7 +781,8 @@ recreate table test(x_date dom22_03); -- date default 'TODAY' check (value >= 'today'); - insert into test default values returning x_date; + -- must pass and issue current date, values will be suppressed, see 'susbtitutions': + insert into test default values returning x_date as DATE_FROM_DOMAIN_DEFAULT; insert into test values(null); commit; @@ -1079,7 +1107,7 @@ def test_1(act: Action, tmp_file: Path): act.expected_stdout = test_expected_stdout act.expected_stderr = test_expected_stderr - act.isql(switches=['-q'], input_file=tmp_file, charset='utf-8') + act.isql(switches=['-q'], input_file=tmp_file, charset='utf8') assert (act.clean_stdout == act.clean_expected_stdout and act.clean_stderr == act.clean_expected_stderr) diff --git a/tests/functional/gtcs/test_fb_sql_for_range.py b/tests/functional/gtcs/test_fb_sql_for_range.py new file mode 100644 index 00000000..26b8d6ae --- /dev/null +++ b/tests/functional/gtcs/test_fb_sql_for_range.py @@ -0,0 +1,175 @@ +#coding:utf-8 + +""" +ID: gtcs.test_fb_sql_for_range +TITLE: Range-based FOR statement. +DESCRIPTION: + Functionality descriprion: https://github.com/FirebirdSQL/firebird/issues/8498 + Original test see in: + https://github.com/FirebirdSQL/fbtcs/blob/master/GTCS/tests/FB_SQL_FOR_RANGE.script + Commit: + https://github.com/FirebirdSQL/firebird/commit/f5b6b0c0fe7595ddee5915328774f2cc10384384 +NOTES: + [06.04.2024] pzotov + Other checks/examples will be added in bugs/tests/test_8498.py + Checked on 6.0.0.717-f5b6b0c (intermediate snapshot). +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +act = python_act('db', substitutions=[ ('[ \\t]+', ' ') ]) + +expected_stdout = """ + SP1_OUT_A 3 + SP1_OUT_A 7 + SP1_OUT_A 10 + SP1_OUT_A 12 + SP1_OUT_A 14 + SP1_OUT_A 16 + SP1_OUT_A -18 + + SP1_OUT_B 11 + SP1_OUT_B 13 + SP1_OUT_B 15 + SP1_OUT_B -17 + + SP2_OUT 3 + SP2_OUT 4 + SP2_OUT 5 + SP2_OUT 5 + + EB1_OUT 10.00 + EB1_OUT 8.90 + EB1_OUT 7.80 + EB1_OUT 6.70 + EB1_OUT 5.60 + EB1_OUT 4.50 + EB1_OUT 3.40 + EB1_OUT 2.30 + EB1_OUT 1.20 + EB1_OUT -0.10 + + EB2_OUT 1 + EB2_OUT 1 + EB2_OUT 2 + EB2_OUT 2 + EB2_OUT 3 + EB2_OUT 3 + EB2_OUT 4 + EB2_OUT 4 + EB2_OUT 5 + EB2_OUT 5 + EB2_OUT 6 + EB2_OUT 6 + EB2_OUT 7 + EB2_OUT 7 + EB2_OUT 8 + EB2_OUT 8 +""" + +@pytest.mark.version('>=6.0') +def test_1(act: Action): + test_sql = """ + set list on; + set term ^; + create or alter procedure p1 (init integer) returns (sp1_out integer) + as + begin + sp1_out = init; + for sp1_out = sp1_out to 16 by 2 do + begin + if (sp1_out = 5) then + continue; + else if (sp1_out = 9) then + sp1_out = sp1_out + 1; + + suspend; + end + + sp1_out = -sp1_out; + suspend; + end^ + select p.sp1_out as sp1_out_a from p1(3) as p + ^ + select p.sp1_out as sp1_out_b from p1(11) as p + ^ + + create or alter procedure p2 (init integer) returns (sp2_out integer) + as + declare finish integer = 5; + declare last integer; + + declare procedure sub1 returns (sp2_out integer) + as + begin + for sp2_out = init to finish do + begin + last = sp2_out; + suspend; + end + end + begin + for select sp2_out from sub1 into :sp2_out do + suspend; + + sp2_out = last; + suspend; + end^ + select * from p2(3) + ^ + + execute block returns (eb1_out numeric(5,2)) + as + declare init integer = 10; + declare finish integer = 1; + declare by_val numeric(5,2) = 1.1; + begin + for eb1_out = :init downto :finish by :by_val do + begin + init = init + 1; + finish = finish + 1; + by_val = by_val + 1; + suspend; + end + + eb1_out = -eb1_out; + suspend; + end + ^ + + execute block returns (eb2_out integer) + as + begin + for eb2_out = null to 10 do + suspend; + + for eb2_out = 1 to null do + suspend; + + for eb2_out = 1 to 10 by null do + suspend; + end + ^ + execute block returns (eb2_out integer) + as + declare i integer; + begin + outer_for: for eb2_out = 1 to 8 do + begin + for i = 1 to 5 do + begin + if (i = 3) then + continue outer_for; + suspend; + end + end + end + ^ + """ + act.expected_stdout = expected_stdout + act.isql(switches=['-q'], input = test_sql, combine_output = True ) + + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/gtcs/test_gtcs_proc_cast_isql.py b/tests/functional/gtcs/test_gtcs_proc_cast_isql.py index 4333d4e6..310aa342 100644 --- a/tests/functional/gtcs/test_gtcs_proc_cast_isql.py +++ b/tests/functional/gtcs/test_gtcs_proc_cast_isql.py @@ -302,6 +302,7 @@ test_expected_stderr = "" tmp_init_run = temp_file('tmp_cast_misc_datatypes_autogen.sql') +@pytest.mark.skip("DUPLICATES test_cast_datatypes.py") @pytest.mark.version('>=3.0.6') def test_1(act: Action, tmp_init_run: Path): diff --git a/tests/functional/gtcs/test_isql_one_line_comments_01.py b/tests/functional/gtcs/test_isql_one_line_comments_01.py new file mode 100644 index 00000000..1e25bccd --- /dev/null +++ b/tests/functional/gtcs/test_isql_one_line_comments_01.py @@ -0,0 +1,131 @@ +#coding:utf-8 + +""" +ID: isql_one_line_comments_01 +TITLE: bug #781610 problems with one line comments (--) +DESCRIPTION: + Original test see in: + https://github.com/FirebirdSQL/fbtcs/blob/master/GTCS/tests/CF_ISQL_19.script +NOTES: + [12.03.2025] pzotov + Checked on 6.0.0.660; 5.0.3.1630; 4.0.6.3190; 3.0.13.33798. +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + create table test (n integer); + insert into test values (1); + + set echo; + + -- I am a comment + + /* BEGIN */ + -- I am a comment + select * from test; + /* END */ + + /* BEGIN */ + -- comment with unclosed 'quoted string + select * from test; + /* END */ + + /* BEGIN */ + -- comment with unclosed "quoted string + select * from test; + /* END */ + + /* BEGIN */ + -- I am a comment; + select * from test; + /* END */ + + /* BEGIN with unclosed "quoted */ + -- I am a comment; + select * from test; + /* END */ + + select * /* + comment + */ + from test; + + select * + /* comment */ + from test; + + select * + -- comment + from test; + + /* + Comment + */ select * from test; +""" + +act = isql_act('db', test_script, substitutions=[('=', ''), ('[ \t]+', ' ')]) + +expected_stdout = """ + -- I am a comment + /* BEGIN */ + -- I am a comment + select * from test; + N + 1 + /* END */ + /* BEGIN */ + -- comment with unclosed 'quoted string + select * from test; + N + 1 + /* END */ + /* BEGIN */ + -- comment with unclosed "quoted string + select * from test; + N + 1 + /* END */ + /* BEGIN */ + -- I am a comment; + select * from test; + N + 1 + /* END */ + /* BEGIN with unclosed "quoted */ + -- I am a comment; + select * from test; + N + 1 + /* END */ + select * /* + comment + */ + from test; + N + 1 + select * + /* comment */ + from test; + N + 1 + select * + -- comment + from test; + N + 1 + /* + Comment + */ select * from test; + N + 1 +""" + +@pytest.mark.version('>=3') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/gtcs/test_minimum_grant_test.py b/tests/functional/gtcs/test_minimum_grant_test.py index 6dd780e5..29a53c51 100644 --- a/tests/functional/gtcs/test_minimum_grant_test.py +++ b/tests/functional/gtcs/test_minimum_grant_test.py @@ -19,72 +19,51 @@ db = db_factory() -test_script = """ - set list on; +tmp_user1 = user_factory('db', name='tmp_gtcs_34_a', password='123') +tmp_user2 = user_factory('db', name='tmp_gtcs_34_b', password='456') - set term ^; - execute block as - begin - begin - execute statement 'drop user tmp$qa_user1' with autonomous transaction; - when any do begin end - end - - begin - execute statement 'drop user tmp$qa_user2' with autonomous transaction; - when any do begin end - end - - end^ - set term ;^ - commit; - - create user tmp$qa_user1 password '123'; - create user tmp$qa_user2 password '456'; - commit; - - create table test (c1 int); - commit; - - grant insert on test to tmp$qa_user1; - grant select on test to tmp$qa_user2; - commit; - - ------------------------------------------------- - connect '$(DSN)' user tmp$qa_user1 password '123'; - select current_user as whoami from rdb$database; - insert into test values(1); -- should pass - select * from test; -- should fail - commit; - - ------------------------------------------------- - connect '$(DSN)' user tmp$qa_user2 password '456'; - select current_user as whoami from rdb$database; - insert into test values(2); -- should fail - select * from test; -- should pass - commit; -""" - -act = isql_act('db', test_script, substitutions=substitutions) - -expected_stdout = """ - WHOAMI TMP$QA_USER1 - WHOAMI TMP$QA_USER2 - C1 1 -""" - -expected_stderr = """ - Statement failed, SQLSTATE = 28000 - no permission for read/select access to TABLE TEST - - Statement failed, SQLSTATE = 28000 - no permission for insert/write access to TABLE TEST -""" +act = python_act('db', substitutions = substitutions) @pytest.mark.version('>=3') -def test_1(act: Action): - act.expected_stdout = expected_stdout - act.expected_stderr = expected_stderr - act.execute() - assert (act.clean_stderr == act.clean_expected_stderr and - act.clean_stdout == act.clean_expected_stdout) +def test_1(act: Action, tmp_user1: User, tmp_user2: User): + + test_sql = f""" + set list on; + + create table test (f01 int); + commit; + + grant insert on test to {tmp_user1.name}; + grant select on test to {tmp_user2.name}; + commit; + + ------------------------------------------------- + connect '{act.db.dsn}' user {tmp_user1.name} password '{tmp_user1.password}'; + select current_user as whoami from rdb$database; + insert into test values(1); -- should pass + select * from test; -- should fail + commit; + + ------------------------------------------------- + connect '{act.db.dsn}' user {tmp_user2.name} password '{tmp_user2.password}'; + select current_user as whoami from rdb$database; + insert into test values(2); -- should fail + select * from test; -- should pass + commit; + """ + + act.expected_stdout = f""" + WHOAMI {tmp_user1.name.upper()} + + Statement failed, SQLSTATE = 28000 + no permission for read access + + WHOAMI {tmp_user2.name.upper()} + + Statement failed, SQLSTATE = 28000 + no permission for write access + + F01 1 + """ + act.isql(input = test_sql, combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/gtcs/test_ref_integ_drop_fk_index.py b/tests/functional/gtcs/test_ref_integ_drop_fk_index.py index 348247c4..a1473757 100644 --- a/tests/functional/gtcs/test_ref_integ_drop_fk_index.py +++ b/tests/functional/gtcs/test_ref_integ_drop_fk_index.py @@ -4,18 +4,24 @@ ID: gtcs.ref-integ-drop-fk-index TITLE: Index that is used for FK should not be avail for DROP DESCRIPTION: - Original test see in: - https://github.com/FirebirdSQL/fbtcs/blob/master/GTCS/tests/REF_INT.4.ISQL.script + Original test see in: + https://github.com/FirebirdSQL/fbtcs/blob/master/GTCS/tests/REF_INT.4.ISQL.script - This test uses pre-created script ( /files/gtcs-ref-integ-init.sql ) which creates two - tables with PK/FK referencing constraint(parent = department, child = employee). - FK-constraint uses index with name = 'ref_key', and here we try to: - * DROP this index; - * insert record in the child table which has no apropriate PK in the parent table. - (see 'sql_addi' variable which stores SQL statements for that). - Both actions should fail. + This test uses pre-created script ( /files/gtcs-ref-integ-init.sql ) which creates two + tables with PK/FK referencing constraint(parent = department, child = employee). + FK-constraint uses index with name = 'ref_key', and here we try to: + * DROP this index; + * insert record in the child table which has no apropriate PK in the parent table. + (see 'sql_addi' variable which stores SQL statements for that). + Both actions should fail. -FBTEST: functional.gtcs.ref_integ_drop_fk_index +FBTEST: functional.gtcs.ref_integ_drop_fk_index +NOTES: + [07.08.2024] pzotov + Splitted expected* text because system triggers now are created in C++/GDML code + See https://github.com/FirebirdSQL/firebird/pull/8202 + Commit (05-aug-2024 13"45): + https://github.com/FirebirdSQL/firebird/commit/0cc8de396a3c2bbe13b161ecbfffa8055e7b4929 """ import pytest @@ -30,7 +36,7 @@ Records affected: 0 """ -test_expected_stderr = """ +expected_stderr_5x = """ Statement failed, SQLSTATE = 27000 unsuccessful metadata update -DROP INDEX REF_KEY failed @@ -43,6 +49,18 @@ -Problematic key value is ("DEPT_NO" = -1) """ +expected_stderr_6x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -DROP INDEX REF_KEY failed + -Cannot delete index used by an Integrity Constraint + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint "REF_KEY" on table "EMPLOYEE" + -Foreign key reference target does not exist + -Problematic key value is ("DEPT_NO" = -1) +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): @@ -54,9 +72,9 @@ def test_1(act: Action): set count on; select * from employee e where e.dept_no < 0; ''' - + act.expected_stdout = test_expected_stdout - act.expected_stderr = test_expected_stderr + act.expected_stderr = expected_stderr_5x if act.is_version('<6') else expected_stderr_6x act.isql(switches=['-q'], input = os.linesep.join( (sql_init, sql_addi) ) ) diff --git a/tests/functional/gtcs/test_ref_integ_drop_pk_constraint.py b/tests/functional/gtcs/test_ref_integ_drop_pk_constraint.py index 787b89f4..08d44748 100644 --- a/tests/functional/gtcs/test_ref_integ_drop_pk_constraint.py +++ b/tests/functional/gtcs/test_ref_integ_drop_pk_constraint.py @@ -4,9 +4,15 @@ ID: gtcs.ref_integ_drop_pk_constraint TITLE: Constraint of PRIMARY KEY should not be avail for DROP if there is FK that depends on it DESCRIPTION: - Original test see in: - https://github.com/FirebirdSQL/fbtcs/blob/master/GTCS/tests/REF_INT.2.ISQL.script + Original test see in: + https://github.com/FirebirdSQL/fbtcs/blob/master/GTCS/tests/REF_INT.2.ISQL.script FBTEST: functional.gtcs.ref_integ_drop_pk_constraint +NOTES: + [07.08.2024] pzotov + Splitted expected* text because system triggers now are created in C++/GDML code + See https://github.com/FirebirdSQL/firebird/pull/8202 + Commit (05-aug-2024 13"45): + https://github.com/FirebirdSQL/firebird/commit/0cc8de396a3c2bbe13b161ecbfffa8055e7b4929 """ import os @@ -17,7 +23,11 @@ act = python_act('db') -test_expected_stderr = """ +test_expected_stdout = """ + Records affected: 0 +""" + +expected_stderr_5x = """ Statement failed, SQLSTATE = 27000 unsuccessful metadata update -DROP INDEX DEPT_KEY failed @@ -29,10 +39,16 @@ -Problematic key value is ("DEPT_NO" = 1) """ -test_expected_stdout = """ - Records affected: 0 -""" +expected_stderr_6x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -DROP INDEX DEPT_KEY failed + -Cannot delete index used by an Integrity Constraint + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "DEPT_KEY" on table "DEPARTMENT" + -Problematic key value is ("DEPT_NO" = 1) +""" @pytest.mark.version('>=3.0') def test_1(act: Action): @@ -46,7 +62,7 @@ def test_1(act: Action): ''' act.expected_stdout = test_expected_stdout - act.expected_stderr = test_expected_stderr + act.expected_stderr = expected_stderr_5x if act.is_version('<6') else expected_stderr_6x act.isql(switches=['-q'], input = os.linesep.join( (sql_init, sql_addi) ) ) diff --git a/tests/functional/gtcs/test_ref_integ_drop_pk_index.py b/tests/functional/gtcs/test_ref_integ_drop_pk_index.py index 3635af9e..6fe9662b 100644 --- a/tests/functional/gtcs/test_ref_integ_drop_pk_index.py +++ b/tests/functional/gtcs/test_ref_integ_drop_pk_index.py @@ -4,9 +4,15 @@ ID: gtcs.ref_integ_drop_pk_index TITLE: Index that is used for PRIMARY KEY should not be avail for DROP DESCRIPTION: - Original test see in: - https://github.com/FirebirdSQL/fbtcs/blob/master/GTCS/tests/REF_INT.3.ISQL.script + Original test see in: + https://github.com/FirebirdSQL/fbtcs/blob/master/GTCS/tests/REF_INT.3.ISQL.script FBTEST: functional.gtcs.ref_integ_drop_pk_index +NOTES: + [07.08.2024] pzotov + Splitted expected* text because system triggers now are created in C++/GDML code + See https://github.com/FirebirdSQL/firebird/pull/8202 + Commit (05-aug-2024 13"45): + https://github.com/FirebirdSQL/firebird/commit/0cc8de396a3c2bbe13b161ecbfffa8055e7b4929 """ import os @@ -17,7 +23,11 @@ act = python_act('db') -test_expected_stderr = """ +test_expected_stdout = """ + Records affected: 0 +""" + +expected_stderr_5x = """ Statement failed, SQLSTATE = 27000 unsuccessful metadata update -DROP INDEX DEPT_KEY failed @@ -29,8 +39,14 @@ -Problematic key value is ("DEPT_NO" = 1) """ -test_expected_stdout = """ - Records affected: 0 +expected_stderr_6x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -DROP INDEX DEPT_KEY failed + -Cannot delete index used by an Integrity Constraint + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "DEPT_KEY" on table "DEPARTMENT" + -Problematic key value is ("DEPT_NO" = 1) """ @pytest.mark.version('>=3.0') @@ -45,7 +61,7 @@ def test_1(act: Action): ''' act.expected_stdout = test_expected_stdout - act.expected_stderr = test_expected_stderr + act.expected_stderr = expected_stderr_5x if act.is_version('<6') else expected_stderr_6x act.isql(switches=['-q'], input = os.linesep.join( (sql_init, sql_addi) ) ) diff --git a/tests/functional/gtcs/test_ref_integ_inactive_fk_index.py b/tests/functional/gtcs/test_ref_integ_inactive_fk_index.py index 652e6186..d6bb0c14 100644 --- a/tests/functional/gtcs/test_ref_integ_inactive_fk_index.py +++ b/tests/functional/gtcs/test_ref_integ_inactive_fk_index.py @@ -4,9 +4,15 @@ ID: gtcs.ref_integ_inactive_fk_index TITLE: Index that is used for FK should not be avail for INACTIVE DESCRIPTION: - Original test see in: - https://github.com/FirebirdSQL/fbtcs/blob/master/GTCS/tests/REF_INT.8.ISQL.script + Original test see in: + https://github.com/FirebirdSQL/fbtcs/blob/master/GTCS/tests/REF_INT.8.ISQL.script FBTEST: functional.gtcs.ref_integ_inactive_fk_index +NOTES: + [07.08.2024] pzotov + Splitted expected* text because system triggers now are created in C++/GDML code + See https://github.com/FirebirdSQL/firebird/pull/8202 + Commit (05-aug-2024 13:45): + https://github.com/FirebirdSQL/firebird/commit/0cc8de396a3c2bbe13b161ecbfffa8055e7b4929 """ import os import pytest @@ -16,7 +22,11 @@ act = python_act('db') -test_expected_stderr = """ +test_expected_stdout = """ + Records affected: 0 +""" + +expected_stderr_5x = """ Statement failed, SQLSTATE = 27000 unsuccessful metadata update -ALTER INDEX REF_KEY failed @@ -29,8 +39,16 @@ -Problematic key value is ("DEPT_NO" = -1) """ -test_expected_stdout = """ - Records affected: 0 +expected_stderr_6x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -ALTER INDEX REF_KEY failed + -Cannot deactivate index used by an integrity constraint + + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint "REF_KEY" on table "EMPLOYEE" + -Foreign key reference target does not exist + -Problematic key value is ("DEPT_NO" = -1) """ @pytest.mark.version('>=3.0') @@ -49,7 +67,7 @@ def test_1(act: Action): ''' act.expected_stdout = test_expected_stdout - act.expected_stderr = test_expected_stderr + act.expected_stderr = expected_stderr_5x if act.is_version('<6') else expected_stderr_6x act.isql(switches=['-q'], input = os.linesep.join( (sql_init, sql_addi) ) ) diff --git a/tests/functional/gtcs/test_ref_integ_inactive_pk_index.py b/tests/functional/gtcs/test_ref_integ_inactive_pk_index.py index 5370494d..de76cb43 100644 --- a/tests/functional/gtcs/test_ref_integ_inactive_pk_index.py +++ b/tests/functional/gtcs/test_ref_integ_inactive_pk_index.py @@ -7,6 +7,12 @@ Original test see in: https://github.com/FirebirdSQL/fbtcs/blob/master/GTCS/tests/REF_INT.7.ISQL.script FBTEST: functional.gtcs.ref_integ_inactive_pk_index +NOTES: + [07.08.2024] pzotov + Splitted expected* text because system triggers now are created in C++/GDML code + See https://github.com/FirebirdSQL/firebird/pull/8202 + Commit (05-aug-2024 13:45): + https://github.com/FirebirdSQL/firebird/commit/0cc8de396a3c2bbe13b161ecbfffa8055e7b4929 """ import os @@ -17,7 +23,11 @@ act = python_act('db') -test_expected_stderr = """ +test_expected_stdout = """ + Records affected: 1 +""" + +expected_stderr_5x = """ Statement failed, SQLSTATE = 27000 unsuccessful metadata update -ALTER INDEX DEPT_KEY failed @@ -28,9 +38,15 @@ violation of PRIMARY or UNIQUE KEY constraint "DEPT_KEY" on table "DEPARTMENT" -Problematic key value is ("DEPT_NO" = 1) """ +expected_stderr_6x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -ALTER INDEX DEPT_KEY failed + -Cannot deactivate index used by an integrity constraint -test_expected_stdout = """ - Records affected: 1 + Statement failed, SQLSTATE = 23000 + violation of PRIMARY or UNIQUE KEY constraint "DEPT_KEY" on table "DEPARTMENT" + -Problematic key value is ("DEPT_NO" = 1) """ @pytest.mark.version('>=3.0') @@ -55,7 +71,7 @@ def test_1(act: Action): ''' act.expected_stdout = test_expected_stdout - act.expected_stderr = test_expected_stderr + act.expected_stderr = expected_stderr_5x if act.is_version('<6') else expected_stderr_6x act.isql(switches=['-q'], input = os.linesep.join( (sql_init, sql_addi) ) ) diff --git a/tests/functional/gtcs/test_ref_integ_inactive_pk_index_2.py b/tests/functional/gtcs/test_ref_integ_inactive_pk_index_2.py index eb4d9d36..f25e8dfc 100644 --- a/tests/functional/gtcs/test_ref_integ_inactive_pk_index_2.py +++ b/tests/functional/gtcs/test_ref_integ_inactive_pk_index_2.py @@ -4,17 +4,24 @@ ID: gtcs.ref_integ_inactive_pk_index_2 TITLE: Index that is used for PRIMARY KEY should not be avail for INACTIVE DESCRIPTION: - Original test see in: - https://github.com/FirebirdSQL/fbtcs/blob/master/GTCS/tests/REF_INT.7.ISQL.script + Original test see in: + https://github.com/FirebirdSQL/fbtcs/blob/master/GTCS/tests/REF_INT.7.ISQL.script - NOTE on difference from GTCS/tests/REF_INT.7.ISQL: - we attampt to insert into child table (employee) record which VIOLATES ref. integrity. - See quote from source test: - attempts to insert records into another table in violation of the referential - integrity constraint. The current behaviour is that even though the - unique index has been inactivated, the insertion fails because of referential - integrity violation.. (bug 7517) + NOTE on difference from GTCS/tests/REF_INT.7.ISQL: + we attampt to insert into child table (employee) record which VIOLATES ref. integrity. + See quote from source test: + attempts to insert records into another table in violation of the referential + integrity constraint. The current behaviour is that even though the + unique index has been inactivated, the insertion fails because of referential + integrity violation.. (bug 7517) FBTEST: functional.gtcs.ref_integ_inactive_pk_index_2 +NOTES: + [07.08.2024] pzotov + Splitted expected* text because system triggers now are created in C++/GDML code + See https://github.com/FirebirdSQL/firebird/pull/8202 + Commit (05-aug-2024 13:45): + https://github.com/FirebirdSQL/firebird/commit/0cc8de396a3c2bbe13b161ecbfffa8055e7b4929 + """ import os @@ -25,7 +32,11 @@ act = python_act('db') -test_expected_stderr = """ +test_expected_stdout = """ + Records affected: 0 +""" + +expected_stderr_5x = """ Statement failed, SQLSTATE = 27000 unsuccessful metadata update -ALTER INDEX DEPT_KEY failed @@ -38,10 +49,17 @@ -Problematic key value is ("DEPT_NO" = -1) """ -test_expected_stdout = """ - Records affected: 0 -""" +expected_stderr_6x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -ALTER INDEX DEPT_KEY failed + -Cannot deactivate index used by an integrity constraint + Statement failed, SQLSTATE = 23000 + violation of FOREIGN KEY constraint "REF_KEY" on table "EMPLOYEE" + -Foreign key reference target does not exist + -Problematic key value is ("DEPT_NO" = -1) +""" @pytest.mark.version('>=3.0') def test_1(act: Action): @@ -55,7 +73,7 @@ def test_1(act: Action): ''' act.expected_stdout = test_expected_stdout - act.expected_stderr = test_expected_stderr + act.expected_stderr = expected_stderr_5x if act.is_version('<6') else expected_stderr_6x act.isql(switches=['-q'], input = os.linesep.join( (sql_init, sql_addi) ) ) diff --git a/tests/functional/index/alter/test_04.py b/tests/functional/index/alter/test_04.py index d3cdae9b..6c581c53 100644 --- a/tests/functional/index/alter/test_04.py +++ b/tests/functional/index/alter/test_04.py @@ -3,33 +3,42 @@ """ ID: index.alter-04 TITLE: ALTER INDEX - INACTIVE PRIMARY KEY -DESCRIPTION: +DESCRIPTION: An index participating in PK-constraint cannot be deactivated FBTEST: functional.index.alter.04 +NOTES: + [08.08.2024] pzotov + Splitted expected* text because system triggers now are created in C++/GDML code + See https://github.com/FirebirdSQL/firebird/pull/8202 + Commit (05-aug-2024 13"45): + https://github.com/FirebirdSQL/firebird/commit/0cc8de396a3c2bbe13b161ecbfffa8055e7b4929 """ import pytest from firebird.qa import * -init_script = """CREATE TABLE t( a INTEGER NOT NULL, - CONSTRAINT pkindx PRIMARY KEY(a) - ); -commit;""" - +init_script = "recreate table test(id int primary key using index test_pk);" db = db_factory(init=init_script) -test_script = """ALTER INDEX pkindx INACTIVE;""" +test_script = "alter index test_pk inactive;" act = isql_act('db', test_script) -expected_stderr = """Statement failed, SQLSTATE = 27000 -unsuccessful metadata update --ALTER INDEX PKINDX failed --action cancelled by trigger (3) to preserve data integrity --Cannot deactivate index used by a PRIMARY/UNIQUE constraint +expected_stdout_5x = """ + Statement failed, SQLSTATE = 27000 + unsuccessful metadata update + -ALTER INDEX TEST_PK failed + -action cancelled by trigger (3) to preserve data integrity + -Cannot deactivate index used by a PRIMARY/UNIQUE constraint +""" +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -ALTER INDEX TEST_PK failed + -Cannot deactivate index used by a PRIMARY/UNIQUE constraint """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/index/alter/test_05.py b/tests/functional/index/alter/test_05.py index 69aebf92..447c9247 100644 --- a/tests/functional/index/alter/test_05.py +++ b/tests/functional/index/alter/test_05.py @@ -3,37 +3,49 @@ """ ID: index.alter-05 TITLE: ALTER INDEX - INACTIVE FOREIGN KEY -DESCRIPTION: +DESCRIPTION: An index participating in FOREIGN KEY constraint cannot be deactivated FBTEST: functional.index.alter.05 +NOTES: + [08.08.2024] pzotov + Splitted expected* text because system triggers now are created in C++/GDML code + See https://github.com/FirebirdSQL/firebird/pull/8202 + Commit (05-aug-2024 13"45): + https://github.com/FirebirdSQL/firebird/commit/0cc8de396a3c2bbe13b161ecbfffa8055e7b4929 """ import pytest from firebird.qa import * -init_script = """CREATE TABLE pk( a INTEGER NOT NULL, - CONSTRAINT pkindx PRIMARY KEY(a) - ); -commit; -CREATE TABLE fk( a INTEGER NOT NULL, - CONSTRAINT fkindx FOREIGN KEY(a) REFERENCES pk(a) - ); -commit;""" +init_script = """ + recreate table test( + id int primary key using index test_pk + ,pid int references test(id) using index test_fk + ); +""" -db = db_factory(init=init_script) +db = db_factory(init = init_script) -test_script = """ALTER INDEX fkindx INACTIVE;""" +test_script = "alter index test_fk inactive;" act = isql_act('db', test_script) -expected_stderr = """Statement failed, SQLSTATE = 27000 -unsuccessful metadata update --ALTER INDEX FKINDX failed --action cancelled by trigger (2) to preserve data integrity --Cannot deactivate index used by an integrity constraint +expected_stdout_5x = """ + Statement failed, SQLSTATE = 27000 + unsuccessful metadata update + -ALTER INDEX TEST_FK failed + -action cancelled by trigger (2) to preserve data integrity + -Cannot deactivate index used by an integrity constraint +""" + +expected_stdout_6x = """ + Statement failed, SQLSTATE = 42000 + unsuccessful metadata update + -ALTER INDEX TEST_FK failed + -Cannot deactivate index used by an integrity constraint """ @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stderr = expected_stderr - act.execute() - assert act.clean_stderr == act.clean_expected_stderr + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/intfunc/string/test_lpad_01.py b/tests/functional/intfunc/string/test_lpad_01.py index 331a8f9e..cbc632e3 100644 --- a/tests/functional/intfunc/string/test_lpad_01.py +++ b/tests/functional/intfunc/string/test_lpad_01.py @@ -1,93 +1,91 @@ -#coding:utf-8 - -""" -ID: intfunc.string.lpad -TITLE: LPAD function, including non-ascii characters -DESCRIPTION: -FBTEST: functional.intfunc.string.lpad_01 -NOTES: - [03.03.2021] pzotov - Re-implemented in order to have ability to run this test on Linux. - Added tests from some COREs which have no apropriate .fbt - Test creates table and fills it with non-ascii characters using charset = UTF8. - Then it generates .sql script for running it in separate ISQL process. - This script makes connection to test DB using charset = ISO8859_1 and perform needed DML. - Result will be redirected to .log which will be opened via codecs.open(...encoding='iso-8859-1'). - Its content will be converted to UTF8 for showing in expected_stdout. - [06.10.2022] pzotov - Could not complete adjustingfor LINUX in new-qa. - DEFERRED. -""" - -import os -import platform -import pytest -from firebird.qa import * - -db = db_factory(charset='ISO8859_1') - -act = python_act('db', substitutions=[('BLOB_ID_.*', ''), ('.*After line \\d+.*', '')]) - -test_expected_stdout = """ - ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ - ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ - TXT_LPAD_03 ¡ - ¡ - TXT_LPAD_05A ¿ - TXT_LPAD_05B À - TXT_LPAD_06A ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄ÷øùúûüýþÿ - TXT_LPAD_06B ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄ÷øùúûüýþÿ -""" - -test_expected_stderr = """ - Statement failed, SQLSTATE = 22001 - arithmetic exception, numeric overflow, or string truncation - -string right truncation - -expected length 32765, actual 32766 -""" - -@pytest.mark.skipif(platform.system() != 'Windows', reason='FIXME: see notes') -@pytest.mark.version('>=3.0') -def test_1(act: Action): - - # NB: do NOT include here character "­ ­ soft hyphen" - # It can not be properly represented on linux in utf8 codepage! - # https://jkorpela.fi/shy.html - # - data = "¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" - - null_dev = os.devnull - test_sql = \ - f''' - set list on; - recreate table test(c varchar(32765), b blob sub_type text); - insert into test(c, b) values( '{data}', '{data}' ); - select lpad(c, 2 * char_length(c), b) as blob_id_01 from test; - select lpad(b, 2 * char_length(b), c) as blob_id_02 from test; - - -- from CORE-2745 ("build in function LPAD result is wrong if argument is longer then length to padd parameter"): - select lpad(c, 1, 'ÿ') as txt_lpad_03 from test; - select lpad(b, 1, 'ÿ') as blob_id_04 from test; - - -- from CORE-2597: LPAD result must be varchar(1) instead of varchar(32765) in this example: - select rpad('¿', 1, '¡') as txt_lpad_05a, rpad('À', 1, 'ÿ') as txt_lpad_05b from rdb$database; - - select - lpad('÷øùúûüýþÿ',32765-char_length('÷øùúûüýþÿ'), '{data}') as txt_lpad_06a - ,lpad('÷øùúûüýþÿ',32765-char_length('÷øùúûüýþÿ'), '{data}') as txt_lpad_06b - from rdb$database; - - out {null_dev}; - select lpad(c, 32765, c) from test; -- this must pass - select lpad(c, 32766, c) from test; -- must fail: SQLSTATE = 22001 / ... string truncation / -expected length 32765, actual 32766 - out; - - -- select char_length(lpad('', 2147483647, b)) as r_03 from test; -- <<< 152s :-) - ''' - - act.expected_stdout = test_expected_stdout - act.expected_stderr = test_expected_stderr - - act.isql(switches=['-q'], input = test_sql ) - - assert (act.clean_stdout == act.clean_expected_stdout and act.clean_stderr == act.clean_expected_stderr) +#coding:utf-8 + +""" +ID: intfunc.string.lpad +TITLE: LPAD function, including non-ascii characters +DESCRIPTION: +FBTEST: functional.intfunc.string.lpad_01 +NOTES: + [03.03.2021] pzotov + Re-implemented in order to have ability to run this test on Linux. + Added tests from some COREs which have no apropriate .fbt + Test creates table and fills it with non-ascii characters using charset = UTF8. + Then it generates .sql script for running it in separate ISQL process. + This script makes connection to test DB using charset = ISO8859_1 and perform needed DML. + + [31.10.2024] pzotov + Finished adjusting for work both on Linux and Windows. + + Checked on 6.0.0.511 (Windows/Linux); 5.0.2.1550; 4.0.6.3165; 3.0.13.33794 +""" +import os +from pathlib import Path + +import pytest +from firebird.qa import * + +db = db_factory(charset='ISO8859_1') +act = python_act('db', substitutions=[('BLOB_ID_.*', ''), ('.*After line \\d+.*', '')]) +tmp_sql = temp_file('tmp_internal_func_lpad_01.sql') + +#@pytest.mark.skipif(platform.system() != 'Windows', reason='FIXME: see notes') +@pytest.mark.version('>=3.0.0') +def test_1(act: Action, tmp_sql: Path): + + # NB: do NOT include here character "­ ­ soft hyphen" + # It can not be properly represented on linux in utf8 codepage! + # https://jkorpela.fi/shy.html + # + data = "¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" + + null_dev = os.devnull + test_sql = \ + f''' + set list on; + recreate table test(c varchar(32765), b blob sub_type text); + insert into test(c, b) values( '{data}', '{data}' ); + select lpad(c, 2 * char_length(c), b) as blob_id_01 from test; + select lpad(b, 2 * char_length(b), c) as blob_id_02 from test; + + -- from CORE-2745 ("build in function LPAD result is wrong if argument is longer then length to padd parameter"): + select lpad(c, 1, 'ÿ') as txt_lpad_03 from test; + select lpad(b, 1, 'ÿ') as blob_id_04 from test; + + -- from CORE-2597: LPAD result must be varchar(1) instead of varchar(32765) in this example: + select rpad('¿', 1, '¡') as txt_lpad_05a, rpad('À', 1, 'ÿ') as txt_lpad_05b from rdb$database; + + select + lpad('÷øùúûüýþÿ',32765-char_length('÷øùúûüýþÿ'), '{data}') as txt_lpad_06a + ,lpad('÷øùúûüýþÿ',32765-char_length('÷øùúûüýþÿ'), '{data}') as txt_lpad_06b + from rdb$database; + + out {null_dev}; + select lpad(c, 32765, c) from test; -- this must pass + select lpad(c, 32766, c) from test; -- must fail: SQLSTATE = 22001 / ... string truncation / -expected length 32765, actual 32766 + out; + + -- select char_length(lpad('', 2147483647, b)) as r_03 from test; -- <<< 152s :-) + ''' + # ::: NB ::: + # For proper output of test, input script must be encoded in iso8859_1. + # + tmp_sql.write_text(test_sql, encoding = 'iso8859_1') + + act.expected_stdout = """ + ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ + ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ + TXT_LPAD_03 ¡ + ¡ + TXT_LPAD_05A ¿ + TXT_LPAD_05B À + TXT_LPAD_06A ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄ÷øùúûüýþÿ + TXT_LPAD_06B ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄ÷øùúûüýþÿ + + Statement failed, SQLSTATE = 22001 + arithmetic exception, numeric overflow, or string truncation + -string right truncation + -expected length 32765, actual 32766 + """ + + act.isql(switches = ['-q'], input_file = tmp_sql, charset = 'iso8859_1', combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/intfunc/string/test_rpad_01.py b/tests/functional/intfunc/string/test_rpad_01.py index eb78ea9b..de94103f 100644 --- a/tests/functional/intfunc/string/test_rpad_01.py +++ b/tests/functional/intfunc/string/test_rpad_01.py @@ -1,90 +1,87 @@ -#coding:utf-8 - -""" -ID: intfunc.string.rpad -TITLE: RPAD function, including non-ascii characters -DESCRIPTION: -NOTES: -FBTEST: functional.intfunc.string.rpad_01 -NOTES: - [03.03.2021] pzotov - Re-implemented in order to have ability to run this test on Linux. - Added tests from some COREs which have no apropriate .fbt - Test creates table and fills it with non-ascii characters using charset = UTF8. - Then it generates .sql script for running it in separate ISQL process. - This script makes connection to test DB using charset = ISO8859_1 and perform needed DML. - Result will be redirected to .log which will be opened via codecs.open(...encoding='iso-8859-1'). - Its content will be converted to UTF8 for showing in expected_stdout. - [06.10.2022] pzotov - Could not complete adjustingfor LINUX in new-qa. - DEFERRED. -""" - -import os -import platform -import pytest -from firebird.qa import * - -db = db_factory(charset='ISO8859_1') - -act = python_act('db', substitutions=[('BLOB_ID_.*', ''), ('.*After line \\d+.*', '')]) - -test_expected_stdout = """ - ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ - ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ - TXT_RPAD_03 ¡ - ¡ - TXT_RPAD_05A ¿ - TXT_RPAD_05B À - TXT_RPAD_06A ÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄ - TXT_RPAD_06B ÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄ -""" - -test_expected_stderr = """ - Statement failed, SQLSTATE = 22001 - arithmetic exception, numeric overflow, or string truncation - -string right truncation - -expected length 32765, actual 32766 -""" - -@pytest.mark.skipif(platform.system() != 'Windows', reason='FIXME: see notes') -@pytest.mark.version('>=3.0') -def test_1(act: Action): - # NB: do NOT include here character "­ ­ soft hyphen" - # It can not be properly represented on linux in utf8 codepage! - # https://jkorpela.fi/shy.html - data = "¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" - - null_dev = os.devnull - test_sql = \ - f""" - set list on; - recreate table test(c varchar(32765), b blob sub_type text); - insert into test(c, b) values( '{data}', '{data}' ); - select rpad(c, 2 * char_length(c), b) as blob_id_01 from test; - select rpad(b, 2 * char_length(b), c) as blob_id_02 from test; - - -- from CORE-2745 ("build in function RPAD result is wrong if argument is longer then length to padd parameter"): - select rpad(c, 1, 'ÿ') as txt_rpad_03 from test; - select rpad(b, 1, 'ÿ') as blob_id_04 from test; - - -- from CORE-2597: RPAD result must be varchar(1) instead of varchar(32765) in this example: - select rpad('¿', 1, '¡') as txt_rpad_05a, rpad('À', 1, 'ÿ') as txt_rpad_05b from rdb$database; - - select - rpad('÷øùúûüýþÿ',32765-char_length('÷øùúûüýþÿ'), '{data}') as txt_rpad_06a - ,rpad('÷øùúûüýþÿ',32765-char_length('÷øùúûüýþÿ'), '{data}') as txt_rpad_06b - from rdb$database; - - out {null_dev}; - select rpad(c, 32765, c) from test; -- this must pass - select rpad(c, 32766, c) from test; -- must fail: SQLSTATE = 22001 / ... string truncation / -expected length 32765, actual 32766 - out; - """ - - act.expected_stdout = test_expected_stdout - act.expected_stderr = test_expected_stderr - - act.isql(switches=['-q'], input = test_sql ) - - assert (act.clean_stdout == act.clean_expected_stdout and act.clean_stderr == act.clean_expected_stderr) +#coding:utf-8 + +""" +ID: intfunc.string.rpad +TITLE: RPAD function, including non-ascii characters +DESCRIPTION: +NOTES: +FBTEST: functional.intfunc.string.rpad_01 +NOTES: + [03.03.2021] pzotov + Re-implemented in order to have ability to run this test on Linux. + Added tests from some COREs which have no apropriate .fbt + Test creates table and fills it with non-ascii characters using charset = UTF8. + Then it generates .sql script for running it in separate ISQL process. + This script makes connection to test DB using charset = ISO8859_1 and perform needed DML. + + [31.10.2024] pzotov + Finished adjusting for work both on Linux and Windows. + + Checked on 6.0.0.511 (Windows/Linux); 5.0.2.1550; 4.0.6.3165; 3.0.13.33794 +""" +import os +from pathlib import Path + +import pytest +from firebird.qa import * + +db = db_factory(charset='ISO8859_1') +act = python_act('db', substitutions=[('BLOB_ID_.*', ''), ('.*After line \\d+.*', '')]) +tmp_sql = temp_file('tmp_internal_func_rpad_01.sql') + +@pytest.mark.version('>=3.0.0') +def test_1(act: Action, tmp_sql: Path): + # NB: do NOT include here character "­ ­ soft hyphen" + # It can not be properly represented on linux in utf8 codepage! + # https://jkorpela.fi/shy.html + data = "¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" + + null_dev = os.devnull + test_sql = \ + f""" + set list on; + recreate table test(c varchar(32765), b blob sub_type text); + insert into test(c, b) values( '{data}', '{data}' ); + select rpad(c, 2 * char_length(c), b) as blob_id_01 from test; + select rpad(b, 2 * char_length(b), c) as blob_id_02 from test; + + -- from CORE-2745 ("build in function RPAD result is wrong if argument is longer then length to padd parameter"): + select rpad(c, 1, 'ÿ') as txt_rpad_03 from test; + select rpad(b, 1, 'ÿ') as blob_id_04 from test; + + -- from CORE-2597: RPAD result must be varchar(1) instead of varchar(32765) in this example: + select rpad('¿', 1, '¡') as txt_rpad_05a, rpad('À', 1, 'ÿ') as txt_rpad_05b from rdb$database; + + select + rpad('÷øùúûüýþÿ',32765-char_length('÷øùúûüýþÿ'), '{data}') as txt_rpad_06a + ,rpad('÷øùúûüýþÿ',32765-char_length('÷øùúûüýþÿ'), '{data}') as txt_rpad_06b + from rdb$database; + + out {null_dev}; + select rpad(c, 32765, c) from test; -- this must pass + select rpad(c, 32766, c) from test; -- must fail: SQLSTATE = 22001 / ... string truncation / -expected length 32765, actual 32766 + out; + """ + # ::: NB ::: + # For proper output of test, input script must be encoded in iso8859_1. + # + tmp_sql.write_text(test_sql, encoding = 'iso8859_1') + + act.expected_stdout = """ + ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ + ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ + TXT_RPAD_03 ¡ + ¡ + TXT_RPAD_05A ¿ + TXT_RPAD_05B À + TXT_RPAD_06A ÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄ + TXT_RPAD_06B ÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄ + + Statement failed, SQLSTATE = 22001 + arithmetic exception, numeric overflow, or string truncation + -string right truncation + -expected length 32765, actual 32766 + """ + + act.isql(switches = ['-q'], input_file = tmp_sql, charset = 'iso8859_1', combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout \ No newline at end of file diff --git a/tests/functional/intfunc/unlist/unlist_ascii_char_0_test.py b/tests/functional/intfunc/unlist/unlist_ascii_char_0_test.py new file mode 100644 index 00000000..c68395f7 --- /dev/null +++ b/tests/functional/intfunc/unlist/unlist_ascii_char_0_test.py @@ -0,0 +1,38 @@ +#coding:utf-8 + +""" +ID: issue-8418 +ISSUE: https://github.com/FirebirdSQL/firebird/pull/8418 +TITLE: UNLIST function. Check ability to use ascii_char(0) as delimiter. +DESCRIPTION: + On 6.0.0.725-a2b05f4-x64 usage of ascii_char(0) as separator causes 100% CPU load and FB service could not be stopped + See: https://github.com/FirebirdSQL/firebird/pull/8418#issuecomment-2792358627 + Fixed 19.04.2025 20:36: + https://github.com/FirebirdSQL/firebird/commit/33ad7e632ae073223f808c8fdc83673d6d04e454 +NOTES: + [23.04.2025] pzotov + Checked on 6.0.0.744 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = f""" + set list on; + set count on; select * from unlist('1', ascii_char(0)) as u(x); +""" + +act = isql_act('db', test_script, substitutions=[ ('[ \\t]+', ' ') ]) + +expected_stdout = f""" + X 1 + Records affected: 1 +""" + +@pytest.mark.version('>=6.0') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/intfunc/unlist/unlist_basic_test.py b/tests/functional/intfunc/unlist/unlist_basic_test.py new file mode 100644 index 00000000..3c815a1a --- /dev/null +++ b/tests/functional/intfunc/unlist/unlist_basic_test.py @@ -0,0 +1,230 @@ +#coding:utf-8 + +""" +ID: issue-8418 +ISSUE: https://github.com/FirebirdSQL/firebird/pull/8418 +TITLE: UNLIST function. Basic examples. +DESCRIPTION: Provided by red-soft. Original file name: "unlist.test_eamples.py" +NOTES: + [09.04.2025] pzotov + Checked on 6.0.0.722 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + + -- Following examples must PASS: + + select u.* from unlist('1,2,3,4,5') as u(example_01); + select u.* from unlist('6,7,8,9,0' returning int) as u(example_02); + select u.* from unlist('11:22:33:44:55',':') as u(example_03); + select u.* from unlist('100:200:300:400:500',':' returning int) as u(example_04); + select u.* from unlist('1:2:3:4:5',':' returning float) as u(example_05); + select u.* from unlist('123:456:789',':' returning varchar(10)) as u(example_06); + select u.* from unlist('monday,tuesday,wednesday,thursday,friday,saturday,sunday') as u(example_07); + select u.* from unlist('monday_tuesday_wednesday_thursday_friday_saturday_sunday', '_' returning varchar(9)) as u(example_08); + + select u.* from unlist('12:00 gmt,11:00 gmt' returning time) as u(example_09); + select u.* from unlist('31.12.2024 23:59:59.9999,31.12.2025 23:59:59.9999' returning timestamp) as u(example_10); + select u.* from unlist('31.12.2024 23:59:59.9999,31.12.2025 23:59:59.9999') as u(example_11); + select u.* from unlist('true,false' returning boolean) as u(example_12); + select u.* from unlist('7,6,8,9,0' returning int) as u(example_13) order by u.example_13; + + select u.* from unlist('a*,as a') as u(example_14); + select u.* from unlist('b,as ab') as u(example_15); + select u.* from unlist('ab,as ab') as u(example_16); + + select u.* from unlist('order by a* as a,1,2,3') as u(example_17) order by 1; + select u.* from unlist('order by ab as ab,1,2,3') as u(example_18) order by 1; + select u.* from unlist('group by a* as a,1,2,3') as u(example_19) group by 1; + select u.* from unlist('order by ab as ab,1,2,3') as u(example_20) group by 1; + + select u.* from unlist('1,0,2,0') as u(example_21) where u.example_21 = 0; + + select w.* from(select u.* from unlist('*,*,as a') as u(example_22)) w; + + select w.* from(select u.* from unlist('*,a*,as a') as u(example_23)) w; + select w.* from(select u.* from unlist('*,ab,as ab') as u(example_24)) w; + + select c.* from(select u.* from unlist('c*,*,as a,as c') as u(example_25)) as c; + select c.* from(select u.* from unlist('c*,a*,as a,as c') as u(example_26)) as c; + select c.* from(select u.* from unlist('c*,ab,as ab,as c') as u(example_27)) as c; + + select c.* from(select u.* from unlist('cd,*,as a,as cd') as u) as c(example_28); + select c.* from(select u.* from unlist('cd,a*,as a,as cd') as u) as c(example_29); + select c.* from(select u.* from unlist('cd,ab,as ab,as cd') as u) as c(example_30); + + recreate view v1 (example_31) as select u.* from unlist('view,2,3') as u(example_31); + select u.* from v1 as u; + + ---------------------------------------------------------------------------------------- + + -- Following examples must FAIL with "SQLSTATE = 42S22 / ... / column unknown UNLIST": + + select * from unlist('1,0,2,0') as a where unlist = 0; + select unlist from unlist('unlist,a,s,a') as u(example_1 ); + select unlist from unlist('unlist,a,s,a' returning varchar(10)) as u(example_1 ); + select a.unlist from unlist('a.unlist,a,s,a') as u(example_1 ); + select a.unlist from unlist('a.unlist,a,s,a' returning varchar(10)) as u(example_1 ); +""" + +act = isql_act('db', test_script, substitutions=[ ('[ \\t]+', ' '), ('(-)?At line \\d+, column \\d+', '') ]) + +expected_stdout = """ + EXAMPLE_01 1 + EXAMPLE_01 2 + EXAMPLE_01 3 + EXAMPLE_01 4 + EXAMPLE_01 5 + EXAMPLE_02 6 + EXAMPLE_02 7 + EXAMPLE_02 8 + EXAMPLE_02 9 + EXAMPLE_02 0 + EXAMPLE_03 11 + EXAMPLE_03 22 + EXAMPLE_03 33 + EXAMPLE_03 44 + EXAMPLE_03 55 + EXAMPLE_04 100 + EXAMPLE_04 200 + EXAMPLE_04 300 + EXAMPLE_04 400 + EXAMPLE_04 500 + EXAMPLE_05 1 + EXAMPLE_05 2 + EXAMPLE_05 3 + EXAMPLE_05 4 + EXAMPLE_05 5 + EXAMPLE_06 123 + EXAMPLE_06 456 + EXAMPLE_06 789 + EXAMPLE_07 monday + EXAMPLE_07 tuesday + EXAMPLE_07 wednesday + EXAMPLE_07 thursday + EXAMPLE_07 friday + EXAMPLE_07 saturday + EXAMPLE_07 sunday + EXAMPLE_08 monday + EXAMPLE_08 tuesday + EXAMPLE_08 wednesday + EXAMPLE_08 thursday + EXAMPLE_08 friday + EXAMPLE_08 saturday + EXAMPLE_08 sunday + EXAMPLE_09 15:00:00.0000 + EXAMPLE_09 14:00:00.0000 + EXAMPLE_10 2024-12-31 23:59:59.9999 + EXAMPLE_10 2025-12-31 23:59:59.9999 + EXAMPLE_11 31.12.2024 23:59:59.9999 + EXAMPLE_11 31.12.2025 23:59:59.9999 + EXAMPLE_12 + EXAMPLE_12 + EXAMPLE_13 0 + EXAMPLE_13 6 + EXAMPLE_13 7 + EXAMPLE_13 8 + EXAMPLE_13 9 + EXAMPLE_14 a* + EXAMPLE_14 as a + EXAMPLE_15 b + EXAMPLE_15 as ab + EXAMPLE_16 ab + EXAMPLE_16 as ab + EXAMPLE_17 1 + EXAMPLE_17 2 + EXAMPLE_17 3 + EXAMPLE_17 order by a* as a + EXAMPLE_18 1 + EXAMPLE_18 2 + EXAMPLE_18 3 + EXAMPLE_18 order by ab as ab + EXAMPLE_19 1 + EXAMPLE_19 2 + EXAMPLE_19 3 + EXAMPLE_19 group by a* as a + EXAMPLE_20 1 + EXAMPLE_20 2 + EXAMPLE_20 3 + EXAMPLE_20 order by ab as ab + EXAMPLE_21 0 + EXAMPLE_21 0 + EXAMPLE_22 * + EXAMPLE_22 * + EXAMPLE_22 as a + EXAMPLE_23 * + EXAMPLE_23 a* + EXAMPLE_23 as a + EXAMPLE_24 * + EXAMPLE_24 ab + EXAMPLE_24 as ab + EXAMPLE_25 c* + EXAMPLE_25 * + EXAMPLE_25 as a + EXAMPLE_25 as c + EXAMPLE_26 c* + EXAMPLE_26 a* + EXAMPLE_26 as a + EXAMPLE_26 as c + EXAMPLE_27 c* + EXAMPLE_27 ab + EXAMPLE_27 as ab + EXAMPLE_27 as c + EXAMPLE_28 cd + EXAMPLE_28 * + EXAMPLE_28 as a + EXAMPLE_28 as cd + EXAMPLE_29 cd + EXAMPLE_29 a* + EXAMPLE_29 as a + EXAMPLE_29 as cd + EXAMPLE_30 cd + EXAMPLE_30 ab + EXAMPLE_30 as ab + EXAMPLE_30 as cd + EXAMPLE_31 view + EXAMPLE_31 2 + EXAMPLE_31 3 + Statement failed, SQLSTATE = 42S22 + Dynamic SQL Error + -SQL error code = -206 + -Column unknown + -UNLIST + -At line 5, column 48 + Statement failed, SQLSTATE = 42S22 + Dynamic SQL Error + -SQL error code = -206 + -Column unknown + -UNLIST + -At line 1, column 8 + Statement failed, SQLSTATE = 42S22 + Dynamic SQL Error + -SQL error code = -206 + -Column unknown + -UNLIST + -At line 1, column 8 + Statement failed, SQLSTATE = 42S22 + Dynamic SQL Error + -SQL error code = -206 + -Column unknown + -A.UNLIST + -At line 1, column 8 + Statement failed, SQLSTATE = 42S22 + Dynamic SQL Error + -SQL error code = -206 + -Column unknown + -A.UNLIST + -At line 1, column 8 +""" + +@pytest.mark.version('>=6.0') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/intfunc/unlist/unlist_for_db_objects_test.py b/tests/functional/intfunc/unlist/unlist_for_db_objects_test.py new file mode 100644 index 00000000..7a869024 --- /dev/null +++ b/tests/functional/intfunc/unlist/unlist_for_db_objects_test.py @@ -0,0 +1,82 @@ +#coding:utf-8 + +""" +ID: issue-8418 +ISSUE: https://github.com/FirebirdSQL/firebird/pull/8418 +TITLE: UNLIST function. Check interacting with various DB objects. +DESCRIPTION: Provided by red-soft. Original file name: "unlist.test_with_objs.py" +NOTES: + [09.04.2025] pzotov + Checked on 6.0.0.722 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + + create view test_view as + select * from unlist('1,2,3,4') as a(b) + ; + select b as test_01 from test_view; + + create view test_view_1 as select * from unlist('1,2,3,4') as a(test_02) + ; + + select v.test_02 as test_02_a from test_view_1 v; + + select test_02 as test_02_b from test_view_1; + + recreate table test_table (id_01 int); + insert into test_table (id_01) + select * from unlist('1,2,3,4' returning int) as a + ; + + select * from test_table; + + recreate table test_table_2 (id_02a int, id_02b int); + insert into test_table_2 (id_02a, id_02b) + select * + from unlist('1,2,3,4' returning int) as a_1(b_1) + join unlist('1,2,3,4' returning int) as a_2(b_2) on b_1=b_2 + ; + select * from test_table_2; +""" + +act = isql_act('db', test_script, substitutions=[ ('[ \\t]+', ' ') ]) + +expected_stdout = """ + TEST_01 1 + TEST_01 2 + TEST_01 3 + TEST_01 4 + TEST_02_A 1 + TEST_02_A 2 + TEST_02_A 3 + TEST_02_A 4 + TEST_02_B 1 + TEST_02_B 2 + TEST_02_B 3 + TEST_02_B 4 + ID_01 1 + ID_01 2 + ID_01 3 + ID_01 4 + ID_02A 1 + ID_02B 1 + ID_02A 2 + ID_02B 2 + ID_02A 3 + ID_02B 3 + ID_02A 4 + ID_02B 4 +""" + +@pytest.mark.version('>=6.0') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/intfunc/unlist/unlist_for_expressions_test.py b/tests/functional/intfunc/unlist/unlist_for_expressions_test.py new file mode 100644 index 00000000..5f9df3e6 --- /dev/null +++ b/tests/functional/intfunc/unlist/unlist_for_expressions_test.py @@ -0,0 +1,134 @@ +#coding:utf-8 + +""" +ID: issue-8418 +ISSUE: https://github.com/FirebirdSQL/firebird/pull/8418 +TITLE: UNLIST function. Check output when and/or are evaluated in SELECT expression or passed as parameter, with [optional] RETURNING clause. +DESCRIPTION: Provided by red-soft. Original file name: "unlist.test_expression.py" +NOTES: + [09.04.2025] pzotov + Checked on 6.0.0.725 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + + recreate table test_table(text varchar(15)); + insert into test_table (text) values('123,123,123'); + + select a.test_01 from unlist( (select * from test_table) ) as a(test_01); + + select a.test_02 from unlist( (select * from test_table), ',' returning varchar(15)) as a(test_02); + + set term ^ ; + ------------------------------------------------------------------------------------------------------- + recreate procedure test_proc(a_text blob sub_type text) returns ( test_03 int128) + as + begin + for + select * + -- from unlist( :a_text ) as a ==> "SQLSTATE = 22001 / -string right truncation / -expected length 32, actual 40" + from unlist(:a_text, ',' returning int128) as a + into + :test_03 + do + suspend; + end + ^ + select a.test_03 from test_proc(',-170141183460469231731687303715884105728,170141183460469231731687303715884105727') as a + ^ + ------------------------------------------------------------------------------------------------------- + recreate function test_func returns blob sub_type text as + begin + return + '-9.999999999999999E+384' + || '`-1.0E-383' + || '`1.0E-383' + || '`9.999999999999999E+384' + || '`-9.999999999999999999999999999999999E+6144' + || '`-1.0E-6143' + || '`1.0E-6143' + || '`9.999999999999999999999999999999999E+6144' + || '`0E+369' + || '`0E-384' + || '`0E-384' + || '`0E+369' + || '`0E+6111' + || '`0E-6144' + || '`0E-6144' + || '`0E+6111' + ; + -- suspend; + end + ^ + select a.test_04 from unlist( (select test_func() from rdb$database), '`' returning decfloat ) as a(test_04) + ^ + ------------------------------------------------------------------------------------------------------- + set term ;^ + + select a.* from unlist( (select '111,555,999' from rdb$database), (select ascii_char(44) from rdb$database) ) as a(test_05); + + select a.* from unlist( (select cast('###1@@@1$$$' as blob sub_type text) from rdb$database), (select row_number()over() from rdb$database) ) as a(test_06); + + select a.* from unlist( ('741' || ',' || '852' || ',' || '963') ) as a(test_07); + + select a.* from unlist( lpad('444,555,333' , 12, '') ) as a(test_08); + + select a.* from unlist( (select blob_append('987', '#', '654', '#', '321') from rdb$database), (select blob_append(null, '#') from rdb$database)) as a(test_09); + +""" + +act = isql_act('db', test_script, substitutions=[ ('[ \\t]+', ' ') ]) + +expected_stdout = """ + TEST_01 123 + TEST_01 123 + TEST_01 123 + TEST_02 123 + TEST_02 123 + TEST_02 123 + TEST_03 -170141183460469231731687303715884105728 + TEST_03 170141183460469231731687303715884105727 + TEST_04 -9.999999999999999E+384 + TEST_04 -1.0E-383 + TEST_04 1.0E-383 + TEST_04 9.999999999999999E+384 + TEST_04 -9.999999999999999999999999999999999E+6144 + TEST_04 -1.0E-6143 + TEST_04 1.0E-6143 + TEST_04 9.999999999999999999999999999999999E+6144 + TEST_04 0E+369 + TEST_04 0E-384 + TEST_04 0E-384 + TEST_04 0E+369 + TEST_04 0E+6111 + TEST_04 0E-6144 + TEST_04 0E-6144 + TEST_04 0E+6111 + TEST_05 111 + TEST_05 555 + TEST_05 999 + TEST_06 ### + TEST_06 @@@ + TEST_06 $$$ + TEST_07 741 + TEST_07 852 + TEST_07 963 + TEST_08 444 + TEST_08 555 + TEST_08 333 + TEST_09 987 + TEST_09 654 + TEST_09 321 +""" + +@pytest.mark.version('>=6.0') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/intfunc/unlist/unlist_output_value_has_separator_test.py b/tests/functional/intfunc/unlist/unlist_output_value_has_separator_test.py new file mode 100644 index 00000000..e14f7162 --- /dev/null +++ b/tests/functional/intfunc/unlist/unlist_output_value_has_separator_test.py @@ -0,0 +1,84 @@ +#coding:utf-8 + +""" +ID: issue-8418 +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8418 +TITLE: Character must NOT occur in any value generated by UNLIST which gets this as separator +DESCRIPTION: + Test restores two databases (clones of employee) that contain blob field that must be passed as argument to UNLIST(). + In both DB blobs were generated as random sequences of unicode characters from any avaliable ranges. + None of values generated by UNLIST() must contain separator. + But under some circumstatences this rule could be violated, see: + https://github.com/FirebirdSQL/firebird/pull/8418#issuecomment-2781057324 + https://github.com/FirebirdSQL/firebird/pull/8418#issuecomment-2789227832 +NOTES: + Confirmed bug on 6.0.0.742 (17-apr-2025): separator presented in some of generated values. + Checked on 6.0.0.744 (19-apr-2025) - all fine. +""" + +from pathlib import Path +import zipfile +import locale +import pytest +from firebird.qa import * +from firebird.driver import connect + +db = db_factory(charset = 'utf8') +act = python_act('db', substitutions=[('[ \t]+', ' ')]) + +tmp_fbk = temp_file('unlist_unexpected.tmp.fbk') +tmp_fdb = temp_file('unlist_unexpected.tmp.fdb') + +@pytest.mark.version('>=6.0.0') +def test_1(act: Action, tmp_fbk: Path, tmp_fdb: Path, capsys): + + test_map = {'unlist-unexpected-6_0_0_716-a5b25c1.fbk' : '0x227b', 'unlist-unexpected-6_0_0_717-fbb6b0c.fbk' : '0x2114'} + expected_out = [] + for i_fbk, i_chr in test_map.items(): + + # 716: select u.* from unlist((select blob_fld from t_longblob), unicode_char(0x227b) returning blob character set utf8) as u(x) where x containing unicode_char(0x227b) + # 717: select u.x, octet_length(u.x), position(unicode_char(0x2114) in u.x), unicode_char(0x2114) from unlist((select blob_fld from t_longblob), unicode_char(0x2114) returning blob character set utf8) as u(x) where x containing unicode_char(0x2114) + + zipped_fbk_file = zipfile.Path(act.files_dir / 'unlist-unexpected.zip', at = i_fbk) + tmp_fbk.write_bytes(zipped_fbk_file.read_bytes()) + + act.gbak(switches = ['-rep', str(tmp_fbk), str(tmp_fdb)], combine_output = True, io_enc = locale.getpreferredencoding()) + assert '' == act.stdout + act.reset() + + test_sql = f""" + with + d as ( + select + blob_fld + ,unicode_char({i_chr}) as separator + from t_longblob + ) + , e as ( + select * from d, unlist(d.blob_fld, d.separator returning blob character set utf8) as u (unlist_token) + ) + select e.unlist_token, position(e.separator in e.unlist_token) as separator_pos + from e + where e.unlist_token containing e.separator + ; + """ + + with connect(str(tmp_fdb), user = act.db.user, password = act.db.password, charset = 'utf8') as con: + print(i_fbk) + expected_out.append(i_fbk) + cur = con.cursor() + cur.execute(test_sql) + data = cur.fetchall() + if data: + print(f'### ERROR ### separator {i_chr} occurs at least in one record issued by UNLIST():') + + col = cur.description + for r in data: + for i in range(len(col)): + print(' '.join((col[i][0], ':', str(r[i])))) + + + act.expected_stdout = '\n'.join(expected_out) + + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/intfunc/unlist/unlist_returning_types_test.py b/tests/functional/intfunc/unlist/unlist_returning_types_test.py new file mode 100644 index 00000000..8deed195 --- /dev/null +++ b/tests/functional/intfunc/unlist/unlist_returning_types_test.py @@ -0,0 +1,479 @@ +#coding:utf-8 + +""" +ID: issue-8418 +ISSUE: https://github.com/FirebirdSQL/firebird/pull/8418 +TITLE: UNLIST function. Check output for different returning types +DESCRIPTION: Provided by red-soft. Original file name: "unlist.test_returning_types.py" +NOTES: + [09.04.2025] pzotov + Checked on 6.0.0.725 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +SELECTED_TIMEZONE = 'Indian/Cocos' +test_script = f""" + set list on; + set blob all; + set time zone '{SELECTED_TIMEZONE}'; + -- bigint + select * from unlist('-9223372036854775808,9223372036854775807' returning bigint) as a(unlist_bigint_01); + select * from unlist('-9223372036854775809' returning bigint) as a(unlist_bigint_02); + + -- Must raise two time: SQLSTATE = 22003 / ... / -numeric value is out of range: + select * from unlist('9223372036854775808' returning bigint) as a(unlist_bigint_03); + + -- boolean + select * from unlist('true,false' returning boolean) as a(unlist_boolean_01); + + -- SQLSTATE = 22018 / conversion error from string "right": + select * from unlist('right' returning boolean) as a(unlist_boolean_02); + + -- binary(n): following must pass: + select * from unlist('1111,12345678' returning binary(8)) as a(unlist_binary_01); + select * from unlist('text,texttext' returning binary(8)) as a(unlist_binary_02); + + -- char(n) + + -- must pass: + select * from unlist('text,texttext,1243' returning char(8)) as a(unlist_char_01); + + -- must raise SQLSTATE = 22001 / ... / -string right truncation / -expected length 1, actual 4: + select * from unlist('text' returning char) as a(unlist_char_02); + + -- must raise SQLSTATE = 42000 / ... / -Positive value expected: + select * from unlist('text' returning char(0)) as a(unlist_char_03); + + -- date + select * from unlist('1.01.0001,31.12.9999,8/06/2315,1245-12-01,5555/12/28' returning date) as a(unlist_date_01); + + -- SQLSTATE = 22008 / value exceeds the range for valid dates: + select * from unlist('1.01.0000' returning date) as a(unlist_date_02); + + -- SQLSTATE = 22018 / conversion error from string "1.01.10000": + select * from unlist('1.01.10000' returning date) as a(unlist_date_03); + + -- must PASS: + select extract( day from (select * from unlist('1.01.2201' returning date) as a(unlist_date_04) ) ) from rdb$database; + + -- decimal(n, m) + select * from unlist('4,2.00,5.83,23.01' returning dec(4,2)) as a(unlist_decimal_01); + select * from unlist('55555555555555555555555555555555555555' returning dec(38,0)) as a(unlist_decimal_02); + + -- SQLSTATE = HY104 / ... / -Precision must be from 1 to 38 + select * from unlist('555555555555555555555555555555555555555555555555555555' returning dec(54,0)) as a(unlist_decimal_03); + + -- decfloat (16|34): following must pass: + select * from unlist('4,2.00,5.83,23.00000000000001,24.000000000000011' returning decfloat(16)) as a(unlist_decfloat16_01); + select * from unlist('4,2.00,5.83,23.00000000000001000000000000000001,24.000000000000010000000000000000011' returning decfloat(34)) as a(unlist_decfloat34_01); + + -- double precision + -- TODO LATER: investigate why such strange low limit for least positive number, '9.9e-307': + select * from unlist('0e0,-0e0,9.9e-307,1.7976931348623157e308,4,2.00,5.83,23.00000000000001, 24.000000000000011' returning double precision) as a(unlist_double_01); + + -- float + select * from unlist('4,2.00,5.83,23.000001, 24.0000011' returning float) as a(unlist_float_01); + + -- int + select * from unlist('-2147483648,2147483647' returning int) as a(unlist_int_01); + + -- SQLSTATE = 22003 / ... / -numeric value is out of range: + select * from unlist('-2147483649' returning int) as a(unlist_int_02); + + -- SQLSTATE = 22003 / ... / -numeric value is out of range: + select * from unlist('2147483648' returning int) as a(unlist_int_03); + + + -- int128 + select * from unlist('-170141183460469231731687303715884105728, 170141183460469231731687303715884105727' returning int128) as a(unlist_int128_01); + + -- SQLSTATE = 22003 / ... / -numeric value is out of range: + select * from unlist('-170141183460469231731687303715884105729' returning int128) as a(unlist_int128_01); + + -- SQLSTATE = 22003 / ... / -numeric value is out of range: + select * from unlist('170141183460469231731687303715884105728' returning int128) as a(unlist_int128_01); + + -- national character (n) + select * from unlist('qwer,asdf,zxcv' returning nchar(8)) as a(unlist_nchar_01); + + -- must raise SQLSTATE = 22001 / ... / -string right truncation / -expected length 1, actual 4: + select * from unlist('tyui' returning nchar) as a(unlist_nchar_02); + + -- must raise SQLSTATE = 42000 / ... / -Positive value expected: + select * from unlist('ghjk' returning nchar(0)) as a(unlist_nchar_03); + + -- varchar + select * from unlist('text,texttext,1243' returning varchar(8)) as a(unlist_varchar_01); + + -- SQLSTATE = 22001 / -string right truncation / -expected length 8, actual 12: + select * from unlist('texttexttext' returning varchar(8)) as a(unlist_varchar_02); + + -- -Token unknown ... -) + select * from unlist('text' returning varchar) as a(unlist_varchar_03); + + -- Positive value expected + select * from unlist('text' returning varchar(0)) as a(unlist_varchar_04); + + -- national character varying (n) + select * from unlist('text,texttext,1243' returning nchar varying(8)) as a(unlist_nvarchar_01); + + -- SQLSTATE = 22001 / -string right truncation / -expected length 8, actual 12: + select * from unlist('texttexttext' returning nchar varying(8)) as a(unlist_nvarchar_02); + + -- -Token unknown ... -) + select * from unlist('text' returning nchar varying) as a(unlist_nvarchar_03); + + -- Positive value expected + select * from unlist('text' returning nchar varying(0)) as a(unlist_nvarchar_04); + + -- numeric(n, m) = decimal(n, m) + + -- smallint + select * from unlist('-32768,32767' returning smallint) as a(unlist_smallint_01); + + -- SQLSTATE = 22003 / ... / -numeric value is out of range: + select * from unlist('-32769' returning smallint) as a(unlist_smallint_02); + + -- SQLSTATE = 22003 / ... / -numeric value is out of range: + select * from unlist('32768' returning smallint) as a(unlist_smallint_03); + + -- time + select * from unlist('00:00:00.0000,23:59:59.9999' returning time) as a(unlist_time_01); + + -- SQLSTATE = 22018 / conversion error from string "00:00:00.10000" + select * from unlist('00:00:00.10000' returning time) as a(unlist_time_02); + + -- SQLSTATE = 22018 / conversion error from string "00" + select * from unlist('00:00:00.10000',':' returning time) as a(unlist_time_03); + + -- SQLSTATE = 22018 / conversion error from string "10000" + select * from unlist('00:00:00.10000','.' returning time) as a(unlist_time_04); + + -- must PASS: + select * from unlist('23:59:59' returning time) as a(unlist_time_05); + + -- time with time zone + -- ::: NB ::: here we do NOT specify name of time zone ==> value from 'set time zone' will be taken: + select * from unlist('00:00:00.0000,23:59:59.9999' returning time with time zone) as a(unlist_tmtz_01); + + -- SQLSTATE = 22018 / conversion error from string "00:00:00.10000" + select * from unlist('00:00:00.10000' returning time with time zone) as a(unlist_tmtz_02); + + -- SQLSTATE = 22018 / conversion error from string "00" + select * from unlist('00:00:00.10000',':' returning time with time zone) as a(unlist_tmtz_03); + + -- SQLSTATE = 22018 / conversion error from string "10000" + select * from unlist('00:00:00.10000','.' returning time with time zone) as a(unlist_tmtz_04); + + -- must PASS: + select * from unlist('23:59:59' returning time with time zone) as a(unlist_tmtz_05); + + -- must PASS: + select * from unlist('23:59:59.9999 europe/moscow,23:59:59.9999 -03:00,23:59:59.9999 gmt,23:59:59.9999 aet,23:59:59.9999 art,23:59:59.9999 etc/gmt+5,23:59:59.9999 america/kentucky/monticello' returning time with time zone) as a(unlist_tmtz_06); + + -- timestamp + -- must PASS: + select * from unlist('8/06/2315 00:00:00.0000,1245-12-01 23:59:59.9999' returning timestamp) as a(unlist_timestamp_01); + + -- SQLSTATE = 22018 / conversion error from string "00:00:00.0000,1245-12-01" + select * from unlist('8/06/2315 00:00:00.0000,1245-12-01 23:59:59.9999',' ' returning timestamp) as a(unlist_timestamp_02); + + -- SQLSTATE = 22018 / conversion error from string "8/06/2315" + select * from unlist('8/06/2315 00:00:00.0000,1245-12-01 23:59:59.9999',':' returning timestamp) as a(unlist_timestamp_03); + + -- SQLSTATE = 22018 / conversion error from string "00:00:00.0000" + select * from unlist('00:00:00.0000,23:59:59.9999' returning timestamp) as a(unlist_timestamp_04); + + -- timestamp with time zone + -- must PASS: + select * from unlist('8/06/2315 00:00:00.0000,1245-12-01 23:59:59.9999' returning timestamp with time zone) as a(unlist_tstz_01); + + -- conversion error from string "00:00:00.0000,1245-12-01" + select * from unlist('8/06/2315 00:00:00.0000,1245-12-01 23:59:59.9999',' ' returning timestamp with time zone) as a(unlist_tstz_02); + + -- conversion error from string "8/06/2315 00" + select * from unlist('8/06/2315 00:00:00.0000,1245-12-01 23:59:59.9999',':' returning timestamp with time zone) as a(unlist_tstz_03); + + -- conversion error from string "00:00:00.0000" + select * from unlist('00:00:00.0000,23:59:59.9999' returning timestamp with time zone) as a(unlist_tstz_04); + + -- must PASS: + select * from unlist('8/06/2315 23:59:59.9999 europe/moscow,8/06/2315 23:59:59.9999 -03:00,8/06/2315 23:59:59.9999 gmt,8/06/2315 23:59:59.9999 aet,8/06/2315 23:59:59.9999 art,8/06/2315 23:59:59.9999 etc/gmt+5,8/06/2315 23:59:59.9999 america/kentucky/monticello' returning timestamp with time zone) as a(unlist_tstz_05); + + -- varbinary(n) + -- must PASS: + select * from unlist('1111,12345678' returning varbinary(8)) as a(unlist_varbin_01); + -- must PASS: + select * from unlist('text,texttext' returning varbinary(8)) as a(unlist_varbin_02); + + -- string right truncation / expected length 8, actual 12 + select * from unlist('texttexttext' returning varbinary(8)) as a(unlist_varbin_03); + + -- token unknown / -) + select * from unlist('text' returning varbinary) as a(unlist_varbin_04); + + -- Positive value expected + select * from unlist('text' returning varbinary(0)) as a(unlist_varbin_05); + + -- blob + -- must PASS: + select * from unlist('1111,12345678,abcdefghijklmnopqrstuvwxyz' returning blob sub_type text) as a(unlist_blob_01); + -- must PASS: + select * from unlist(0x13 || ',' || 0x14 returning blob sub_type binary) as a(unlist_blob_02); + + -- token unknown / -) + select * from unlist('' returning ) as a; + + -- domain + create domain test_domain as varchar(8); + + -- must PASS: + select * from unlist('text,texttext,1243' returning test_domain) as a(unlist_domain_01); + -- string right truncation / expected length 8, actual 12 + select * from unlist('texttexttext' returning test_domain) as a(unlist_domain_02); +""" + +act = isql_act('db', test_script, substitutions=[ ('[ \\t]+', ' ') ]) + +expected_stdout = f""" + UNLIST_BIGINT_01 -9223372036854775808 + UNLIST_BIGINT_01 9223372036854775807 + Statement failed, SQLSTATE = 22003 + arithmetic exception, numeric overflow, or string truncation + -numeric value is out of range + Statement failed, SQLSTATE = 22003 + arithmetic exception, numeric overflow, or string truncation + -numeric value is out of range + UNLIST_BOOLEAN_01 + UNLIST_BOOLEAN_01 + Statement failed, SQLSTATE = 22018 + conversion error from string "right" + UNLIST_BINARY_01 3131313100000000 + UNLIST_BINARY_01 3132333435363738 + UNLIST_BINARY_02 7465787400000000 + UNLIST_BINARY_02 7465787474657874 + UNLIST_CHAR_01 text + UNLIST_CHAR_01 texttext + UNLIST_CHAR_01 1243 + Statement failed, SQLSTATE = 22001 + arithmetic exception, numeric overflow, or string truncation + -string right truncation + -expected length 1, actual 4 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -842 + -Positive value expected + -At line 2, column 48 + UNLIST_DATE_01 0001-01-01 + UNLIST_DATE_01 9999-12-31 + UNLIST_DATE_01 2315-08-06 + UNLIST_DATE_01 1245-12-01 + UNLIST_DATE_01 5555-12-28 + Statement failed, SQLSTATE = 22008 + value exceeds the range for valid dates + Statement failed, SQLSTATE = 22018 + conversion error from string "1.01.10000" + EXTRACT 1 + UNLIST_DECIMAL_01 4.00 + UNLIST_DECIMAL_01 2.00 + UNLIST_DECIMAL_01 5.83 + UNLIST_DECIMAL_01 23.01 + UNLIST_DECIMAL_02 55555555555555555555555555555555555555 + Statement failed, SQLSTATE = HY104 + Dynamic SQL Error + -SQL error code = -842 + -Precision must be from 1 to 38 + -At line 2, column 97 + UNLIST_DECFLOAT16_01 4 + UNLIST_DECFLOAT16_01 2.00 + UNLIST_DECFLOAT16_01 5.83 + UNLIST_DECFLOAT16_01 23.00000000000001 + UNLIST_DECFLOAT16_01 24.00000000000001 + UNLIST_DECFLOAT34_01 4 + UNLIST_DECFLOAT34_01 2.00 + UNLIST_DECFLOAT34_01 5.83 + UNLIST_DECFLOAT34_01 23.00000000000001000000000000000001 + UNLIST_DECFLOAT34_01 24.00000000000001000000000000000001 + UNLIST_DOUBLE_01 0.000000000000000 + UNLIST_DOUBLE_01 -0.000000000000000 + UNLIST_DOUBLE_01 9.900000000000000e-307 + UNLIST_DOUBLE_01 1.797693134862316e+308 + UNLIST_DOUBLE_01 4.000000000000000 + UNLIST_DOUBLE_01 2.000000000000000 + UNLIST_DOUBLE_01 5.830000000000000 + UNLIST_DOUBLE_01 23.00000000000001 + UNLIST_DOUBLE_01 24.00000000000001 + UNLIST_FLOAT_01 4 + UNLIST_FLOAT_01 2 + UNLIST_FLOAT_01 5.8299999 + UNLIST_FLOAT_01 23.000002 + UNLIST_FLOAT_01 24.000002 + UNLIST_INT_01 -2147483648 + UNLIST_INT_01 2147483647 + Statement failed, SQLSTATE = 22003 + arithmetic exception, numeric overflow, or string truncation + -numeric value is out of range + Statement failed, SQLSTATE = 22003 + arithmetic exception, numeric overflow, or string truncation + -numeric value is out of range + UNLIST_INT128_01 -170141183460469231731687303715884105728 + UNLIST_INT128_01 170141183460469231731687303715884105727 + Statement failed, SQLSTATE = 22003 + arithmetic exception, numeric overflow, or string truncation + -numeric value is out of range + Statement failed, SQLSTATE = 22003 + arithmetic exception, numeric overflow, or string truncation + -numeric value is out of range + UNLIST_NCHAR_01 qwer + UNLIST_NCHAR_01 asdf + UNLIST_NCHAR_01 zxcv + Statement failed, SQLSTATE = 22001 + arithmetic exception, numeric overflow, or string truncation + -string right truncation + -expected length 1, actual 4 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -842 + -Positive value expected + -At line 2, column 49 + UNLIST_VARCHAR_01 text + UNLIST_VARCHAR_01 texttext + UNLIST_VARCHAR_01 1243 + Statement failed, SQLSTATE = 22001 + arithmetic exception, numeric overflow, or string truncation + -string right truncation + -expected length 8, actual 12 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Token unknown - line 2, column 50 + -) + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -842 + -Positive value expected + -At line 2, column 51 + UNLIST_NVARCHAR_01 text + UNLIST_NVARCHAR_01 texttext + UNLIST_NVARCHAR_01 1243 + Statement failed, SQLSTATE = 22001 + arithmetic exception, numeric overflow, or string truncation + -string right truncation + -expected length 8, actual 12 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Token unknown - line 2, column 56 + -) + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -842 + -Positive value expected + -At line 2, column 57 + UNLIST_SMALLINT_01 -32768 + UNLIST_SMALLINT_01 32767 + Statement failed, SQLSTATE = 22003 + arithmetic exception, numeric overflow, or string truncation + -numeric value is out of range + Statement failed, SQLSTATE = 22003 + arithmetic exception, numeric overflow, or string truncation + -numeric value is out of range + UNLIST_TIME_01 00:00:00.0000 + UNLIST_TIME_01 23:59:59.9999 + Statement failed, SQLSTATE = 22018 + conversion error from string "00:00:00.10000" + Statement failed, SQLSTATE = 22018 + conversion error from string "00" + Statement failed, SQLSTATE = 22018 + conversion error from string "10000" + UNLIST_TIME_05 23:59:59.0000 + UNLIST_TMTZ_01 00:00:00.0000 {SELECTED_TIMEZONE} + UNLIST_TMTZ_01 23:59:59.9999 {SELECTED_TIMEZONE} + Statement failed, SQLSTATE = 22018 + conversion error from string "00:00:00.10000" + Statement failed, SQLSTATE = 22018 + conversion error from string "00" + Statement failed, SQLSTATE = 22018 + conversion error from string "10000" + UNLIST_TMTZ_05 23:59:59.0000 {SELECTED_TIMEZONE} + UNLIST_TMTZ_06 23:59:59.9999 Europe/Moscow + UNLIST_TMTZ_06 23:59:59.9999 -03:00 + UNLIST_TMTZ_06 23:59:59.9999 GMT + UNLIST_TMTZ_06 23:59:59.9999 AET + UNLIST_TMTZ_06 23:59:59.9999 ART + UNLIST_TMTZ_06 23:59:59.9999 Etc/GMT+5 + UNLIST_TMTZ_06 23:59:59.9999 America/Kentucky/Monticello + UNLIST_TIMESTAMP_01 2315-08-06 00:00:00.0000 + UNLIST_TIMESTAMP_01 1245-12-01 23:59:59.9999 + Statement failed, SQLSTATE = 22018 + conversion error from string "00:00:00.0000,1245-12-01" + Statement failed, SQLSTATE = 22018 + conversion error from string "8/06/2315 00" + Statement failed, SQLSTATE = 22018 + conversion error from string "00:00:00.0000" + UNLIST_TSTZ_01 2315-08-06 00:00:00.0000 {SELECTED_TIMEZONE} + UNLIST_TSTZ_01 1245-12-01 23:59:59.9999 {SELECTED_TIMEZONE} + Statement failed, SQLSTATE = 22018 + conversion error from string "00:00:00.0000,1245-12-01" + Statement failed, SQLSTATE = 22018 + conversion error from string "8/06/2315 00" + Statement failed, SQLSTATE = 22018 + conversion error from string "00:00:00.0000" + UNLIST_TSTZ_05 2315-08-06 23:59:59.9999 Europe/Moscow + UNLIST_TSTZ_05 2315-08-06 23:59:59.9999 -03:00 + UNLIST_TSTZ_05 2315-08-06 23:59:59.9999 GMT + UNLIST_TSTZ_05 2315-08-06 23:59:59.9999 AET + UNLIST_TSTZ_05 2315-08-06 23:59:59.9999 ART + UNLIST_TSTZ_05 2315-08-06 23:59:59.9999 Etc/GMT+5 + UNLIST_TSTZ_05 2315-08-06 23:59:59.9999 America/Kentucky/Monticello + UNLIST_VARBIN_01 31313131 + UNLIST_VARBIN_01 3132333435363738 + UNLIST_VARBIN_02 74657874 + UNLIST_VARBIN_02 7465787474657874 + Statement failed, SQLSTATE = 22001 + arithmetic exception, numeric overflow, or string truncation + -string right truncation + -expected length 8, actual 12 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Token unknown - line 2, column 52 + -) + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -842 + -Positive value expected + -At line 2, column 53 + UNLIST_BLOB_01 0:1 + 1111 + UNLIST_BLOB_01 0:2 + 12345678 + UNLIST_BLOB_01 0:3 + abcdefghijklmnopqrstuvwxyz + UNLIST_BLOB_02 0:7 + 19 + UNLIST_BLOB_02 0:8 + 20 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Token unknown - line 2, column 39 + -) + UNLIST_DOMAIN_01 text + UNLIST_DOMAIN_01 texttext + UNLIST_DOMAIN_01 1243 + Statement failed, SQLSTATE = 22001 + arithmetic exception, numeric overflow, or string truncation + -string right truncation + -expected length 8, actual 12 +""" + +@pytest.mark.version('>=6.0') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/intfunc/unlist/unlist_separators_ascii_literals_test.py b/tests/functional/intfunc/unlist/unlist_separators_ascii_literals_test.py new file mode 100644 index 00000000..1141d83e --- /dev/null +++ b/tests/functional/intfunc/unlist/unlist_separators_ascii_literals_test.py @@ -0,0 +1,48 @@ +#coding:utf-8 + +""" +ID: issue-8418 +ISSUE: https://github.com/FirebirdSQL/firebird/pull/8418 +TITLE: UNLIST function. Check work with separator that is specified as one ASCII literal. +DESCRIPTION: + Provided by red-soft. Original file name: "unlist.test_separators.py" + Code from original test was modified : we check here UNLIST output when separator is every + ASCII character except letters (A...Z, a..z), chr(13) and chr(26). +NOTES: + [10.04.2025] pzotov + 1. ascii_char(0) can not be used as separator because FB hangs, see + https://github.com/FirebirdSQL/firebird/pull/8418#issuecomment-2792358627 + 2. ascii_char(13) is not included in the list of checked separators in order avoid excessive + complexity of expression that is used to construct expected_out. + Checked on 6.0.0.725 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +FIELD_NAME_PREFIX = 'unlist using separator' +SOURCE_LIST = '1.2.3.4.5' +base_sql = f"""select /* check ascii_char(%d) */ * from unlist(q'#{SOURCE_LIST}#', '%s') as a("{FIELD_NAME_PREFIX} ascii_char(%d) = %s");""" + +separator_list = [chr(x) for x in range(1, 48)] # control characters (1...31); space; !"#$%&\'()*+,-./ +separator_list.extend( [chr(x) for x in range(58, 65)] ) # :;<=>?@ +separator_list.extend( [chr(x) for x in range(91, 97)] ) # [\\]^_` +separator_list.extend( [chr(x) for x in range(124, 128)] ) # |}~ and \x7f [`DEL`] + +separator_list.remove( chr(13) ) # remove it in order to simplify expression to construct expected_out +separator_list.remove( chr(26) ) # remove it because ISQL can not rpepare such expr., see #8512 + +queries_lst = ['set list on;',] +queries_lst.extend( [ (base_sql % (ord(x), x if x !="'"else x+x, ord(x), x if x != '"' else x+x)).replace('.', x) for x in separator_list ] ) + +act = isql_act('db', substitutions=[ ('[ \\t]+', ' ') ]) + +expected_out_lst = [ f"{FIELD_NAME_PREFIX} ascii_char({ord(x)}) = {x} {y}" for x in separator_list for y in SOURCE_LIST.split('.') ] + +@pytest.mark.version('>=6.0') +def test_1(act: Action): + act.expected_stdout = '\n'.join(expected_out_lst) + act.isql(switches=['-q'], input = '\n'.join(queries_lst), combine_output=True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/intfunc/unlist/unlist_separators_at_eol_test.py b/tests/functional/intfunc/unlist/unlist_separators_at_eol_test.py new file mode 100644 index 00000000..9b481d6e --- /dev/null +++ b/tests/functional/intfunc/unlist/unlist_separators_at_eol_test.py @@ -0,0 +1,75 @@ +#coding:utf-8 + +""" +ID: issue-8418 +ISSUE: https://github.com/FirebirdSQL/firebird/pull/8418 +TITLE: UNLIST function. Check work with separator that is specified as one ASCII literal. +DESCRIPTION: + Provided by red-soft. Original file name: "unlist.test_end_separators.py" + Code from original test was modified: we check here UNLIST output when literal separator belongs + to several ASCII ranges, namely: + control characters (1...31); space; !"#$%&\'()*+,-./ + :;<=>?@ + [\\]^_` + |}~ and \x7f [`DEL`] + + We check several strings that end with one or more such separators (see 'CHECKED_LISTS'). + Particularly, we also check result when string ends with extremely long sequence containing ~32K + separators. + +NOTES: + [10.04.2025] pzotov + 1. ascii_char(0) can not be used as separator because FB hangs, see + https://github.com/FirebirdSQL/firebird/pull/8418#issuecomment-2792358627 + 2. ascii_char(13) is not included in the list of checked separators in order avoid excessive + complexity of expression that is used to construct expected_out. + 3. semicolon (ascii_char(59), ';') has odd behaviour: delay for ~8s can be seen before PREPARE_STATEMENT + in the trace when string containing ~32K trailing separators is parsed, see: + https://github.com/FirebirdSQL/firebird/pull/8418#issuecomment-2792461612 + + Test execution time: ~30s. + Checked on 6.0.0.725 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +#separator_list = [] +#separator_list.extend( [chr(x) for x in range(58, 65)] ) +#CHECKED_LISTS = [ 't.y' + '.' * 65532 ] + +separator_list = [chr(x) for x in range(1, 48)] # control characters (1...31); space; !"#$%&\'()*+,-./ +separator_list.extend( [chr(x) for x in range(58, 65)] ) # :;<=>?@ +separator_list.extend( [chr(x) for x in range(91, 97)] ) # [\\]^_` +separator_list.extend( [chr(x) for x in range(124, 128)] ) # |}~ and \x7f [`DEL`] + + +# separator_list.remove( chr(59) ) # perormance impact! See: https://github.com/FirebirdSQL/firebird/pull/8418#issuecomment-2794067192 +separator_list.remove( chr(13) ) # remove it in order to simplify expression to construct expected_out +separator_list.remove( chr(26) ) # remove it because ISQL can not rpepare such expr., see #8512 + +# 65533 --> Statement failed, SQLSTATE = 42000 / ... / -String literal with 65536 bytes exceeds the maximum length of 65535 bytes +CHECKED_LISTS = [ 'q.w.', 'e.r..', 't.y...', 't.y' + '.' * 65532 ] + +FIELD_NAME_PREFIX = 'unlist using separator' + +queries_lst = ['set list on;',] + +expected_out_lst = [] +for i_dup, checked_item in enumerate(CHECKED_LISTS): + base_sql = f"""select /* check ascii_char(%d) */ * from unlist(q'#{checked_item}#', '%s') as a("{FIELD_NAME_PREFIX} {i_dup+1}*ascii_char(%d) = %s");""" + queries_lst.extend( [ (base_sql % (ord(x), x if x !="'"else x+x, ord(x), x if x != '"' else x+x)).replace('.', x) for x in separator_list ] ) + expected_out_lst.extend( [ f"{FIELD_NAME_PREFIX} {i_dup+1}*ascii_char({ord(x)}) = {x} {y}" for x in separator_list for y in checked_item.rstrip('.').split('.') ] ) + +#with open('tmp-long-duplicated-separators-at-end.sql', 'w') as f: +# f.write('\n'.join(queries_lst)) + +act = isql_act('db', substitutions=[ ('[ \\t]+', ' ') ]) + +@pytest.mark.version('>=6.0') +def test_1(act: Action): + act.expected_stdout = '\n'.join(expected_out_lst) + act.isql(switches=['-q'], input = '\n'.join(queries_lst), combine_output=True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/intl/test_non_ascii_firebird_and_trace_utf8.py b/tests/functional/intl/test_non_ascii_firebird_and_trace_utf8.py new file mode 100644 index 00000000..0e0b173b --- /dev/null +++ b/tests/functional/intl/test_non_ascii_firebird_and_trace_utf8.py @@ -0,0 +1,197 @@ +#coding:utf-8 + +""" +ID: functional.intl.test_non_ascii_firebird_and_trace_utf8.py +TITLE: Check ability to obtain non-ascii content from firebird.log and trace when use ON DISCONNECT trigger and exception with non-ascii names, charset = UTF8. +DESCRIPTION: + We can make engine to put something in firebird.log by creating ON DISCONNECT trigger which raises exception. + (see https://github.com/FirebirdSQL/firebird/issues/4282). + In order this trigger not fired when we just create DB, aux user ('TMP_WORKER') is created and trigger will fire only for him. + Before making connection as user 'TMP_WORKER', we: + * get content of firebird.log + * launch trace + After tmp_worker completes its work (running ISQL with single command: 'quit'), we get again content of + firebird.log and trace for further parsing. + + Finally, we check that: + * trace contains messages about raised exception (with message) and failed trigger (with call stack); + * difference in firebird.log contains messages similar to trace +NOTES: + [04.09.2024] pzotov + Test makes connections to DB using charset = 'utf8' and uses io_enc = 'utf-8' when obtaining content of firebird.log and trace. + Checked 6.0.0.450, 5.0.2.1493, 4.0.6.3142 (on Windows). + + [13.03.2025] pzotov + LINUX, FB 4.x: error message do not contain 'ERROR AT purge_attachment'. + It must be considered as 'Known Bug' with minor priority. + Decided to separate expected out after discuss with Alex, 13.03.2025. +""" + +import os +import pytest +from firebird.qa import * +import re +import locale +from pathlib import Path +from difflib import unified_diff +import time + +tmp_worker = user_factory('db', name='tmp_worker', password='123') +#tmp_sql = temp_file('tmp_non_ascii.sql') + +db = db_factory(charset = 'utf8') +substitutions = [ ('.* FAILED EXECUTE_TRIGGER_FINISH', 'FAILED EXECUTE_TRIGGER_FINISH'), + ('.* ERROR AT purge_attachment', 'ERROR AT purge_attachment'), + ('(,)?\\s+line(:)?\\s+\\d+(,)?\\s+col(:)?\\s+\\d+', '') + ] + +act = python_act('db', substitutions = substitutions) + +@pytest.mark.intl +@pytest.mark.trace +@pytest.mark.version('>=4.0') +def test_1(act: Action, tmp_worker: User, capsys): + + init_sql = f""" + set list on; + set bail on; + create exception "paramètre non trouvé" q'#Paramètre "@1" a une valeur incorrecte ou n'a pas été trouvé dans#'; + set term ^; + ^ + create trigger "gâchette de déconnexion" on disconnect as + begin + if ( current_user != '{act.db.user}' ) then + begin + exception "paramètre non trouvé" using ('fréquence fermée'); + end + end + ^ + set term ;^ + commit; + """ + + #tmp_sql.write_bytes( bytes(init_sql.encode('utf-8')) ) + #act.isql(switches=['-q'], input_file = tmp_sql, combine_output = True, charset = 'utf8', io_enc = 'utf-8') + + act.expected_stdout = '' + act.isql(switches=['-q'], input = init_sql, combine_output = True, charset = 'utf8', io_enc = 'utf-8') + assert act.clean_stdout == act.clean_expected_stdout + act.reset() + + # ---------------------------------------------------------------------------------------------- + with act.connect_server(encoding = 'utf-8') as srv: + srv.info.get_log() + fb_log_init = srv.readlines() + # ---------------------------------------------------------------------------------------------- + + trace_cfg_items = [ + 'log_connections = true', + 'log_transactions = true', + 'time_threshold = 0', + 'log_errors = true', + 'log_statement_finish = true', + 'log_trigger_finish = true', + 'max_sql_length = 32768', + ] + + with act.trace(db_events = trace_cfg_items, encoding='utf-8'): + test_sql = f""" + set names utf8; + connect '{act.db.dsn}' user {tmp_worker.name} password '{tmp_worker.password}'; + quit; + """ + act.isql(switches = ['-q'], input = test_sql, connect_db=False, credentials = False, combine_output = True, io_enc = 'utf-8') + assert act.clean_stdout == act.clean_expected_stdout + act.reset() + + # ---------------------------------------------------------------------------------------------- + + allowed_patterns = \ + ( + re.escape(') FAILED EXECUTE_TRIGGER_FINISH') + ,re.escape(') ERROR AT purge_attachment') + ,re.escape('335544382 :') # name of exception: paramètre non trouvé (without quotes) + ,re.escape('335545016 :') # message of exception: Paramètre "fréquence fermée" a une valeur incorrecte ou n'a pas été trouvé dans + ,re.escape('335544842 : At trigger') + ) + allowed_patterns = [ re.compile(p, re.IGNORECASE) for p in allowed_patterns ] + + # Example of trace: + # 2024-09-04T18:57:20.7950 (2184:00000000016B23C0) ERROR AT purge_attachment + # ... + # 335544517 : exception 1 + # 335544382 : paramètre non trouvé + # 335545016 : Paramètre "fréquence fermée" a une valeur incorrecte ou n'a pas été trouvé dans + # 335544842 : At trigger 'gâchette de déconnexion' line: 5, col: 17 + + for line in act.trace_log: + #print(line) + if line.strip(): + if act.match_any(line.strip(), allowed_patterns): + print(line.strip()) + + if os.name != 'nt' and act.is_version('<5'): + # LINUX, FB 4.x: error message do not contain 'ERROR AT purge_attachment'. + # It must be considered as 'Known Bug' with minor priority. + # Decided to separate expected out after discuss with Alex, 13.03.2025. + expected_trace_log = """ + FAILED EXECUTE_TRIGGER_FINISH + 335544382 : paramètre non trouvé + 335545016 : Paramètre "fréquence fermée" a une valeur incorrecte ou n'a pas été trouvé dans + 335544842 : At trigger 'gâchette de déconnexion' + """ + else: + expected_trace_log = """ + FAILED EXECUTE_TRIGGER_FINISH + ERROR AT purge_attachment + 335544382 : paramètre non trouvé + 335545016 : Paramètre "fréquence fermée" a une valeur incorrecte ou n'a pas été trouvé dans + 335544842 : At trigger 'gâchette de déconnexion' + """ + + act.expected_stdout = expected_trace_log + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() + + # ---------------------------------------------------------------------------------------------- + + with act.connect_server(encoding = 'utf-8') as srv: + srv.info.get_log() + fb_log_curr = srv.readlines() + srv.wait() + + # Example of firebird.log: + # + # Error at disconnect: + # exception 1 + # paramètre non trouvé + # Paramètre "fréquence fermée" a une valeur incorrecte ou n'a pas été trouvé dans + # At trigger 'gâchette de déconnexion' + + fb_log_diff_patterns = \ + ( + 'Error at disconnect' + ,'exception' + ,'paramètre non trouvé' + ,'Paramètre "fréquence fermée"' + ,'At trigger' + ) + fb_log_diff_patterns = [ re.compile(p, re.IGNORECASE) for p in fb_log_diff_patterns ] + + for line in unified_diff(fb_log_init, fb_log_curr): + if line.startswith('+'): + if act.match_any(line[1:].strip(), fb_log_diff_patterns): + print(line[1:].strip()) + + expected_log_diff = """ + Error at disconnect: + exception 1 + paramètre non trouvé + Paramètre "fréquence fermée" a une valeur incorrecte ou n'a pas été trouvé dans + At trigger 'gâchette de déconnexion' + """ + + act.expected_stdout = expected_log_diff + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/replication/test_blob_access_when_no_grant_for_select.py b/tests/functional/replication/test_blob_access_when_no_grant_for_select.py new file mode 100644 index 00000000..80422e6e --- /dev/null +++ b/tests/functional/replication/test_blob_access_when_no_grant_for_select.py @@ -0,0 +1,479 @@ +#coding:utf-8 + +""" +ID: replication.test_blob_access_when_no_grant_for_select +ISSUE: None. +TITLE: Replicator must have access to the table with blob regardless SELECT grant on this table to the user who created blob. +DESCRIPTION: + We create table and user ('tmp_nonpriv_user'), with revoking all grants from him except only one: allow INSERT to that table (w/o select!). + The last DDL that we do is creating table with name 't_completed'. It serves as 'flag' to be checked that all DDL actions + on master finished. + + After this we wait until replica becomes actual to master, and this delay will last no more then threshold that + is defined by MAX_TIME_FOR_WAIT_DATA_IN_REPLICA variable (measured in seconds), see QA_ROOT/files/test_config.ini + + Then we connect to master as and run DML against table 'test': insert one record. + Normally it must NOT raise any error (see 'dml_err') on master, but before fix replicator thread failed at this point with access problems + to blob data and replication.log was filled with messages: + ERROR: no permission for SELECT access to ... + Effective user is + ERROR: Replication is stopped due to critical error(s) + + We must check here that replica DB eventually will have appropriate blob in the test table (see 'isql_expected_out'). + NOTE. We have to avoid query of replication log - not only verbose can be disabled, but also because code is too complex. + + Further, we invoke ISQL with executing auxiliary script for drop all DB objects on master (with '-nod' command switch). + After all objects will be dropped, we have to wait again until replica becomes actual with master. + Check that both DB have no custom objects is performed (see UNION-ed query to rdb$ tables + filtering on rdb$system_flag). + + Finally, we extract metadata for master and replica and make comparison. + The only difference in metadata must be 'CREATE DATABASE' statement with different DB names - we suppress it, + thus metadata difference must not be issued. + +NOTES: + There is no ticket for this test. + Frontported fix was 30.06.2024: + * for 5.x: https://github.com/FirebirdSQL/firebird/commit/97358d012c798aa4382c863404f3dd22befe1af6 + * for 6.x: https://github.com/FirebirdSQL/firebird/commit/caf7a6bcf8dd13790c4c0ffa52ff1bec9e46f07f + Log message: Frontported bugfix for blob access vs replicator + + We use 'assert' only at the final point of test, with printing detalization about encountered problem(s). + During all previous steps, we only store unexpected output to variables, e.g.: out_main = capsys.readouterr().out etc. + + Confirmed bug on 6.0.0.387, 5.0.1.1428. + Checked on 6.0.0.395 (SS/CS), 5.0.1.1439, 4.0.5.3127 + + Thanks to dimitr for suggestions. +""" +import os +import shutil +from difflib import unified_diff +from pathlib import Path +import time + +import pytest +from firebird.qa import * +from firebird.driver import connect, create_database, DbWriteMode, ReplicaMode, ShutdownMode, ShutdownMethod, DatabaseError + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +repl_settings = QA_GLOBALS['replication'] + +MAX_TIME_FOR_WAIT_DATA_IN_REPLICA = int(repl_settings['max_time_for_wait_data_in_replica']) +MAIN_DB_ALIAS = repl_settings['main_db_alias'] +REPL_DB_ALIAS = repl_settings['repl_db_alias'] +RUN_SWEEP_AT_END = int(repl_settings['run_sweep_at_end']) + +db_main = db_factory( filename = '#' + MAIN_DB_ALIAS, do_not_create = True, do_not_drop = True) +db_repl = db_factory( filename = '#' + REPL_DB_ALIAS, do_not_create = True, do_not_drop = True) + +tmp_nonpriv_user = user_factory('db_main', name='tmp_replicator_blob_access', password='123') + +substitutions = [('Start removing objects in:.*', 'Start removing objects'), + ('Finish. Total objects removed: [1-9]\\d*', 'Finish. Total objects removed'), + ('.* CREATE DATABASE .*', ''), + ('[\t ]+', ' '), + ('FOUND message about replicated segment N .*', 'FOUND message about replicated segment')] + +act_db_main = python_act('db_main', substitutions=substitutions) +act_db_repl = python_act('db_repl', substitutions=substitutions) + +#-------------------------------------------- + +def cleanup_folder(p): + # Removed all files and subdirs in the folder

+ # Used for cleanup and when replication must be reset + # in case when any error occurred during test execution. + assert os.path.dirname(p) != p, f"@@@ ABEND @@@ CAN NOT operate in the file system root directory. Check your code!" + + for root, dirs, files in os.walk(p): + for f in files: + # ::: NB ::: 22.12.2023. + # We have to expect that attempt to delete of GUID and (maybe) archived segments can FAIL with + # PermissionError: [WinError 32] The process cannot ... used by another process: /path/to/{GUID} + # Also, we have to skip exception if file (segment) was just deleted by engine + try: + Path(root +'/' + f).unlink(missing_ok = True) + except PermissionError as x: + pass + + for d in dirs: + shutil.rmtree(os.path.join(root, d), ignore_errors = True) + + return os.listdir(p) + +#-------------------------------------------- + +def reset_replication(act_db_main, act_db_repl, db_main_file, db_repl_file): + out_reset = '' + failed_shutdown_db_map = {} # K = 'db_main', 'db_repl'; V = error that occurred when we attempted to change DB state to full shutdown (if it occurred) + + with act_db_main.connect_server() as srv: + + # !! IT IS ASSUMED THAT REPLICATION FOLDERS ARE IN THE SAME DIR AS !! + # DO NOT use 'a.db.db_path' for ALIASED database! + # It will return '.' rather than full path+filename. + + repl_root_path = Path(db_main_file).parent + repl_jrn_sub_dir = repl_settings['journal_sub_dir'] + repl_arc_sub_dir = repl_settings['archive_sub_dir'] + + for f in (db_main_file, db_repl_file): + # Method db.drop() changes LINGER to 0, issues 'delete from mon$att' with suppressing exceptions + # and calls 'db.drop_database()' (also with suppressing exceptions). + # We change DB state to FULL SHUTDOWN instead of call action.db.drop() because + # this is more reliable (it kills all attachments in all known cases and does not use mon$ table) + # + try: + srv.database.shutdown(database = f, mode = ShutdownMode.FULL, method = ShutdownMethod.FORCED, timeout = 0) + + # REMOVE db file from disk: we can safely assume that this can be done because DB in full shutdown state. + ########################### + os.unlink(f) + except DatabaseError as e: + failed_shutdown_db_map[ f ] = e.__str__() + + + # Clean folders repl_journal and repl_archive: remove all files from there. + # NOTE: test must NOT raise unrecoverable error if some of files in these folders can not be deleted. + # Rather, this must be displayed as diff and test must be considered as just failed. + for p in (repl_jrn_sub_dir,repl_arc_sub_dir): + + remained_files = cleanup_folder(repl_root_path/p) + + if remained_files: + out_reset += '\n'.join( (f"Directory '{str(repl_root_path/p)}' remains non-empty. Could not delete file(s):", '\n'.join(remained_files)) ) + + # xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + # xxx r e c r e a t e d b _ m a i n a n d d b _ r e p l xxx + # xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + for a in (act_db_main,act_db_repl): + d = a.db.db_path + failed_shutdown_msg = failed_shutdown_db_map.get( str(d), '' ) + if failed_shutdown_msg: + # we could NOT change state of this database to full shutdown --> we must NOT recreate it. + # Accumulate error messages in OUT arg (for displaying as diff): + # + out_reset += '\n'.join( failed_shutdown_msg ) + else: + try: + dbx = create_database(str(d), user = a.db.user) + dbx.close() + with a.connect_server() as srv: + srv.database.set_write_mode(database = d, mode = DbWriteMode.ASYNC) + srv.database.set_sweep_interval(database = d, interval = 0) + if a == act_db_repl: + srv.database.set_replica_mode(database = d, mode = ReplicaMode.READ_ONLY) + else: + with a.db.connect() as con: + con.execute_immediate('alter database enable publication') + con.execute_immediate('alter database include all to publication') + con.commit() + except DatabaseError as e: + out_reset += e.__str__() + + # Must remain EMPTY: + #################### + return out_reset + +#-------------------------------------------- + +def watch_replica( a: Action, max_allowed_time_for_wait, ddl_ready_query = '', isql_check_script = '', replica_expected_out = ''): + + retcode = 1; + ready_to_check = False + if ddl_ready_query: + with a.db.connect(no_db_triggers = True) as con: + with con.cursor() as cur: + for i in range(0,max_allowed_time_for_wait): + cur.execute(ddl_ready_query) + count_actual = cur.fetchone() + if count_actual: + ready_to_check = True + break + else: + con.rollback() + time.sleep(1) + else: + ready_to_check = True + + if not ready_to_check: + print( f'UNEXPECTED. Initial check query did not return any rows for {max_allowed_time_for_wait} seconds.' ) + print('Initial check query:') + print(ddl_ready_query) + return + + final_check_pass = False + if isql_check_script: + retcode = 0 + for i in range(max_allowed_time_for_wait): + a.reset() + a.expected_stdout = replica_expected_out + a.isql(switches=['-q', '-nod'], input = isql_check_script, combine_output = True) + + if a.return_code: + # "Token unknown", "Name longer than database column size" etc: we have to + # immediately break from this loop because isql_check_script is incorrect! + break + + if a.clean_stdout == a.clean_expected_stdout: + final_check_pass = True + break + if i < max_allowed_time_for_wait-1: + time.sleep(1) + + if not final_check_pass: + print(f'UNEXPECTED. Final check query did not return expected dataset for {max_allowed_time_for_wait} seconds.') + print('Final check query:') + print(isql_check_script) + print('Expected output:') + print(a.clean_expected_stdout) + print('Actual output:') + print(a.clean_stdout) + print(f'ISQL return_code={a.return_code}') + print(f'Waited for {i} seconds') + + a.reset() + + else: + final_check_pass = True + + return + +#-------------------------------------------- + +def drop_db_objects(act_db_main: Action, act_db_repl: Action, capsys): + + # return initial state of master DB: + # remove all DB objects (tables, views, ...): + # + db_main_meta, db_repl_meta = '', '' + for a in (act_db_main,act_db_repl): + if a == act_db_main: + sql_clean = (a.files_dir / 'drop-all-db-objects.sql').read_text() + a.expected_stdout = """ + Start removing objects + Finish. Total objects removed + """ + a.isql(switches=['-q', '-nod'], input = sql_clean, combine_output = True) + + if a.clean_stdout == a.clean_expected_stdout: + a.reset() + else: + print(a.clean_expected_stdout) + a.reset() + break + + # NB: one need to remember that rdb$system_flag can be NOT ONLY 1 for system used objects! + # For example, it has value =3 for triggers that are created to provide CHECK-constraints, + # Custom DB objects always have rdb$system_flag = 0 (or null for some very old databases). + # We can be sure that there are no custom DB objects if following query result is NON empty: + # + ddl_ready_query = """ + select 1 + from rdb$database + where NOT exists ( + select custom_db_object_flag + from ( + select rt.rdb$system_flag as custom_db_object_flag from rdb$triggers rt + UNION ALL + select rt.rdb$system_flag from rdb$relations rt + UNION ALL + select rt.rdb$system_flag from rdb$functions rt + UNION ALL + select rt.rdb$system_flag from rdb$procedures rt + UNION ALL + select rt.rdb$system_flag from rdb$exceptions rt + UNION ALL + select rt.rdb$system_flag from rdb$fields rt + UNION ALL + select rt.rdb$system_flag from rdb$collations rt + UNION ALL + select rt.rdb$system_flag from rdb$generators rt + UNION ALL + select rt.rdb$system_flag from rdb$roles rt + UNION ALL + select rt.rdb$system_flag from rdb$auth_mapping rt + UNION ALL + select 1 from sec$users s + where upper(s.sec$user_name) <> 'SYSDBA' + ) t + where coalesce(t.custom_db_object_flag,0) = 0 + ) + """ + + + ############################################################################## + ### W A I T U N T I L R E P L I C A B E C O M E S A C T U A L ### + ############################################################################## + watch_replica( act_db_repl, MAX_TIME_FOR_WAIT_DATA_IN_REPLICA, ddl_ready_query) + + # Must be EMPTY: + print(capsys.readouterr().out) + + db_main_meta = a.extract_meta(charset = 'utf8', io_enc = 'utf8') + else: + db_repl_meta = a.extract_meta(charset = 'utf8', io_enc = 'utf8') + + if RUN_SWEEP_AT_END: + # Following sweep was mandatory during 2021...2022. Problem was fixed: + # * for FB 4.x: 26-jan-2023, commit 2ed48a62c60c029cd8cb2b0c914f23e1cb56580a + # * for FB 5.x: 20-apr-2023, commit 5af209a952bd2ec3723d2c788f2defa6b740ff69 + # (log message: 'Avoid random generation of field IDs, respect the user-specified order instead'). + # Until this problem was solved, subsequent runs of this test caused to fail with: + # 'ERROR: Record format with length NN is not found for table TEST' + # + a.gfix(switches=['-sweep', a.db.dsn]) + + # Final point: metadata must become equal: + # + diff_meta = ''.join(unified_diff( \ + [x for x in db_main_meta.splitlines() if 'CREATE DATABASE' not in x], + [x for x in db_repl_meta.splitlines() if 'CREATE DATABASE' not in x]) + ) + # Must be EMPTY: + print(diff_meta) + +#-------------------------------------------- + +@pytest.mark.replication +@pytest.mark.version('>=4.0.5') +def test_1(act_db_main: Action, act_db_repl: Action, tmp_nonpriv_user: User, capsys): + + out_prep, out_main, out_drop, dml_err = '', '', '', '' + # Obtain full path + filename for DB_MAIN and DB_REPL aliases. + # NOTE: we must NOT use 'a.db.db_path' for ALIASED databases! + # It will return '.' rather than full path+filename. + # Use only con.info.name for that! + # + db_info = {} + for a in (act_db_main, act_db_repl): + with a.db.connect() as con: + db_info[a, 'db_full_path'] = con.info.name + + # Must be EMPTY: + out_prep = capsys.readouterr().out + if out_prep: + # Some problem raised during change DB header(s) + pass + else: + sql_init = f""" + set bail on; + set wng off; + recreate table test ( + id int generated by default as identity constraint test_pk primary key + ,who varchar(31) default current_user + ,dts timestamp default 'now' + -- NB: we have to specify 'sub_tupe' here, otherwise attempt to insert data in this blob will fail with + -- "TypeError: String value is not acceptable type for a non-textual BLOB column." + -- (in cur.execute() with parametrized expr, see below). + -- It looks strange but this error does NOT appear in case if we use con.execute_immediate(). + ,bdata blob sub_type 1 + ); + commit; + recreate view v_test as select id, who, bdata from test; + revoke all on all from {tmp_nonpriv_user.name}; + grant insert on v_test to {tmp_nonpriv_user.name}; + commit; + + recreate table t_completed(id int primary key); + commit; + """ + act_db_main.isql(switches=['-q'], input = sql_init, combine_output = True) + out_prep = act_db_main.clean_stdout + act_db_main.reset() + + if out_prep: + # Some problem raised during init_sql execution + pass + else: + # Query to be used for check that all DB objects present in replica (after last DML statement completed on master DB): + ddl_ready_query = "select 1 from rdb$relations where rdb$relation_name = upper('t_completed')" + ############################################################################## + ### W A I T U N T I L R E P L I C A B E C O M E S A C T U A L ### + ############################################################################## + watch_replica( act_db_repl, MAX_TIME_FOR_WAIT_DATA_IN_REPLICA, ddl_ready_query) + # Must be EMPTY: + out_prep = capsys.readouterr().out + + test_blob_data = 'test blob data' + + if out_prep: + # Some problem raised with delivering DDL changes to replica + pass + else: + + with act_db_main.db.connect(user = tmp_nonpriv_user.name, password = tmp_nonpriv_user.password) as con: # , charset = 'utf-8') as con: + cur = con.cursor() + try: + # ERROR: no permission for SELECT access to ... + # Effective user is ... + # ERROR: Replication is stopped due to critical error(s) + # + cur.execute("insert into v_test(bdata) values(?)", (test_blob_data,)) + con.commit() + except DatabaseError as e: + dml_err = e.__str__() + + if dml_err: + # Some problem raised with writing blob into master DB + pass + else: + # No errors must be now. We have to wait now until blob from MASTER be delivered to REPLICA. + # Query to be used that replica DB contains all expected data (after last DML statement completed on master DB): + isql_check_script = """ + set bail on; + set list on; + set count on; + select + rdb$get_context('SYSTEM','REPLICA_MODE') replica_mode + ,who + ,cast(bdata as varchar(32760)) as bdata + from v_test; + """ + + isql_expected_out = f""" + REPLICA_MODE READ-ONLY + WHO {tmp_nonpriv_user.name.upper()} + BDATA {test_blob_data} + Records affected: 1 + """ + + ############################################################################## + ### W A I T U N T I L R E P L I C A B E C O M E S A C T U A L ### + ############################################################################## + watch_replica( act_db_repl, MAX_TIME_FOR_WAIT_DATA_IN_REPLICA, '', isql_check_script, isql_expected_out) + # Must be EMPTY: + out_main = capsys.readouterr().out + + drop_db_objects(act_db_main, act_db_repl, capsys) + + # Must be EMPTY: + out_drop = capsys.readouterr().out + + if [ x for x in (out_prep, out_main, dml_err, out_drop) if x.strip() ]: + # We have a problem either with DDL/DML or with dropping DB objects. + # First, we have to RECREATE both master and slave databases + # (otherwise further execution of this test or other replication-related tests most likely will fail): + out_reset = reset_replication(act_db_main, act_db_repl, db_info[act_db_main,'db_full_path'], db_info[act_db_repl,'db_full_path']) + + # Next, we display out_main, out_drop and out_reset: + # + print('Problem(s) detected:') + if out_prep.strip(): + print('out_prep:') + print(out_prep) + if out_main.strip(): + print('out_main:') + print(out_main) + if dml_err.strip(): + print('dml_err:') + print(dml_err) + if out_drop.strip(): + print('out_drop:') + print(out_drop) + if out_reset.strip(): + print('out_reset:') + print(out_reset) + + assert '' == capsys.readouterr().out diff --git a/tests/functional/replication/test_bugcheck_in_rw_replica_after_conflicting_insert.py b/tests/functional/replication/test_bugcheck_in_rw_replica_after_conflicting_insert.py index 53be2f38..538aa7b0 100644 --- a/tests/functional/replication/test_bugcheck_in_rw_replica_after_conflicting_insert.py +++ b/tests/functional/replication/test_bugcheck_in_rw_replica_after_conflicting_insert.py @@ -45,6 +45,7 @@ its previous code (i.e. to keep the way it is). Checked on 6.0.0.299 b1ba859 (SS/CS). + Checked on 4.0.5.3112-d2e612c, 5.0.1.1416-b4b3559, 6.0.0.374-0097d28 """ import os import shutil @@ -62,6 +63,7 @@ repl_settings = QA_GLOBALS['replication'] MAX_TIME_FOR_WAIT_DATA_IN_REPLICA = int(repl_settings['max_time_for_wait_data_in_replica']) + MAIN_DB_ALIAS = repl_settings['main_db_alias'] REPL_DB_ALIAS = repl_settings['repl_db_alias'] RUN_SWEEP_AT_END = int(repl_settings['run_sweep_at_end']) @@ -208,12 +210,12 @@ def check_repl_log( act_db_main: Action, max_allowed_time_for_wait, expected_out cur.execute("select rdb$get_context('SYSTEM','REPLICATION_SEQUENCE') from rdb$database") last_generated_repl_segment = int(cur.fetchone()[0]) - p_info_line_02 = re.compile('Added \\d+ segment\\(s\\) to the queue') - p_info_line_02 = re.compile('Record being inserted .* exists, updating instead') - p_info_line_03 = re.compile(f'Segment {last_generated_repl_segment} \\(\\d+ bytes\\) is replicated .* deleting') - info_01_lines_unq = set() - info_02_lines_unq = set() - info_03_lines_unq = set() + ptn_repl_chk_01 = re.compile('Added \\d+ segment\\(s\\) to the queue') + ptn_repl_chk_02 = re.compile('Record being inserted .* exists, updating instead') + ptn_repl_chk_03 = re.compile(f'Segment {last_generated_repl_segment} \\(\\d+ bytes\\) is replicated .* deleting') + line_match_01 = set() + line_match_02 = set() + line_match_03 = set() found_all = False @@ -228,18 +230,18 @@ def check_repl_log( act_db_main: Action, max_allowed_time_for_wait, expected_out for k,diff_line in enumerate(diff_data): expr = f"select {k} as iter, current_timestamp, q'<{diff_line}>' from rdb$database" - if p_info_line_02.search(diff_line): - info_01_lines_unq.add(k) - expr = f"select {k} as info_01_line, q'<{diff_line}>' as info_01_text, {len(info_01_lines_unq)} as info_01_found, {len(info_02_lines_unq)} as info_02_found, {len(info_03_lines_unq)} as info_03_found from rdb$database" - if p_info_line_02.search(diff_line): - info_02_lines_unq.add(k) - expr = f"select {k} as info_02_line, q'<{diff_line}>' as info_02_text, {len(info_02_lines_unq)} as info_02_found, {len(info_02_lines_unq)} as info_02_found, {len(info_03_lines_unq)} as info_03_found from rdb$database" - if p_info_line_02.search(diff_line): - info_03_lines_unq.add(k) - expr = f"select {k} as info_03_line, q'<{diff_line}>' as info_03_text, {len(info_03_lines_unq)} as info_03_found, {len(info_02_lines_unq)} as info_02_found, {len(info_03_lines_unq)} as info_03_found from rdb$database" + if ptn_repl_chk_01.search(diff_line): + line_match_01.add(k) + expr = f"select {k} as info_01_line, q'<{diff_line}>' as info_01_text, {len(line_match_01)} as info_01_found, {len(line_match_02)} as info_02_found, {len(line_match_03)} as info_03_found from rdb$database" + if ptn_repl_chk_02.search(diff_line): + line_match_02.add(k) + expr = f"select {k} as info_02_line, q'<{diff_line}>' as info_02_text, {len(line_match_02)} as info_02_found, {len(line_match_02)} as info_02_found, {len(line_match_03)} as info_03_found from rdb$database" + if ptn_repl_chk_03.search(diff_line): + line_match_03.add(k) + expr = f"select {k} as info_03_line, q'<{diff_line}>' as info_03_text, {len(line_match_03)} as info_03_found, {len(line_match_02)} as info_02_found, {len(line_match_03)} as info_03_found from rdb$database" # _dummy_ = cur.execute(expr).fetchone() - if len(info_03_lines_unq) >= 1 and len(info_03_lines_unq) >= 1 and len(info_03_lines_unq) >= 1: + if len(line_match_01) >= 1 and len(line_match_02) >= 1 and len(line_match_03) >= 1: found_all = True break @@ -247,7 +249,11 @@ def check_repl_log( act_db_main: Action, max_allowed_time_for_wait, expected_out break if not found_all: - result = f'UNEXPECTED: messages about replicated segment {last_generated_repl_segment} did not appear for {max_allowed_time_for_wait} seconds.' + unexp_msg = f'UNEXPECTED: messages about replicated segment {last_generated_repl_segment} did not appear for {max_allowed_time_for_wait} seconds.' + repllog_diff = '\n'.join( [ ('%4d ' %i) + r for i,r in enumerate(diff_data) ] ) + checked_ptn_msg = f'{ptn_repl_chk_01=}\n{ptn_repl_chk_02=}\n{ptn_repl_chk_03=}' + lines_match_msg = f'{line_match_01=}; {line_match_02=}; {line_match_03=}; ' + result = '\n'.join( [unexp_msg, 'Lines in replication.log:', repllog_diff, 'Checked patterns:', checked_ptn_msg, 'Lines NN that match patterns:', lines_match_msg] ) else: result = expected_output_str diff --git a/tests/functional/replication/test_grantor_not_changes_in_replica_if_owner_not_sysdba.py b/tests/functional/replication/test_grantor_not_changes_in_replica_if_owner_not_sysdba.py new file mode 100644 index 00000000..3ee9df89 --- /dev/null +++ b/tests/functional/replication/test_grantor_not_changes_in_replica_if_owner_not_sysdba.py @@ -0,0 +1,495 @@ +#coding:utf-8 + +""" +ID: replication.test_grantor_not_changes_in_replica_if_owner_not_sysdba +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8058 +TITLE: DDL-Changes in replication does not set the correct grantor +DESCRIPTION: + Test creates user with admin rights (see 'db_main_owner') and calls 'reset_replication' function in order + to re-create db_main database with new OWNER = , i.e. it must differ from SYSDBA. + Then it does actions described in the ticket. + Final REVOKE command being issued against db_main must apply also in db_repl, w/o error. + Test verifies that by checking result of query: + select 1 as db_repl_privilege_unexp_remains from rdb$database + where exists ( + select 1 from rdb$user_privileges p where p.rdb$relation_name = upper('test') and p.rdb$privilege = upper('D') + ); + Outcome of this query on REPLICA database must become empty for no more than MAX_TIME_FOR_WAIT_DATA_IN_REPLICA seconds. + Otherwise test is considered as failed. + +NOTES: + [15.12.2024] pzotov + Before fix, following messages did appear in replication log: + ERROR: unsuccessful metadata update + REVOKE failed + is not grantor of DELETE on TEST to . + + We have to restore owner = SYSDBA for db_main, so we call 'reset_replication' function second time at final point. + Test execution time is about 7...8 seconds (for snapshots that have fix). + + Confirmed bug on 6.0.0.299, 5.0.1.1371, 4.0.5.3082 + Checked on 6.0.0.552, 5.0.2.1569, 4.0.6.3170. +""" +import os +import shutil +import re +import locale +from difflib import unified_diff +from pathlib import Path +import datetime +import time + +import pytest +from firebird.qa import * +from firebird.driver import * + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +repl_settings = QA_GLOBALS['replication'] + +MAX_TIME_FOR_WAIT_SEGMENT_IN_LOG = int(repl_settings['max_time_for_wait_segment_in_log']) +MAX_TIME_FOR_WAIT_DATA_IN_REPLICA = int(repl_settings['max_time_for_wait_data_in_replica']) + +MAIN_DB_ALIAS = repl_settings['main_db_alias'] +REPL_DB_ALIAS = repl_settings['repl_db_alias'] +RUN_SWEEP_AT_END = int(repl_settings['run_sweep_at_end']) + + +db_main = db_factory( filename = '#' + MAIN_DB_ALIAS, do_not_create = True, do_not_drop = True) +db_repl = db_factory( filename = '#' + REPL_DB_ALIAS, do_not_create = True, do_not_drop = True) + +db_main_owner = user_factory('db_main', name = 'tmp_gh_8058', password = '456', admin = True) + +substitutions = [('Start removing objects in:.*', 'Start removing objects'), + ('Finish. Total objects removed: [1-9]\\d*', 'Finish. Total objects removed'), + ('.* CREATE DATABASE .*', ''), + ('[\t ]+', ' '), + ('FOUND message about replicated segment N .*', 'FOUND message about replicated segment')] + +act_db_main = python_act('db_main', substitutions=substitutions) +act_db_repl = python_act('db_repl', substitutions=substitutions) + +#-------------------------------------------- + +def cleanup_folder(p): + # Removed all files and subdirs in the folder

+ # Used for cleanup and when replication must be reset + # in case when any error occurred during test execution. + assert os.path.dirname(p) != p, f"@@@ ABEND @@@ CAN NOT operate in the file system root directory. Check your code!" + + for root, dirs, files in os.walk(p): + for f in files: + # ::: NB ::: 22.12.2023. + # We have to expect that attempt to delete of GUID and (maybe) archived segments can FAIL with + # PermissionError: [WinError 32] The process cannot ... used by another process: /path/to/{GUID} + # Also, we have to skip exception if file (segment) was just deleted by engine + try: + Path(root +'/' + f).unlink(missing_ok = True) + except PermissionError as x: + pass + + for d in dirs: + shutil.rmtree(os.path.join(root, d), ignore_errors = True) + + return os.listdir(p) + +#-------------------------------------------- + +# ::: NB ::: +# THIS FUNCTION HAS ADDITIONAL INPUT PARAMETER: 'db_main_owner' +# +def reset_replication(act_db_main: Action, act_db_repl: Action, db_main_file, db_repl_file, db_main_owner: User = None): + out_reset = '' + failed_shutdown_db_map = {} # K = 'db_main', 'db_repl'; V = error that occurred when we attempted to change DB state to full shutdown (if it occurred) + + with act_db_main.connect_server() as srv: + + # !! IT IS ASSUMED THAT REPLICATION FOLDERS ARE IN THE SAME DIR AS !! + # DO NOT use 'a.db.db_path' for ALIASED database! + # It will return '.' rather than full path+filename. + + repl_root_path = Path(db_main_file).parent + repl_jrn_sub_dir = repl_settings['journal_sub_dir'] + repl_arc_sub_dir = repl_settings['archive_sub_dir'] + + for f in (db_main_file, db_repl_file): + # Method db.drop() changes LINGER to 0, issues 'delete from mon$att' with suppressing exceptions + # and calls 'db.drop_database()' (also with suppressing exceptions). + # We change DB state to FULL SHUTDOWN instead of call action.db.drop() because + # this is more reliable (it kills all attachments in all known cases and does not use mon$ table) + # + try: + srv.database.shutdown(database = f, mode = ShutdownMode.FULL, method = ShutdownMethod.FORCED, timeout = 0) + + # REMOVE db file from disk: we can safely assume that this can be done because DB in full shutdown state. + ########################### + os.unlink(f) + except DatabaseError as e: + failed_shutdown_db_map[ f ] = e.__str__() + + + # Clean folders repl_journal and repl_archive: remove all files from there. + # NOTE: test must NOT raise unrecoverable error if some of files in these folders can not be deleted. + # Rather, this must be displayed as diff and test must be considered as just failed. + for p in (repl_jrn_sub_dir,repl_arc_sub_dir): + + remained_files = cleanup_folder(repl_root_path/p) + + if remained_files: + out_reset += '\n'.join( (f"Directory '{str(repl_root_path/p)}' remains non-empty. Could not delete file(s):", '\n'.join(remained_files)) ) + + # xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + # xxx r e c r e a t e d b _ m a i n a n d d b _ r e p l xxx + # xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + for a in (act_db_main,act_db_repl): + d = a.db.db_path + failed_shutdown_msg = failed_shutdown_db_map.get( str(d), '' ) + if failed_shutdown_msg: + # we could NOT change state of this database to full shutdown --> we must NOT recreate it. + # Accumulate error messages in OUT arg (for displaying as diff): + # + out_reset += '\n'.join( failed_shutdown_msg ) + else: + try: + if db_main_owner and a == act_db_main: + dbx = create_database( str(d), user = db_main_owner.name, password = db_main_owner.password ) + else: + dbx = create_database( str(d), user = a.db.user, password = a.db.password ) + dbx.close() + with a.connect_server() as srv: + srv.database.set_write_mode(database = d, mode = DbWriteMode.ASYNC) + srv.database.set_sweep_interval(database = d, interval = 0) + if a == act_db_repl: + srv.database.set_replica_mode(database = d, mode = ReplicaMode.READ_ONLY) + else: + with a.db.connect() as con: + con.execute_immediate(f'alter database enable publication') + con.execute_immediate('alter database include all to publication') + con.commit() + except DatabaseError as e: + out_reset += e.__str__() + + # Must remain EMPTY: + #################### + return out_reset + +#-------------------------------------------- + +def watch_repl_log_pattern( act_db_main: Action, pattern_to_check, replold_lines, max_allowed_time_for_wait, consider_found_as_unexpected = False): + + replication_log = act_db_main.home_dir / 'replication.log' + + result = '' + found_required_message = False + found_required_line = '' + t0 = time.time() + for i in range(0,max_allowed_time_for_wait): + + time.sleep(1) + + with open(replication_log, 'r') as f: + diff_data = unified_diff( + replold_lines, + f.readlines() + ) + + for k,d in enumerate(diff_data): + if pattern_to_check.search(d): + found_required_message = True + break + + if found_required_message: + break + t1 = time.time() + if not consider_found_as_unexpected and not found_required_message or consider_found_as_unexpected and found_required_message: + # ACHTUNG! This looks weird but we have to either re-read replication log now or wait at least seconds + # if we want to see FULL (actual) content of this log! Otherwise last part of log will be missed. I have no explanations for that :( + repllog_diff = '' + with open(replication_log, 'r') as f: + diff_data = unified_diff( + replold_lines, + f.readlines() + ) + repllog_diff = '\n'.join( ( ('%4d ' %i) + r.rstrip() for i,r in enumerate(diff_data) ) ) + + if consider_found_as_unexpected: + unexp_msg = f"UNEXPECTED outcome: pattern '{pattern_to_check.pattern}' must not occur in log but was ENCOUNTERED there for {int(t1-t0)} seconds." + else: + unexp_msg = f"MISSED outcome: pattern '{pattern_to_check.pattern}' was NOT FOUND for {int(t1-t0)} seconds." + + result = '\n'.join( (unexp_msg, 'replication log diff:', repllog_diff) ) + + return result + +#-------------------------------------------- + +def watch_replica( a: Action, max_allowed_time_for_wait, ddl_ready_query = '', isql_check_script = '', replica_expected_out = ''): + + retcode = 1; + ready_to_check = False + if ddl_ready_query: + with a.db.connect(no_db_triggers = True) as con: + with con.cursor() as cur: + for i in range(0,max_allowed_time_for_wait): + cur.execute(ddl_ready_query) + count_actual = cur.fetchone() + if count_actual: + ready_to_check = True + break + else: + con.rollback() + time.sleep(1) + else: + ready_to_check = True + + if not ready_to_check: + print( f'UNEXPECTED. Query to verify DDL completion did not return any rows for {max_allowed_time_for_wait} seconds.' ) + print('Query:') + print(ddl_ready_query) + + return + + final_check_pass = False + if isql_check_script: + retcode = 0 + for i in range(max_allowed_time_for_wait): + a.reset() + a.expected_stdout = replica_expected_out + a.isql(switches=['-q', '-nod'], input = isql_check_script, combine_output = True) + + if a.return_code: + # "Token unknown", "Name longer than database column size" etc: we have to + # immediately break from this loop because isql_check_script is incorrect! + break + + if a.clean_stdout == a.clean_expected_stdout: + final_check_pass = True + break + if i < max_allowed_time_for_wait-1: + time.sleep(1) + + if not final_check_pass: + print(f'UNEXPECTED. Final check query did not return expected dataset for {max_allowed_time_for_wait} seconds.') + print('Final check query:') + print(isql_check_script) + print('Expected output:') + print(a.clean_expected_stdout) + print('Actual output:') + print(a.clean_stdout) + print(f'ISQL return_code={a.return_code}') + print(f'Waited for {i} seconds') + + a.reset() + + else: + final_check_pass = True + + return + +#-------------------------------------------- + +def drop_db_objects(act_db_main: Action, act_db_repl: Action, capsys): + + # return initial state of master DB: + # remove all DB objects (tables, views, ...): + # + db_main_meta, db_repl_meta = '', '' + for a in (act_db_main,act_db_repl): + if a == act_db_main: + sql_clean = (a.files_dir / 'drop-all-db-objects.sql').read_text() + a.expected_stdout = """ + Start removing objects + Finish. Total objects removed + """ + a.isql(switches=['-q', '-nod'], input = sql_clean, combine_output = True) + + if a.clean_stdout == a.clean_expected_stdout: + a.reset() + else: + print(a.clean_expected_stdout) + a.reset() + break + + # NB: one need to remember that rdb$system_flag can be NOT ONLY 1 for system used objects! + # For example, it has value =3 for triggers that are created to provide CHECK-constraints, + # Custom DB objects always have rdb$system_flag = 0 (or null for some very old databases). + # We can be sure that there are no custom DB objects if following query result is NON empty: + # + ddl_ready_query = """ + select 1 + from rdb$database + where NOT exists ( + select custom_db_object_flag + from ( + select rt.rdb$system_flag as custom_db_object_flag from rdb$triggers rt + UNION ALL + select rt.rdb$system_flag from rdb$relations rt + UNION ALL + select rt.rdb$system_flag from rdb$functions rt + UNION ALL + select rt.rdb$system_flag from rdb$procedures rt + UNION ALL + select rt.rdb$system_flag from rdb$exceptions rt + UNION ALL + select rt.rdb$system_flag from rdb$fields rt + UNION ALL + select rt.rdb$system_flag from rdb$collations rt + UNION ALL + select rt.rdb$system_flag from rdb$generators rt + UNION ALL + select rt.rdb$system_flag from rdb$roles rt + UNION ALL + select rt.rdb$system_flag from rdb$auth_mapping rt + UNION ALL + select 1 from sec$users s + where upper(s.sec$user_name) <> 'SYSDBA' + ) t + where coalesce(t.custom_db_object_flag,0) = 0 + ) + """ + + + ############################################################################## + ### W A I T U N T I L R E P L I C A B E C O M E S A C T U A L ### + ############################################################################## + watch_replica( act_db_repl, MAX_TIME_FOR_WAIT_DATA_IN_REPLICA, ddl_ready_query) + + # Must be EMPTY: + print(capsys.readouterr().out) + + db_main_meta = a.extract_meta(charset = 'utf8', io_enc = 'utf8') + else: + db_repl_meta = a.extract_meta(charset = 'utf8', io_enc = 'utf8') + + if RUN_SWEEP_AT_END: + # Following sweep was mandatory during 2021...2022. Problem was fixed: + # * for FB 4.x: 26-jan-2023, commit 2ed48a62c60c029cd8cb2b0c914f23e1cb56580a + # * for FB 5.x: 20-apr-2023, commit 5af209a952bd2ec3723d2c788f2defa6b740ff69 + # (log message: 'Avoid random generation of field IDs, respect the user-specified order instead'). + # Until this problem was solved, subsequent runs of this test caused to fail with: + # 'ERROR: Record format with length NN is not found for table TEST' + # + a.gfix(switches=['-sweep', a.db.dsn]) + + # Final point: metadata must become equal: + # + diff_meta = ''.join(unified_diff( \ + [x for x in db_main_meta.splitlines() if 'CREATE DATABASE' not in x], + [x for x in db_repl_meta.splitlines() if 'CREATE DATABASE' not in x]) + ) + # Must be EMPTY: + print(diff_meta) + +#-------------------------------------------- + +def get_repl_log(act_db_main: Action): + replication_log = act_db_main.home_dir / 'replication.log' + rlog_lines = [] + with open(replication_log, 'r') as f: + rlog_lines = f.readlines() + + return rlog_lines + +#-------------------------------------------- + +@pytest.mark.replication +@pytest.mark.version('>=4.0.5') +def test_1(act_db_main: Action, act_db_repl: Action, db_main_owner: User, capsys): + + # Map for storing mnemonas and details for every FAILED step: + run_errors_map = {} + + # Obtain full path + filename for DB_MAIN and DB_REPL aliases. + # NOTE: we must NOT use 'a.db.db_path' for ALIASED databases! + # It will return '.' rather than full path+filename. + # Use only con.info.name for that! + # + db_info = {} + for a in (act_db_main, act_db_repl): + with a.db.connect() as con: + db_info[a, 'db_full_path'] = con.info.name + + run_errors_map['init_reset'] = reset_replication(act_db_main, act_db_repl, db_info[act_db_main,'db_full_path'], db_info[act_db_repl,'db_full_path'], db_main_owner) + + # Result: owner of db_main_alias = db_main_owner, i.e. NOT 'SYSDBA' + + sql_init = f""" + set bail on; + recreate table test ( + id int generated by default as identity constraint test_pk primary key + ,f01 int + ); + + recreate table t_completed(id int primary key); + commit; + """ + act_db_main.isql(switches=['-q', '-user', db_main_owner.name, '-pass', db_main_owner.password], credentials = False, input = sql_init, combine_output = True) + run_errors_map['out_prep_ddl'] = act_db_main.clean_stdout + act_db_main.reset() + + if max(v.strip() for v in run_errors_map.values()): + # Some problem raised during init_sql execution + pass + else: + # Query to be used for check that all DB objects present in replica (after last DML statement completed on master DB): + ddl_ready_query = "select 1 from rdb$relations where rdb$relation_name = upper('t_completed')" + ###################################################### + ### WAIT UNTIL REPLICA GETS INITIAL DDL AND DATA ### + ###################################################### + watch_replica( act_db_repl, MAX_TIME_FOR_WAIT_DATA_IN_REPLICA, ddl_ready_query) + + # Must be EMPTY: + run_errors_map['out_repl_ddl'] = capsys.readouterr().out + + + if max(v.strip() for v in run_errors_map.values()): + # Some problem raised with delivering DDL changes to replica + pass + else: + + sql_revoke_access = f""" + set wng off; + set list on; + revoke delete on test from {db_main_owner.name}; + commit; + select 1 as db_main_privilege_unexp_remains + from rdb$database + where exists ( + select 1 from rdb$user_privileges p + where p.rdb$relation_name = upper('test') and p.rdb$privilege = upper('D') + ); + commit; + """ + act_db_main.isql(switches=['-q', '-user', db_main_owner.name, '-pass', db_main_owner.password], credentials = False, input = sql_revoke_access, combine_output = True) + run_errors_map['db_main_privilege_unexp_remains'] = act_db_repl.stdout # must be EMPTY + act_db_main.reset() + + + if max(v.strip() for v in run_errors_map.values()): + # Some problem was in just executed statement + pass + else: + ############################################################ + ### WAIT UNTIL REPLICA APPLY 'REVOKE' PRIVILEGE COMMAND ### + ############################################################ + # ( a: Action, max_allowed_time_for_wait, ddl_ready_query = '', isql_check_script = '', replica_expected_out = ''): + chk_repl_sql = f"set list on;select 1 as db_repl_privilege_unexp_remains from rdb$database where exists(select 1 from rdb$user_privileges p where p.rdb$relation_name = upper('test') and p.rdb$privilege = upper('D'));" + watch_replica( act_db_repl, MAX_TIME_FOR_WAIT_DATA_IN_REPLICA, ddl_ready_query = '', isql_check_script = chk_repl_sql, replica_expected_out = '' ) + # Must be EMPTY: + run_errors_map['db_repl_privilege_not_deleted'] = capsys.readouterr().out + + # This test changes OWNER of db_main to NON-sysdba. + # We have to revert this change regardless on test outcome. + run_errors_map['final_reset'] = reset_replication(act_db_main, act_db_repl, db_info[act_db_main,'db_full_path'], db_info[act_db_repl,'db_full_path']) + + # NO NEEDED because we have done reset just now: drop_db_objects(act_db_main, act_db_repl, capsys) + + if max(v.strip() for v in run_errors_map.values()): + print(f'Problem(s) detected, check run_errors_map:') + for k,v in run_errors_map.items(): + if v.strip(): + print(k,':') + print(v.strip()) + print('-' * 40) + + assert '' == capsys.readouterr().out diff --git a/tests/functional/replication/test_invalid_msg_if_target_db_has_no_replica_flag.py b/tests/functional/replication/test_invalid_msg_if_target_db_has_no_replica_flag.py index 811741e8..188d1053 100644 --- a/tests/functional/replication/test_invalid_msg_if_target_db_has_no_replica_flag.py +++ b/tests/functional/replication/test_invalid_msg_if_target_db_has_no_replica_flag.py @@ -51,7 +51,12 @@ [23.11.2023] pzotov Make final SWEEP optional, depending on setting RUN_SWEEP_AT_END - see $QA_ROOT/files/test_config.ini. - Checked on Windows, 6.0.0.193, 5.0.0.1304, 4.0.5.3042 (SS/CS for all). + [21.06.2024] pzotov + Partially re-implemented: + * full replacement of func check_repl_log; its new name = 'wait_for_repl_err' + * added code for simplifying debug; + * removed check of capsys content from most places (replaced with comparison of result with empty string). + Checked on Windows, 4.0.5.3112-d2e612c, 5.0.1.1416-b4b3559, 6.0.0.374-0097d28 (SS and CS). """ import os import shutil @@ -72,6 +77,7 @@ MAX_TIME_FOR_WAIT_SEGMENT_IN_LOG = int(repl_settings['max_time_for_wait_segment_in_log']) MAX_TIME_FOR_WAIT_DATA_IN_REPLICA = int(repl_settings['max_time_for_wait_data_in_replica']) + MAIN_DB_ALIAS = repl_settings['main_db_alias'] REPL_DB_ALIAS = repl_settings['repl_db_alias'] RUN_SWEEP_AT_END = int(repl_settings['run_sweep_at_end']) @@ -190,38 +196,19 @@ def reset_replication(act_db_main, act_db_repl, db_main_file, db_repl_file): #-------------------------------------------- -def check_repl_log( act_db_main: Action, max_allowed_time_for_wait, prefix_msg = '' ): +def wait_for_repl_err( act_db_main: Action, replold_lines, max_allowed_time_for_wait): replication_log = act_db_main.home_dir / 'replication.log' - replold_lines = [] - with open(replication_log, 'r') as f: - replold_lines = f.readlines() - - with act_db_main.db.connect(no_db_triggers = True) as con: - with con.cursor() as cur: - cur.execute("select rdb$get_context('SYSTEM','REPLICATION_SEQUENCE') from rdb$database") - last_generated_repl_segment = cur.fetchone()[0] - - # VERBOSE: Segment 1 (2582 bytes) is replicated in 1 second(s), deleting the file - # VERBOSE: Segment 2 (200 bytes) is replicated in 82 ms, deleting the file - p_successfully_replicated = re.compile( f'\\+\\s+verbose:\\s+segment\\s+{last_generated_repl_segment}\\s+\\(\\d+\\s+bytes\\)\\s+is\\s+replicated.*deleting', re.IGNORECASE) - - # VERBOSE: Segment 16 replication failure at offset 33628 - p_replication_failure = re.compile('segment\\s+\\d+\\s+replication\\s+failure', re.IGNORECASE) - # ERROR: Database is not in the replica mode p_database_not_replica = re.compile('ERROR:\\s+Database.* not.* replica', re.IGNORECASE) found_required_message = False - found_replfail_message = False - found_common_error_msg = False - + found_required_line = '' for i in range(0,max_allowed_time_for_wait): time.sleep(1) - # Get content of fb_home/replication.log _after_ isql finish: with open(replication_log, 'r') as f: diff_data = unified_diff( replold_lines, @@ -229,39 +216,35 @@ def check_repl_log( act_db_main: Action, max_allowed_time_for_wait, prefix_msg = ) for k,d in enumerate(diff_data): - if p_successfully_replicated.search(d): - # We FOUND phrase "VERBOSE: Segment ... is replicated ..." in the replication log. - # This is expected success, break! - print( (prefix_msg + ' ' if prefix_msg else '') + f'FOUND message about replicated segment N {last_generated_repl_segment}.' ) - found_required_message = True - break - - if p_replication_failure.search(d): - print( (prefix_msg + ' ' if prefix_msg else '') + 'SEGMENT_FAILURE: ' + d ) - found_replfail_message = True - break - if p_database_not_replica.search(d): - print( (prefix_msg + ' ' if prefix_msg else '') + 'EXPECTED_NOT_REPL') found_required_message = True break - if 'ERROR:' in d: - print( (prefix_msg + ' ' if prefix_msg else '') + 'COMMON_FAILURE: ' + d ) - found_common_error_msg = True - break - - if found_required_message or found_replfail_message or found_common_error_msg: + if found_required_message: break if not found_required_message: - print(f'UNEXPECTED RESULT: no message about replicating segment N {last_generated_repl_segment} for {max_allowed_time_for_wait} seconds.') + # ACHTUNG! This looks weird but we have to either re-read replication log now or wait at least seconds + # if we want to see FULL (actual) content of this log! Otherwise last part of log will be missed. I have no explanations for that :( + with open(replication_log, 'r') as f: + diff_data = unified_diff( + replold_lines, + f.readlines() + ) + unexp_msg = f"Expected ERROR message was not found for {max_allowed_time_for_wait} seconds." + repllog_diff = '\n'.join( ( ('%4d ' %i) + r.rstrip() for i,r in enumerate(diff_data) ) ) + result = '\n'.join( (unexp_msg, 'Lines in replication.log:', repllog_diff) ) + else: + result = '' + + return result #-------------------------------------------- def watch_replica( a: Action, max_allowed_time_for_wait, ddl_ready_query = '', isql_check_script = '', replica_expected_out = ''): + result = '' retcode = 1; ready_to_check = False if ddl_ready_query: @@ -279,48 +262,49 @@ def watch_replica( a: Action, max_allowed_time_for_wait, ddl_ready_query = '', i else: ready_to_check = True - if not ready_to_check: - print( f'UNEXPECTED. Initial check query did not return any rows for {max_allowed_time_for_wait} seconds.' ) - print('Initial check query:') - print(ddl_ready_query) - return - - final_check_pass = False - if isql_check_script: - retcode = 0 - for i in range(max_allowed_time_for_wait): - a.reset() - a.expected_stdout = replica_expected_out - a.isql(switches=['-q', '-nod'], input = isql_check_script, combine_output = True) - - if a.return_code: - # "Token unknown", "Name longer than database column size" etc: we have to - # immediately break from this loop because isql_check_script is incorrect! - break - - if a.clean_stdout == a.clean_expected_stdout: - final_check_pass = True - break - if i < max_allowed_time_for_wait-1: - time.sleep(1) - - if not final_check_pass: - print(f'UNEXPECTED. Final check query did not return expected dataset for {max_allowed_time_for_wait} seconds.') - print('Final check query:') - print(isql_check_script) - print('Expected output:') - print(a.clean_expected_stdout) - print('Actual output:') - print(a.clean_stdout) - print(f'ISQL return_code={a.return_code}') - print(f'Waited for {i} seconds') - - a.reset() + msg = f'UNEXPECTED. Check query did not return any rows for {max_allowed_time_for_wait} seconds.' + if not ready_to_check: + result = '\n'.join((msg, 'ddl_ready_query:', ddl_ready_query)) else: - final_check_pass = True + final_check_pass = False + if isql_check_script: + retcode = 0 + for i in range(max_allowed_time_for_wait): + a.reset() + a.expected_stdout = replica_expected_out + a.isql(switches=['-q', '-nod'], input = isql_check_script, combine_output = True) + + if a.return_code: + # "Token unknown", "Name longer than database column size" etc: we have to + # immediately break from this loop because isql_check_script is incorrect! + break + + if a.clean_stdout == a.clean_expected_stdout: + final_check_pass = True + break + if i < max_allowed_time_for_wait-1: + time.sleep(1) + + if not final_check_pass: + + result = '\n'.join( + ( msg + ,'Final check query:' + ,isql_check_script + ,'Expected output:' + ,a.clean_expected_stdout + ,'Actual output:' + ,a.clean_stdout + ,f'ISQL return_code={a.return_code}' + ,f'Waited for {i} seconds' + ) + ) + a.reset() + else: + final_check_pass = True - return + return result #-------------------------------------------- @@ -330,6 +314,7 @@ def drop_db_objects(act_db_main: Action, act_db_repl: Action, capsys): # remove all DB objects (tables, views, ...): # db_main_meta, db_repl_meta = '', '' + result = '' for a in (act_db_main,act_db_repl): if a == act_db_main: sql_clean = (a.files_dir / 'drop-all-db-objects.sql').read_text() @@ -342,7 +327,8 @@ def drop_db_objects(act_db_main: Action, act_db_repl: Action, capsys): if a.clean_stdout == a.clean_expected_stdout: a.reset() else: - print(a.clean_expected_stdout) + result = a.clean_stdout + # print(a.clean_stdout) a.reset() break @@ -388,10 +374,10 @@ def drop_db_objects(act_db_main: Action, act_db_repl: Action, capsys): ############################################################################## ### W A I T U N T I L R E P L I C A B E C O M E S A C T U A L ### ############################################################################## - watch_replica( act_db_repl, MAX_TIME_FOR_WAIT_DATA_IN_REPLICA, ddl_ready_query) + result = watch_replica( act_db_repl, MAX_TIME_FOR_WAIT_DATA_IN_REPLICA, ddl_ready_query) # Must be EMPTY: - print(capsys.readouterr().out) + #print(capsys.readouterr().out) db_main_meta = a.extract_meta(charset = 'utf8', io_enc = 'utf8') else: @@ -407,14 +393,17 @@ def drop_db_objects(act_db_main: Action, act_db_repl: Action, capsys): # a.gfix(switches=['-sweep', a.db.dsn]) - # Final point: metadata must become equal: - # - diff_meta = ''.join(unified_diff( \ - [x for x in db_main_meta.splitlines() if 'CREATE DATABASE' not in x], - [x for x in db_repl_meta.splitlines() if 'CREATE DATABASE' not in x]) - ) - # Must be EMPTY: - print(diff_meta) + if result == '': + # Final point: metadata must become equal: + # + diff_meta = ''.join(unified_diff( \ + [x for x in db_main_meta.splitlines() if 'CREATE DATABASE' not in x], + [x for x in db_repl_meta.splitlines() if 'CREATE DATABASE' not in x]) + ) + # Must be EMPTY: + return diff_meta + else: + return result #-------------------------------------------- @@ -424,9 +413,14 @@ def drop_db_objects(act_db_main: Action, act_db_repl: Action, capsys): def test_1(act_db_main: Action, act_db_repl: Action, capsys): db_info = {} - out_prep, out_main, out_drop, out_reset = '', '', '', '' - smth_failed = None + out_prep = out_main = out_back = out_drop = out_reset = '' + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + replication_log = act_db_main.home_dir / 'replication.log' + replold_lines = [] + with open(replication_log, 'r') as f: + replold_lines = f.readlines() + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Obtain full path + filename for DB_MAIN and DB_REPL aliases. # NOTE: we must NOT use 'a.db.db_path' for ALIASED databases! @@ -456,6 +450,10 @@ def test_1(act_db_main: Action, act_db_repl: Action, capsys): insert into test(id, x) values(1, 100); commit; ''' + + # Applying script on master but replica will NOT accept it because its replication mode = NONE. + # Message "ERROR: Database is not in the replica mode" must appear after this ISQL call: + # act_db_main.isql(switches=['-q'], input = sql_init, combine_output = True) out_prep = act_db_main.clean_stdout act_db_main.reset() @@ -463,29 +461,22 @@ def test_1(act_db_main: Action, act_db_repl: Action, capsys): if out_prep: pass else: - try: - # During next seconds message - # "ERROR: Database is not in the replica mode" must appear in replication log: - - act_db_main.expected_stdout = 'EXPECTED_NOT_REPL' - ############################################################### - ### W A I T F O R E R R O R I N R E P L . L O G ### - ############################################################### - check_repl_log( act_db_main, MAX_TIME_FOR_WAIT_SEGMENT_IN_LOG, '' ) - act_db_main.stdout = capsys.readouterr().out - assert act_db_main.clean_stdout == act_db_main.clean_expected_stdout - act_db_main.reset() + # During next seconds message + # "ERROR: Database is not in the replica mode" must appear in replication log - #--------------------------------------------------- + ################################################################## + ### W A I T F O R E R R O R I N R E P L . L O G ### + ################################################################## + out_main = wait_for_repl_err( act_db_main, replold_lines, MAX_TIME_FOR_WAIT_SEGMENT_IN_LOG) + try: # Return db_repl mode to 'read-only' (as it was before this test) # and wait after it for seconds # for previously created segment be replicated: with act_db_repl.connect_server() as srv: srv.database.set_replica_mode(database = act_db_repl.db.db_path, mode = ReplicaMode.READ_ONLY) - # Query to be used for check that all DB objects present in replica (after last DML statement completed on master DB): ddl_ready_query = "select 1 from rdb$relations where rdb$relation_name = upper('test')" @@ -511,22 +502,16 @@ def test_1(act_db_main: Action, act_db_repl: Action, capsys): ############################################################################## ### W A I T U N T I L R E P L I C A B E C O M E S A C T U A L ### ############################################################################## - watch_replica( act_db_repl, MAX_TIME_FOR_WAIT_DATA_IN_REPLICA, ddl_ready_query, isql_check_script, isql_expected_out) - - # Must be EMPTY: - out_main = capsys.readouterr().out + out_back = watch_replica( act_db_repl, MAX_TIME_FOR_WAIT_DATA_IN_REPLICA, ddl_ready_query, isql_check_script, isql_expected_out) except Exception as e: - out_main = e.__str__() - - - drop_db_objects(act_db_main, act_db_repl, capsys) + out_back = e.__str__() # Must be EMPTY: - out_drop = capsys.readouterr().out + out_drop = drop_db_objects(act_db_main, act_db_repl, capsys) - if [ x for x in (out_prep, out_main, out_drop) if x.strip() ]: + if [ x for x in (out_prep, out_main, out_back, out_drop) if x.strip() ]: # We have a problem either with DDL/DML or with dropping DB objects. # First, we have to RECREATE both master and slave databases # (otherwise further execution of this test or other replication-related tests most likely will fail): @@ -541,6 +526,9 @@ def test_1(act_db_main: Action, act_db_repl: Action, capsys): if out_main.strip(): print('out_main:') print(out_main) + if out_back.strip(): + print('out_back:') + print(out_back) if out_drop.strip(): print('out_drop:') print(out_drop) diff --git a/tests/functional/replication/test_make_async_reinit_reliable.py b/tests/functional/replication/test_make_async_reinit_reliable.py new file mode 100644 index 00000000..e441360d --- /dev/null +++ b/tests/functional/replication/test_make_async_reinit_reliable.py @@ -0,0 +1,689 @@ +#coding:utf-8 + +""" +ID: replication.test_make_async_reinit_reliable +ISSUE: https://github.com/FirebirdSQL/firebird/pull/8324 +TITLE: Make asynchronous replica re-initialization reliable #8324 +DESCRIPTION: + To reproduce problem from ticket one need to do following: + * create two tables ('test' and 'test2'), one of them must have quite long text column; + * add about one-two hundred rows in the 'test', see var. ADD_ROWS_INIT; + * start Tx on master (it will be OAT, see var. 'tx_oat') and: + ** add several thousands rows in the table 'test' (see var. ADD_ROWS_IN_OAT), but do NOT commit Tx; + ** stay idle (wait) until message with phrase "Segment... preserving (OAT: ... in segment ...)" + will appear in the replication log + * start one more Tx on master (see var 'tx_bef') and run some DML within this Tx against table 'TEST2', + with obtaining value of rdb$get_context('SYSTEM', 'REPLICATION_SEQUENCE') and store it for further + check (see var 'preserved_segment_no'). Commit this Tx. + Note: one need to use DIFFERENT table here. Do not use 'TEST'. + * Stay idle (wait) until replica becomes actual with master, i.e. table 'TEST2' will have same value + that was just used on master (see 'select count(*) from test2 where id = {TEST2_VALUE}') + * Change state of replica DB to full shutdown (in order to have ability to overwrite it further). + * Run on master: 'nacbkup -b 0' (see var. 'db_nbk0'). NOTE: this action increases ID of segment that + will be used for further writes. It is NOT so for command 'NBACKUP -L'. + * Handle just created .nbk0: run 'nbackup -SEQ -F', then set is as REPLICA and put fo FULL SHUTDOWN. + * Overwrite "old" replica with just created copy of master that has been prepared to serve as replica. + * Bring replica online; replication log will have following messages at this point: + BEFORE fix: + VERBOSE: Segment 2 (15729380 bytes) is replicated in 1.296s, preserving (OAT: 13 in segment 2) + VERBOSE: Segment 3 (646 bytes) is replicated in 0.013s, preserving (OAT: 13 in segment 2) + VERBOSE: Added 2 segment(s) to the queue + VERBOSE: Deleting segment 2 due to fast forward + VERBOSE: Deleting segment 3 due to fast forward + AFTER fix: + VERBOSE: Segment 2 (15729380 bytes) is replicated in 0.944s, preserving (OAT: 14 in segment 2) + VERBOSE: Added 1 segment(s) to the queue + VERBOSE: Added 2 segment(s) to the queue + VERBOSE: Segment 2 (15729380 bytes) is replayed in 1.438s, preserving (OAT: 14 in segment 2) + VERBOSE: Segment 3 (646 bytes) is replicated in 0.009s, preserving (OAT: 14 in segment 2) + VERBOSE: Added 2 segment(s) to the queue + ERROR: database shutdown + VERBOSE: Database sequence has been changed to 3, preparing for replication reset + * Master DB: do COMMIT for tx_oat. After this replication log following messages: + BEFORE fix: + VERBOSE: Added 1 segment(s) to the queue + VERBOSE: Resetting replication to continue from segment 4 + ERROR: Transaction 13 is not found + At segment 4, offset 48 + AFTER fix: + VERBOSE: Database sequence has been changed to 3, preparing for replication reset + VERBOSE: Added 2 segment(s) to the queue + VERBOSE: Database sequence has been changed to 3, preparing for replication reset + VERBOSE: Added 3 segment(s) to the queue + VERBOSE: Database sequence has been changed to 3, preparing for replication reset + VERBOSE: Segment 2 (15729380 bytes) is scanned in 0.006s, preserving (OAT: 14 in segment 2) + VERBOSE: Segment 3 (646 bytes) is scanned in 0.014s, preserving (OAT: 14 in segment 2) + VERBOSE: Resetting replication to continue from segment 4 (new OAT: 14 in segment 2) + VERBOSE: Segment 2 (15729380 bytes) is replayed in 1.343s, preserving (OAT: 14 in segment 2) + VERBOSE: Segment 3 (646 bytes) is replayed in 0.014s, preserving (OAT: 14 in segment 2) + VERBOSE: Segment 4 (321088 bytes) is replicated in 0.105s, deleting + VERBOSE: Deleting segment 2 as no longer needed + VERBOSE: Deleting segment 3 as no longer needed + * Check replication log that there is NO error message with text 'Transaction is not found' + * Stay idle (wait) until replica becomes actual with master, i.e. query 'select max(id) from test' + being issued on replica will return expected value = ADD_ROWS_INIT + ADD_ROWS_IN_OAT. + +NOTES: + [10.12.2024] pzotov + Confirmed bug on 5.0.2.1567-9fbd574 (16.11.2024 20:15). + Checked on Windows: + 5.0.2.1569-684bb87 (27.11.2024 20:40). + 4.0.6.3170-cc44002 (10.12.2024 07:02) + 6.0.0.548-a8c5b9f (10.12.2024 10:13) + Great thanks to dimitr for suggestions about test implementation. +""" +import os +import shutil +import re +import locale +from difflib import unified_diff +from pathlib import Path +import datetime +import time + +import pytest +from firebird.qa import * +from firebird.driver import * + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +repl_settings = QA_GLOBALS['replication'] + +MAX_TIME_FOR_WAIT_SEGMENT_IN_LOG = int(repl_settings['max_time_for_wait_segment_in_log']) +MAX_TIME_FOR_WAIT_DATA_IN_REPLICA = int(repl_settings['max_time_for_wait_data_in_replica']) + +MAIN_DB_ALIAS = repl_settings['main_db_alias'] +REPL_DB_ALIAS = repl_settings['repl_db_alias'] +RUN_SWEEP_AT_END = int(repl_settings['run_sweep_at_end']) + +db_main = db_factory( filename = '#' + MAIN_DB_ALIAS, do_not_create = True, do_not_drop = True) +db_repl = db_factory( filename = '#' + REPL_DB_ALIAS, do_not_create = True, do_not_drop = True) + +# Where we want to store result of 'nbackup -b 0 ': +db_nbk0 = temp_file('gh_8324_tmp.nbk0') + +substitutions = [('Start removing objects in:.*', 'Start removing objects'), + ('Finish. Total objects removed: [1-9]\\d*', 'Finish. Total objects removed'), + ('.* CREATE DATABASE .*', ''), + ('[\t ]+', ' '), + ('FOUND message about replicated segment N .*', 'FOUND message about replicated segment')] + +act_db_main = python_act('db_main', substitutions=substitutions) +act_db_repl = python_act('db_repl', substitutions=substitutions) + +#-------------------------------------------- + +def cleanup_folder(p): + # Removed all files and subdirs in the folder

+ # Used for cleanup and when replication must be reset + # in case when any error occurred during test execution. + assert os.path.dirname(p) != p, f"@@@ ABEND @@@ CAN NOT operate in the file system root directory. Check your code!" + + for root, dirs, files in os.walk(p): + for f in files: + # ::: NB ::: 22.12.2023. + # We have to expect that attempt to delete of GUID and (maybe) archived segments can FAIL with + # PermissionError: [WinError 32] The process cannot ... used by another process: /path/to/{GUID} + # Also, we have to skip exception if file (segment) was just deleted by engine + try: + Path(root +'/' + f).unlink(missing_ok = True) + except PermissionError as x: + pass + + for d in dirs: + shutil.rmtree(os.path.join(root, d), ignore_errors = True) + + return os.listdir(p) + +#-------------------------------------------- + +def reset_replication(act_db_main, act_db_repl, db_main_file, db_repl_file): + out_reset = '' + failed_shutdown_db_map = {} # K = 'db_main', 'db_repl'; V = error that occurred when we attempted to change DB state to full shutdown (if it occurred) + + with act_db_main.connect_server() as srv: + + # !! IT IS ASSUMED THAT REPLICATION FOLDERS ARE IN THE SAME DIR AS !! + # DO NOT use 'a.db.db_path' for ALIASED database! + # It will return '.' rather than full path+filename. + + repl_root_path = Path(db_main_file).parent + repl_jrn_sub_dir = repl_settings['journal_sub_dir'] + repl_arc_sub_dir = repl_settings['archive_sub_dir'] + + for f in (db_main_file, db_repl_file): + # Method db.drop() changes LINGER to 0, issues 'delete from mon$att' with suppressing exceptions + # and calls 'db.drop_database()' (also with suppressing exceptions). + # We change DB state to FULL SHUTDOWN instead of call action.db.drop() because + # this is more reliable (it kills all attachments in all known cases and does not use mon$ table) + # + try: + srv.database.shutdown(database = f, mode = ShutdownMode.FULL, method = ShutdownMethod.FORCED, timeout = 0) + + # REMOVE db file from disk: we can safely assume that this can be done because DB in full shutdown state. + ########################### + os.unlink(f) + except DatabaseError as e: + failed_shutdown_db_map[ f ] = e.__str__() + + + # Clean folders repl_journal and repl_archive: remove all files from there. + # NOTE: test must NOT raise unrecoverable error if some of files in these folders can not be deleted. + # Rather, this must be displayed as diff and test must be considered as just failed. + for p in (repl_jrn_sub_dir,repl_arc_sub_dir): + + remained_files = cleanup_folder(repl_root_path/p) + + if remained_files: + out_reset += '\n'.join( (f"Directory '{str(repl_root_path/p)}' remains non-empty. Could not delete file(s):", '\n'.join(remained_files)) ) + + # xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + # xxx r e c r e a t e d b _ m a i n a n d d b _ r e p l xxx + # xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + for a in (act_db_main,act_db_repl): + d = a.db.db_path + failed_shutdown_msg = failed_shutdown_db_map.get( str(d), '' ) + if failed_shutdown_msg: + # we could NOT change state of this database to full shutdown --> we must NOT recreate it. + # Accumulate error messages in OUT arg (for displaying as diff): + # + out_reset += '\n'.join( failed_shutdown_msg ) + else: + try: + dbx = create_database(str(d), user = a.db.user) + dbx.close() + with a.connect_server() as srv: + srv.database.set_write_mode(database = d, mode = DbWriteMode.ASYNC) + srv.database.set_sweep_interval(database = d, interval = 0) + if a == act_db_repl: + srv.database.set_replica_mode(database = d, mode = ReplicaMode.READ_ONLY) + else: + with a.db.connect() as con: + con.execute_immediate('alter database enable publication') + con.execute_immediate('alter database include all to publication') + con.commit() + except DatabaseError as e: + out_reset += e.__str__() + + # Must remain EMPTY: + #################### + return out_reset + +#-------------------------------------------- + +def watch_repl_log_pattern( act_db_main: Action, pattern_to_check, replold_lines, max_allowed_time_for_wait, consider_found_as_unexpected = False): + + replication_log = act_db_main.home_dir / 'replication.log' + + result = '' + found_required_message = False + found_required_line = '' + t0 = time.time() + for i in range(0,max_allowed_time_for_wait): + + time.sleep(1) + + with open(replication_log, 'r') as f: + diff_data = unified_diff( + replold_lines, + f.readlines() + ) + + for k,d in enumerate(diff_data): + if pattern_to_check.search(d): + found_required_message = True + break + + if found_required_message: + break + t1 = time.time() + if not consider_found_as_unexpected and not found_required_message or consider_found_as_unexpected and found_required_message: + # ACHTUNG! This looks weird but we have to either re-read replication log now or wait at least seconds + # if we want to see FULL (actual) content of this log! Otherwise last part of log will be missed. I have no explanations for that :( + repllog_diff = '' + with open(replication_log, 'r') as f: + diff_data = unified_diff( + replold_lines, + f.readlines() + ) + repllog_diff = '\n'.join( ( ('%4d ' %i) + r.rstrip() for i,r in enumerate(diff_data) ) ) + + if consider_found_as_unexpected: + unexp_msg = f"UNEXPECTED outcome: pattern '{pattern_to_check.pattern}' must not occur in log but was ENCOUNTERED there for {int(t1-t0)} seconds." + else: + unexp_msg = f"MISSED outcome: pattern '{pattern_to_check.pattern}' was NOT FOUND for {int(t1-t0)} seconds." + + result = '\n'.join( (unexp_msg, 'replication log diff:', repllog_diff) ) + + return result + +#-------------------------------------------- + +def watch_replica( a: Action, max_allowed_time_for_wait, ddl_ready_query = '', isql_check_script = '', replica_expected_out = ''): + + retcode = 1; + ready_to_check = False + if ddl_ready_query: + with a.db.connect(no_db_triggers = True) as con: + with con.cursor() as cur: + for i in range(0,max_allowed_time_for_wait): + cur.execute(ddl_ready_query) + count_actual = cur.fetchone() + if count_actual: + ready_to_check = True + break + else: + con.rollback() + time.sleep(1) + else: + ready_to_check = True + + if not ready_to_check: + print( f'UNEXPECTED. Query to verify DDL completion did not return any rows for {max_allowed_time_for_wait} seconds.' ) + print('Query:') + print(ddl_ready_query) + + return + + final_check_pass = False + if isql_check_script: + retcode = 0 + for i in range(max_allowed_time_for_wait): + a.reset() + a.expected_stdout = replica_expected_out + a.isql(switches=['-q', '-nod'], input = isql_check_script, combine_output = True) + + if a.return_code: + # "Token unknown", "Name longer than database column size" etc: we have to + # immediately break from this loop because isql_check_script is incorrect! + break + + if a.clean_stdout == a.clean_expected_stdout: + final_check_pass = True + break + if i < max_allowed_time_for_wait-1: + time.sleep(1) + + if not final_check_pass: + print(f'UNEXPECTED. Final check query did not return expected dataset for {max_allowed_time_for_wait} seconds.') + print('Final check query:') + print(isql_check_script) + print('Expected output:') + print(a.clean_expected_stdout) + print('Actual output:') + print(a.clean_stdout) + print(f'ISQL return_code={a.return_code}') + print(f'Waited for {i} seconds') + + a.reset() + + else: + final_check_pass = True + + return + +#-------------------------------------------- + +def drop_db_objects(act_db_main: Action, act_db_repl: Action, capsys): + + # return initial state of master DB: + # remove all DB objects (tables, views, ...): + # + db_main_meta, db_repl_meta = '', '' + for a in (act_db_main,act_db_repl): + if a == act_db_main: + sql_clean = (a.files_dir / 'drop-all-db-objects.sql').read_text() + a.expected_stdout = """ + Start removing objects + Finish. Total objects removed + """ + a.isql(switches=['-q', '-nod'], input = sql_clean, combine_output = True) + + if a.clean_stdout == a.clean_expected_stdout: + a.reset() + else: + print(a.clean_expected_stdout) + a.reset() + break + + # NB: one need to remember that rdb$system_flag can be NOT ONLY 1 for system used objects! + # For example, it has value =3 for triggers that are created to provide CHECK-constraints, + # Custom DB objects always have rdb$system_flag = 0 (or null for some very old databases). + # We can be sure that there are no custom DB objects if following query result is NON empty: + # + ddl_ready_query = """ + select 1 + from rdb$database + where NOT exists ( + select custom_db_object_flag + from ( + select rt.rdb$system_flag as custom_db_object_flag from rdb$triggers rt + UNION ALL + select rt.rdb$system_flag from rdb$relations rt + UNION ALL + select rt.rdb$system_flag from rdb$functions rt + UNION ALL + select rt.rdb$system_flag from rdb$procedures rt + UNION ALL + select rt.rdb$system_flag from rdb$exceptions rt + UNION ALL + select rt.rdb$system_flag from rdb$fields rt + UNION ALL + select rt.rdb$system_flag from rdb$collations rt + UNION ALL + select rt.rdb$system_flag from rdb$generators rt + UNION ALL + select rt.rdb$system_flag from rdb$roles rt + UNION ALL + select rt.rdb$system_flag from rdb$auth_mapping rt + UNION ALL + select 1 from sec$users s + where upper(s.sec$user_name) <> 'SYSDBA' + ) t + where coalesce(t.custom_db_object_flag,0) = 0 + ) + """ + + + ############################################################################## + ### W A I T U N T I L R E P L I C A B E C O M E S A C T U A L ### + ############################################################################## + watch_replica( act_db_repl, MAX_TIME_FOR_WAIT_DATA_IN_REPLICA, ddl_ready_query) + + # Must be EMPTY: + print(capsys.readouterr().out) + + db_main_meta = a.extract_meta(charset = 'utf8', io_enc = 'utf8') + else: + db_repl_meta = a.extract_meta(charset = 'utf8', io_enc = 'utf8') + + if RUN_SWEEP_AT_END: + # Following sweep was mandatory during 2021...2022. Problem was fixed: + # * for FB 4.x: 26-jan-2023, commit 2ed48a62c60c029cd8cb2b0c914f23e1cb56580a + # * for FB 5.x: 20-apr-2023, commit 5af209a952bd2ec3723d2c788f2defa6b740ff69 + # (log message: 'Avoid random generation of field IDs, respect the user-specified order instead'). + # Until this problem was solved, subsequent runs of this test caused to fail with: + # 'ERROR: Record format with length NN is not found for table TEST' + # + a.gfix(switches=['-sweep', a.db.dsn]) + + # Final point: metadata must become equal: + # + diff_meta = ''.join(unified_diff( \ + [x for x in db_main_meta.splitlines() if 'CREATE DATABASE' not in x], + [x for x in db_repl_meta.splitlines() if 'CREATE DATABASE' not in x]) + ) + # Must be EMPTY: + print(diff_meta) + +#-------------------------------------------- + +def get_repl_log(act_db_main: Action): + replication_log = act_db_main.home_dir / 'replication.log' + rlog_lines = [] + with open(replication_log, 'r') as f: + rlog_lines = f.readlines() + + return rlog_lines + +#-------------------------------------------- + +@pytest.mark.replication +@pytest.mark.version('>=4.0.6') +def test_1(act_db_main: Action, act_db_repl: Action, db_nbk0: Path, capsys): + + FLD_WIDTH = 500 + ADD_ROWS_INIT = 100 + ADD_ROWS_IN_OAT = 30000 + TEST2_VALUE = -2147483648 + + # Map for storing mnemonas and details for every FAILED step: + run_errors_map = {} + + # Obtain full path + filename for DB_MAIN and DB_REPL aliases. + # NOTE: we must NOT use 'a.db.db_path' for ALIASED databases! + # It will return '.' rather than full path+filename. + # Use only con.info.name for that! + # + db_info = {} + for a in (act_db_main, act_db_repl): + with a.db.connect() as con: + db_info[a, 'db_full_path'] = con.info.name + + sql_init = f""" + set bail on; + set wng off; + recreate table test ( + id int generated by default as identity constraint test_pk primary key + ,dts timestamp default 'now' + ,trn bigint default current_transaction + ,s varchar({FLD_WIDTH}) unique using index test_f01_unq + ); + + recreate table test2 ( + id int generated by default as identity constraint test2_pk primary key + ,dts timestamp default 'now' + ,trn bigint default current_transaction + ,s varchar({FLD_WIDTH}) unique using index test2_f01_unq + ); + commit; + + insert into test(s) select lpad( '', {FLD_WIDTH}, uuid_to_char(gen_uuid()) ) from rdb$types rows {ADD_ROWS_INIT}; + commit; + + recreate table t_completed(id int primary key); + commit; + """ + act_db_main.isql(switches=['-q'], input = sql_init, combine_output = True) + run_errors_map['out_prep_ddl'] = act_db_main.clean_stdout + act_db_main.reset() + + if max(v.strip() for v in run_errors_map.values()): + # Some problem raised during init_sql execution + pass + else: + # Query to be used for check that all DB objects present in replica (after last DML statement completed on master DB): + ddl_ready_query = "select 1 from rdb$relations where rdb$relation_name = upper('t_completed')" + ##################################################################### + ### WAIT UNTIL REPLICA GET SEGMENT(S) WITH INITIAL DDL AND DATA ### + ##################################################################### + watch_replica( act_db_repl, MAX_TIME_FOR_WAIT_DATA_IN_REPLICA, ddl_ready_query) + + # Must be EMPTY: + run_errors_map['out_repl_ddl'] = capsys.readouterr().out + + if max(v.strip() for v in run_errors_map.values()): + # Some problem raised with delivering DDL changes to replica + pass + else: + + with act_db_main.db.connect() as con: + preserved_segment_no = -1 + + tp_oat = tpb(isolation = Isolation.SNAPSHOT, lock_timeout = 0) + tx_oat = con.transaction_manager(tp_oat) + cur_oat = tx_oat.cursor() + + tx_oat.begin() + replold_lines = get_repl_log(act_db_main) + + try: + cur_oat.execute(f"insert /* trace_tag OAT */ into test(s) select lpad('', {FLD_WIDTH}, uuid_to_char(gen_uuid())) from rdb$types,rdb$types rows {ADD_ROWS_IN_OAT}") + cur_oat.execute('select max(id) from test') + cur_oat.fetchone() + except DatabaseError as e: + run_errors_map['main_oat_init_err'] = e.__str__() + + if max(v.strip() for v in run_errors_map.values()): + # Some problem was in just executed OAT-statement + pass + else: + ############################################################## + ### CHECK REPL.LOG: WAITING FOR "PRESERVING OAT" MESSAGE ### + ############################################################## + # VERBOSE: Segment 2 (11534562 bytes) is replicated in 0.934s, preserving (OAT: 10 in segment 2) + pattern_to_check = re.compile( r'preserving \(oat:\s+\d+\s+in\s+segment\s+\d+', re.IGNORECASE ) + run_errors_map['repl_preserve_oat_err'] = watch_repl_log_pattern( act_db_main, pattern_to_check, replold_lines, MAX_TIME_FOR_WAIT_SEGMENT_IN_LOG) + + if max(v.strip() for v in run_errors_map.values()): + # Timeout expired: message "Segment ... preserving (OAT: ... in segment ...)" did not appeared + # in replication log for seconds. + pass + else: + + ############################################### + ### MASTER: RUN DML BEFORE 'NBACKUP -B 0' ### + ############################################### + # tp_wrk = tpb(isolation = Isolation.READ_COMMITTED_READ_CONSISTENCY, lock_timeout = 3) # ?!?! check trace !!! + tp_bef = tpb(isolation = Isolation.READ_COMMITTED, lock_timeout = 3) + tx_bef = con.transaction_manager(tp_bef) + cur_bef = tx_bef.cursor() + try: + tx_bef.begin() + cur_bef.execute(f"insert /* trace_tag before creating nbk-0 */ into test2(id) values(?) returning rdb$get_context('SYSTEM', 'REPLICATION_SEQUENCE')", (TEST2_VALUE,)) + + # If returned value of rdb$get_context('SYSTEM', 'REPLICATION_SEQUENCE') is then + # message in replication log contains this number PLUS 1, e.g. (for REPLICATION_SEQUENCE = 1): + # "Segment 2 (11534562 bytes) is replicated in 0.934s, preserving (OAT: 10 in segment 2)" + # We store this result in order to check further content of replication log that this + # segment was eventuially deleted (this will mean SUCCESSFUL finish of test): + # + preserved_segment_no = int(cur_bef.fetchone()[0]) + 1 + tx_bef.commit() + except DatabaseError as e: + run_errors_map['main_dml_afte_nbk0_err'] = e.__str__() + + if max(v.strip() for v in run_errors_map.values()): + # Some problem was in just executed statement + pass + else: + ############################################## + ### WAIT UNTIL REPLICA DB BECOMES ACTUAL ### + ############################################## + # ( a: Action, max_allowed_time_for_wait, ddl_ready_query = '', isql_check_script = '', replica_expected_out = ''): + chk_bef_sql = f'set heading off;select count(*) from test2 where id = {TEST2_VALUE};' + watch_replica( act_db_repl, MAX_TIME_FOR_WAIT_DATA_IN_REPLICA, ddl_ready_query = '', isql_check_script = chk_bef_sql, replica_expected_out = '1' ) + + # Must be EMPTY: + run_errors_map['repl_chk_addi_data_err'] = capsys.readouterr().out + + if max(v.strip() for v in run_errors_map.values()): + # Timeout expired: expected data did not appear in replica DB for seconds. + pass + else: + ################################################ + ### REPLICA: CHANGE STATE TO FULL SHUTDOWN ### + ################################################ + act_db_repl.gfix(switches=['-shut', 'full', '-force', '0', act_db_repl.db.dsn], io_enc = locale.getpreferredencoding(), combine_output = True ) + run_errors_map['repl_full_shutdown_err'] = act_db_repl.stdout + act_db_repl.reset() + + if max(v.strip() for v in run_errors_map.values()): + pass + else: + ########################################## + ### M A S T E R: M A K E N B K 0 ## + ########################################## + # DO NOT use 'combine_output = True' here: nbackup produces non-empty output when successfully completes. + act_db_main.nbackup( switches = ['-b', '0', act_db_main.db.dsn, db_nbk0], io_enc = locale.getpreferredencoding(), combine_output = False ) + run_errors_map['main_create_nbk0_err'] = act_db_main.stderr + act_db_main.reset() + + if max(v.strip() for v in run_errors_map.values()): + # Some errors occurres during 'nbackup -b 0 db_main_alias ...' + pass + else: + ############################################################################################## + ### NBK0: FIXUP, SET AS REPLICA, CHANGE STATE TO FULL SHUTDOWN, MOVE TO OLD 'DB_REPL' ### + ############################################################################################## + try: + act_db_repl.nbackup(switches = ['-SEQ', '-F', str(db_nbk0)], combine_output = True, io_enc = locale.getpreferredencoding()) + # act_db_main.svcmgr(switches=['action_nfix', 'dbname', str(db_nbk0)], io_enc = locale.getpreferredencoding()) + with act_db_main.connect_server() as srv: + #srv.database.nfix_database(database = db_nbk0) --> "Internal error when using clumplet API: attempt to store data in dataless clumplet" // see also: core_5085_test.py + srv.database.set_replica_mode(database = db_nbk0, mode = ReplicaMode.READ_ONLY) + srv.database.shutdown(database = db_nbk0, mode = ShutdownMode.FULL, method = ShutdownMethod.FORCED, timeout = 0) + except DatabaseError as e: + run_errors_map['nbk0_make_new_replica_err'] = e.__str__() + + if max(v.strip() for v in run_errors_map.values()): + pass + else: + + ################################## + ### OVERWRITE OLD REPLICA ### + ################################## + shutil.move(db_nbk0, db_info[act_db_repl,'db_full_path']) + + ################################## + ### REPLICA: BRING ONLINE ### + ################################## + act_db_repl.gfix(switches=['-online', act_db_repl.db.dsn], io_enc = locale.getpreferredencoding(), combine_output = True ) + run_errors_map['repl_bring_online_err'] = act_db_repl.stdout + act_db_repl.reset() + + replold_lines = get_repl_log(act_db_main) + if max(v.strip() for v in run_errors_map.values()): + pass + else: + ############################################## + ### MASTER: FINAL ACTION WITHIN TX = OAT ### + ############################################## + try: + tx_oat.commit() + except DatabaseError as e: + run_errors_map['main_oat_fini_err'] = e.__str__() + + if max(v.strip() for v in run_errors_map.values()): + pass + else: + + ############################################################################ + ### CHECK REPL.LOG: WAITING FOR "Transaction is not found" MESSAGE ### + ############################################################################ + # Before fix: "ERROR: Transaction 13 is not found" appeared in the repl log at thios point. + pattern_to_check = re.compile( r'Error:\s+Transaction\s+\d+\s+(is\s+)?not\s+found', re.IGNORECASE ) + run_errors_map['repl_tx_not_found_err'] = watch_repl_log_pattern( act_db_main, pattern_to_check, replold_lines, MAX_TIME_FOR_WAIT_SEGMENT_IN_LOG, consider_found_as_unexpected = True) + + if max(v.strip() for v in run_errors_map.values()): + pass + else: + ############################################## + ### WAIT UNTIL REPLICA DB BECOMES ACTUAL ### + ############################################## + # ( a: Action, max_allowed_time_for_wait, ddl_ready_query = '', isql_check_script = '', replica_expected_out = ''): + chk_oat_sql = 'set heading off;select max(id) from test;' + watch_replica( act_db_repl, MAX_TIME_FOR_WAIT_DATA_IN_REPLICA, ddl_ready_query = '', isql_check_script = chk_oat_sql, replica_expected_out = str(ADD_ROWS_INIT+ADD_ROWS_IN_OAT) ) + # Must be EMPTY: + run_errors_map['repl_chk_fini_err'] = capsys.readouterr().out + + if max(v.strip() for v in run_errors_map.values()): + pass + else: + ####################################################################################### + ### CHECK REPL.LOG: WAITING FOR "DELETING SEGMENT {} AS NO LONGER NEEDED" MESSAGE ### + ####################################################################################### + # VERBOSE: Deleting segment 2 as no longer needed + # NB: before fix message was: "VERBOSE: Deleting segment 2 due to fast forward" + pattern_to_check = re.compile( r'VERBOSE:\s+Deleting\s+segment\s+%d\s+as\s+no\s+longer\s+needed' % preserved_segment_no, re.IGNORECASE ) + run_errors_map['repl_deleted_oat_segm'] = watch_repl_log_pattern( act_db_main, pattern_to_check, replold_lines, MAX_TIME_FOR_WAIT_SEGMENT_IN_LOG) + + if max(v.strip() for v in run_errors_map.values()): + # We had a problem in some of previous steps. + # First, we have to RECREATE both master and slave databases + # (otherwise further execution of this test or other replication-related tests most likely will fail): + run_errors_map['out_reset'] = reset_replication(act_db_main, act_db_repl, db_info[act_db_main,'db_full_path'], db_info[act_db_repl,'db_full_path']) + else: + drop_db_objects(act_db_main, act_db_repl, capsys) + # Must be EMPTY: + run_errors_map['out_drop'] = capsys.readouterr().out + + if max(v.strip() for v in run_errors_map.values()): + print(f'Problem(s) detected, check run_errors_map:') + for k,v in run_errors_map.items(): + if v.strip(): + print(k,':') + print(v.strip()) + print('-' * 40) + + assert '' == capsys.readouterr().out diff --git a/tests/functional/replication/test_oltp_emul_ddl.py b/tests/functional/replication/test_oltp_emul_ddl.py index 763b9e3e..f5289ce6 100644 --- a/tests/functional/replication/test_oltp_emul_ddl.py +++ b/tests/functional/replication/test_oltp_emul_ddl.py @@ -444,6 +444,9 @@ def get_replication_log(a: Action): @pytest.mark.version('>=4.0.1') def test_1(act_db_main: Action, act_db_repl: Action, tmp_oltp_build_sql: Path, tmp_oltp_build_log: Path, capsys): + if act_db_main.is_version('>=6.0'): + pytest.skip('Since #8473 empty columns not allowed') + tmp_oltp_sql_files = [] out_prep, out_init, out_main, out_drop = '', '', '', '' diff --git a/tests/functional/replication/test_uk_violation_in_rw_repl_if_constraint_name_is_used.py b/tests/functional/replication/test_uk_violation_in_rw_repl_if_constraint_name_is_used.py new file mode 100644 index 00000000..86555f26 --- /dev/null +++ b/tests/functional/replication/test_uk_violation_in_rw_repl_if_constraint_name_is_used.py @@ -0,0 +1,541 @@ +#coding:utf-8 + +""" +ID: replication.test_uk_violation_in_rw_repl_if_constraint_name_is_used +ISSUE: https://github.com/FirebirdSQL/firebird/issues/8139 +TITLE: PK/UK violation error raises in RW replica if constraint name is used to check uniqueness instead of index name +DESCRIPTION: + We create table with DDL described here: + https://github.com/FirebirdSQL/firebird/issues/8139#issuecomment-2940164974 + + Then we insert two records: first in REPLICA and after this in master, with same value of column 'ID1'. + After this we have to wait for seconds until record in replica will be updated + and have data that was used in master (i.e. data in replica must be overwritten). + + Message "WARNING: Record being inserted into table TEST already exists, updating instead" must appear + in the replication log at this point. + + Further, we invoke ISQL with executing auxiliary script for drop all DB objects on master (with '-nod' command switch). + After all objects will be dropped, we have to wait again until replica becomes actual with master. + Check that both DB have no custom objects is performed (see UNION-ed query to rdb$ tables + filtering on rdb$system_flag). + + Finally, we extract metadata for master and replica and make comparison. + The only difference in metadata must be 'CREATE DATABASE' statement with different DB names - we suppress it, + thus metadata difference must not be issued. + +NOTES: + [07.06.2025] pzotov + 1. We use 'assert' only at the final point of test, with printing detalization about encountered problem(s). + During all previous steps, we only store unexpected output to variables, e.g.: out_main = capsys.readouterr().out etc. + 2. Related tickets: + https://github.com/FirebirdSQL/firebird/issues/8040 + https://github.com/FirebirdSQL/firebird/issues/8042 + + Thanks to Vlad for explanation related to test implementation. + + Confirmed bug on 6.0.0.792-d90992f + Checked on 6.0.0.797-303e8d4. +""" +import os +import re +import shutil +from difflib import unified_diff +from pathlib import Path +import time + +import pytest +from firebird.qa import * +from firebird.driver import connect, create_database, DbWriteMode, ReplicaMode, ShutdownMode, ShutdownMethod, DatabaseError + +# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings +# from act.files_dir/'test_config.ini': +repl_settings = QA_GLOBALS['replication'] + +MAX_TIME_FOR_WAIT_SEGMENT_IN_LOG = int(repl_settings['max_time_for_wait_segment_in_log']) +MAX_TIME_FOR_WAIT_DATA_IN_REPLICA = int(repl_settings['max_time_for_wait_data_in_replica']) + +MAIN_DB_ALIAS = repl_settings['main_db_alias'] +REPL_DB_ALIAS = repl_settings['repl_db_alias'] +RUN_SWEEP_AT_END = int(repl_settings['run_sweep_at_end']) + +db_main = db_factory( filename = '#' + MAIN_DB_ALIAS, do_not_create = True, do_not_drop = True) +db_repl = db_factory( filename = '#' + REPL_DB_ALIAS, do_not_create = True, do_not_drop = True) + +substitutions = [('Start removing objects in:.*', 'Start removing objects'), + ('Finish. Total objects removed: [1-9]\\d*', 'Finish. Total objects removed'), + ('.* CREATE DATABASE .*', ''), + ('[\t ]+', ' '), + ('FOUND message about replicated segment N .*', 'FOUND message about replicated segment')] + +act_db_main = python_act('db_main', substitutions=substitutions) +act_db_repl = python_act('db_repl', substitutions=substitutions) +tmp_data = temp_file(filename = 'tmp_blob_for_replication.dat') + +#-------------------------------------------- + +def cleanup_folder(p): + # Removed all files and subdirs in the folder

+ # Used for cleanup and when replication must be reset + # in case when any error occurred during test execution. + assert os.path.dirname(p) != p, f"@@@ ABEND @@@ CAN NOT operate in the file system root directory. Check your code!" + + for root, dirs, files in os.walk(p): + for f in files: + # ::: NB ::: 22.12.2023. + # We have to expect that attempt to delete of GUID and (maybe) archived segments can FAIL with + # PermissionError: [WinError 32] The process cannot ... used by another process: /path/to/{GUID} + # Also, we have to skip exception if file (segment) was just deleted by engine + try: + Path(root +'/' + f).unlink(missing_ok = True) + except PermissionError as x: + pass + + for d in dirs: + shutil.rmtree(os.path.join(root, d), ignore_errors = True) + + return os.listdir(p) + +#-------------------------------------------- + +def reset_replication(act_db_main, act_db_repl, db_main_file, db_repl_file): + out_reset = '' + failed_shutdown_db_map = {} # K = 'db_main', 'db_repl'; V = error that occurred when we attempted to change DB state to full shutdown (if it occurred) + + with act_db_main.connect_server() as srv: + + # !! IT IS ASSUMED THAT REPLICATION FOLDERS ARE IN THE SAME DIR AS !! + # DO NOT use 'a.db.db_path' for ALIASED database! + # It will return '.' rather than full path+filename. + + repl_root_path = Path(db_main_file).parent + repl_jrn_sub_dir = repl_settings['journal_sub_dir'] + repl_arc_sub_dir = repl_settings['archive_sub_dir'] + + for f in (db_main_file, db_repl_file): + # Method db.drop() changes LINGER to 0, issues 'delete from mon$att' with suppressing exceptions + # and calls 'db.drop_database()' (also with suppressing exceptions). + # We change DB state to FULL SHUTDOWN instead of call action.db.drop() because + # this is more reliable (it kills all attachments in all known cases and does not use mon$ table) + # + try: + srv.database.shutdown(database = f, mode = ShutdownMode.FULL, method = ShutdownMethod.FORCED, timeout = 0) + + # REMOVE db file from disk: we can safely assume that this can be done because DB in full shutdown state. + ########################### + os.unlink(f) + except DatabaseError as e: + failed_shutdown_db_map[ f ] = e.__str__() + + + # Clean folders repl_journal and repl_archive: remove all files from there. + # NOTE: test must NOT raise unrecoverable error if some of files in these folders can not be deleted. + # Rather, this must be displayed as diff and test must be considered as just failed. + for p in (repl_jrn_sub_dir,repl_arc_sub_dir): + + remained_files = cleanup_folder(repl_root_path/p) + + if remained_files: + out_reset += '\n'.join( (f"Directory '{str(repl_root_path/p)}' remains non-empty. Could not delete file(s):", '\n'.join(remained_files)) ) + + # xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + # xxx r e c r e a t e d b _ m a i n a n d d b _ r e p l xxx + # xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + for a in (act_db_main,act_db_repl): + d = a.db.db_path + failed_shutdown_msg = failed_shutdown_db_map.get( str(d), '' ) + if failed_shutdown_msg: + # we could NOT change state of this database to full shutdown --> we must NOT recreate it. + # Accumulate error messages in OUT arg (for displaying as diff): + # + out_reset += '\n'.join( failed_shutdown_msg ) + else: + try: + dbx = create_database(str(d), user = a.db.user) + dbx.close() + with a.connect_server() as srv: + srv.database.set_write_mode(database = d, mode = DbWriteMode.ASYNC) + srv.database.set_sweep_interval(database = d, interval = 0) + if a == act_db_repl: + srv.database.set_replica_mode(database = d, mode = ReplicaMode.READ_ONLY) + else: + with a.db.connect() as con: + con.execute_immediate('alter database enable publication') + con.execute_immediate('alter database include all to publication') + con.commit() + except DatabaseError as e: + out_reset += e.__str__() + + # Must remain EMPTY: + #################### + return out_reset + + +#-------------------------------------------- + +def wait_for_repl_err( act_db_main: Action, replold_lines, max_allowed_time_for_wait): + + replication_log = act_db_main.home_dir / 'replication.log' + + # ERROR: Database is not in the replica mode + p_warn_upd_instead_ins = re.compile('WARNING: .* already exists, updating instead', re.IGNORECASE) + + found_required_message = False + found_required_line = '' + for i in range(0,max_allowed_time_for_wait): + + time.sleep(1) + + with open(replication_log, 'r') as f: + diff_data = unified_diff( + replold_lines, + f.readlines() + ) + + for k,d in enumerate(diff_data): + if p_warn_upd_instead_ins.search(d): + found_required_message = True + break + + if found_required_message: + break + + if not found_required_message: + # ACHTUNG! This looks weird but we have to either re-read replication log now or wait at least seconds + # if we want to see FULL (actual) content of this log! Otherwise last part of log will be missed. I have no explanations for that :( + with open(replication_log, 'r') as f: + diff_data = unified_diff( + replold_lines, + f.readlines() + ) + unexp_msg = f"Expected pattern '{p_warn_upd_instead_ins.pattern}' - was not found for {max_allowed_time_for_wait} seconds." + repllog_diff = '\n'.join( ( ('%4d ' %i) + r.rstrip() for i,r in enumerate(diff_data) ) ) + result = '\n'.join( (unexp_msg, 'Lines in replication.log:', repllog_diff) ) + else: + result = '' + + return result + +#-------------------------------------------- + +def watch_replica( a: Action, max_allowed_time_for_wait, ddl_ready_query = '', isql_check_script = '', replica_expected_out = ''): + + retcode = 1; + ready_to_check = False + if ddl_ready_query: + with a.db.connect(no_db_triggers = True) as con: + with con.cursor() as cur: + for i in range(0,max_allowed_time_for_wait): + cur.execute(ddl_ready_query) + count_actual = cur.fetchone() + if count_actual: + ready_to_check = True + break + else: + con.rollback() + time.sleep(1) + else: + ready_to_check = True + + if not ready_to_check: + print( f'UNEXPECTED. Initial check query did not return any rows for {max_allowed_time_for_wait} seconds.' ) + print('Initial check query:') + print(ddl_ready_query) + return + + final_check_pass = False + if isql_check_script: + retcode = 0 + for i in range(max_allowed_time_for_wait): + a.reset() + a.expected_stdout = replica_expected_out + a.isql(switches=['-q', '-nod'], input = isql_check_script, combine_output = True) + + if a.return_code: + # "Token unknown", "Name longer than database column size" etc: we have to + # immediately break from this loop because isql_check_script is incorrect! + break + + if a.clean_stdout == a.clean_expected_stdout: + final_check_pass = True + break + if i < max_allowed_time_for_wait-1: + time.sleep(1) + + if not final_check_pass: + print(f'UNEXPECTED. Final check query did not return expected dataset for {max_allowed_time_for_wait} seconds.') + print('Final check query:') + print(isql_check_script) + print('Expected output:') + print(a.clean_expected_stdout) + print('Actual output:') + print(a.clean_stdout) + print(f'ISQL return_code={a.return_code}') + print(f'Waited for {i} seconds') + + a.reset() + + else: + final_check_pass = True + + return + +#-------------------------------------------- + +def drop_db_objects(act_db_main: Action, act_db_repl: Action, capsys): + + # return initial state of master DB: + # remove all DB objects (tables, views, ...): + # + db_main_meta, db_repl_meta = '', '' + for a in (act_db_main,act_db_repl): + if a == act_db_main: + sql_clean = (a.files_dir / 'drop-all-db-objects.sql').read_text() + a.expected_stdout = """ + Start removing objects + Finish. Total objects removed + """ + a.isql(switches=['-q', '-nod'], input = sql_clean, combine_output = True) + + if a.clean_stdout == a.clean_expected_stdout: + a.reset() + else: + print(a.clean_expected_stdout) + a.reset() + break + + # NB: one need to remember that rdb$system_flag can be NOT ONLY 1 for system used objects! + # For example, it has value =3 for triggers that are created to provide CHECK-constraints, + # Custom DB objects always have rdb$system_flag = 0 (or null for some very old databases). + # We can be sure that there are no custom DB objects if following query result is NON empty: + # + ddl_ready_query = """ + select 1 + from rdb$database + where NOT exists ( + select custom_db_object_flag + from ( + select rt.rdb$system_flag as custom_db_object_flag from rdb$triggers rt + UNION ALL + select rt.rdb$system_flag from rdb$relations rt + UNION ALL + select rt.rdb$system_flag from rdb$functions rt + UNION ALL + select rt.rdb$system_flag from rdb$procedures rt + UNION ALL + select rt.rdb$system_flag from rdb$exceptions rt + UNION ALL + select rt.rdb$system_flag from rdb$fields rt + UNION ALL + select rt.rdb$system_flag from rdb$collations rt + UNION ALL + select rt.rdb$system_flag from rdb$generators rt + UNION ALL + select rt.rdb$system_flag from rdb$roles rt + UNION ALL + select rt.rdb$system_flag from rdb$auth_mapping rt + UNION ALL + select 1 from sec$users s + where upper(s.sec$user_name) <> 'SYSDBA' + ) t + where coalesce(t.custom_db_object_flag,0) = 0 + ) + """ + + + ############################################################################## + ### W A I T U N T I L R E P L I C A B E C O M E S A C T U A L ### + ############################################################################## + watch_replica( act_db_repl, MAX_TIME_FOR_WAIT_DATA_IN_REPLICA, ddl_ready_query) + + # Must be EMPTY: + print(capsys.readouterr().out) + + db_main_meta = a.extract_meta(charset = 'utf8', io_enc = 'utf8') + else: + db_repl_meta = a.extract_meta(charset = 'utf8', io_enc = 'utf8') + + + if RUN_SWEEP_AT_END: + # Following sweep was mandatory during 2021...2022. Problem was fixed: + # * for FB 4.x: 26-jan-2023, commit 2ed48a62c60c029cd8cb2b0c914f23e1cb56580a + # * for FB 5.x: 20-apr-2023, commit 5af209a952bd2ec3723d2c788f2defa6b740ff69 + # (log message: 'Avoid random generation of field IDs, respect the user-specified order instead'). + # Until this problem was solved, subsequent runs of this test caused to fail with: + # 'ERROR: Record format with length NN is not found for table TEST' + # + a.gfix(switches=['-sweep', a.db.dsn]) + + # Final point: metadata must become equal: + # + diff_meta = ''.join(unified_diff( \ + [x for x in db_main_meta.splitlines() if 'CREATE DATABASE' not in x], + [x for x in db_repl_meta.splitlines() if 'CREATE DATABASE' not in x]) + ) + # Must be EMPTY: + print(diff_meta) + +#-------------------------------------------- + +@pytest.mark.replication +@pytest.mark.version('>=6.0') +def test_1(act_db_main: Action, act_db_repl: Action, tmp_data: Path, capsys): + + out_prep = out_main = out_log = out_drop = '' + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + replication_log = act_db_main.home_dir / 'replication.log' + replold_lines = [] + with open(replication_log, 'r') as f: + replold_lines = f.readlines() + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + # Obtain full path + filename for DB_MAIN and DB_REPL aliases. + # NOTE: we must NOT use 'a.db.db_path' for ALIASED databases! + # It will return '.' rather than full path+filename. + # Use only con.info.name for that! + # + db_info = {} + for a in (act_db_main, act_db_repl): + with a.db.connect(no_db_triggers = True) as con: + #if a == act_db_main and a.vars['server-arch'] == 'Classic' and os.name != 'nt': + # pytest.skip("Waiting for FIX: 'Engine is shutdown' in replication log for CS. Linux only.") + db_info[a, 'db_full_path'] = con.info.name + db_info[a, 'db_fw_initial'] = con.info.write_mode + + + with act_db_repl.connect_server() as srv: + srv.database.set_replica_mode(database = act_db_repl.db.db_path, mode = ReplicaMode.READ_WRITE) + + # Must be EMPTY: + out_prep = capsys.readouterr().out + if out_prep: + # Some problem raised during change DB header(s) + pass + else: + sql_init = ''' + set bail on; + recreate table test ( + id1 int constraint test_id2_unq unique using index test_id1_unq, + id2 int constraint test_id1_unq unique using index test_id2_unq, + name varchar(10) + ); + commit; + ''' + act_db_main.isql(switches=['-q'], input = sql_init, combine_output = True) + out_prep = act_db_main.clean_stdout + act_db_main.reset() + + if out_prep: + # Some problem raised during init_sql execution + pass + else: + # Query to be used for check that all DB objects present in replica (after last DML statement completed on master DB): + ddl_ready_query = "select 1 from rdb$relations where rdb$relation_name = upper('test')" + ############################################################################## + ### W A I T U N T I L R E P L I C A B E C O M E S A C T U A L ### + ############################################################################## + watch_replica( act_db_repl, MAX_TIME_FOR_WAIT_DATA_IN_REPLICA, ddl_ready_query) + # Must be EMPTY: + out_prep = capsys.readouterr().out + + if out_prep: + # Some problem raised with delivering DDL changes to replica + pass + else: + blob_inserted_hashes = {} + + # NB: first we put data into REPLICA database! + ############################################## + for a in (act_db_repl, act_db_main): + with a.db.connect(no_db_triggers = True) as con: + cur = con.cursor() + dml = 'insert into test(id1, id2, name) values(?, ?, ?)' + if a == act_db_repl: + cur.execute(dml, (1, 2, '1-2')) + else: + # Put data in MASTER. Replication log must contain after this warning (and no errors). + # "WARNING: Record being inserted into table TEST already exists, updating instead" + # Before fix following messages started to appear in replication log: + # Database: + # ERROR: violation of PRIMARY or UNIQUE KEY constraint "TEST_ID2_UNQ" on table "TEST" + # Problematic key value is ("ID1" = 1) + # At segment 2, offset 48 + # + cur.execute(dml, (1, 3, '1-3')) + con.commit() + + # Must be EMPTY: + out_main = capsys.readouterr().out + + if out_main: + # Some problem raised with writing data into replica or master DB: + pass + else: + # No errors must be now. We have to wait now until data from MASTER be delivered to REPLICA + # Query to be used that replica DB contains all expected data (after last DML statement completed on master DB): + isql_check_script = """ + set bail on; + set blob all; + set list on; + set count on; + select + rdb$get_context('SYSTEM','REPLICA_MODE') replica_mode + ,id1 + ,id2 + from test; + """ + + isql_expected_out = f""" + REPLICA_MODE READ-WRITE + ID1 1 + ID2 3 + Records affected: 1 + """ + + ############################################################################## + ### W A I T U N T I L R E P L I C A B E C O M E S A C T U A L ### + ############################################################################## + watch_replica( act_db_repl, MAX_TIME_FOR_WAIT_DATA_IN_REPLICA, '', isql_check_script, isql_expected_out) + # Must be EMPTY: + out_main = capsys.readouterr().out + + ###################################################################### + ### W A I T F O R W A R N I N G I N R E P L . L O G ### + ###################################################################### + out_log = wait_for_repl_err( act_db_main, replold_lines, MAX_TIME_FOR_WAIT_SEGMENT_IN_LOG) + + drop_db_objects(act_db_main, act_db_repl, capsys) + + # Return replica mode to its 'normal' value: READ-ONLY: + with act_db_repl.connect_server() as srv: + srv.database.set_replica_mode(database = act_db_repl.db.db_path, mode = ReplicaMode.READ_ONLY) + + # Must be EMPTY: + out_drop = capsys.readouterr().out + + if [ x for x in (out_prep, out_main, out_log, out_drop) if x.strip() ]: + # We have a problem either with DDL/DML or with dropping DB objects. + # First, we have to RECREATE both master and slave databases + # (otherwise further execution of this test or other replication-related tests most likely will fail): + out_reset = reset_replication(act_db_main, act_db_repl, db_info[act_db_main,'db_full_path'], db_info[act_db_repl,'db_full_path']) + + # Next, we display out_main, out_drop and out_reset: + # + print('Problem(s) detected:') + if out_prep.strip(): + print('out_prep:') + print(out_prep) + if out_main.strip(): + print('out_main:') + print(out_main) + if out_log.strip(): + print('out_log:') + print(out_log) + if out_drop.strip(): + print('out_drop:') + print(out_drop) + if out_reset.strip(): + print('out_reset:') + print(out_reset) + + assert '' == capsys.readouterr().out diff --git a/tests/functional/services/test_role_in_service_attachment.py b/tests/functional/services/test_role_in_service_attachment.py index 11d72181..ab9dee3f 100644 --- a/tests/functional/services/test_role_in_service_attachment.py +++ b/tests/functional/services/test_role_in_service_attachment.py @@ -43,6 +43,7 @@ #expected_stdout = 'SUCCESS: found expected line format in the trace log: :' +@pytest.mark.trace @pytest.mark.version('>=4.0') def test_1(act: Action, tmp_user: User, tmp_role:Role, tmp_trace_cfg: Path, tmp_trace_log: Path, capsys): diff --git a/tests/functional/services/test_user_management.py b/tests/functional/services/test_user_management.py index 537d8084..df67e4b7 100644 --- a/tests/functional/services/test_user_management.py +++ b/tests/functional/services/test_user_management.py @@ -9,8 +9,8 @@ 2) Python firebird-driver functions (from class Server) NB. - User with name 'tmp$test$user$' must NOT present in security_db. - Correctness of adding user is verified by establishing TCP-based attachment to test DB using its login/password. + User with name 'tmp_vaclav' must NOT present in security_db. + Results are verified by attempts to make TCP connection to the test DB using login/password of this user. See doc: https://firebird-driver.readthedocs.io/en/latest/usage-guide.html#user-maintenance @@ -18,77 +18,103 @@ https://firebird-driver.readthedocs.io/en/latest/ref-core.html#serveruserservices FBTEST: functional.services.user_management """ - +import sys import pytest from firebird.qa import * import firebird.driver -from firebird.driver import TPB, Isolation, core as fb_core +from firebird.driver import TPB, Isolation, core as fb_core, DatabaseError + +#sys.stdout.reconfigure(encoding='utf-8') db = db_factory(init = "create sequence g;") act = python_act('db') -test_expected_stdout = """ - POINT: 1 - SEC$USER_NAME: TMP$TEST$USER - SEC$FIRST_NAME: John - SEC$LAST_NAME: Smith - SEC$ADMIN: True - - POINT: 2 - SEC$USER_NAME: TMP$TEST$USER - SEC$FIRST_NAME: Robert - SEC$LAST_NAME: Jackson - SEC$ADMIN: False - - POINT: 3 - SEC$USER_NAME: None - SEC$FIRST_NAME: None - SEC$LAST_NAME: None - SEC$ADMIN: None -""" - @pytest.mark.version('>=3') def test_1(act: Action, capsys): #---------------------------------------------------------- - def print_user_data(con, cur, prepared_sttm): + def print_user_data(con, cur, prepared_sttm, mode, uname): con.commit() con.begin() - cur.execute(prepared_sttm, (TMP_USER_NAME,)) + cur.execute(prepared_sttm, (uname,)) col_hdr=cur.description for r in cur: for i in range(0,len(col_hdr)): print( (col_hdr[i][0] +':').ljust(32), r[i] ) + if mode != 'del': + if cur.affected_rows == 0: + cur.execute('select sec$user_name from sec$users') + existing_users = '\n'.join( [r[0].rstrip() for r in cur.fetchall()] ) + assert False, f"Problem with security.db for {mode=}: could not find expected record with sec$user_name = '{uname}' using charset = {con.charset}, {existing_users=}" + else: + assert cur.affected_rows == 0, f"Problem with security.db for {mode=}: UNEXPECTED record encountered with sec$user_name = '{uname}', {cur.affected_rows=}" + #---------------------------------------------------------- - TMP_USER_NAME = 'tmp$test$user'.upper() - sttm = 'select gen_id(g,1) as point, sec$user_name, sec$first_name, sec$last_name, sec$admin from rdb$database left join sec$users on sec$user_name = ?' + TMP_USER_NAME = 'tmp_vaclav'.upper() + + # ::: NOTE ::: + # > spb.insert_string(SrvUserOption.USER_NAME, user_name, encoding=self._srv().encoding) + # self.vtable.insertString(self, self.status, tag, value.encode(encoding, errors)) + # UnicodeEncodeError: 'ascii' codec can't encode character '\xe1' in position 5: ordinal not in range(128) + #TMP_USER_NAME = '"tmp_Václav"' + + sttm = 'select gen_id(g,1) as point, sec$user_name, sec$first_name, sec$last_name, sec$admin from rdb$database join sec$users on sec$user_name = ?' - with act.db.connect() as con: + with act.db.connect(charset = 'utf8') as con: con.begin() cur = con.cursor() + prepared_sttm = cur.prepare(sttm) with act.connect_server() as srv: svc = fb_core.ServerUserServices(srv) + + # not helped for non-ascii user name: + svc._srv().encoding = con.charset + # print(svc._srv().encoding) + if svc.exists(user_name = TMP_USER_NAME): svc.delete(user_name = TMP_USER_NAME) svc.add( user_name = TMP_USER_NAME, password = '123', first_name = 'John', last_name = 'Smith', admin = True) - print_user_data(con, cur, prepared_sttm) + print_user_data(con, cur, prepared_sttm, 'add', TMP_USER_NAME) # Here we make sure that user actually exists and can make connecttion: - with act.db.connect(user = TMP_USER_NAME, password = '123') as con_check: + with act.db.connect(user = TMP_USER_NAME, password = '123', charset = con.charset) as con_check: pass svc.update( user_name = TMP_USER_NAME, last_name = 'Jackson', admin = False, first_name = 'Robert') - print_user_data(con, cur, prepared_sttm) + print_user_data(con, cur, prepared_sttm, 'upd', TMP_USER_NAME) svc.delete(user_name = TMP_USER_NAME) - print_user_data(con, cur, prepared_sttm) - - act.expected_stdout = test_expected_stdout + print_user_data(con, cur, prepared_sttm, 'del', TMP_USER_NAME) + try: + with act.db.connect(user = TMP_USER_NAME, password = '123') as con_check: + print('UNEXPECTED: user must not exist at this point!') + except DatabaseError as e: + # 335544472 ==> Your user name and password are not defined ... + for x in e.gds_codes: + print(x) + + expected_out = f""" + POINT: 1 + SEC$USER_NAME: {TMP_USER_NAME} + SEC$FIRST_NAME: John + SEC$LAST_NAME: Smith + SEC$ADMIN: True + + POINT: 2 + SEC$USER_NAME: {TMP_USER_NAME} + SEC$FIRST_NAME: Robert + SEC$LAST_NAME: Jackson + SEC$ADMIN: False + + 335544472 + """ + + act.expected_stdout = expected_out act.stdout = capsys.readouterr().out assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/session/test_ext_conn_pool_01.py b/tests/functional/session/test_ext_conn_pool_01.py index e0f80695..8cadd802 100644 --- a/tests/functional/session/test_ext_conn_pool_01.py +++ b/tests/functional/session/test_ext_conn_pool_01.py @@ -73,6 +73,7 @@ # ITER_LOOP_CNT = 3 +@pytest.mark.es_eds @pytest.mark.version('>=4.0') def test_1(act: Action, tmp_user_freq: User, tmp_user_rare: User, tmp_cleaner_role: Role, capsys): @@ -244,14 +245,6 @@ def test_1(act: Action, tmp_user_freq: User, tmp_user_rare: User, tmp_cleaner_ro commit; ''' - ''' - print(sql_init) - act.expected_stdout = '' - act.stdout = capsys.readouterr().out - assert act.clean_stdout == act.clean_expected_stdout - act.reset() - ''' - act.expected_stdout = '' act.isql(switches = ['-q'], input = sql_init, combine_output = True, io_enc = locale.getpreferredencoding()) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/shadow/test_create_01.py b/tests/functional/shadow/test_create_01.py index 195dab2b..ea6be873 100644 --- a/tests/functional/shadow/test_create_01.py +++ b/tests/functional/shadow/test_create_01.py @@ -2,9 +2,15 @@ """ ID: shadow.create-01 -TITLE: CREATE SHADOW +TITLE: CREATE SHADOW: check basic usage DESCRIPTION: -FBTEST: functional.shadow.create_01 +NOTES: + [30.12.2024] pzotov + Splitted expected out for FB 6.x because columns rdb$file_sequence, rdb$file_start and rdb$file_length + have NULLs instead of zeroes, see: + https://github.com/FirebirdSQL/firebird/commit/f0740d2a3282ed92a87b8e0547139ba8efe61173 + ("Wipe out multi-file database support (#8047)") + Checked on 6.0.0.565 """ import pytest @@ -34,9 +40,9 @@ from rdb$files; """ -act = isql_act('db', test_script) +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) -expected_stdout = """ +expected_stdout_5x = """ CHECK_SHD_FILE_NAME OK FILE_SEQUENCE 0 FILE_START 0 @@ -46,8 +52,18 @@ Records affected: 1 """ +expected_stdout_6x = """ + CHECK_SHD_FILE_NAME OK + FILE_SEQUENCE + FILE_START + FILE_LENGTH + FILE_FLAGS 1 + SHADOW_NUMBER 1 + Records affected: 1 +""" + @pytest.mark.version('>=3.0') def test_1(act: Action): - act.expected_stdout = expected_stdout + act.expected_stdout = expected_stdout_5x if act.is_version('<6') else expected_stdout_6x act.execute() assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/shadow/test_create_02.py b/tests/functional/shadow/test_create_02.py index 06828193..2214e1b2 100644 --- a/tests/functional/shadow/test_create_02.py +++ b/tests/functional/shadow/test_create_02.py @@ -2,9 +2,13 @@ """ ID: shadow.create-02 -TITLE: CREATE SHADOW +TITLE: CREATE SHADOW: check usage with presense of 'FILE <...>' clause. DESCRIPTION: -FBTEST: functional.shadow.create_02 +NOTES: + [29.12.2024] pzotov + Added restriction for FB 6.x: this test now must be skipped, see: + https://github.com/FirebirdSQL/firebird/commit/f0740d2a3282ed92a87b8e0547139ba8efe61173 + ("Wipe out multi-file database support (#8047)") """ import pytest @@ -47,7 +51,7 @@ Records affected: 2 """ -@pytest.mark.version('>=3') +@pytest.mark.version('>=3,<6') def test_1(act: Action): act.expected_stdout = expected_stdout act.execute() diff --git a/tests/functional/sqlancer/__init__.py b/tests/functional/sqlancer/__init__.py new file mode 100644 index 00000000..4adc9840 --- /dev/null +++ b/tests/functional/sqlancer/__init__.py @@ -0,0 +1 @@ +# Python module diff --git a/tests/functional/sqlancer/arch_2312_17510_example_01_test.py b/tests/functional/sqlancer/arch_2312_17510_example_01_test.py new file mode 100644 index 00000000..aff2070b --- /dev/null +++ b/tests/functional/sqlancer/arch_2312_17510_example_01_test.py @@ -0,0 +1,48 @@ +#coding:utf-8 + +""" +ID: n/a +ISSUE: https://arxiv.org/pdf/2312.17510 +TITLE: PARTIAL INDICES. NATURAL RIGHT JOIN results in an unexpected "Unknown column" error +DESCRIPTION: + https://arxiv.org/pdf/2312.17510 page #2 listing 1 +NOTES: + [05.06.2025] pzotov + Support for partial indices in FB: + https://github.com/FirebirdSQL/firebird/pull/7257 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + --Listing 1. A bug found by QPG in SQLite due to an incorrect use of an + --index in combination with a JOIN. Given the same SELECT, the left query + --plan is produced if no index is present, while the right one uses the index. + CREATE TABLE t1(a INT, b INT); + CREATE TABLE t2(c INT); + CREATE TABLE t3(d INT); + + INSERT INTO t1(a) VALUES(2); + INSERT INTO t3 VALUES(1); + commit; + + CREATE INDEX i0 ON t2(c) + WHERE c = 3 -- ::: NB ::: partial index + ; + + SELECT * + FROM t2 + RIGHT JOIN t3 ON d <> 0 + LEFT JOIN t1 ON c = 3 + WHERE t1.a <> 0; -- output must be empty resultset +""" + +act = isql_act('db', test_script) + +@pytest.mark.version('>=5.0') +def test_1(act: Action): + act.execute(combine_output = True) + assert act.clean_stdout == '' diff --git a/tests/functional/sqlancer/arch_2312_17510_example_02_test.py b/tests/functional/sqlancer/arch_2312_17510_example_02_test.py new file mode 100644 index 00000000..afa7a483 --- /dev/null +++ b/tests/functional/sqlancer/arch_2312_17510_example_02_test.py @@ -0,0 +1,46 @@ +#coding:utf-8 + +""" +ID: n/a +ISSUE: https://arxiv.org/pdf/2312.17510 +TITLE: Bug in RIGHT JOIN +DESCRIPTION: + https://arxiv.org/pdf/2312.17510 page #7 listing 2 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + CREATE TABLE t1(a CHAR); + CREATE TABLE t2(b CHAR); + CREATE TABLE t3(c CHAR NOT NULL); + CREATE TABLE t4(d CHAR); + + INSERT INTO t2 VALUES('x'); + INSERT INTO t3 VALUES('y'); + + SELECT * + FROM t4 + LEFT JOIN t3 ON TRUE + INNER JOIN t1 ON t3.c='' + RIGHT JOIN t2 ON t3.c='' + WHERE t3.c IS NULL; + +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + act.expected_stdout = """ + D + C + A + B x + """ + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/sqlancer/doi_10_1145_3428279_example_01_test.py b/tests/functional/sqlancer/doi_10_1145_3428279_example_01_test.py new file mode 100644 index 00000000..032e1a87 --- /dev/null +++ b/tests/functional/sqlancer/doi_10_1145_3428279_example_01_test.py @@ -0,0 +1,59 @@ +#coding:utf-8 + +""" +ID: n/a +ISSUE: https://dl.acm.org/doi/pdf/10.1145/3428279 +TITLE: predicate 0 = -0 to incorrectly evaluate to FALSE. +DESCRIPTION: + Manuel Rigger and Zhendong Su + Finding Bugs in Database Systems via Query Partitioning + https://dl.acm.org/doi/pdf/10.1145/3428279 + page 2 listing 1 +NOTES: + [01.06.2025] pzotov + Bug exists on Firebird 3.0.13.33807 (18.04.2025). +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + recreate table t0 ( c0 int ); + recreate table t1 ( c0 double precision ); + commit; + + insert into t0 values (0) ; + insert into t1 values ( -0e0 ); + + -- select (t0.c0 = t1.c0) is true from t0 cross join t1; + set count on; + + select t0.c0 as q1_t0_c0, t1.c0 as q1_t1_c0 + from t0 cross join t1 where t0.c0 = t1.c0 ; -- expected: {0, -0}; found: {} + ---------------------------------------------------------------------- + select t0.c0 as q2_t0_c0, t1.c0 as q2_t1_c0 from t0 cross join t1 where t0.c0 = t1.c0 + union all + select * from t0 cross join t1 where not ( t0.c0 = t1.c0 ) + union all + select * from t0 cross join t1 where ( t0.c0 = t1.c0 ) is null ; -- -- expected: {0, -0}; found: {} + ---------------------------------------------------------------------- +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +@pytest.mark.version('>=4.0') +def test_1(act: Action): + act.expected_stdout = """ + Q1_T0_C0 0 + Q1_T1_C0 -0.000000000000000 + Records affected: 1 + + Q2_T0_C0 0 + Q2_T1_C0 -0.000000000000000 + Records affected: 1 + """ + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/sqlancer/doi_10_1145_3428279_example_02_test.py b/tests/functional/sqlancer/doi_10_1145_3428279_example_02_test.py new file mode 100644 index 00000000..cf09d845 --- /dev/null +++ b/tests/functional/sqlancer/doi_10_1145_3428279_example_02_test.py @@ -0,0 +1,53 @@ +#coding:utf-8 + +""" +ID: n/a +ISSUE: https://dl.acm.org/doi/pdf/10.1145/3428279 +TITLE: Wrong result of UNION DISTINCT +DESCRIPTION: + Manuel Rigger and Zhendong Su + Finding Bugs in Database Systems via Query Partitioning + https://dl.acm.org/doi/pdf/10.1145/3428279 + page 10 listing 2 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + recreate view v0 as select 1 x from rdb$database; + recreate table t0 ( c0 int ); + recreate view v0 as select cast ( t0.c0 as integer ) as c0 from t0; + + insert into t0 ( c0 ) values (0); + + set count on; + + select distinct t0.c0 as q1_table_c0, v0.c0 as q1_view_c0 + from t0 left outer join v0 on v0.c0 >= '0'; -- expected = found = {0|0} + + select t0.c0 as q2_table_c0, v0.c0 as q2_view_c0 from t0 left outer join v0 on v0.c0 >= '0' where true + union + select * from t0 left join v0 on v0.c0 >= '0' where not true + union + select * from t0 left join v0 on v0.c0 >= '0' where true is null ; -- expected: {0|0}, found: {0|null} +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + act.expected_stdout = """ + Q1_TABLE_C0 0 + Q1_VIEW_C0 0 + Records affected: 1 + + Q2_TABLE_C0 0 + Q2_VIEW_C0 0 + Records affected: 1 + """ + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/sqlancer/doi_10_1145_3428279_example_04_test.py b/tests/functional/sqlancer/doi_10_1145_3428279_example_04_test.py new file mode 100644 index 00000000..b5a3a846 --- /dev/null +++ b/tests/functional/sqlancer/doi_10_1145_3428279_example_04_test.py @@ -0,0 +1,54 @@ +#coding:utf-8 + +""" +ID: n/a +ISSUE: https://dl.acm.org/doi/pdf/10.1145/3428279 +TITLE: Wrong result of AVG evaluation as result of SUM / COUNT +DESCRIPTION: + Manuel Rigger and Zhendong Su + Finding Bugs in Database Systems via Query Partitioning + https://dl.acm.org/doi/pdf/10.1145/3428279 + page 11 listing 4 +NOTES: + [01.06.2025] pzotov + Bug exists on Firebird 3.0.13.33807 (18.04.2025): + Statement failed, SQLSTATE = 22003 + Integer overflow. The result of an integer operation caused the most significant bit of the result to carry. +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + + recreate table t0 ( c0 bigint ); + insert into t0 (c0) values (2) ; + insert into t0 (c0) values (9223372036854775807) ; + select avg (t0.c0) as avg_func from t0; + commit; + + select sum (s)/ sum (c) as avg_eval + from ( + select sum ( t0.c0 ) as s , count ( t0.c0 ) as c from t0 where c0 > 0 + union all + select sum ( t0.c0 ) as s , count ( t0.c0 ) as c from t0 where not (c0 > 0) + union all + select sum ( t0.c0 ) as s , count ( t0.c0 ) as c from t0 where c0 is null + ); -- { -4611686018427387903} + commit; + +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +@pytest.mark.version('>=4.0') +def test_1(act: Action): + act.expected_stdout = """ + AVG_FUNC 4611686018427387904 + AVG_EVAL 4611686018427387904 + """ + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/sqlancer/doi_10_1145_3428279_example_06_test.py b/tests/functional/sqlancer/doi_10_1145_3428279_example_06_test.py new file mode 100644 index 00000000..38a82836 --- /dev/null +++ b/tests/functional/sqlancer/doi_10_1145_3428279_example_06_test.py @@ -0,0 +1,38 @@ +#coding:utf-8 + +""" +ID: n/a +ISSUE: https://dl.acm.org/doi/pdf/10.1145/3428279 +TITLE: Failed to fetch a row from a view. +DESCRIPTION: + Manuel Rigger and Zhendong Su + Finding Bugs in Database Systems via Query Partitioning + https://dl.acm.org/doi/pdf/10.1145/3428279 + page 12 listing 6 +NOTES: +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + recreate table t0 (c0 int); + recreate view v0 as select t0.c0, true as c1 from t0; + insert into t0 values (0); + select v0.c0 from v0 cross join t0 where v0.c1; + commit; + +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + act.expected_stdout = """ + C0 0 + """ + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/sqlancer/doi_10_1145_3428279_example_09_test.py b/tests/functional/sqlancer/doi_10_1145_3428279_example_09_test.py new file mode 100644 index 00000000..3b1146b9 --- /dev/null +++ b/tests/functional/sqlancer/doi_10_1145_3428279_example_09_test.py @@ -0,0 +1,41 @@ +#coding:utf-8 + +""" +ID: n/a +ISSUE: https://dl.acm.org/doi/pdf/10.1145/3428279 +TITLE: GROUP BY ignores COLLATE with case insensitive attribute +DESCRIPTION: + Manuel Rigger and Zhendong Su + Finding Bugs in Database Systems via Query Partitioning + https://dl.acm.org/doi/pdf/10.1145/3428279 + page 13 listing 9 +NOTES: +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + create collation name_coll for utf8 from unicode case insensitive; + create domain dm_test varchar(1) character set utf8 collate name_coll; + + create table t0 (c0 dm_test); + insert into t0 (c0) values ( 'a'); + insert into t0 (c0) values ( 'A'); + select count(*) as grouping_cnt from ( + select t0.c0 from t0 group by t0.c0 + ); +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + act.expected_stdout = """ + GROUPING_CNT 1 + """ + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/sqlancer/doi_10_1145_3428279_example_10_test.py b/tests/functional/sqlancer/doi_10_1145_3428279_example_10_test.py new file mode 100644 index 00000000..7b7fe260 --- /dev/null +++ b/tests/functional/sqlancer/doi_10_1145_3428279_example_10_test.py @@ -0,0 +1,37 @@ +#coding:utf-8 + +""" +ID: n/a +ISSUE: https://dl.acm.org/doi/pdf/10.1145/3428279 +TITLE: Unnexpectedly optimized VARIANCE(0) to FALSE +DESCRIPTION: + Manuel Rigger and Zhendong Su + Finding Bugs in Database Systems via Query Partitioning + https://dl.acm.org/doi/pdf/10.1145/3428279 + page 14 listing 10 +NOTES: +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + create table t0 (c0 int); + insert into t0 (c0) values (0); + select t0.c0 + from t0 + group by t0.c0 + having not ( (select var_pop(0) from rdb$database where false) is null ); +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +@pytest.mark.version('>=4.0') +def test_1(act: Action): + act.expected_stdout = """ + """ + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/sqlancer/doi_10_1145_3428279_example_11_test.py b/tests/functional/sqlancer/doi_10_1145_3428279_example_11_test.py new file mode 100644 index 00000000..5588e6cc --- /dev/null +++ b/tests/functional/sqlancer/doi_10_1145_3428279_example_11_test.py @@ -0,0 +1,53 @@ +#coding:utf-8 + +""" +ID: n/a +ISSUE: https://dl.acm.org/doi/pdf/10.1145/3428279 +TITLE: Non-deterministic output when using MAX() function +DESCRIPTION: + Manuel Rigger and Zhendong Su + Finding Bugs in Database Systems via Query Partitioning + https://dl.acm.org/doi/pdf/10.1145/3428279 + page 14 listing 11 +NOTES: +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + recreate table t0 ( c0 int ); + recreate table t1 ( c0 varchar(100) ); + insert into t1 values (0.9201898334673894); + insert into t1 values (0); + insert into t0 values (0); + + select * + from t0 cross join t1 + group by t0.c0, t1.c0 + having t1.c0 != max(t1.c0) + + UNION ALL + + select * + from t0 cross join t1 + group by t0.c0, t1.c0 + having not t1.c0 > max (t1.c0) + ; +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + act.expected_stdout = """ + C0 0 + C0 0 + C0 0 + C0 0.9201898334673894 + """ + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/sqlancer/doi_10_1145_3428279_example_15_test.py b/tests/functional/sqlancer/doi_10_1145_3428279_example_15_test.py new file mode 100644 index 00000000..4a4ae707 --- /dev/null +++ b/tests/functional/sqlancer/doi_10_1145_3428279_example_15_test.py @@ -0,0 +1,38 @@ +#coding:utf-8 + +""" +ID: n/a +ISSUE: https://dl.acm.org/doi/pdf/10.1145/3428279 +TITLE: Wrong evaluation of MIN when bitwise shift is applied to the source value +DESCRIPTION: + Manuel Rigger and Zhendong Su + Finding Bugs in Database Systems via Query Partitioning + https://dl.acm.org/doi/pdf/10.1145/3428279 + page 15 listing 15 +NOTES: +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + recreate table t0 ( c0 int ); + insert into t0 values (-1) ; + commit; + select min(bin_shl(cast(c0 as bigint ),63)) as min_shl_63 from t0; + select min(bin_shl(cast(c0 as int128 ), 127)) as min_shl_127 from t0; +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +@pytest.mark.version('>=5.0') +def test_1(act: Action): + act.expected_stdout = """ + MIN_SHL_63 -9223372036854775808 + MIN_SHL_127 -170141183460469231731687303715884105728 + """ + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/sqlancer/doi_10_1145_3428279_example_17_test.py b/tests/functional/sqlancer/doi_10_1145_3428279_example_17_test.py new file mode 100644 index 00000000..bf805c01 --- /dev/null +++ b/tests/functional/sqlancer/doi_10_1145_3428279_example_17_test.py @@ -0,0 +1,34 @@ +#coding:utf-8 + +""" +ID: n/a +ISSUE: https://dl.acm.org/doi/pdf/10.1145/3428279 +TITLE: Conversion of character string to boolean value +DESCRIPTION: + Manuel Rigger and Zhendong Su + Finding Bugs in Database Systems via Query Partitioning + https://dl.acm.org/doi/pdf/10.1145/3428279 + page 23 listing 17 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + recreate table t0 (c0 boolean); + insert into t0 values (false); + select * from t0 where not (c0 != 'true' and c0); +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +@pytest.mark.version('>=4.0') +def test_1(act: Action): + act.expected_stdout = """ + C0 + """ + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/sqlancer/doi_10_1145_3428279_example_18_test.py b/tests/functional/sqlancer/doi_10_1145_3428279_example_18_test.py new file mode 100644 index 00000000..818271b3 --- /dev/null +++ b/tests/functional/sqlancer/doi_10_1145_3428279_example_18_test.py @@ -0,0 +1,40 @@ +#coding:utf-8 + +""" +ID: n/a +ISSUE: https://dl.acm.org/doi/pdf/10.1145/3428279 +TITLE: Comparison of string and numeric literals +DESCRIPTION: + Manuel Rigger and Zhendong Su + Finding Bugs in Database Systems via Query Partitioning + https://dl.acm.org/doi/pdf/10.1145/3428279 + page 23 listing 18 +NOTES: + [02.06.2025] pzotov + This test issues only ONE row ('C0 -1') which differs from expected result shown in the source. + Sent report to dimitr et al. +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set bail on; + set list on; + recreate table t0 (c0 varchar(2) unique); + insert into t0 values (-1); + insert into t0 values (-2); + select * from t0 where c0 >= -1; +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + act.expected_stdout = """ + C0 -1 + """ + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/sqlancer/osdi20_rigger_example_01_test.py b/tests/functional/sqlancer/osdi20_rigger_example_01_test.py new file mode 100644 index 00000000..95db4db4 --- /dev/null +++ b/tests/functional/sqlancer/osdi20_rigger_example_01_test.py @@ -0,0 +1,40 @@ +#coding:utf-8 + +""" +ID: n/a +ISSUE: https://www.usenix.org/system/files/osdi20-rigger.pdf +TITLE: Partial index must not use incorrect assumption that 'c0 IS NOT 1' implied 'c0 NOT NULL' +DESCRIPTION: + https://www.usenix.org/system/files/osdi20-rigger.pdf + page 3 listing 1 +NOTES: + [05.06.2025] pzotov + Support for partial indices in FB: + https://github.com/FirebirdSQL/firebird/pull/7257 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + recreate table t0 ( c0 int ); + create index i0 on t0 (c0) where c0 is not null; + insert into t0 values (0); + insert into t0 values (1); + insert into t0 values (null); + select c0 from t0 where c0 is distinct from 1; +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +@pytest.mark.version('>=5.0') +def test_1(act: Action): + act.expected_stdout = """ + C0 0 + C0 + """ + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/sqlancer/osdi20_rigger_example_04_test.py b/tests/functional/sqlancer/osdi20_rigger_example_04_test.py new file mode 100644 index 00000000..8b0b034e --- /dev/null +++ b/tests/functional/sqlancer/osdi20_rigger_example_04_test.py @@ -0,0 +1,55 @@ +#coding:utf-8 + +""" +ID: n/a +ISSUE: https://www.usenix.org/system/files/osdi20-rigger.pdf +TITLE: Incorrect result for DISTINCT. +DESCRIPTION: + https://www.usenix.org/system/files/osdi20-rigger.pdf + page 10 listing 4 + + COULD NOT REPRODUCE PROPER (EXPECTED) REQULT. SOURCE QUERY SYNTAX SEEMS WEIRD! +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + -- set list on; + recreate table t0 (c0 int, c1 int, c2 int, c3 int /* generated by default as identity */, unique (c3 ,c2 )); + insert into t0 ( c2 ) values(0); + insert into t0 ( c2 ) values(0); + insert into t0 ( c2 ) values(0); + insert into t0 ( c2 ) values(0); + insert into t0 ( c2 ) values(0); + insert into t0 ( c2 ) values(0); + insert into t0 ( c2 ) values(0); + insert into t0 ( c2 ) values(0); + insert into t0 ( c2 ) values(0); + insert into t0 ( c2 ) values(0); + insert into t0 ( c2 ) values(null); + insert into t0 ( c2 ) values(1); + insert into t0 ( c2 ) values(0); + + update t0 set c1 = 0; + insert into t0 ( c0 ) values (0); + insert into t0 ( c0 ) values (0); + insert into t0 ( c0 ) values (null); + insert into t0 ( c0 ) values (0); + + update t0 set c2 = 1; + select distinct * from t0 where c2 = 1; + +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +@pytest.mark.skip("Could not reproduce expected output. Source query syntax seems weird") +@pytest.mark.version('>=3.0') +def test_1(act: Action): + act.expected_stdout = """ + """ + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/sqlancer/osdi20_rigger_example_05_test.py b/tests/functional/sqlancer/osdi20_rigger_example_05_test.py new file mode 100644 index 00000000..9173496f --- /dev/null +++ b/tests/functional/sqlancer/osdi20_rigger_example_05_test.py @@ -0,0 +1,35 @@ +#coding:utf-8 + +""" +ID: n/a +ISSUE: https://www.usenix.org/system/files/osdi20-rigger.pdf +TITLE: Incorrect result of LIKE. +DESCRIPTION: + https://www.usenix.org/system/files/osdi20-rigger.pdf + page 10 listing 5 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + create collation name_coll for utf8 from unicode case insensitive; + create domain dm_test varchar(2) character set utf8 collate name_coll; + + create table t0 (c0 dm_test unique); + insert into t0 (c0) values ( './'); + select * from t0 where c0 like './'; +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + act.expected_stdout = """ + C0 ./ + """ + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/sqlancer/osdi20_rigger_example_12_test.py b/tests/functional/sqlancer/osdi20_rigger_example_12_test.py new file mode 100644 index 00000000..bdc48c58 --- /dev/null +++ b/tests/functional/sqlancer/osdi20_rigger_example_12_test.py @@ -0,0 +1,48 @@ +#coding:utf-8 + +""" +ID: n/a +ISSUE: https://www.usenix.org/system/files/osdi20-rigger.pdf +TITLE: Unexpected null value when index is used +DESCRIPTION: + https://www.usenix.org/system/files/osdi20-rigger.pdf + page 11 listing 12 +NOTES: + [05.06.2025] pzotov + page_size = 32K is used in this test. +""" + +import pytest +from firebird.qa import * + +db = db_factory(page_size = 32768) + +test_script = """ + set list on; + recreate table t0 (c0 varchar(8183)); + insert into t0 (c0) values('b'); + insert into t0 (c0) values('a'); + + insert into t0 (c0) values (null); + update t0 set c0 = 'a'; + commit; + + create index i0 on t0 (c0); + + set count on; + select * from t0 where 'baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' > t0.c0 ; -- error : found unexpected null value in index " i0 " + commit; +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +@pytest.mark.version('>=4.0') +def test_1(act: Action): + act.expected_stdout = """ + C0 a + C0 a + C0 a + Records affected: 3 + """ + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/sqlancer/stonedb_misc_examples_test.py b/tests/functional/sqlancer/stonedb_misc_examples_test.py new file mode 100644 index 00000000..76ccef6a --- /dev/null +++ b/tests/functional/sqlancer/stonedb_misc_examples_test.py @@ -0,0 +1,131 @@ +#coding:utf-8 + +""" +ID: n/a +ISSUE: https://docs.google.com/document/d/1N-oUGVATV0l6tG87uOtPNmfLS7g_fuo7HIckFobD-Yo/edit?tab=t.0 +TITLE: Bugs Found in StoneDB by SQLancer +DESCRIPTION: + This test contains several tiny examples provided in "GSoC 2023: Midterm Report on Support of StoneDB" +NOTES: + See also: + https://sqlancer.github.io/blog/gsoc-sqlancer-midterm-zhenglin/ +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + -- https://github.com/stoneatom/stonedb/issues/1941 + -- StoneDB will crash when executing the command: + set heading off; + select 'stonedb_1941' as msg from rdb$database; + recreate table t0 (c0 int); + SELECT * + FROM t0 + GROUP BY t0.c0 + HAVING (t0.c0 IS NULL) + UNION ALL + SELECT * + FROM t0 + GROUP BY t0.c0 + HAVING (NOT (t0.c0 IS NULL)) + UNION ALL + SELECT * + FROM t0 + GROUP BY t0.c0 + HAVING (((t0.c0 IS NULL)) IS NULL); + commit; + --################################################### + -- https://github.com/stoneatom/stonedb/issues/1952 + -- StoneDB crash: PRIMARY KEY, HAVING, IS NULL + select 'stonedb_1952' as msg from rdb$database; + RECREATE TABLE t0(c0 INT PRIMARY KEY); + -- NB: mysql *allows* such query and issues empty resultset. + -- FB raises "Invalid expression in the select list": + SELECT * FROM t0 HAVING (t0.c0 IS NULL); + commit; + --################################################### + -- https://github.com/stoneatom/stonedb/issues/1954 + -- StoneDB crash: HAVING, IS NULL + select 'stonedb_1954' as msg from rdb$database; + RECREATE TABLE t0(c0 INT PRIMARY KEY); + -- NB: mysql *allows* such query and issues empty resultset. + -- FB raises "Invalid expression in the select list": + SELECT c0 FROM t0 HAVING (1 IS NULL); + commit; + + --################################################### + -- https://github.com/stoneatom/stonedb/issues/1949 + -- StoneDB crash: HAVING, IS NULL + select 'stonedb_1949' as msg from rdb$database; + RECREATE TABLE t0(c0 blob); + INSERT INTO t0 DEFAULT values; + DELETE FROM t0; + INSERT INTO t0 DEFAULT values; + SELECT * FROM t0; + commit; + --################################################### + -- https://github.com/stoneatom/stonedb/issues/1950 + -- query result wrong: CASE WHEN THEN ELSE + select 'stonedb_1950' as msg from rdb$database; + RECREATE TABLE t0(c0 INT); + INSERT INTO t0 default values; + SELECT * FROM t0 WHERE (CASE (t0.c0 IN (t0.c0)) WHEN TRUE THEN 'TRUE' ELSE 'FALSE' END) = true; + commit; + --################################################### + -- https://github.com/stoneatom/stonedb/issues/1955 + -- query result wrong: ALTER, DEFAULT + select 'stonedb_1955' as msg from rdb$database; + RECREATE TABLE t0(c0 INT) ; + INSERT INTO t0 DEFAULT VALUES; + COMMIT; + ALTER TABLE t0 ADD c1 INT DEFAULT 1; + INSERT INTO t0 DEFAULT VALUES; + SELECT * FROM t0 order by c0,c1 nulls first; + commit; + + --################################################### + -- https://docs.google.com/document/d/1N-oUGVATV0l6tG87uOtPNmfLS7g_fuo7HIckFobD-Yo/edit?tab=t.0 + -- BUGS OR CRASHES FOUND BUT NOT REPORTED + -- query result wrong: >=, IS NULL + -- expected empty set: + select 'stonedb_non_reported_01' as msg from rdb$database; + RECREATE TABLE t0(c0 INT) ; + INSERT INTO t0 default VALUES; + SELECT t0.c0 FROM t0 WHERE (('false')>=(((t0.c0) IS NULL))); +""" + +act = isql_act('db', test_script, substitutions=[('[ \t]+', ' ')]) + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + act.expected_stdout = """ + stonedb_1941 + + stonedb_1952 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Invalid expression in the select list (not contained in either an aggregate function or the GROUP BY clause) + + stonedb_1954 + Statement failed, SQLSTATE = 42000 + Dynamic SQL Error + -SQL error code = -104 + -Invalid expression in the select list (not contained in either an aggregate function or the GROUP BY clause) + + stonedb_1949 + + + stonedb_1950 + + stonedb_1955 + + 1 + + stonedb_non_reported_01 + """ + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/sqlancer/tidb_15844_test.py b/tests/functional/sqlancer/tidb_15844_test.py new file mode 100644 index 00000000..80f1c289 --- /dev/null +++ b/tests/functional/sqlancer/tidb_15844_test.py @@ -0,0 +1,28 @@ +#coding:utf-8 + +""" +ID: 15844 +ISSUE: https://github.com/pingcap/tidb/issues/15844 +TITLE: NATURAL RIGHT JOIN results in an unexpected "Unknown column" error +DESCRIPTION: + https://github.com/sqlancer/sqlancer/blob/main/CONTRIBUTING.md#unfixed-bugs + https://github.com/sqlancer/sqlancer/blob/4c20a94b3ad2c037e1a66c0b637184f8c20faa7e/src/sqlancer/tidb/TiDBBugs.java +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + CREATE TABLE t0(c0 boolean); + CREATE TABLE t1(c0 boolean); + SELECT t0.c0 FROM t0 NATURAL RIGHT JOIN t1 WHERE t1.c0; -- Unknown column 't0.c0' in 'field list' +""" + +act = isql_act('db', test_script) + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + act.execute(combine_output = True) + assert act.clean_stdout == '' diff --git a/tests/functional/sqlancer/tidb_16028_test.py b/tests/functional/sqlancer/tidb_16028_test.py new file mode 100644 index 00000000..26b572ef --- /dev/null +++ b/tests/functional/sqlancer/tidb_16028_test.py @@ -0,0 +1,28 @@ +#coding:utf-8 + +""" +ID: 16028 +ISSUE: https://github.com/pingcap/tidb/issues/16028 +TITLE: Incorrect result when comparing a FLOAT/DOUBLE UNSIGNED with a negative number +DESCRIPTION: + https://github.com/sqlancer/sqlancer/blob/main/CONTRIBUTING.md#unfixed-bugs + https://github.com/sqlancer/sqlancer/blob/4c20a94b3ad2c037e1a66c0b637184f8c20faa7e/src/sqlancer/tidb/TiDBBugs.java +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + CREATE TABLE t0(c0 double precision unique); + INSERT INTO t0(c0) VALUES (0); + SELECT * FROM t0 WHERE t0.c0 = -1; -- expected: {}, actual: {0} +""" + +act = isql_act('db', test_script) + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + act.execute(combine_output = True) + assert act.clean_stdout == '' diff --git a/tests/functional/sqlancer/tidb_35522_test.py b/tests/functional/sqlancer/tidb_35522_test.py new file mode 100644 index 00000000..3ca0b244 --- /dev/null +++ b/tests/functional/sqlancer/tidb_35522_test.py @@ -0,0 +1,28 @@ +#coding:utf-8 + +""" +ID: n/a +ISSUE: https://github.com/pingcap/tidb/issues/35522 +TITLE: incorrect unresolved column when using natural join +DESCRIPTION: + https://github.com/sqlancer/sqlancer/blob/main/CONTRIBUTING.md#unfixed-bugs + https://github.com/sqlancer/sqlancer/blob/4c20a94b3ad2c037e1a66c0b637184f8c20faa7e/src/sqlancer/tidb/TiDBBugs.java +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + CREATE TABLE t0(c0 CHAR); + CREATE TABLE t1(c0 CHAR); + SELECT t1.c0 FROM t1 NATURAL RIGHT JOIN t0 WHERE true IS NULL; -- ERROR 1054 (42S22) at line 4: Unknown column 't1.c0' in 'field list' +""" + +act = isql_act('db', test_script) + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + act.execute(combine_output = True) + assert act.clean_stdout == '' diff --git a/tests/functional/sqlancer/tidb_challenge_19_test.py b/tests/functional/sqlancer/tidb_challenge_19_test.py new file mode 100644 index 00000000..6c6af3ee --- /dev/null +++ b/tests/functional/sqlancer/tidb_challenge_19_test.py @@ -0,0 +1,31 @@ +#coding:utf-8 + +""" +ID: n/a +ISSUE: https://github.com/tidb-challenge-program/bug-hunting-issue/issues/19 +TITLE: Incorrect result for LEFT JOIN and CASE operator +DESCRIPTION: + https://github.com/sqlancer/sqlancer/blob/main/CONTRIBUTING.md#unfixed-bugs + https://github.com/sqlancer/sqlancer/blob/4c20a94b3ad2c037e1a66c0b637184f8c20faa7e/src/sqlancer/tidb/TiDBBugs.java +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + CREATE TABLE t0(c0 INT); + CREATE TABLE t1(c0 INT); + INSERT INTO t0 VALUES (0); + INSERT INTO t1 VALUES (0); + SELECT * FROM t1 LEFT JOIN t0 ON t0.c0 = t1.c0 WHERE (CASE t0.c0 WHEN 0 THEN t1.c0 ELSE 1 END) <> 0; -- expected: {}, actual: {0|NULL} +""" + +act = isql_act('db', test_script) + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + act.execute(combine_output = True) + assert act.clean_stdout == '' diff --git a/tests/functional/sqlancer/tidb_challenge_48_test.py b/tests/functional/sqlancer/tidb_challenge_48_test.py new file mode 100644 index 00000000..25888b99 --- /dev/null +++ b/tests/functional/sqlancer/tidb_challenge_48_test.py @@ -0,0 +1,29 @@ +#coding:utf-8 + +""" +ID: n/a +ISSUE: https://github.com/tidb-challenge-program/bug-hunting-issue/issues/19 +TITLE: UNIQUE constraint on DECIMAL/floating-point columns causes incorrect result for NULL in AND +DESCRIPTION: + https://github.com/sqlancer/sqlancer/blob/main/CONTRIBUTING.md#unfixed-bugs + https://github.com/sqlancer/sqlancer/blob/4c20a94b3ad2c037e1a66c0b637184f8c20faa7e/src/sqlancer/tidb/TiDBBugs.java +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + CREATE TABLE t0(c0 DOUBLE PRECISION UNIQUE); + INSERT INTO t0(c0) VALUES (NULL); + SELECT t0.c0 FROM t0 WHERE NOT (t0.c0 is null AND true); -- expected: {}, actual: {NULL} +""" + +act = isql_act('db', test_script) + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + act.execute(combine_output = True) + assert act.clean_stdout == '' diff --git a/tests/functional/syspriv/test_access_shutdown_database.py b/tests/functional/syspriv/test_access_shutdown_database.py index c2eb1e31..554516b5 100644 --- a/tests/functional/syspriv/test_access_shutdown_database.py +++ b/tests/functional/syspriv/test_access_shutdown_database.py @@ -62,6 +62,7 @@ -At block line """ +@pytest.mark.es_eds @pytest.mark.version('>=4.0') def test_1(act: Action, tmp_user: User, tmp_role:Role, capsys): diff --git a/tests/functional/syspriv/test_monitor_any_attachment.py b/tests/functional/syspriv/test_monitor_any_attachment.py index 0f26491c..87d41696 100644 --- a/tests/functional/syspriv/test_monitor_any_attachment.py +++ b/tests/functional/syspriv/test_monitor_any_attachment.py @@ -139,6 +139,7 @@ Records affected: 1 """ +@pytest.mark.es_eds @pytest.mark.version('>=4.0') def test_1(act: Action, test_user, test_role): act.expected_stdout = expected_stdout diff --git a/tests/functional/syspriv/test_trace_any_attachment.py b/tests/functional/syspriv/test_trace_any_attachment.py index 77a74b8e..bcb43483 100644 --- a/tests/functional/syspriv/test_trace_any_attachment.py +++ b/tests/functional/syspriv/test_trace_any_attachment.py @@ -29,6 +29,7 @@ act = python_act('db') +@pytest.mark.trace @pytest.mark.version('>=4.0') def test_1(act: Action, tmp_user: User, tmp_role: Role, tmp_usr2: User, capsys): diff --git a/tests/functional/tabloid/test_3123a5a0.py b/tests/functional/tabloid/test_3123a5a0.py new file mode 100644 index 00000000..4fa21fa8 --- /dev/null +++ b/tests/functional/tabloid/test_3123a5a0.py @@ -0,0 +1,54 @@ +#coding:utf-8 + +""" +ID: n/a +ISSUE: None +TITLE: Check ability to reuse internal connection by EXECUTE STATEMENT. +DESCRIPTION: +NOTES: +""" + +import pytest +from firebird.qa import * + +test_sql = f""" + set list on; + set count on; + set term ^; + execute block returns(sttm_state smallint) as + declare cnt smallint; + declare stm varchar(256) = 'select /* trace_me1 */ count(*) from rdb$relation_fields where rdb$relation_name = ?'; + begin + for + select trim(rdb$relation_name) as rel_name + from rdb$relations r + where r.rdb$system_flag = 1 and r.rdb$relation_name starting with 'RDB$' + order by 1 rows 5 + as cursor c + do + execute statement (stm) (c.rel_name) into cnt; + -------------------------------------------------- + for + select /* trace_me2 */ s.mon$state + from mon$statements s + where mon$sql_text NOT containing 'execute block' + into sttm_state + do + suspend; + end + ^ +""" + +db = db_factory() +act = isql_act('db', test_sql, substitutions=[('[ \t]+', ' ')]) + +@pytest.mark.version('>=3') +def test_1(act: Action): + + act.expected_stdout = """ + STTM_STATE 0 + Records affected: 1 + """ + act.execute(combine_output = True) + + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/tabloid/test_35f56933.py b/tests/functional/tabloid/test_35f56933.py new file mode 100644 index 00000000..9239c3d3 --- /dev/null +++ b/tests/functional/tabloid/test_35f56933.py @@ -0,0 +1,68 @@ +#coding:utf-8 + +""" +ID: n/a +ISSUE: https://github.com/FirebirdSQL/firebird/commit/35f56933306d9d486a5c66da8f85b4be214860d9 +TITLE: Fixed cardinality mistake for invariant booleans +DESCRIPTION: +NOTES: + [18.11.2024] pzotov + 1. No ticket has been created for this test. + 2. Currently one may see cardinality to nodes of explained plan only in the output of rdb$sql.explain() + 3. Before fix, last two nodes of query like 'select ... from where 1=1' had different values + of cardinality. If cardinality for last node was is then for node it was wrongly evaluated + as power(C,2). After fix these two values must be the same. + + Thanks to dimitr for the explaiantion on implementing the test. + + Confirmed problem on 6.0.0.520. + Checked on 6.0.0.532 -- all fine. +""" + +import pytest +from firebird.qa import * + +test_sql = f""" + set list on; + recreate sequence g; + recreate table test(id int primary key); + insert into test select gen_id(g,1) from rdb$types,rdb$types rows 1000; + commit; + select + t.access_path + ,iif( count(distinct t.cardinality)over() = 1 + ,'EXPECTED: THE SAME.' + ,'UNEXPECTED: min = ' || min(t.cardinality)over() || ', max=' || max(t.cardinality)over() + ) as cardinality_values + from ( + select + p.plan_line + ,p.record_source_id + ,p.parent_record_source_id + ,p.level + ,p.cardinality + ,cast(p.access_path as varchar(30)) as access_path + ,max(p.plan_line)over() - p.plan_line as mx + from rdb$sql.explain('select count(*) from test where 1=1') as p + ) t + where t.mx in (0,1) + ; +""" + +db = db_factory() +act = isql_act('db', test_sql, substitutions=[('[ \t]+', ' ')]) + + +@pytest.mark.version('>=6') +def test_1(act: Action): + + act.expected_stdout = """ + ACCESS_PATH -> Filter (preliminary) + CARDINALITY_VALUES EXPECTED: THE SAME. + + ACCESS_PATH -> Table "TEST" Full Scan + CARDINALITY_VALUES EXPECTED: THE SAME. + """ + act.execute(combine_output = True) + + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/tabloid/test_3b372197.py b/tests/functional/tabloid/test_3b372197.py new file mode 100644 index 00000000..2e4e7ae1 --- /dev/null +++ b/tests/functional/tabloid/test_3b372197.py @@ -0,0 +1,194 @@ +#coding:utf-8 + +""" +ID: None +ISSUE: https://github.com/FirebirdSQL/firebird/issues/6798 +TITLE: built-in functions UNICODE_CHAR and UNICODE_VAL +DESCRIPTION: + Test verifies ability to call UNICODE_CHAR/UNICODE_VAL for each code point + in Unicode ranges defined in https://jrgraphix.net/r/Unicode/, except following: + (0xD800, 0xDB7F), # High Surrogates + (0xDB80, 0xDBFF), # High Private Use Surrogates + (0xDC00, 0xDFFF), # Low Surrogates + Result of UNICODE_VAL(UNICODE_CHAR()) must be for all code points. + Commit in FB 5.x (14-may-2021): + https://github.com/FirebirdSQL/firebird/commit/3b372197e4bec60842a7ca974a07546858b6dd30 +NOTES: + [02.09.2024] pzotov + Test duration on Windows: about 205 seconds. + Checked on 6.0.0.446, 5.0.2.1487 +""" +import pytest +from firebird.qa import * + +init_sql = """ + set term ^; + create procedure sp_get_unicode_char(a_code_point int) returns(u char(1) character set utf8) + as + begin + u = unicode_char(a_code_point); + suspend; + end + ^ + create procedure sp_get_unicode_val(u char(1) character set utf8) returns(code_point int) + as + begin + code_point = unicode_val(u); + suspend; + end + ^ + set term ;^ + commit; +""" +db = db_factory(init = init_sql) +act = python_act('db', substitutions=[('[ \t]+', ' ')]) + +#------------------------------------------------ + +@pytest.mark.version('>=5.0.2') +def test_1(act: Action, capsys): + + # https://jrgraphix.net/r/Unicode/ + UNICODE_RANGES_MAP = { + (0x0020, 0x007F) : 'Basic Latin', + (0x00A0, 0x00FF) : 'Latin-1 Supplement', + (0x0100, 0x017F) : 'Latin Extended-A', + (0x0180, 0x024F) : 'Latin Extended-B', + (0x0400, 0x04FF) : 'Cyrillic', + (0x0500, 0x052F) : 'Cyrillic Supplementary', + (0x0300, 0x036F) : 'Combining Diacritical Marks', + (0x0250, 0x02AF) : 'IPA Extensions', + (0x0370, 0x03FF) : 'Greek and Coptic', + (0x0530, 0x058F) : 'Armenian', + (0x02B0, 0x02FF) : 'Spacing Modifier Letters', + (0x0590, 0x05FF) : 'Hebrew', + (0x0600, 0x06FF) : 'Arabic', + (0x0700, 0x074F) : 'Syriac', + (0x0780, 0x07BF) : 'Thaana', + (0x0900, 0x097F) : 'Devanagari', + (0x0980, 0x09FF) : 'Bengali', + (0x0A00, 0x0A7F) : 'Gurmukhi', + (0x0A80, 0x0AFF) : 'Gujarati', + (0x0B00, 0x0B7F) : 'Oriya', + (0x0B80, 0x0BFF) : 'Tamil', + (0x0C00, 0x0C7F) : 'Telugu', + (0x0C80, 0x0CFF) : 'Kannada', + (0x0D00, 0x0D7F) : 'Malayalam', + (0x0D80, 0x0DFF) : 'Sinhala', + (0x0E00, 0x0E7F) : 'Thai', + (0x0E80, 0x0EFF) : 'Lao', + (0x0F00, 0x0FFF) : 'Tibetan', + (0x1000, 0x109F) : 'Myanmar', + (0x10A0, 0x10FF) : 'Georgian', + (0x1100, 0x11FF) : 'Hangul Jamo', + (0x1200, 0x137F) : 'Ethiopic', + (0x13A0, 0x13FF) : 'Cherokee', + (0x1400, 0x167F) : 'Unified Canadian Aboriginal Syllabics', + (0x1680, 0x169F) : 'Ogham', + (0x16A0, 0x16FF) : 'Runic', + (0x1700, 0x171F) : 'Tagalog', + (0x1720, 0x173F) : 'Hanunoo', + (0x1740, 0x175F) : 'Buhid', + (0x1760, 0x177F) : 'Tagbanwa', + (0x1780, 0x17FF) : 'Khmer', + (0x1800, 0x18AF) : 'Mongolian', + (0x1900, 0x194F) : 'Limbu', + (0x1950, 0x197F) : 'Tai Le', + (0x19E0, 0x19FF) : 'Khmer Symbols', + (0x1D00, 0x1D7F) : 'Phonetic Extensions', + (0x1E00, 0x1EFF) : 'Latin Extended Additional', + (0x1F00, 0x1FFF) : 'Greek Extended', + (0x2000, 0x206F) : 'General Punctuation', + (0x2070, 0x209F) : 'Superscripts and Subscripts', + (0x20A0, 0x20CF) : 'Currency Symbols', + (0x20D0, 0x20FF) : 'Combining Diacritical Marks for Symbols', + (0x2100, 0x214F) : 'Letterlike Symbols', + (0x2150, 0x218F) : 'Number Forms', + (0x2190, 0x21FF) : 'Arrows', + (0x2200, 0x22FF) : 'Mathematical Operators', + (0x2300, 0x23FF) : 'Miscellaneous Technical', + (0x2400, 0x243F) : 'Control Pictures', + (0x2440, 0x245F) : 'Optical Character Recognition', + (0x2460, 0x24FF) : 'Enclosed Alphanumerics', + (0x2500, 0x257F) : 'Box Drawing', + (0x2580, 0x259F) : 'Block Elements', + (0x25A0, 0x25FF) : 'Geometric Shapes', + (0x2600, 0x26FF) : 'Miscellaneous Symbols', + (0x2700, 0x27BF) : 'Dingbats', + (0x27C0, 0x27EF) : 'Miscellaneous Mathematical Symbols-A', + (0x27F0, 0x27FF) : 'Supplemental Arrows-A', + (0x2800, 0x28FF) : 'Braille Patterns', + (0x2900, 0x297F) : 'Supplemental Arrows-B', + (0x2980, 0x29FF) : 'Miscellaneous Mathematical Symbols-B', + (0x2A00, 0x2AFF) : 'Supplemental Mathematical Operators', + (0x2B00, 0x2BFF) : 'Miscellaneous Symbols and Arrows', + (0x2E80, 0x2EFF) : 'CJK Radicals Supplement', + (0x2F00, 0x2FDF) : 'Kangxi Radicals', + (0x2FF0, 0x2FFF) : 'Ideographic Description Characters', + (0x3000, 0x303F) : 'CJK Symbols and Punctuation', + (0x3040, 0x309F) : 'Hiragana', + (0x30A0, 0x30FF) : 'Katakana', + (0x3100, 0x312F) : 'Bopomofo', + (0x3130, 0x318F) : 'Hangul Compatibility Jamo', + (0x3190, 0x319F) : 'Kanbun', + (0x31A0, 0x31BF) : 'Bopomofo Extended', + (0x31F0, 0x31FF) : 'Katakana Phonetic Extensions', + (0x3200, 0x32FF) : 'Enclosed CJK Letters and Months', + (0x3300, 0x33FF) : 'CJK Compatibility', + (0x3400, 0x4DBF) : 'CJK Unified Ideographs Extension A', + (0x4DC0, 0x4DFF) : 'Yijing Hexagram Symbols', + (0x4E00, 0x9FFF) : 'CJK Unified Ideographs', + (0xA000, 0xA48F) : 'Yi Syllables', + (0xA490, 0xA4CF) : 'Yi Radicals', + (0xAC00, 0xD7AF) : 'Hangul Syllables', + (0xE000, 0xF8FF) : 'Private Use Area', + (0xF900, 0xFAFF) : 'CJK Compatibility Ideographs', + (0xFB00, 0xFB4F) : 'Alphabetic Presentation Forms', + (0xFB50, 0xFDFF) : 'Arabic Presentation Forms-A', + (0xFE00, 0xFE0F) : 'Variation Selectors', + (0xFE20, 0xFE2F) : 'Combining Half Marks', + (0xFE30, 0xFE4F) : 'CJK Compatibility Forms', + (0xFE50, 0xFE6F) : 'Small Form Variants', + (0xFE70, 0xFEFF) : 'Arabic Presentation Forms-B', + (0xFF00, 0xFFEF) : 'Halfwidth and Fullwidth Forms', + (0xFFF0, 0xFFFF) : 'Specials', + (0x10000, 0x1007F) : 'Linear B Syllabary', + (0x10080, 0x100FF) : 'Linear B Ideograms', + (0x10100, 0x1013F) : 'Aegean Numbers', + (0x10300, 0x1032F) : 'Old Italic', + (0x10330, 0x1034F) : 'Gothic', + (0x10380, 0x1039F) : 'Ugaritic', + (0x10400, 0x1044F) : 'Deseret', + (0x10450, 0x1047F) : 'Shavian', + (0x10480, 0x104AF) : 'Osmanya', + (0x10800, 0x1083F) : 'Cypriot Syllabary', + (0x1D000, 0x1D0FF) : 'Byzantine Musical Symbols', + (0x1D100, 0x1D1FF) : 'Musical Symbols', + (0x1D300, 0x1D35F) : 'Tai Xuan Jing Symbols', + (0x1D400, 0x1D7FF) : 'Mathematical Alphanumeric Symbols', + (0x20000, 0x2A6DF) : 'CJK Unified Ideographs Extension B', + (0x2F800, 0x2FA1F) : 'CJK Compatibility Ideographs Supplement', + (0xE0000, 0xE007F) : 'Tags', + } + + mismatches = set() + with act.db.connect(charset = 'utf-8') as con: + cur = con.cursor() + for bound_points, range_name in UNICODE_RANGES_MAP.items(): + for code_point in range(bound_points[0],bound_points[1]): + cur.callproc( "sp_get_unicode_char", (code_point,) ) + unicode_chr = cur.fetchone()[0] + cur.callproc( "sp_get_unicode_val", (unicode_chr,) ) + checked_code_point = cur.fetchone()[0] + if checked_code_point == code_point: + pass + else: + mismatches.add( (range_name, code_point, unicode_chr, checked_code_point) ) + + print(len(mismatches)) + for s in mismatches: + print(s) + act.expected_stdout = '0' + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() diff --git a/tests/functional/tabloid/test_657d86fe.py b/tests/functional/tabloid/test_657d86fe.py new file mode 100644 index 00000000..1bf357a3 --- /dev/null +++ b/tests/functional/tabloid/test_657d86fe.py @@ -0,0 +1,72 @@ +#coding:utf-8 + +""" +ID: None +ISSUE: https://github.com/FirebirdSQL/firebird/commit/657d86fed65e647dc162980836d24a2e19c1342c +TITLE: RemoteAuxPort is per-database +DESCRIPTION: + Test checks ability to set value of free port in DPB for RemoteAuxPort parameter. + This is done two times with verifying that value is actually changed by querying to rdb$config. +NOTES: + [31.08.2024] pzotov + 1. No ticket has been created for this test. + 2. Custom driver-config object must be used for DPB. + + Checked on 6.0.0.4444, 5.0.2.1487, 4.0.6.3142 +""" + +import socket +from contextlib import closing +import pytest +from firebird.qa import * +from firebird.driver import driver_config, connect, DatabaseError + +db = db_factory() +act = python_act('db') + +#----------------------------------------------------------- + +def find_free_port(): + # AF_INET - constant represent the address (and protocol) families, used for the first argument to socket() + # A pair (host, port) is used for the AF_INET address family, where host is a string representing either a + # hostname in Internet domain notation like 'daring.cwi.nl' or an IPv4 address like '100.50.200.5', and port is an integer. + # SOCK_STREAM means that it is a TCP socket. + # SOCK_DGRAM means that it is a UDP socket. + with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: + s.bind(('', 0)) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + free_port = s.getsockname()[1] + return free_port + +#----------------------------------------------------------- + +@pytest.mark.version('>=4.0') +def test_1(act: Action, capsys): + + for iter in range(2): + free_aux_port = find_free_port() + srv_cfg = driver_config.register_server(name = f'srv_cfg_657d86fe_{iter}', config = '') + db_cfg_name = f'db_cfg_657d86fe_{iter}' + db_cfg_object = driver_config.register_database(name = db_cfg_name) + db_cfg_object.server.value = srv_cfg.name + db_cfg_object.database.value = str(act.db.db_path) + db_cfg_object.config.value = f""" + RemoteAuxPort = {free_aux_port} + """ + + with connect(db_cfg_name, user = act.db.user, password = act.db.password) as con: + try: + cur = con.cursor() + cur.execute("select g.rdb$config_name, g.rdb$config_value from rdb$database r left join rdb$config g on g.rdb$config_name = 'RemoteAuxPort'") + for r in cur: + print(iter, r[0], r[1]) + + except DatabaseError as e: + print(e.__str__()) + print(e.gds_codes) + + act.expected_stdout = f""" + {iter} RemoteAuxPort {free_aux_port} + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/tabloid/test_6cfc9b5d.py b/tests/functional/tabloid/test_6cfc9b5d.py new file mode 100644 index 00000000..a4f76f01 --- /dev/null +++ b/tests/functional/tabloid/test_6cfc9b5d.py @@ -0,0 +1,410 @@ +#coding:utf-8 +""" +ID: issue-6cfc9b5d +ISSUE: https://github.com/FirebirdSQL/firebird/commit/6cfc9b5d7dc343295968cc4545c6fa184966cfc9 +TITLE: Scrollable cursors. Automated test with randomly generated mission for positioning. +DESCRIPTION: + Test verifies commit "Fixed the remaining issues with scrollable cursor re-positioning" (06.12.2021 07:47). + We generate data (see usage of table 'TEST' and SP sp_generate_test_data), and this data (see TEST.BINARY_DATA) + can be one of following type: + * NULL in every record; + * be compressible with MAX ratio (string like 'AAA....AAA'); + * be absolutely incompressible (UUID-based values). + Number of rows in the TEST table is defined by settings N_ROWS + Number of operations for change cursor position is defined by settings FETCH_OPERATIONS_TO_PERFORM + + Then we perform miscelaneous operations that change position of scrollable cursor (on random basis). + These operations are stored in the table 'job_for_cursor_movement', see call of SP sp_make_job_for_cursor_movement. + Outcome of that: randomly generated operations are stored in the table 'move_operations'. + + Result of positioning (value of obtained ID) is compared with expected one. + These actions are performed for all possible combinations of the following criteria: + * data compressibility (see 'a_suitable_for_compression') + * connection protocol ('local' or 'remote') + * value of WireCrypt parameter (Enabled or DIsabled) +NOTES: + [20.07.2024] pzotov + 1. Values of TEST.ID must start with 1 rather rthan 0. + 2. Custom driver-config object must be used for DPB because two values of WireCrypt parameter must be checked: + Enabled and Disabled (see 'w_crypt'). + Also, we verify result for two protocols that are used for connection: local and remote (INET). + + Checked on 6.0.0.396, 5.0.1.1440 (both SS/CS). +""" + +import pytest +from firebird.qa import * +from firebird.driver import driver_config, connect, NetProtocol +import time + +######################### +### S E T T I N G S ### +######################### +N_ROWS = 20 +N_WIDTH = 32765 +FETCH_OPERATIONS_TO_PERFORM = 100 +#------------------------- + +init_script = f""" + create or alter procedure sp_generate_test_data as begin end; + create or alter procedure sp_make_job_for_cursor_movement as begin end; + create or alter view v_fetch_expected_data as select 1 x from rdb$database; + create or alter view v_fetch_job as select 1 x from rdb$database; + + recreate table move_operations(nm varchar(10)); + insert into move_operations(nm) values('first'); + insert into move_operations(nm) values('last'); + insert into move_operations(nm) values('next'); + insert into move_operations(nm) values('prior'); + insert into move_operations(nm) values('absolute'); + insert into move_operations(nm) values('relative'); + commit; + + recreate table job_for_cursor_movement( + rn int primary key + ,nm varchar(10) + ,arg_n int + ,cnt int + ,id_min int + ,id_max int + ,bof int + ,eof int + ,id_expected int + -- ,constraint job_valid check( nm in ('absolute', 'relative') and arg_n is not null ) + ) + ; + + --------------------- + recreate table test( id int primary key, binary_data varchar( {N_WIDTH} ) character set octets ); + comment on table test is q'#Textual column width was substitued from .py script, see variable 'N_WIDTH' there.#' + ; + commit; + --------------------- + + create or alter view v_fetch_job as + with + i as ( + select + count(*)+1 as cnt + ,min(id) as id_min + ,max(id) as id_max + from test + ) + ,a as ( + select + f.nm + ,iif(f.nm in ('absolute', 'relative'), 1, null) as def_sign + ,i.cnt + ,i.id_min + ,i.id_max + ,iif(f.nm = 'absolute', 0, -i.cnt) as rnd_min + ,iif(f.nm = 'absolute', i.cnt, 2*i.cnt) as rnd_max + from move_operations f + cross join i + ) + select + row_number()over(order by rand()) as rn + ,a.nm + ,a.def_sign * cast(a.rnd_min + rand() * a.rnd_max as int) as arg_n + ,a.cnt + ,a.id_min + ,a.id_max + from a + cross join(select rand() as x from rdb$types a, rdb$types b rows 1000) b + ; + + ---------------------------- + create or alter view v_fetch_expected_data as + with recursive + r as ( + select + rn + ,cast(null as int) as id_expect_bak + ,nm + ,arg_n + ,case nm + when 'first' then id_min + when 'last' then id_max + when 'prior' then bof + when 'next' then id_min + when 'relative' then iif( id_min + arg_n - 1 > id_max, eof, iif( id_min + arg_n <= id_min, bof, id_min + arg_n - 1 ) ) + when 'absolute' then -- iif( arg_n > id_max, eof, iif( id_min + arg_n <=0, bof, id_min + arg_n ) ) + case + when b.arg_n > b.id_max then b.eof + when b.arg_n > 0 then b.id_min + (b.arg_n-1) + when b.arg_n = 0 or b.arg_n < -b.cnt then b.bof + else -- b.arg_n between -b.cnt and -1 + b.id_max + b.arg_n - 1 + end + end as id_expected + ,cnt + ,id_min + ,id_max + ,bof + ,eof + from job_for_cursor_movement b + where rn = 1 + + UNION ALL + + select + x.rn + ,r.id_expected as id_expect_bak + ,x.nm + ,x.arg_n + ,case x.nm + when 'first' then r.id_min + when 'last' then r.id_max + when 'prior' then iif(r.id_expected = r.eof, r.id_max, iif( r.id_expected <= r.id_min, r.bof, r.id_expected - 1 ) ) + when 'next' then iif( r.id_expected = r.bof, r.id_min, iif( r.id_expected >= r.id_max, r.eof, r.id_expected + 1 ) ) + when 'relative' then -- iif( r.id_expected + x.arg_n > r.id_max, r.eof, iif( r.id_expected + x.arg_n < r.id_min, r.bof, r.id_expected + x.arg_n ) ) + case + when r.id_expected = r.bof then iif(x.arg_n <= 0, r.bof, iif(x.arg_n >= r.cnt, r.eof, x.arg_n)) + when r.id_expected = r.eof then iif(x.arg_n >= 0, r.eof, iif(abs(x.arg_n) >= r.cnt, r.bof, r.id_max + 1 - abs(x.arg_n))) + else iif( r.id_expected + x.arg_n > r.id_max, r.eof, iif( r.id_expected + x.arg_n < r.id_min, r.bof, r.id_expected + x.arg_n ) ) + end + when 'absolute' then + case + when x.arg_n > r.id_max then r.eof + when x.arg_n > 0 then r.id_min + (x.arg_n-1) + when x.arg_n = 0 or x.arg_n < -r.cnt then r.bof + else -- x.arg_n between -r.cnt and -1 + r.id_max + x.arg_n - 1 + end + end as id_expected + ,r.cnt + ,r.id_min + ,r.id_max + ,r.bof + ,r.eof + from r + join job_for_cursor_movement x on r.rn + 1 = x.rn + --where x.rn <= 3 + + ) + select * from r + ; + ----------------------------- + + set term ^; + create or alter procedure sp_make_job_for_cursor_movement(a_rows_to_add int = 20) as + declare cnt int; + declare bof int = -2147483648; + declare eof int = 2147483647; + begin + delete from job_for_cursor_movement; + select count(*) from test into cnt; + + insert into job_for_cursor_movement( + rn, + nm, + arg_n, + cnt, + id_min, + id_max, + bof, + eof + ) + select + rn, + nm, + arg_n, + cnt, + id_min, + id_max, + :bof, + :eof + from v_fetch_job + rows :a_rows_to_add + ; + + merge into job_for_cursor_movement t + using (select rn, id_expected from v_fetch_expected_data) s on t.rn = s.rn + when MATCHED then update set t.id_expected = s.id_expected + ; + + end + ^ + create or alter procedure sp_generate_test_data(a_suitable_for_compression smallint, a_rows int) as + declare i int = 1; + declare dummy int; + declare s varchar({N_WIDTH}); + declare uuid_text varchar({N_WIDTH}) character set octets; + declare uuid_addi varchar(16) character set octets; + begin + delete from test; + select count(*) from test into dummy; + s = iif(a_suitable_for_compression = 0, null, lpad('', {N_WIDTH}, 'A')); + if (a_suitable_for_compression in (0,1) ) then + begin + while( i <= a_rows ) do + begin + insert into test(id, binary_data) values(:i, :s); + i = i + 1; + end + end + else + begin + -- generate NON-compressible data + while (i <= a_rows) do + begin + uuid_text = ''; + uuid_addi = ''; + while ( 1=1 ) do + begin + uuid_addi = gen_uuid(); + if ( octet_length(uuid_text) < {N_WIDTH} - octet_length(uuid_addi) ) then + uuid_text = uuid_text || trim(uuid_addi); + else + begin + uuid_text = uuid_text || left(uuid_addi, {N_WIDTH} - octet_length(uuid_text)); + leave; + end + + + end + insert into test(id,binary_data) values( :i, :uuid_text); + --execute statement ('insert into test(id,binary_data) values(?, ?)') ( :i, :uuid_text); + i = i + 1; + end + end + end + ^ + set term ;^ + commit; +""" + +db = db_factory(init = init_script, charset='win1251') +act = python_act('db') + +#---------------------------- + +def print_row(row, cur = None): + if row: + print(f"{row[0]}") + if cur and (cur.is_bof() or cur.is_eof()): + print('### STRANGE BOF/EOR WHILE SOME DATA CAN BE SEEN ###') + else: + msg = '*** NO_DATA***' + if cur: + msg += ' BOF=%r EOF=%r' % ( cur.is_bof(), cur.is_eof() ) + print(msg) + +#---------------------------- + +@pytest.mark.scroll_cur +@pytest.mark.version('>=5.0.0') +def test_1(act: Action, capsys): + + for a_suitable_for_compression in (0,1,2): + move_ops = {} + with act.db.connect() as con: + cur = con.cursor() + cur.callproc('sp_generate_test_data', (a_suitable_for_compression, N_ROWS)) + con.commit() + cur.callproc('sp_make_job_for_cursor_movement', (FETCH_OPERATIONS_TO_PERFORM,)) + con.commit() + + cur.execute('select rn, nm, arg_n, id_expected from job_for_cursor_movement order by rn') + for r in cur: + move_ops[ r[0] ] = (r[1], '' if r[2] == None else r[2], r[3]) + + # result: + # table 'test' contains rows with data that has character appropriated to a_suitable_for_compression value + # table 'job_for_cursor_movement' contains rows with randomly generated 'jobs' for cursor positioning. + # column job_for_cursor_movement.id_expected has EXPECTED values for ID after each position operation against table 'test'; we will compare ID with it. + + # Example of move_ops: + # 1 : oper = fetch_last , id_expected: 19 + # 2 : oper = fetch_first , id_expected: 0 + # 3 : oper = fetch_relative(5) , id_expected: 5 + # 4 : oper = fetch_relative(16) , id_expected: 2147483647 + # ... + # 96 : oper = fetch_absolute(2) , id_expected: 1 + # 97 : oper = fetch_prior , id_expected: 0 + # 98 : oper = fetch_relative(5) , id_expected: 5 + # 99 : oper = fetch_relative(-9) , id_expected: -2147483648 + # 100 : oper = fetch_absolute(10) , id_expected: 9 + + srv_cfg = driver_config.register_server(name = f'test_6cfc9b5d_srv_{a_suitable_for_compression}', config = '') + + for protocol_name in ('local', 'remote'): + for w_crypt in ('Enabled', 'Disabled'): + + PASSED_MSG = f'{a_suitable_for_compression=}, {protocol_name=}, {w_crypt=}: PASSED.' + + db_cfg_name = f'test_90129c6d_wcrypt_{a_suitable_for_compression}_{w_crypt}_{protocol_name}' + db_cfg_object = driver_config.register_database(name = db_cfg_name) + db_cfg_object.server.value = srv_cfg.name + db_cfg_object.protocol.value = None if protocol_name == 'local' else NetProtocol.INET + db_cfg_object.database.value = str(act.db.db_path) + db_cfg_object.config.value = f""" + WireCrypt = w_crypt + """ + + details_lst = [] + chk_retcode = 0 + with connect(db_cfg_name, user = act.db.user, password = act.db.password) as con: + + cur = con.cursor() + cur.open("select id, binary_data from test order by id") + + for k,v in sorted(move_ops.items()): + crow = None + chk_val = None + f_kind = v[0].strip() + if f_kind == 'prior': + crow = cur.fetch_prior() + elif f_kind == 'next': + crow = cur.fetch_next() + elif f_kind == 'first': + crow = cur.fetch_first() + elif f_kind == 'last': + crow = cur.fetch_last() + elif f_kind == 'relative': + crow = cur.fetch_relative( v[1] ) + elif f_kind == 'absolute': + crow = cur.fetch_absolute( v[1] ) + + wrong_found_msg = '' + if crow: + chk_val = crow[0] + if cur.is_bof() or cur.is_eof(): + wrong_found_msg += ('One of BOF/EOF is true (BOF=%r, EOF=%r), but cursor returns data: %d' % (cur.is_bof(), cur.is_eof(), crow[0])) + else: + if cur.is_bof(): + chk_val = -2147483648 + elif cur.is_eof(): + chk_val = 2147483647 + else: + wrong_found_msg += '!! No data from cursor but also neither BOF nor EOF.' + + msg = ''.join( + ( '%5d ' % k + ,'+++ passed +++' if wrong_found_msg == '' and v[2] == chk_val else '## MISMATCH ##' + ,f'fetch oper: {f_kind},{v[1]}; id_expected: {v[2]}' + ,f'; id_actual: {chk_val};' + ,'BOF' if cur.is_bof() else '' + ,'EOF' if cur.is_eof() else '' + ) + ) + details_lst.append(msg) + + if not v[2] == chk_val or wrong_found_msg: + chk_retcode = 1 + print(f'{a_suitable_for_compression=}, {protocol_name=}, {w_crypt=}: FAILED.') + for x in details_lst: + print(x) + break + + if chk_retcode == 0: + print(PASSED_MSG) + else: + print('###############') + print('### F A I L ###') + print('###############') + + act.expected_stdout = PASSED_MSG + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() diff --git a/tests/functional/tabloid/test_80fd0682.py b/tests/functional/tabloid/test_80fd0682.py new file mode 100644 index 00000000..352c63b4 --- /dev/null +++ b/tests/functional/tabloid/test_80fd0682.py @@ -0,0 +1,179 @@ +#coding:utf-8 + +""" +ID: None +ISSUE: https://github.com/FirebirdSQL/firebird/commit/80fd06828e72f9e8335150c923350730013b3b28 +TITLE: Fixed bug with index names patterns in online validation service +DESCRIPTION: + Sources to read (19-feb-2021): + https://sourceforge.net/p/firebird/mailman/message/37222898/ + https://sourceforge.net/p/firebird/mailman/message/37223338/ + (Firebird-devel Digest, Vol 178, Issue 34; Vol 178, Issue 35) +NOTES: + Confirmed bug on 4.0.0.2369. + Checked on 4.0.0.2372 -- all fine. + Checked on 6.0.0.423, 5.0.2.1477. +""" + +import re +import pytest +from firebird.qa import * + +init_sql = """ + create table a(id int); + create index a on a(id); + create table b(id int); + create index x on b(id); + create table c(id int); + create index c on c(id); + commit; +""" +db = db_factory(init = init_sql) + +substitutions = [ ('[ \t]+', ' ') + ] +act = python_act('db', substitutions = substitutions) + +#------------------------------------------------------------- + +def clean_text(line: str): + # Used to remove timestamp from the beginning of line, + # then remove ID of relation / index.: + line = re.sub(r'^\d{2}:\d{2}:\d{2}.\d{2,3}\s+', '', line) + line = re.sub('Relation \\d+ \\(', 'Relation (', line) + line = re.sub('Index \\d+ \\(', 'Index (', line) + return line + +#------------------------------------------------------------- + +@pytest.mark.version('>=4.0') +def test_1(act: Action, capsys): + + validation_log = [] + # Run online database validation: + with act.connect_server() as srv: + for ix_key in ('incl', 'excl'): + for tab_name in ('A', 'B'): + if ix_key == 'incl': + srv.database.validate(database=act.db.db_path, include_table = tab_name.upper()) + else: + srv.database.validate(database=act.db.db_path, exclude_table = tab_name.upper()) + validation_log.append(f'{ix_key=}, {tab_name=}') + validation_log.extend( [clean_text(line) for line in srv.readlines()] ) + for idx_name in ('A', 'X'): + if ix_key == 'incl': + srv.database.validate(database=act.db.db_path, include_index = idx_name.upper()) + else: + srv.database.validate(database=act.db.db_path, exclude_index = idx_name.upper()) + validation_log.append(f'{ix_key=}, {idx_name=}') + validation_log.extend( [clean_text(line) for line in srv.readlines()] ) + # validation_log.extend( [ re.sub('Index \\d+ \\(', 'Index (', re.sub(r'^\d{2}:\d{2}:\d{2}.\d{2,3}\s+', '', line)) for line in srv.readlines() ] ) + #for line in srv.readlines(): + # validation_log.append( clean_text(line) ) + + + for line in validation_log: + print(line) + + act.expected_stdout = """ + ix_key='incl', tab_name='A' + Validation started + Relation (A) + process pointer page 0 of 1 + Index (A) + Relation (A) is ok + Validation finished + + ix_key='incl', tab_name='B' + Validation started + Relation (B) + process pointer page 0 of 1 + Index (X) + Relation (B) is ok + Validation finished + + ix_key='incl', idx_name='A' + Validation started + Relation (A) + process pointer page 0 of 1 + Index (A) + Relation (A) is ok + Relation (B) + process pointer page 0 of 1 + Relation (B) is ok + Relation (C) + process pointer page 0 of 1 + Relation (C) is ok + Validation finished + + ix_key='incl', idx_name='X' + Validation started + Relation (A) + process pointer page 0 of 1 + Relation (A) is ok + Relation (B) + process pointer page 0 of 1 + Index (X) + Relation (B) is ok + Relation (C) + process pointer page 0 of 1 + Relation (C) is ok + Validation finished + + ix_key='excl', tab_name='A' + Validation started + Relation (B) + process pointer page 0 of 1 + Index (X) + Relation (B) is ok + Relation (C) + process pointer page 0 of 1 + Index (C) + Relation (C) is ok + Validation finished + + ix_key='excl', tab_name='B' + Validation started + Relation (A) + process pointer page 0 of 1 + Index (A) + Relation (A) is ok + Relation (C) + process pointer page 0 of 1 + Index (C) + Relation (C) is ok + Validation finished + + ix_key='excl', idx_name='A' + Validation started + Relation (A) + process pointer page 0 of 1 + Relation (A) is ok + Relation (B) + process pointer page 0 of 1 + Index (X) + Relation (B) is ok + Relation (C) + process pointer page 0 of 1 + Index (C) + Relation (C) is ok + Validation finished + + ix_key='excl', idx_name='X' + Validation started + Relation (A) + process pointer page 0 of 1 + Index (A) + Relation (A) is ok + Relation (B) + process pointer page 0 of 1 + Relation (B) is ok + Relation (C) + process pointer page 0 of 1 + Index (C) + Relation (C) is ok + Validation finished + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() diff --git a/tests/functional/tabloid/test_883200d1.py b/tests/functional/tabloid/test_883200d1.py new file mode 100644 index 00000000..8d6c01a0 --- /dev/null +++ b/tests/functional/tabloid/test_883200d1.py @@ -0,0 +1,58 @@ +#coding:utf-8 + +""" +ID: None +ISSUE: https://github.com/FirebirdSQL/firebird/commit/883200d1927f74baadc7eb14293d1a9fb4e517ce +TITLE: Do not re-prepare statements when execute DDL in ISQL +DESCRIPTION: +NOTES: + Confirmed duplicated PREPARE_STATEMENT in 6.0.0.454 + Checked on 6.0.0.457 - all OK. +""" + +import re +import time + +import pytest +from firebird.qa import * + +db = db_factory() + +act = python_act('db', substitutions=[('[ \t]+', ' ')]) + +expected_stdout = """ +""" + +@pytest.mark.trace +@pytest.mark.version('>=6.0') +def test_1(act: Action, capsys): + + trace_cfg_items = [ + 'time_threshold = 0', + 'log_statement_prepare = true', + 'log_initfini = false', + 'log_errors = true', + ] + + DDL_STTM = 'recreate table test(id int)' + with act.trace(db_events = trace_cfg_items, encoding='utf-8'): + act.isql(switches = ['-q'], input = DDL_STTM + ';', combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout + act.reset() + + allowed_patterns = ( re.escape(DDL_STTM),) + allowed_patterns = [ re.compile(p, re.IGNORECASE) for p in allowed_patterns ] + + for line in act.trace_log: + if line.strip(): + if act.match_any(line.strip(), allowed_patterns): + print(line.strip()) + + + expected_trace_log = f""" + {DDL_STTM} + """ + act.expected_stdout = expected_trace_log + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() diff --git a/tests/functional/tabloid/test_90129c6d.py b/tests/functional/tabloid/test_90129c6d.py new file mode 100644 index 00000000..41d2a173 --- /dev/null +++ b/tests/functional/tabloid/test_90129c6d.py @@ -0,0 +1,165 @@ +#coding:utf-8 + +""" +ID: issue-90129c6d +ISSUE: https://github.com/FirebirdSQL/firebird/commit/90129c6d3ebd5d1c9a7b44d287a4791dffcd031e +TITLE: Scrollable cursors. Fixed isBof/isEof when the prefetch is active +DESCRIPTION: + is_bof()/is_eof() could became True much earlier than actual position of cursor did come to BOF/EOF. + This occurred on fetch_prior() / fetch_next() calls and caused wrong result for counting number of + processed rows. + Discussed with dimitr, see letters with subj = "firebird-driver & scrollable cursors". + Dates: 27.11.2021 21:20, 28.11.2021 19:00, 29.11.2021 07:40 +NOTES: + [19.07.2024] pzotov + 1. No ticket has been created for described problem. + Problem was fixed 28.11.2021 at 16:09, commit #90129c6d. + Confirmed bug on 5.0.0.321 (28.11.2021). Fixed in 5.0.0.324 (29.11.2021). + 2. Initial test contained too big values for check: + N_ROWS = 10000 + N_WIDTH = 32765 + LOOP_COUNT = 1000 + They can be safely replaced with minimal possible values in order to see difference before and after fix. + 3. NOTE that argument passed to cur.stream_blobs.append() must be equal to the name of blob column as it is + stored in RDB$ tables, i.e. in uppercase. Because of that, variable 'BLOB_FLD_NAME' is used instead of + repeating blob column name in DDL and cur.stream_blobs.append(). + 4. Custom driver-config object must be used for DPB because two values of WireCrypt parameter must be checked: + Enabled and Disabled (see 'w_crypt'). + + Checked on 6.0.0.396, 5.0.1.1440. +""" + +import pytest +from firebird.qa import * +from firebird.driver import driver_config, connect, NetProtocol +import time + +db = db_factory() +act = python_act('db') + +N_ROWS = 1 +N_WIDTH = 1 +LOOP_COUNT = 2 +BLOB_FLD_NAME = 'BINARY_DATA' + +@pytest.mark.scroll_cur +@pytest.mark.version('>=5.0') +def test_1(act: Action, capsys): + + srv_cfg = driver_config.register_server(name = 'test_90129c6d_srv', config = '') + + for w_crypt in ('Enabled', 'Disabled'): + db_cfg_name = f'test_90129c6d_wcrypt_{w_crypt}' + db_cfg_object = driver_config.register_database(name = db_cfg_name) + db_cfg_object.server.value = srv_cfg.name + db_cfg_object.protocol.value = NetProtocol.INET + db_cfg_object.database.value = str(act.db.db_path) + db_cfg_object.config.value = f""" + WireCrypt = w_crypt + """ + + for suitable_for_compression in (0,1): + if suitable_for_compression: + data_dml = f""" + execute block as + declare n int = {N_ROWS}; + begin + while ( n > 0 ) do + begin + insert into ts(id,{BLOB_FLD_NAME}) values(:n, lpad('', {N_WIDTH}, 'A')); + n = n - 1; + end + end + """ + else: + data_dml = f""" + execute block as + declare n int = {N_ROWS}; + declare n_wid int = {N_WIDTH}; + declare encrypted_text varchar({N_WIDTH}) character set octets; + declare encr_addition varchar(16) character set octets; + begin + while (n > 0) do + begin + encrypted_text = ''; + encr_addition = ''; + while ( 1 = 1 ) do + begin + encr_addition = gen_uuid(); + if ( octet_length(encrypted_text) < n_wid - octet_length(encr_addition) ) then + encrypted_text = encrypted_text || trim(encr_addition); + else + begin + encrypted_text = encrypted_text || left(encr_addition, n_wid - octet_length(encrypted_text)); + leave; + end + end + insert into ts(id,{BLOB_FLD_NAME}) values(:n, :encrypted_text); + n = n - 1; + end + end + """ + + init_sql = f""" + recreate table ts(id int primary key, {BLOB_FLD_NAME} blob) + ^ + commit + ^ + {data_dml} + ^ + commit + ^ + """ + + with act.db.connect() as con: + cur = con.cursor() + for x in init_sql.split("^"): + s = x.lower().strip() + if s == "commit": + con.commit() + elif s: + cur.execute(s) + + with connect(db_cfg_name, user = act.db.user, password = act.db.password) as con: + assert (True if w_crypt else False) == con.info.is_encrypted(), f'Value of {con.info.is_encrypted()=} not equals to required: {w_crypt=}' + + cur = con.cursor() + cur.stream_blobs.append(BLOB_FLD_NAME.upper()) + cur.open(f'select id, {BLOB_FLD_NAME} from ts order by id') + + for iter_no in range(LOOP_COUNT): + cnt_fwrd=0 + while True: + fetched_row_data = cur.fetch_next() + if cur.is_eof(): + break + v_id, v_blob_data = fetched_row_data + with v_blob_data: + v_blob_data.read() + cnt_fwrd += 1 + + cnt_back=0 + while True: + fetched_row_data = cur.fetch_prior() + if cur.is_bof(): + break + v_id, v_blob_data = fetched_row_data + with v_blob_data: + v_blob_data.read() + cnt_back += 1 + + print(f'{w_crypt=}, {suitable_for_compression=}, {iter_no=}: {cnt_fwrd=}, {cnt_back=}') + + act.expected_stdout = """ + w_crypt='Enabled', suitable_for_compression=0, iter_no=0: cnt_fwrd=1, cnt_back=1 + w_crypt='Enabled', suitable_for_compression=0, iter_no=1: cnt_fwrd=1, cnt_back=1 + w_crypt='Enabled', suitable_for_compression=1, iter_no=0: cnt_fwrd=1, cnt_back=1 + w_crypt='Enabled', suitable_for_compression=1, iter_no=1: cnt_fwrd=1, cnt_back=1 + w_crypt='Disabled', suitable_for_compression=0, iter_no=0: cnt_fwrd=1, cnt_back=1 + w_crypt='Disabled', suitable_for_compression=0, iter_no=1: cnt_fwrd=1, cnt_back=1 + w_crypt='Disabled', suitable_for_compression=1, iter_no=0: cnt_fwrd=1, cnt_back=1 + w_crypt='Disabled', suitable_for_compression=1, iter_no=1: cnt_fwrd=1, cnt_back=1 + """ + + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/tabloid/test_95442bdf.py b/tests/functional/tabloid/test_95442bdf.py new file mode 100644 index 00000000..11f9a6bd --- /dev/null +++ b/tests/functional/tabloid/test_95442bdf.py @@ -0,0 +1,83 @@ +#coding:utf-8 + +""" +ID: issue-95442bdf +ISSUE: https://github.com/FirebirdSQL/firebird/commit/95442bdfff76d22aafb57b58894047be2a89c6ea +TITLE: Attempt to avoid hash joining for possible cardinality under-estimations +DESCRIPTION: + Test verifies explained plan for three forms of inner join: + * 'normal' (or 'traditional'): 'from A join B on ' + * 'using': 'from A join B using () + * 'natural': 'from A natural join B' + All forms must generate same plan with nested loops (i.e. without hash join). + Lines in each explained plan are LETF-PADDED with dot character in order to keep indentation while + analyzing differences between expected and actual output. +NOTES: + [29.05.2024] pzotov + Checked on intermediate snapshot 6.0.0.363 #95442bd. + Thanks to dimitr for provided example. +""" + +import pytest +from firebird.qa import * + +init_sql = f""" + create table t1(id int); + create table t2(id int primary key using index t2_pk); + insert into t1(id) select row_number()over() from rdb$types,rdb$types; + commit; +""" + +db = db_factory(init = init_sql) +act = python_act('db') + +#--------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#--------------------------------------------------------- + +@pytest.mark.version('>=6.0') +def test_1(act: Action, capsys): + + join_expr_lst = ( + 't1 a join t2 b on a.id = b.id' + ,'t1 u join t2 v using(id)' + ,'t1 x natural join t2 y' + ) + + with act.db.connect() as con: + cur = con.cursor() + for x in join_expr_lst: + with cur.prepare(f'select * from ' + x) as ps: + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan .split('\n')]) ) + + act.expected_stdout = """ + Select Expression + ....-> Nested Loop Join (inner) + ........-> Table "T1" as "A" Full Scan + ........-> Filter + ............-> Table "T2" as "B" Access By ID + ................-> Bitmap + ....................-> Index "T2_PK" Unique Scan + + Select Expression + ....-> Nested Loop Join (inner) + ........-> Table "T1" as "U" Full Scan + ........-> Filter + ............-> Table "T2" as "V" Access By ID + ................-> Bitmap + ....................-> Index "T2_PK" Unique Scan + + Select Expression + ....-> Nested Loop Join (inner) + ........-> Table "T1" as "X" Full Scan + ........-> Filter + ............-> Table "T2" as "Y" Access By ID + ................-> Bitmap + ....................-> Index "T2_PK" Unique Scan + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/tabloid/test_aae2ae32.py b/tests/functional/tabloid/test_aae2ae32.py new file mode 100644 index 00000000..a828e427 --- /dev/null +++ b/tests/functional/tabloid/test_aae2ae32.py @@ -0,0 +1,157 @@ +#coding:utf-8 + +""" +ID: issue-8252 +ISSUE: https://github.com/FirebirdSQL/firebird/commit/aae2ae3291855f51ff587d0da055aed270137e8f +TITLE: Better fix for #8252: incorrect subquery unnesting with complex dependencies, it re-allows converting nested IN/EXISTS into multiple semi-join +DESCRIPTION: + Test verifies additional commit related to https://github.com/FirebirdSQL/firebird/issues/8252 + Example #1 from https://github.com/FirebirdSQL/firebird/issues/8265 is used for check + (suggested by dimitr, letter 25.09.2024 13:33) +NOTES: + [26.09.2024] pzotov + 1. No ticket has been created for this test. + 2. Confirmed problem on 5.0.2.1516-fe6ba50 (23.09.2024), got for SubQueryConversion = true plan with subquery: + # Sub-query + # -> Filter + # -> Filter + # -> Hash Join (semi) + # ... and ... + # Select Expression + # -> Aggregate + # -> Filter + # -> Table "TEST1" as "A" Full Scan + 3. Parameter 'SubQueryConversion' currently presents only in FB 5.x and _NOT_ in FB 6.x. + Because of that, testing version are limited only for 5.0.2. FB 6.x currently is NOT tested. + 4. Custom driver config objects are created here, one with SubQueryConversion = true and second with false. + + [18.01.2025] pzotov + Resultset of cursor that executes using instance of selectable PreparedStatement must be stored + in some variable in order to have ability close it EXPLICITLY (before PS will be freed). + Otherwise access violation raises during Python GC and pytest hangs at final point (does not return control to OS). + This occurs at least for: Python 3.11.2 / pytest: 7.4.4 / firebird.driver: 1.10.6 / Firebird.Qa: 0.19.3 + The reason of that was explained by Vlad, 26.10.24 17:42 ("oddities when use instances of selective statements"). + + Checked on 5.0.2.1516-92316F0 -- all ok: two hash joins instead of subquery. + Thanks to dimitr for the advice on implementing the test. + + [16.04.2025] pzotov + Re-implemented in order to check FB 5.x with set 'SubQueryConversion = true' and FB 6.x w/o any changes in its config. + Checked on 6.0.0.687-730aa8f, 5.0.3.1647-8993a57 +""" + +import pytest +from firebird.qa import * +from firebird.driver import driver_config, connect, DatabaseError + + +init_script = """ + create table test1(id int not null); + create table test2(id int not null, pid int not null); + create table test3(id int not null, pid int not null, name varchar(30) not null); + commit; + + insert into test1(id) select row_number()over()-1 from rdb$types rows 10; + insert into test2(id, pid) select row_number()over()-1, mod(row_number()over()-1, 10) from rdb$types rows 100; + insert into test3(id, pid, name) select row_number()over()-1, mod(row_number()over()-1, 100), 'QWEABCRTY' from rdb$types, rdb$types rows 1000; + commit; + + -- alter table test1 add constraint test1_pk primary key(id); + -- alter table test2 add constraint test2_pk primary key(id); + -- alter table test3 add constraint test3_pk primary key(id); + -- alter table test2 add constraint test2_fk foreign key(pid) references test1; + -- alter table test3 add constraint test3_fk foreign key(pid) references test2; +""" + +db = db_factory(init=init_script) + +# Substitusions are needed here in order to ignore concrete numbers in explained plan parts, e.g.: +# Hash Join (semi) (keys: 1, total key length: 4) +# Sort (record length: 28, key length: 8) +# Record Buffer (record length: 25) +substitutions = [ + (r'Hash Join \(semi\) \(keys: \d+, total key length: \d+\)','Hash Join (semi)') + ,(r'record length: \d+', 'record length: NN') + ,(r'key length: \d+', 'key length: NN') +] + +act = python_act('db', substitutions = substitutions) + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- + +@pytest.mark.version('>=5.0.2') +def test_1(act: Action, capsys): + + test_sql = """ + select count(*) from test1 a + where + a.id in ( + select b.pid from test2 b + where + b.id in ( + select c.pid from test3 c + where name like '%ABC%' + ) + ); + """ + + srv_cfg = driver_config.register_server(name = f'srv_cfg_aae2ae32', config = '') + db_cfg_name = f'db_cfg_aae2ae32' + db_cfg_object = driver_config.register_database(name = db_cfg_name) + db_cfg_object.server.value = srv_cfg.name + db_cfg_object.database.value = str(act.db.db_path) + if act.is_version('<6'): + db_cfg_object.config.value = f""" + SubQueryConversion = true + """ + + with connect(db_cfg_name, user = act.db.user, password = act.db.password) as con: + ps, rs = None, None + try: + cur = con.cursor() + ps = cur.prepare(test_sql) + + # Print explained plan with padding eash line by dots in order to see indentations: + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + + # ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression. + # We have to store result of cur.execute() in order to + # close it explicitly. + # Otherwise AV can occur during Python garbage collection and this + # causes pytest to hang on its final point. + # Explained by hvlad, email 26.10.24 17:42 + rs = cur.execute(ps) + for r in rs: + print(r[0]) + except DatabaseError as e: + print(e.__str__()) + print(e.gds_codes) + finally: + if rs: + rs.close() # <<< EXPLICITLY CLOSING CURSOR RESULTS + if ps: + ps.free() + + act.expected_stdout = f""" + Select Expression + ....-> Aggregate + ........-> Filter + ............-> Hash Join (semi) + ................-> Table "TEST1" as "A" Full Scan + ................-> Record Buffer (record length: 82) + ....................-> Filter + ........................-> Hash Join (semi) + ............................-> Table "TEST2" as "B" Full Scan + ............................-> Record Buffer (record length: 57) + ................................-> Filter + ....................................-> Table "TEST3" as "C" Full Scan + 10 + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/tabloid/test_c8849946.py b/tests/functional/tabloid/test_c8849946.py new file mode 100644 index 00000000..2504fe03 --- /dev/null +++ b/tests/functional/tabloid/test_c8849946.py @@ -0,0 +1,107 @@ +#coding:utf-8 + +""" +ID: issue-c8849946 +ISSUE: https://github.com/FirebirdSQL/firebird/commit/c884994653523d7d6d614075af45c7ecf338008e +TITLE: Scrollable cursors. Inconsistent cursor repositioning +DESCRIPTION: + Test case provided by dimitr, see letters with subj = "firebird-driver & scrollable cursors". + Date: 01.12.2021 08:20. +NOTES: + [19.07.2024] pzotov + 1. No ticket has been created for described problem. + Problem was fixed 30.11.2021 at 16:55, commit = push = c8849946 + 2. Confirmed bug on 5.0.0.324. Fixed in 5.0.0.325-c884994 + Checked on 6.0.0.396, 5.0.1.1440. +""" + +import pytest +from firebird.qa import * + +db = db_factory() +act = python_act('db') + +N_ROWS = 1010 +############# + +def print_row(row, cur = None): + if row: + print(f"{row[0]}") + if cur and (cur.is_bof() or cur.is_eof()): + print('### STRANGE BOF/EOR WHILE SOME DATA CAN BE SEEN ###') + else: + msg = '*** NO_DATA***' + if cur: + msg += ' BOF=%r EOF=%r' % ( cur.is_bof(), cur.is_eof() ) + print(msg) + +#---------------------------- + +@pytest.mark.scroll_cur +@pytest.mark.version('>=5.0') +def test_1(act: Action, capsys): + + with act.db.connect() as con: + cur = con.cursor() + cur.open(f'select row_number()over() as id from rdb$types, rdb$types rows {N_ROWS}') + + print_row(cur.fetch_next(), cur) + print_row(cur.fetch_next(), cur) + print_row(cur.fetch_next(), cur) + print_row(cur.fetch_prior(), cur) + print_row(cur.fetch_prior(), cur) + print_row(cur.fetch_prior(), cur) + print_row(cur.fetch_next(), cur) + print_row(cur.fetch_next(), cur) + print_row(cur.fetch_next(), cur) + print_row(cur.fetch_prior(), cur) + print_row(cur.fetch_prior(), cur) + print_row(cur.fetch_prior(), cur) + + print_row(cur.fetch_last(), cur) + print_row(cur.fetch_next(), cur) + + print_row(cur.fetch_prior(), cur) + print_row(cur.fetch_prior(), cur) + print_row(cur.fetch_prior(), cur) + print_row(cur.fetch_next(), cur) + print_row(cur.fetch_next(), cur) + print_row(cur.fetch_next(), cur) + print_row(cur.fetch_prior(), cur) + print_row(cur.fetch_prior(), cur) + print_row(cur.fetch_prior(), cur) + print_row(cur.fetch_next(), cur) + print_row(cur.fetch_next(), cur) + print_row(cur.fetch_next(), cur) + + + act.expected_stdout = """ + 1 + 2 + 3 + 2 + 1 + *** NO_DATA*** BOF=True EOF=False + 1 + 2 + 3 + 2 + 1 + *** NO_DATA*** BOF=True EOF=False + 1010 + *** NO_DATA*** BOF=False EOF=True + 1010 + 1009 + 1008 + 1009 + 1010 + *** NO_DATA*** BOF=False EOF=True + 1010 + 1009 + 1008 + 1009 + 1010 + *** NO_DATA*** BOF=False EOF=True + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/tabloid/test_cc183f59.py b/tests/functional/tabloid/test_cc183f59.py new file mode 100644 index 00000000..c6a537d1 --- /dev/null +++ b/tests/functional/tabloid/test_cc183f59.py @@ -0,0 +1,102 @@ +#coding:utf-8 + +""" +ID: None +ISSUE: https://github.com/FirebirdSQL/firebird/commit/cc183f599ee09233d6da294893f651de0ab76136 +TITLE: Add key info to the merge join plan output +DESCRIPTION: +NOTES: + MERGE JOIN will be chosen by optimizer when several conditions are met: + * data sources are joined by INNER join; + * data sources are ORDERED, BUT *NOT* via GROUP BY (because optimizer tends to think about grouped data + that its cardinality *much* less than cardinality of source and it causes to decide using hash join instead). + This was explained by dimitr privately, letter: 24-sep-2024 16:30. + * number of conflicts in the hash table must be above 1009 * 1000 = 1009000. + Experimental shows that minimal threshold for switching from HJ to MJ is 1009883 rows. + This value must NOT depend on machine but can have limited dependency on page_size + (see letter from dimitr, 24-sep-2024 19:09). + * No ticket has been created for this test. + + Checked on 6.0.0.467. +""" + +import re +import time + +import pytest +from firebird.qa import * + +init_sql = """ + create table test1(id int not null); + create table test2(id int not null, pid int not null); + + set stat on; + set echo on; + set term ^; + execute block as + declare n_cnt int = 1009883; -- OK, plan MERGE JOIN + -- declare n_cnt int = 1009882; -- plan HJ + declare i int = 0; + begin + while (i < n_cnt) do + begin + insert into test1(id) values(:i); + i = i + 1; + end + end + ^ + set term ;^ + insert into test2(id, pid) select row_number()over(), id from test1; + commit; +""" +db = db_factory(init = init_sql, page_size = 8192) + +substitutions = [ ('[ \t]+', ' ') + ,('keys: \\d+, total key length: \\d+', 'keys, total key length') + ,('record length: \\d+, key length: \\d+', 'record length, key length') + ] + +act = python_act('db', substitutions = substitutions) + + +#----------------------------------------------------------- + +def replace_leading(source, char="."): + stripped = source.lstrip() + return char * (len(source) - len(stripped)) + stripped + +#----------------------------------------------------------- + +@pytest.mark.version('>=6.0') +def test_1(act: Action, capsys): + + test_sql = """ + select a.id, b.pid + from ( + select id from test1 order by id + ) a + join + ( + select pid from test2 b order by pid + ) b + on a.id = b.pid + """ + + with act.db.connect() as con: + cur = con.cursor() + ps = cur.prepare(test_sql) + print( '\n'.join([replace_leading(s) for s in ps.detailed_plan.split('\n')]) ) + + act.expected_stdout = """ + Select Expression + ....-> Filter + ........-> Merge Join (inner) (keys: 1, total key length: 8) + ............-> Sort (record length: 28, key length: 8) + ................-> Sort (record length: 28, key length: 8) + ....................-> Table "TEST1" as "A TEST1" Full Scan + ............-> Sort (record length: 28, key length: 8) + ................-> Sort (record length: 28, key length: 8) + ....................-> Table "TEST2" as "B B" Full Scan + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/tabloid/test_crash_when_too_long_username_for_auth.py b/tests/functional/tabloid/test_crash_when_too_long_username_for_auth.py index a57ac711..5d17cc41 100644 --- a/tests/functional/tabloid/test_crash_when_too_long_username_for_auth.py +++ b/tests/functional/tabloid/test_crash_when_too_long_username_for_auth.py @@ -4,10 +4,19 @@ TITLE: crash on assert or memory corruption when too long username is used for authentication NOTES: [19.07.2023] pzotov - Confirmed problem on 3.x only (3.0.11.33690). + Confirmed problem on 3.x only (3.0.11.33690): server crashed, client got: + Statement failed, SQLSTATE = 08004 + connection rejected by remote interface FB 4.0.3.2956 and 5.0.0.1093 (builds before 29-JUN-2023) issued "SQLSTATE = 28000 / Your user name and password..." but not crashed. After fix, all three FB issue "SQLSTATE = 08006 / Error occurred during login, please check server firebird.log for details" Checked on 3.0.11.33965 -- all OK. + + [14.07.2024] pzotov + Customized for run against dev build after Dimitry Sibiryakov request. + Dev build issues status vector containing TWO elements: + 335545106 ==> Error occurred during login, please check server firebird.log for details + 335544882 ==> Login name too long (@1 characters, maximum allowed @2) + We can filter out its second item.. """ import locale import re @@ -15,26 +24,32 @@ import pytest from firebird.qa import * +from firebird.driver import DatabaseError -db = db_factory() # charset = 'utf8', init = init_sql) +db = db_factory() -act = python_act( 'db', substitutions=[('[ \t]+',' ')] ) +act = python_act( 'db', substitutions=[('[ \t]+',' '), ('.*Login name too long.*', '')] ) @pytest.mark.version('>=3.0.11') def test_1(act: Action, capsys): - MAX_NAME_LEN = 31 if act.is_version('<=3') else 63 - TOO_LONG_USR = 'u1111111111222222222233333333334444444444555555555566666666667777777777' - test_sql = f""" - connect '{act.db.dsn}' user '{TOO_LONG_USR}' password 'qqq'; - quit; - """ - - expected_stdout = """ - Statement failed, SQLSTATE = 08006 + TOO_LONG_USR = 'u2345678901234567890123456789012345678901234567890123456789012345678901' + # 1 2 3 4 5 6 7 + try: + with act.db.connect(user = TOO_LONG_USR, password = 'qwe', charset = 'win1251'): + pass + except DatabaseError as e: + # ACHTUNG: dev-build will raise error with TWO gdscodes: [335545106, 335544882]. + # 335545106 ==> Error occurred during login, please check server firebird.log for details + # 335544882 ==> Login name too long (@1 characters, maximum allowed @2) + # We have to check only first of them. DO NOT iterate through gds_codes tuple! + print( e.gds_codes[0] ) + print( e.__str__() ) + + act.stdout = capsys.readouterr().out + act.expected_stdout = """ + 335545106 Error occurred during login, please check server firebird.log for details """ - act.expected_stdout = expected_stdout - act.isql(switches=['-q'], charset = 'win1251', credentials = False, connect_db = False, input = test_sql, combine_output = True, io_enc = locale.getpreferredencoding()) assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/tabloid/test_e260ced8.py b/tests/functional/tabloid/test_e260ced8.py index 60b46e21..4b7f3b8c 100644 --- a/tests/functional/tabloid/test_e260ced8.py +++ b/tests/functional/tabloid/test_e260ced8.py @@ -6,7 +6,7 @@ TITLE: Allow computable but non-invariant lists to be used for index lookup DESCRIPTION: NOTES: - [08.09.2023] + [08.09.2023] pzotov Before improvement explained plan was: ======= Sub-query @@ -32,6 +32,11 @@ (it is desirable to leading indents). Checked on 5.0.0.1190. + + [04.02.2025] pzotov + Adjusted execution plan for EXISTS() part of recursive query: "List Scan" was replaced with "Range Scan" for + "where b.x in (a.x, 2*a.x, 3*a.x)". This change caused by commit 0cc77c89 ("Fix #8109: Plan/Performance regression ...") + Checked on 6.0.0.607-1985b88, 5.0.2.1610-5e63ad0 """ import pytest @@ -50,7 +55,7 @@ act = python_act('db') -def replace_leading(source, char="#"): +def replace_leading(source, char="."): stripped = source.lstrip() return char * (len(source) - len(stripped)) + stripped @@ -63,19 +68,28 @@ def test_1(act: Action, capsys): where exists( select * from t1 b + -- 04-feb-2025: bitmap_Or for three values will be used here since commit + -- 0cc77c89 ('Fix #8109: Plan/Performance regression ...') + -- Before this change plan has: 'Index "T1_X" List Scan (full match)' where b.x in (a.x, 2*a.x, 3*a.x) ) """ act.expected_stdout = """ Sub-query - ####-> Filter - ########-> Table "T1" as "B" Access By ID - ############-> Bitmap - ################-> Index "T1_X" List Scan (full match) + ....-> Filter + ........-> Table "T1" as "B" Access By ID + ............-> Bitmap Or + ................-> Bitmap Or + ....................-> Bitmap + ........................-> Index "T1_X" Range Scan (full match) + ....................-> Bitmap + ........................-> Index "T1_X" Range Scan (full match) + ................-> Bitmap + ....................-> Index "T1_X" Range Scan (full match) Select Expression - ####-> Filter - ########-> Table "T1" as "A" Full Scan + ....-> Filter + ........-> Table "T1" as "A" Full Scan """ with act.db.connect() as con: cur = con.cursor() diff --git a/tests/functional/tabloid/test_fd0fa8a3.py b/tests/functional/tabloid/test_fd0fa8a3.py new file mode 100644 index 00000000..d3a6768e --- /dev/null +++ b/tests/functional/tabloid/test_fd0fa8a3.py @@ -0,0 +1,137 @@ +#coding:utf-8 + +""" +ID: None +ISSUE: https://github.com/FirebirdSQL/firebird/commit/fd0fa8a3a58fbfe7fdc0641b4e48258643d72127 +TITLE: Let include file name into error message when creation of temp file failed. +DESCRIPTION: + Test uses pre-created databases.conf which has alias 'tmp_fd0fa8a3_alias' (see variable REQUIRED_ALIAS). + Database file for that alias must NOT exist in the $(dir_sampleDb)/qa/ subdirectory: it will be created here. + For this alias parameter TempTableDirectory is defined and it points to invalid/inaccessible directory. + Currently its value is: '<>' (without single quotes), so there is no way to create any file in it. + We check that: + * client still has ability to create GTT and put data in it, without getting error; + * firebird.log will have appropriate message about problem with creating file ('fb_*****') in TempTableDirectory +NOTES: + [12.08.2024] pzotov + 1. One need to be sure that firebird.conf does NOT contain DatabaseAccess = None. + 2. Value of REQUIRED_ALIAS must be EXACTLY the same as alias specified in the pre-created databases.conf + (for LINUX this equality is case-sensitive, even when aliases are compared!) + 3. Content of databases.conf must be taken from $QA_ROOT/files/qa-databases.conf (one need to replace it before every test session). + Discussed with pcisar, letters since 30-may-2022 13:48, subject: + "new qa, core_4964_test.py: strange outcome when use... shutil.copy() // comparing to shutil.copy2()" + 4. Invalid value of TempTableDirectory causes appropriate message to appear in firebird.log TWO times: first when QA-plugin prepares + test database (and this is out of scope to be checked by this test), and second when we try to insert data in the GTT. + DDL statement ('create global temporary table') does NOT cause any message in firebird.log until we do not try to add data in it. + Because of that, difference between content of firebird.log will contain only ONE message "Error creating file...". + + Parameter 'TempTableDirectory' exists in FB-4.x since 20.04.2021, commit f2805020a6f34d253c93b8edac6068c1b35f9b89., build 4.0.0.2436. + Checked on Windows/Linux, 6.0.0.423, 5.0.2.1477, 4.0.6.3141. +""" + +import os +import re +import locale +from difflib import unified_diff +from pathlib import Path + +import pytest +from firebird.qa import * + +REQUIRED_ALIAS = 'tmp_fd0fa8a3_alias' + +substitutions = [ ('[ \t]+', ' ') + ,('Error creating file in TempTableDirectory.*', 'Error creating file in TempTableDirectory') + ,('I/O error during "((CreateFile\\s+\\(create\\))|open)" operation for file.*', 'I/O error during CreateFile operation for file') + ] +db = db_factory(filename = '#' + REQUIRED_ALIAS) +act = python_act('db', substitutions = substitutions) + +@pytest.mark.version('>=4.0') +def test_1(act: Action, capsys): + + # Scan line-by-line through databases.conf, find line starting with REQUIRED_ALIAS and extract name of file that + # must be created in the $(dir_sampleDb)/qa/ folder. This name will be used further as target database (tmp_fdb). + # NOTE: we have to SKIP lines which are commented out, i.e. if they starts with '#': + p_required_alias_ptn = re.compile( '^(?!#)((^|\\s+)' + REQUIRED_ALIAS + ')\\s*=\\s*\\$\\(dir_sampleDb\\)/qa/', re.IGNORECASE ) + fname_in_dbconf = None + + with open(act.home_dir/'databases.conf', 'r') as f: + for line in f: + if p_required_alias_ptn.search(line): + # If databases.conf contains line like this: + # tmp_NNNN_alias = $(dir_sampleDb)/qa/tmp_qa_NNNN.fdb + # - then we extract filename: 'tmp_qa_NNNN.fdb' (see below): + fname_in_dbconf = Path(line.split('=')[1].strip()).name + break + + # if 'fname_in_dbconf' remains undefined here then propably REQUIRED_ALIAS not equals to specified in the databases.conf! + # + assert fname_in_dbconf + + check_sql = f""" + set bail on; + recreate global temporary table test(x int); + set count on; + insert into test(x) values(1); + """ + + # Check-1: no error must be issued on client-side, all records have to be inserted: + # + expected_stdout = f""" + Records affected: 1 + """ + + # Get content of firebird.log BEFORE test. + # ::: NB ::: + # At this point firebird.log must already contain message about unable to create file because of inaccessible TempTableDirectory value. + # This message was added when test database have been created by QA-plugin, i.e. out of this test code. So, the difference between + # log content will NOT contain this message! + # + log_before = act.get_firebird_log() + + act.expected_stdout = expected_stdout + act.isql(switches=['-q'], input = check_sql, combine_output = True, io_enc = locale.getpreferredencoding()) + assert act.clean_stdout == act.clean_expected_stdout + act.reset() + + # Get content of firebird.log AFTER test. + # ONLY ONE message about invalid TempTableDirectory value will be taken in account! + # + log_after = act.get_firebird_log() + + #---------------------------------------------------- + + # Check-2: firebird.log must have message (partially localized): + # on Windows: + # Database: ... + # Error creating file in TempTableDirectory "..." + # I/O error during "CreateFile (create)" operation for file "..." + # Error while trying to create file + # Syntax error in file name, folder name, or volume label [ NB: THIS LINE CAN BE LOCALIZED ] + # + # on LINUX: + # Database: ... + # Error creating file in TempTableDirectory "..." + # I/O error during "open" operation for file "..." + # Error while trying to create file + # No such file or directory + + allowed_patterns = [ re.compile('Error creating file in TempTableDirectory',re.IGNORECASE), + re.compile('I/O error during "((CreateFile\\s+\\(create\\))|open)" operation for file',re.IGNORECASE), + re.compile('Error while trying to create file',re.IGNORECASE) + ] + + for line in unified_diff(log_before, log_after): + if (msg := line.strip()): + if msg.startswith('+') and act.match_any(msg, allowed_patterns): + print(msg[1:]) + + act.expected_stdout = """ + Error creating file in TempTableDirectory + I/O error during "CreateFile (create)" operation for file + Error while trying to create file + """ + act.stdout = capsys.readouterr().out + assert act.clean_stdout == act.clean_expected_stdout + act.reset() diff --git a/tests/functional/tabloid/test_ibp_124.py b/tests/functional/tabloid/test_ibp_124.py new file mode 100644 index 00000000..c6dec8bf --- /dev/null +++ b/tests/functional/tabloid/test_ibp_124.py @@ -0,0 +1,98 @@ +#coding:utf-8 + +""" +ID: n/a +TITLE: WHERE CURRENT OF within block that has savepoint: statement #2 must see changes produced by statement #1 +DESCRIPTION: + Original issue: + https://quality.embarcadero.com/projects/IBP/issues/IBP-124?filter=allopenissues&orderby=priority+ASC%2C+updated+DESC +NOTES: + [02.06.2025] pzotov + 1. Test has similar code to described in CORE-5794 (GH-6057) but adds WHEN-block in order to create implicit savepoint. + This causes changes that have been done by statement #1 to be seen for (same) cursor when it runs statement #2. + 2. Difference of this begaviour (comparing to 2.5) relates to CURSOR STABILITY feature that appeared since 3.x + Workaround was provided by hvlad, see: + https://github.com/FirebirdSQL/firebird/issues/6057#issuecomment-826242377 +""" + +import pytest +from firebird.qa import * + +db = db_factory() + +test_script = """ + set list on; + create or alter procedure sp_test as begin end; + recreate table test ( + id int generated by default as identity, + data1 int, + data2 int + ); + recreate table dml_audit(log_id int generated by default as identity, log_msg varchar(255)); + + set term ^; + create trigger trg_test_bu0 for test active before update position 0 as + declare log_msg type of column dml_audit.log_msg; + begin + log_msg = + 'old.data1: ' || old.data1 || + ', new.data1: ' || new.data1 || + ', old.data2: ' || old.data2 || + ', new.data2: ' || new.data2 + ; + insert into dml_audit(log_msg) values(:log_msg); + end + ^ + create or alter procedure sp_test as + declare var_id integer; + begin + for + select t.id from test t where t.id = 1 + into + :var_id + as cursor + cur_tst + do + begin + update test t set t.data1 = 1 -- ....... statement #1 + where current of cur_tst; + + update test t set t.data2 = 2 -- ....... statement #2 + where current of cur_tst; + + -- following 'when'-block causes engine to create implicit savepoint and + -- changed value of data1 will be seen here. + -- Without this block value of 'DATA1' column will be seen here as old one, i.e. 0. + -- https://github.com/FirebirdSQL/firebird/issues/6057#issuecomment-826242377 + when any do + exception; + end + end ^ + set term ;^ + commit; + + insert into test (data1, data2) values (0, 0); + execute procedure sp_test; + + select * from test; + select * from dml_audit; +""" + +act = isql_act('db', test_script) + +expected_stdout = """ + ID 1 + DATA1 1 + DATA2 2 + + LOG_ID 1 + LOG_MSG old.data1: 0, new.data1: 1, old.data2: 0, new.data2: 0 + LOG_ID 2 + LOG_MSG old.data1: 1, new.data1: 1, old.data2: 0, new.data2: 2 +""" + +@pytest.mark.version('>=3.0') +def test_1(act: Action): + act.expected_stdout = expected_stdout + act.execute(combine_output = True) + assert act.clean_stdout == act.clean_expected_stdout diff --git a/tests/functional/transactions/test_read_consist_statement_delete_undone_01.py b/tests/functional/transactions/test_read_consist_statement_delete_undone_01.py index 72acd756..e657b745 100644 --- a/tests/functional/transactions/test_read_consist_statement_delete_undone_01.py +++ b/tests/functional/transactions/test_read_consist_statement_delete_undone_01.py @@ -213,6 +213,7 @@ def wait_for_record_become_locked(tx_monitoring, cur_monitoring, sql_to_lock_rec #----------------------------------------------------------------------------------------------------------------------------------------------------- +@pytest.mark.trace @pytest.mark.version('>=4.0.3') def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: Path, capsys): sql_init = (act.files_dir / 'read-consist-sttm-restart-DDL.sql').read_text() diff --git a/tests/functional/transactions/test_read_consist_statement_delete_undone_02.py b/tests/functional/transactions/test_read_consist_statement_delete_undone_02.py index 2bd7ec7a..97e7028e 100644 --- a/tests/functional/transactions/test_read_consist_statement_delete_undone_02.py +++ b/tests/functional/transactions/test_read_consist_statement_delete_undone_02.py @@ -126,6 +126,16 @@ NB! Worker transaction must running in WAIT mode - in contrary to Tx that we start in our monitoring loop. Checked on WI-T6.0.0.48, WI-T5.0.0.1211, WI-V4.0.4.2988. + + [20.11.2024] pzotov + ::: ACHTUNG ::: ENGINE MUST NOT USE 'PLAN SORT' IN THE QUERY WHICH HAS TO BE RESTARTED IN THIS TEST! + Number of statement restarts CAN BE GREATER than expected! This occurs if a table (which is handled) has no appropriate index or if optimizer decides to use + external sort (e.g. 'PLAN SORT') instead of index navigation. This affects only FB 6.x and can be seen on snapshots since 14.11.2024, see: + https://github.com/FirebirdSQL/firebird/commit/26e64e9c08f635d55ac7a111469498b3f0c7fe81 ("Cost-based decision between ORDER and SORT plans (#8316)"). + This result was explained by Vlad (letter 19.11.2024 09:59): external sort forces engine to materialize cursor resultset. In such state, in turn, cursor can not + see records which not fall to this cursor expression or did not exist when cursor started its job. + Because of that, SQL_TO_BE_RESTARTED expression was changed: 'ROWS 10' was added after 'ORDER BY' clause to make optimizer choose 'PLAN ORDER' every time. + Perhaps, this change is temporary solution. """ import subprocess @@ -203,6 +213,7 @@ def wait_for_record_become_locked(tx_monitoring, cur_monitoring, sql_to_lock_rec #----------------------------------------------------------------------------------------------------------------------------------------------------- +@pytest.mark.trace @pytest.mark.version('>=4.0.3') def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: Path, capsys): sql_init = (act.files_dir / 'read-consist-sttm-restart-DDL.sql').read_text() @@ -210,7 +221,7 @@ def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: for checked_mode in('table', 'view'): target_obj = 'test' if checked_mode == 'table' else 'v_test' - SQL_TO_BE_RESTARTED = f'delete /* {SQL_TAG_THAT_WE_WAITING_FOR} */ from {target_obj} where x not in (select x from {target_obj} where id >= 4) order by id desc' + SQL_TO_BE_RESTARTED = f'delete /* {SQL_TAG_THAT_WE_WAITING_FOR} */ from {target_obj} where x not in (select x from {target_obj} where id >= 4) order by id desc ROWS 10' # add rows with ID = 1,2,3,4,5: sql_addi=''' diff --git a/tests/functional/transactions/test_read_consist_sttm_merge_deny_multiple_matches.py b/tests/functional/transactions/test_read_consist_sttm_merge_deny_multiple_matches.py index e439284e..d97100a4 100644 --- a/tests/functional/transactions/test_read_consist_sttm_merge_deny_multiple_matches.py +++ b/tests/functional/transactions/test_read_consist_sttm_merge_deny_multiple_matches.py @@ -172,6 +172,7 @@ def wait_for_record_become_locked(tx_monitoring, cur_monitoring, sql_to_lock_rec #----------------------------------------------------------------------------------------------------------------------------------------------------- +@pytest.mark.trace @pytest.mark.version('>=4.0.2') def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: Path, capsys): sql_init = (act.files_dir / 'read-consist-sttm-restart-DDL.sql').read_text() @@ -184,6 +185,7 @@ def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: using ( select s.id, s.x from {target_obj} as s where s.id <= 1 + order by s.id DESC -- added only 05-jun-2024; thanks to Vlad. ) s on abs(t.id) = abs(s.id) when matched then diff --git a/tests/functional/transactions/test_read_consist_sttm_restart_max_limit.py b/tests/functional/transactions/test_read_consist_sttm_restart_max_limit.py index d441b563..9b4bad40 100644 --- a/tests/functional/transactions/test_read_consist_sttm_restart_max_limit.py +++ b/tests/functional/transactions/test_read_consist_sttm_restart_max_limit.py @@ -166,6 +166,7 @@ def wait_for_record_become_locked(tx_monitoring, cur_monitoring, sql_to_lock_rec #----------------------------------------------------------------------------------------------------------------------------------------------------- +@pytest.mark.trace @pytest.mark.version('>=4.0.2') def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: Path, capsys): diff --git a/tests/functional/transactions/test_read_consist_sttm_restart_on_delete_01.py b/tests/functional/transactions/test_read_consist_sttm_restart_on_delete_01.py index 68da7f96..91bdb750 100644 --- a/tests/functional/transactions/test_read_consist_sttm_restart_on_delete_01.py +++ b/tests/functional/transactions/test_read_consist_sttm_restart_on_delete_01.py @@ -234,6 +234,7 @@ def wait_for_record_become_locked(tx_monitoring, cur_monitoring, sql_to_lock_rec #----------------------------------------------------------------------------------------------------------------------------------------------------- +@pytest.mark.trace @pytest.mark.version('>=4.0.2') def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: Path, capsys): sql_init = (act.files_dir / 'read-consist-sttm-restart-DDL.sql').read_text() diff --git a/tests/functional/transactions/test_read_consist_sttm_restart_on_delete_02.py b/tests/functional/transactions/test_read_consist_sttm_restart_on_delete_02.py index 3cadea0d..12b21601 100644 --- a/tests/functional/transactions/test_read_consist_sttm_restart_on_delete_02.py +++ b/tests/functional/transactions/test_read_consist_sttm_restart_on_delete_02.py @@ -224,6 +224,7 @@ def wait_for_record_become_locked(tx_monitoring, cur_monitoring, sql_to_lock_rec #----------------------------------------------------------------------------------------------------------------------------------------------------- +@pytest.mark.trace @pytest.mark.version('>=4.0.2') def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: Path, capsys): sql_init = (act.files_dir / 'read-consist-sttm-restart-DDL.sql').read_text() diff --git a/tests/functional/transactions/test_read_consist_sttm_restart_on_delete_03.py b/tests/functional/transactions/test_read_consist_sttm_restart_on_delete_03.py index f3bef90e..8025706b 100644 --- a/tests/functional/transactions/test_read_consist_sttm_restart_on_delete_03.py +++ b/tests/functional/transactions/test_read_consist_sttm_restart_on_delete_03.py @@ -244,6 +244,7 @@ def wait_for_record_become_locked(tx_monitoring, cur_monitoring, sql_to_lock_rec #----------------------------------------------------------------------------------------------------------------------------------------------------- +@pytest.mark.trace @pytest.mark.version('>=4.0.2') def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: Path, capsys): sql_init = (act.files_dir / 'read-consist-sttm-restart-DDL.sql').read_text() diff --git a/tests/functional/transactions/test_read_consist_sttm_restart_on_delete_04.py b/tests/functional/transactions/test_read_consist_sttm_restart_on_delete_04.py index f6f1c217..45a4e504 100644 --- a/tests/functional/transactions/test_read_consist_sttm_restart_on_delete_04.py +++ b/tests/functional/transactions/test_read_consist_sttm_restart_on_delete_04.py @@ -225,6 +225,7 @@ def wait_for_record_become_locked(tx_monitoring, cur_monitoring, sql_to_lock_rec #----------------------------------------------------------------------------------------------------------------------------------------------------- +@pytest.mark.trace @pytest.mark.version('>=4.0') def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: Path, capsys): sql_init = (act.files_dir / 'read-consist-sttm-restart-DDL.sql').read_text() diff --git a/tests/functional/transactions/test_read_consist_sttm_restart_on_merge_01.py b/tests/functional/transactions/test_read_consist_sttm_restart_on_merge_01.py index dbdb5cf2..d3b5281e 100644 --- a/tests/functional/transactions/test_read_consist_sttm_restart_on_merge_01.py +++ b/tests/functional/transactions/test_read_consist_sttm_restart_on_merge_01.py @@ -142,6 +142,16 @@ [25.11.2023] pzotov Writing code requires more care since 6.0.0.150: ISQL does not allow specifying duplicate delimiters without any statements between them (two semicolon, two carets etc). Merge expression defined in 'SQL_TO_BE_RESTARTED' must NOT end with semicolon! + + [20.11.2024] pzotov + ::: ACHTUNG ::: ENGINE MUST NOT USE 'PLAN SORT' IN THE QUERY WHICH HAS TO BE RESTARTED IN THIS TEST! + Number of statement restarts CAN BE GREATER than expected! This occurs if a table (which is handled) has no appropriate index or if optimizer decides to use + external sort (e.g. 'PLAN SORT') instead of index navigation. This affects only FB 6.x and can be seen on snapshots since 14.11.2024, see: + https://github.com/FirebirdSQL/firebird/commit/26e64e9c08f635d55ac7a111469498b3f0c7fe81 ("Cost-based decision between ORDER and SORT plans (#8316)"). + This result was explained by Vlad (letter 19.11.2024 09:59): external sort forces engine to materialize cursor resultset. In such state, in turn, cursor can not + see records which not fall to this cursor expression or did not exist when cursor started its job. + Because of that, SQL_TO_BE_RESTARTED expression was changed: 'ROWS 10' was added after 'ORDER BY' clause to make optimizer choose 'PLAN ORDER' every time. + Perhaps, this change is temporary solution. """ import subprocess @@ -216,6 +226,7 @@ def wait_for_record_become_locked(tx_monitoring, cur_monitoring, sql_to_lock_rec #----------------------------------------------------------------------------------------------------------------------------------------------------- +@pytest.mark.trace @pytest.mark.version('>=4.0.2') def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: Path, capsys): sql_init = (act.files_dir / 'read-consist-sttm-restart-DDL.sql').read_text() @@ -225,7 +236,7 @@ def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: SQL_TO_BE_RESTARTED = f""" merge /* {SQL_TAG_THAT_WE_WAITING_FOR} */ into {target_obj} t - using (select * from {target_obj} order by id) s on s.id=t.id + using (select * from {target_obj} order by id ROWS 10) s on s.id=t.id when matched then update set t.id = -t.id, t.x = -s.x when not matched then diff --git a/tests/functional/transactions/test_read_consist_sttm_restart_on_merge_02.py b/tests/functional/transactions/test_read_consist_sttm_restart_on_merge_02.py index 5e737b29..e04d8c23 100644 --- a/tests/functional/transactions/test_read_consist_sttm_restart_on_merge_02.py +++ b/tests/functional/transactions/test_read_consist_sttm_restart_on_merge_02.py @@ -270,6 +270,7 @@ def wait_for_record_become_locked(tx_monitoring, cur_monitoring, sql_to_lock_rec #----------------------------------------------------------------------------------------------------------------------------------------------------- +@pytest.mark.trace @pytest.mark.version('>=4.0.2') def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: Path, capsys): sql_init = (act.files_dir / 'read-consist-sttm-restart-DDL.sql').read_text() diff --git a/tests/functional/transactions/test_read_consist_sttm_restart_on_merge_03.py b/tests/functional/transactions/test_read_consist_sttm_restart_on_merge_03.py index ea2dd34c..070c96b6 100644 --- a/tests/functional/transactions/test_read_consist_sttm_restart_on_merge_03.py +++ b/tests/functional/transactions/test_read_consist_sttm_restart_on_merge_03.py @@ -216,6 +216,7 @@ def wait_for_record_become_locked(tx_monitoring, cur_monitoring, sql_to_lock_rec #----------------------------------------------------------------------------------------------------------------------------------------------------- +@pytest.mark.trace @pytest.mark.version('>=4.0.2') def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: Path, capsys): sql_init = (act.files_dir / 'read-consist-sttm-restart-DDL.sql').read_text() diff --git a/tests/functional/transactions/test_read_consist_sttm_restart_on_merge_04.py b/tests/functional/transactions/test_read_consist_sttm_restart_on_merge_04.py index 5cf6736c..d6be7be4 100644 --- a/tests/functional/transactions/test_read_consist_sttm_restart_on_merge_04.py +++ b/tests/functional/transactions/test_read_consist_sttm_restart_on_merge_04.py @@ -238,6 +238,7 @@ def wait_for_record_become_locked(tx_monitoring, cur_monitoring, sql_to_lock_rec #----------------------------------------------------------------------------------------------------------------------------------------------------- +@pytest.mark.trace @pytest.mark.version('>=4.0') def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: Path, capsys): sql_init = (act.files_dir / 'read-consist-sttm-restart-DDL.sql').read_text() diff --git a/tests/functional/transactions/test_read_consist_sttm_restart_on_update_01.py b/tests/functional/transactions/test_read_consist_sttm_restart_on_update_01.py index 52d6eab6..db931d0d 100644 --- a/tests/functional/transactions/test_read_consist_sttm_restart_on_update_01.py +++ b/tests/functional/transactions/test_read_consist_sttm_restart_on_update_01.py @@ -216,6 +216,7 @@ def wait_for_record_become_locked(tx_monitoring, cur_monitoring, sql_to_lock_rec #----------------------------------------------------------------------------------------------------------------------------------------------------- +@pytest.mark.trace @pytest.mark.version('>=4.0.2') def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: Path, capsys): sql_init = (act.files_dir / 'read-consist-sttm-restart-DDL.sql').read_text() diff --git a/tests/functional/transactions/test_read_consist_sttm_restart_on_update_02.py b/tests/functional/transactions/test_read_consist_sttm_restart_on_update_02.py index 1f4096c0..071519ba 100644 --- a/tests/functional/transactions/test_read_consist_sttm_restart_on_update_02.py +++ b/tests/functional/transactions/test_read_consist_sttm_restart_on_update_02.py @@ -221,6 +221,7 @@ def wait_for_record_become_locked(tx_monitoring, cur_monitoring, sql_to_lock_rec #----------------------------------------------------------------------------------------------------------------------------------------------------- +@pytest.mark.trace @pytest.mark.version('>=4.0') def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: Path, capsys): sql_init = (act.files_dir / 'read-consist-sttm-restart-DDL.sql').read_text() diff --git a/tests/functional/transactions/test_read_consist_sttm_restart_on_update_03.py b/tests/functional/transactions/test_read_consist_sttm_restart_on_update_03.py index 8763a9fb..41deef2b 100644 --- a/tests/functional/transactions/test_read_consist_sttm_restart_on_update_03.py +++ b/tests/functional/transactions/test_read_consist_sttm_restart_on_update_03.py @@ -151,8 +151,19 @@ NB! Worker transaction must running in WAIT mode - in contrary to Tx that we start in our monitoring loop. Checked on WI-T6.0.0.48, WI-T5.0.0.1211, WI-V4.0.4.2988. + + [20.11.2024] pzotov + ::: ACHTUNG ::: ENGINE MUST NOT USE 'PLAN SORT' IN THE QUERY WHICH HAS TO BE RESTARTED IN THIS TEST! + Number of statement restarts CAN BE GREATER than expected! This occurs if a table (which is handled) has no appropriate index or if optimizer decides to use + external sort (e.g. 'PLAN SORT') instead of index navigation. This affects only FB 6.x and can be seen on snapshots since 14.11.2024, see: + https://github.com/FirebirdSQL/firebird/commit/26e64e9c08f635d55ac7a111469498b3f0c7fe81 ("Cost-based decision between ORDER and SORT plans (#8316)"). + This result was explained by Vlad (letter 19.11.2024 09:59): external sort forces engine to materialize cursor resultset. In such state, in turn, cursor can not + see records which not fall to this cursor expression or did not exist when cursor started its job. + Because of that, SQL_TO_BE_RESTARTED expression was changed: 'ROWS 10' was added after 'ORDER BY' clause to make optimizer choose 'PLAN ORDER' every time. + Perhaps, this change is temporary solution. """ +import inspect import subprocess import re from pathlib import Path @@ -187,11 +198,18 @@ def wait_for_record_become_locked(tx_monitoring, cur_monitoring, sql_to_lock_rec t1=py_dt.datetime.now() required_concurrent_found = None concurrent_tx_pattern = re.compile('concurrent transaction number is \\d+', re.IGNORECASE) + + iter = 0 while True: concurrent_tx_number = None concurrent_runsql = '' tx_monitoring.begin() try: + sql_tag_wait_for_lock_record = f'/* {inspect.stack()[0][3]}(), iter: {iter} */' + sql_words = sql_to_lock_record.split() + sql_words.insert(1, sql_tag_wait_for_lock_record) + sql_to_lock_record = ' '.join(sql_words) + iter += 1 cur_monitoring.execute(sql_to_lock_record) except DatabaseError as exc: # Failed: SQL execution failed with: deadlock @@ -206,7 +224,6 @@ def wait_for_record_become_locked(tx_monitoring, cur_monitoring, sql_to_lock_rec if SQL_TAG_THAT_WE_WAITING_FOR in concurrent_runsql: required_concurrent_found = 1 - # pytest.fail(f"Can not upd, concurrent TX = {concurrent_tx_number}, sql: {concurrent_runsql}") finally: tx_monitoring.rollback() @@ -225,6 +242,7 @@ def wait_for_record_become_locked(tx_monitoring, cur_monitoring, sql_to_lock_rec #----------------------------------------------------------------------------------------------------------------------------------------------------- +@pytest.mark.trace @pytest.mark.version('>=4.0.2') def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: Path, capsys): sql_init = (act.files_dir / 'read-consist-sttm-restart-DDL.sql').read_text() @@ -232,7 +250,7 @@ def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: for checked_mode in('table', 'view'): target_obj = 'test' if checked_mode == 'table' else 'v_test' - SQL_TO_BE_RESTARTED = f"update /* {SQL_TAG_THAT_WE_WAITING_FOR} */ {target_obj} set id = -id order by id" + SQL_TO_BE_RESTARTED = f"update /* {SQL_TAG_THAT_WE_WAITING_FOR} */ {target_obj} set id = -id order by id ROWS 10" # add rows with ID = 1, 2: sql_addi = f''' @@ -276,7 +294,7 @@ def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: ### L O C K E R - 1 ### ######################### - con_lock_1.execute_immediate( f'update {target_obj} set id=id where id = 2' ) + con_lock_1.execute_immediate( f'update /* LOCKER-1 */ {target_obj} set id=id where id = 2' ) worker_sql = f''' set list on; @@ -297,7 +315,7 @@ def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: set wng off; set count on; - -- THIS MUST BE LOCKED: + -- WORKER. THIS MUST BE LOCKED NOW BY LOCKER_i: {SQL_TO_BE_RESTARTED}; -- check results: @@ -339,11 +357,11 @@ def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: ######################### ### L O C K E R - 2 ### ######################### - con_lock_2.execute_immediate( f'insert into {target_obj}(id) values(110)' ) - con_lock_2.execute_immediate( f'insert into {target_obj}(id) values(-11)' ) + con_lock_2.execute_immediate( f'insert /* LOCKER-2 */ into {target_obj}(id) values(110)' ) + con_lock_2.execute_immediate( f'insert /* LOCKER-2 */ into {target_obj}(id) values(-11)' ) con_lock_2.commit() - con_lock_2.execute_immediate( f'update {target_obj} set id=id where id = 110' ) - con_lock_2.execute_immediate( f'update {target_obj} set id=id where id = -11' ) + con_lock_2.execute_immediate( f'update /* LOCKER-2 */ {target_obj} set id=id where id = 110' ) + con_lock_2.execute_immediate( f'update /* LOCKER-2 */ {target_obj} set id=id where id = -11' ) ######################### ### L O C K E R - 1 ### @@ -355,11 +373,11 @@ def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: wait_for_record_become_locked(tx_monitoring, cur_monitoring, f'update {target_obj} set id=id where id=2', SQL_TAG_THAT_WE_WAITING_FOR) # If we come here then it means that record with ID = 2 for sure is locked by WORKER. - con_lock_1.execute_immediate( f'insert into {target_obj}(id) values(120)' ) - con_lock_1.execute_immediate( f'insert into {target_obj}(id) values(-12)' ) + con_lock_1.execute_immediate( f'insert /* LOCKER-1 */ into {target_obj}(id) values(120)' ) + con_lock_1.execute_immediate( f'insert /* LOCKER-1 */ into {target_obj}(id) values(-12)' ) con_lock_1.commit() - con_lock_1.execute_immediate( f'update {target_obj} set id=id where id = 120' ) - con_lock_1.execute_immediate( f'update {target_obj} set id=id where id = -12' ) + con_lock_1.execute_immediate( f'update /* LOCKER-1 */ {target_obj} set id=id where id = 120' ) + con_lock_1.execute_immediate( f'update /* LOCKER-1 */ {target_obj} set id=id where id = -12' ) ######################### @@ -373,11 +391,11 @@ def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: # If we come here then it means that TWO records with ID = -11 and 110 for sure are locked by WORKER. - con_lock_2.execute_immediate( f'insert into {target_obj}(id) values(130)' ) - con_lock_2.execute_immediate( f'insert into {target_obj}(id) values(-13)' ) + con_lock_2.execute_immediate( f'insert into /* LOCKER-2 */ {target_obj}(id) values(130)' ) + con_lock_2.execute_immediate( f'insert into /* LOCKER-2 */ {target_obj}(id) values(-13)' ) con_lock_2.commit() - con_lock_2.execute_immediate( f'update {target_obj} set id=id where id = 130' ) - con_lock_2.execute_immediate( f'update {target_obj} set id=id where id = -13' ) + con_lock_2.execute_immediate( f'update /* LOCKER-2 */ {target_obj} set id=id where id = 130' ) + con_lock_2.execute_immediate( f'update /* LOCKER-2 */ {target_obj} set id=id where id = -13' ) ######################### ### L O C K E R - 1 ### @@ -390,11 +408,11 @@ def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: # If we come here then it means that TWO records with ID = -12 and 120 for sure are locked by WORKER. - con_lock_1.execute_immediate( f'insert into {target_obj}(id) values(140)' ) - con_lock_1.execute_immediate( f'insert into {target_obj}(id) values(-14)' ) + con_lock_1.execute_immediate( f'insert /* LOCKER-1 */ into {target_obj}(id) values(140)' ) + con_lock_1.execute_immediate( f'insert /* LOCKER-1 */ into {target_obj}(id) values(-14)' ) con_lock_1.commit() - con_lock_1.execute_immediate( f'update {target_obj} set id=id where id = 140' ) - con_lock_1.execute_immediate( f'update {target_obj} set id=id where id = -14' ) + con_lock_1.execute_immediate( f'update /* LOCKER-1 */ {target_obj} set id=id where id = 140' ) + con_lock_1.execute_immediate( f'update /* LOCKER-1 */ {target_obj} set id=id where id = -14' ) ######################### ### L O C K E R - 2 ### @@ -428,32 +446,32 @@ def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: expected_stdout_worker = f""" checked_mode: {checked_mode}, STDLOG: Records affected: 10 - checked_mode: {checked_mode}, STDLOG: ID - checked_mode: {checked_mode}, STDLOG: ======= - checked_mode: {checked_mode}, STDLOG: -140 - checked_mode: {checked_mode}, STDLOG: -130 - checked_mode: {checked_mode}, STDLOG: -120 - checked_mode: {checked_mode}, STDLOG: -110 - checked_mode: {checked_mode}, STDLOG: -2 - checked_mode: {checked_mode}, STDLOG: -1 - checked_mode: {checked_mode}, STDLOG: 11 - checked_mode: {checked_mode}, STDLOG: 12 - checked_mode: {checked_mode}, STDLOG: 13 - checked_mode: {checked_mode}, STDLOG: 14 + checked_mode: {checked_mode}, STDLOG: ID + checked_mode: {checked_mode}, STDLOG: + checked_mode: {checked_mode}, STDLOG: -140 + checked_mode: {checked_mode}, STDLOG: -130 + checked_mode: {checked_mode}, STDLOG: -120 + checked_mode: {checked_mode}, STDLOG: -110 + checked_mode: {checked_mode}, STDLOG: -2 + checked_mode: {checked_mode}, STDLOG: -1 + checked_mode: {checked_mode}, STDLOG: 11 + checked_mode: {checked_mode}, STDLOG: 12 + checked_mode: {checked_mode}, STDLOG: 13 + checked_mode: {checked_mode}, STDLOG: 14 checked_mode: {checked_mode}, STDLOG: Records affected: 10 - checked_mode: {checked_mode}, STDLOG: OLD_ID OP SNAP_NO_RANK - checked_mode: {checked_mode}, STDLOG: ======= ====== ===================== - checked_mode: {checked_mode}, STDLOG: 1 UPD 1 - checked_mode: {checked_mode}, STDLOG: -14 UPD 2 - checked_mode: {checked_mode}, STDLOG: -13 UPD 2 - checked_mode: {checked_mode}, STDLOG: -12 UPD 2 - checked_mode: {checked_mode}, STDLOG: -11 UPD 2 - checked_mode: {checked_mode}, STDLOG: 1 UPD 2 - checked_mode: {checked_mode}, STDLOG: 2 UPD 2 - checked_mode: {checked_mode}, STDLOG: 110 UPD 2 - checked_mode: {checked_mode}, STDLOG: 120 UPD 2 - checked_mode: {checked_mode}, STDLOG: 130 UPD 2 - checked_mode: {checked_mode}, STDLOG: 140 UPD 2 + checked_mode: {checked_mode}, STDLOG: OLD_ID OP SNAP_NO_RANK + checked_mode: {checked_mode}, STDLOG: + checked_mode: {checked_mode}, STDLOG: 1 UPD 1 + checked_mode: {checked_mode}, STDLOG: -14 UPD 2 + checked_mode: {checked_mode}, STDLOG: -13 UPD 2 + checked_mode: {checked_mode}, STDLOG: -12 UPD 2 + checked_mode: {checked_mode}, STDLOG: -11 UPD 2 + checked_mode: {checked_mode}, STDLOG: 1 UPD 2 + checked_mode: {checked_mode}, STDLOG: 2 UPD 2 + checked_mode: {checked_mode}, STDLOG: 110 UPD 2 + checked_mode: {checked_mode}, STDLOG: 120 UPD 2 + checked_mode: {checked_mode}, STDLOG: 130 UPD 2 + checked_mode: {checked_mode}, STDLOG: 140 UPD 2 checked_mode: {checked_mode}, STDLOG: Records affected: 11 """ diff --git a/tests/functional/transactions/test_read_consist_sttm_restart_on_update_04.py b/tests/functional/transactions/test_read_consist_sttm_restart_on_update_04.py index ea6c94bf..1b9918a6 100644 --- a/tests/functional/transactions/test_read_consist_sttm_restart_on_update_04.py +++ b/tests/functional/transactions/test_read_consist_sttm_restart_on_update_04.py @@ -223,6 +223,7 @@ def wait_for_record_become_locked(tx_monitoring, cur_monitoring, sql_to_lock_rec #----------------------------------------------------------------------------------------------------------------------------------------------------- +@pytest.mark.trace @pytest.mark.version('>=4.0') def test_1(act: Action, fn_worker_sql: Path, fn_worker_log: Path, fn_worker_err: Path, capsys): sql_init = (act.files_dir / 'read-consist-sttm-restart-DDL.sql').read_text() diff --git a/tests/functional/trigger/database/test_disconnect_05.py b/tests/functional/trigger/database/test_disconnect_05.py index 2f44e11d..8a54dae4 100644 --- a/tests/functional/trigger/database/test_disconnect_05.py +++ b/tests/functional/trigger/database/test_disconnect_05.py @@ -71,6 +71,7 @@ act = python_act('db', substitutions = substitutions) +@pytest.mark.trace @pytest.mark.version('>=5.0') def test_1(act: Action, tmp_worker: User, capsys): diff --git a/tests/functional/trigger/database/test_disconnect_06.py b/tests/functional/trigger/database/test_disconnect_06.py index 4b439761..b59368c0 100644 --- a/tests/functional/trigger/database/test_disconnect_06.py +++ b/tests/functional/trigger/database/test_disconnect_06.py @@ -44,6 +44,7 @@ act = python_act('db', substitutions = substitutions) +@pytest.mark.trace @pytest.mark.version('>=5.0') def test_1(act: Action, tmp_worker: User, capsys):