diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 000000000..d9a7f7e0c --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,42 @@ +build: false +version: dev-{build} +shallow_clone: true +clone_folder: C:\projects\yii2 + +environment: + matrix: + - php_ver: 7.2.4 + +cache: + - '%APPDATA%\Composer' + - '%LOCALAPPDATA%\Composer' + - C:\tools\php -> .appveyor.yml + - C:\tools\composer.phar -> .appveyor.yml + +init: + - SET PATH=C:\tools\php;%PATH% + +install: + - ps: Set-Service wuauserv -StartupType Manual + - IF NOT EXIST C:\tools\php (choco install --yes --allow-empty-checksums php --version %php_ver% --params '/InstallDir:C:\tools\php') + - cd C:\tools\php + - copy php.ini-production php.ini + - echo date.timezone="UTC" >> php.ini + - echo memory_limit=512M >> php.ini + - echo extension_dir=ext >> php.ini + - echo extension=php_curl.dll >> php.ini + - echo extension=php_fileinfo.dll >> php.ini + - echo extension=php_gd2.dll >> php.ini + - echo extension=php_intl.dll >> php.ini + - echo extension=php_mbstring.dll >> php.ini + - echo extension=php_openssl.dll >> php.ini + - echo extension=php_pdo_sqlite.dll >> php.ini + - IF NOT EXIST C:\tools\composer.phar (cd C:\tools && appveyor DownloadFile https://getcomposer.org/download/1.4.1/composer.phar) + +before_test: + - cd C:\projects\yii2 + - php C:\tools\composer.phar update --no-interaction --no-progress --prefer-stable --no-ansi + +test_script: + - cd C:\projects\yii2 + - vendor\bin\phpunit --exclude-group mssql,mysql,pgsql,sqlite,db,oci,wincache,xcache,zenddata,cubrid diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 000000000..08b73bbaa --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,24 @@ +engines: + duplication: + enabled: true + config: + languages: + - javascript + - php + eslint: + enabled: true + fixme: + enabled: true + phpmd: + enabled: true + config: + rulesets: "codesize,design,unusedcode,tests/data/codeclimate/phpmd_ruleset.xml" +ratings: + paths: + - "**.js" + - "**.php" +exclude_paths: +- tests/ +- build/ +- docs/ +- framework/messages/ diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..5c156024d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git +vendor +docs \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..257221d23 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..96212a359 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +**/*{.,-}min.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000..9faa37508 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,213 @@ +ecmaFeatures: + modules: true + jsx: true + +env: + amd: true + browser: true + es6: true + jquery: true + node: true + +# http://eslint.org/docs/rules/ +rules: + # Possible Errors + comma-dangle: [2, never] + no-cond-assign: 2 + no-console: 0 + no-constant-condition: 2 + no-control-regex: 2 + no-debugger: 2 + no-dupe-args: 2 + no-dupe-keys: 2 + no-duplicate-case: 2 + no-empty: 2 + no-empty-character-class: 2 + no-ex-assign: 2 + no-extra-boolean-cast: 2 + no-extra-parens: 0 + no-extra-semi: 2 + no-func-assign: 2 + no-inner-declarations: [2, functions] + no-invalid-regexp: 2 + no-irregular-whitespace: 2 + no-negated-in-lhs: 2 + no-obj-calls: 2 + no-regex-spaces: 2 + no-sparse-arrays: 2 + no-unexpected-multiline: 2 + no-unreachable: 2 + use-isnan: 2 + valid-jsdoc: 0 + valid-typeof: 2 + + # Best Practices + accessor-pairs: 2 + block-scoped-var: 0 + complexity: [2, 6] + consistent-return: 0 + curly: 0 + default-case: 0 + dot-location: 0 + dot-notation: 0 + eqeqeq: 2 + guard-for-in: 2 + no-alert: 2 + no-caller: 2 + no-case-declarations: 2 + no-div-regex: 2 + no-else-return: 0 + no-empty-label: 2 + no-empty-pattern: 2 + no-eq-null: 2 + no-eval: 2 + no-extend-native: 2 + no-extra-bind: 2 + no-fallthrough: 2 + no-floating-decimal: 0 + no-implicit-coercion: 0 + no-implied-eval: 2 + no-invalid-this: 0 + no-iterator: 2 + no-labels: 0 + no-lone-blocks: 2 + no-loop-func: 2 + no-magic-number: 0 + no-multi-spaces: 0 + no-multi-str: 0 + no-native-reassign: 2 + no-new-func: 2 + no-new-wrappers: 2 + no-new: 2 + no-octal-escape: 2 + no-octal: 2 + no-proto: 2 + no-redeclare: 2 + no-return-assign: 2 + no-script-url: 2 + no-self-compare: 2 + no-sequences: 0 + no-throw-literal: 0 + no-unused-expressions: 2 + no-useless-call: 2 + no-useless-concat: 2 + no-void: 2 + no-warning-comments: 0 + no-with: 2 + radix: 2 + vars-on-top: 0 + wrap-iife: 2 + yoda: 0 + + # Strict + strict: 0 + + # Variables + init-declarations: 0 + no-catch-shadow: 2 + no-delete-var: 2 + no-label-var: 2 + no-shadow-restricted-names: 2 + no-shadow: 0 + no-undef-init: 2 + no-undef: 0 + no-undefined: 0 + no-unused-vars: 0 + no-use-before-define: 0 + + # Node.js and CommonJS + callback-return: 2 + global-require: 2 + handle-callback-err: 2 + no-mixed-requires: 0 + no-new-require: 0 + no-path-concat: 2 + no-process-exit: 2 + no-restricted-modules: 0 + no-sync: 0 + + # Stylistic Issues + array-bracket-spacing: 0 + block-spacing: 0 + brace-style: 0 + camelcase: 0 + comma-spacing: 0 + comma-style: 0 + computed-property-spacing: 0 + consistent-this: 0 + eol-last: 0 + func-names: 0 + func-style: 0 + id-length: 0 + id-match: 0 + indent: 0 + jsx-quotes: 0 + key-spacing: 0 + linebreak-style: 0 + lines-around-comment: 0 + max-depth: 0 + max-len: 0 + max-nested-callbacks: 0 + max-params: 0 + max-statements: [2, 30] + new-cap: 0 + new-parens: 0 + newline-after-var: 0 + no-array-constructor: 0 + no-bitwise: 0 + no-continue: 0 + no-inline-comments: 0 + no-lonely-if: 0 + no-mixed-spaces-and-tabs: 0 + no-multiple-empty-lines: 0 + no-negated-condition: 0 + no-nested-ternary: 0 + no-new-object: 0 + no-plusplus: 0 + no-restricted-syntax: 0 + no-spaced-func: 0 + no-ternary: 0 + no-trailing-spaces: 0 + no-underscore-dangle: 0 + no-unneeded-ternary: 0 + object-curly-spacing: 0 + one-var: 0 + operator-assignment: 0 + operator-linebreak: 0 + padded-blocks: 0 + quote-props: 0 + quotes: 0 + require-jsdoc: 0 + semi-spacing: 0 + semi: 0 + sort-vars: 0 + space-after-keywords: 0 + space-before-blocks: 0 + space-before-function-paren: 0 + space-before-keywords: 0 + space-in-parens: 0 + space-infix-ops: 0 + space-return-throw-case: 0 + space-unary-ops: 0 + spaced-comment: 0 + wrap-regex: 0 + + # ECMAScript 6 + arrow-body-style: 0 + arrow-parens: 0 + arrow-spacing: 0 + constructor-super: 0 + generator-star-spacing: 0 + no-arrow-condition: 0 + no-class-assign: 0 + no-const-assign: 0 + no-dupe-class-members: 0 + no-this-before-super: 0 + no-var: 0 + object-shorthand: 0 + prefer-arrow-callback: 0 + prefer-const: 0 + prefer-reflect: 0 + prefer-spread: 0 + prefer-template: 0 + require-yield: 0 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..08f809af7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,38 @@ +# Autodetect text files +* text=auto + +# ...Unless the name matches the following overriding patterns + +# Definitively text files +*.php text +*.css text +*.js text +*.txt text +*.md text +*.xml text +*.json text +*.bat text +*.sql text +*.yml text + +# Ensure those won't be messed up with +*.png binary +*.jpg binary +*.gif binary +*.ttf binary + +# Ignore some meta files when creating an archive of this repository +# We do not ignore any content, because this repo represents the +# `yiisoft/yii2-dev` package, which is expected to ship all tests and docs. +/.appveyor.yml export-ignore +/.github export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.scrutinizer.yml export-ignore +/.travis.yml export-ignore + +# Avoid merge conflicts in CHANGELOG +# https://about.gitlab.com/2015/02/10/gitlab-reduced-merge-conflicts-by-90-percent-with-changelog-placeholders/ +/framework/CHANGELOG.md merge=union + diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 000000000..3813ff4fd --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,7 @@ +Contributing to Yii 2 +===================== + +- [Report an issue](../docs/internals/report-an-issue.md) +- [Translate documentation or messages](../docs/internals/translation-workflow.md) +- [Give us feedback or start a design discussion](http://www.yiiframework.com/forum/index.php/forum/42-general-discussions-for-yii-20/) +- [Contribute to the core code or fix bugs](../docs/internals/git-workflow.md) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..9dfde405a --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,19 @@ + + +### What steps will reproduce the problem? + +### What is the expected result? + +### What do you get instead? + + +### Additional info + +| Q | A +| ---------------- | --- +| Yii version | 2.0.? +| PHP version | +| Operating system | diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..968a845de --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,7 @@ +| Q | A +| ------------- | --- +| Is bugfix? | yes/no +| New feature? | yes/no +| Breaks BC? | yes/no +| Tests pass? | yes/no +| Fixed issues | comma-separated list of tickets # fixed by the PR, if any diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..c33d11d17 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# phpstorm project files +.idea + +# netbeans project files +nbproject + +# zend studio for eclipse project files +.buildpath +.project +.settings + +# sublime text project / workspace files +*.sublime-project +*.sublime-workspace + +# windows thumbnail cache +Thumbs.db + +# composer vendor dir +/vendor +/composer.lock + +# composer itself is not needed +composer.phar + +# composer.lock in applications is ignored since it's automatically created by composer when application is installed +/apps/*/composer.lock + +# Mac DS_Store Files +.DS_Store + +# phpunit itself is not needed +phpunit.phar +# local phpunit config +/phpunit.xml + +# ignore dev installed apps and extensions +/apps +/extensions +/packages + +# NPM packages +/node_modules +.env diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 000000000..06757cd0a --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,100 @@ +image: docker:latest + +services: + - docker:dind + +variables: + DOCKER_YII2_PHP_IMAGE: yiisoftware/yii2-php:7.1-apache + DOCKER_MYSQL_IMAGE: percona:5.7 + DOCKER_POSTGRES_IMAGE: postgres:9.3 + +before_script: + - apk add --no-cache python py2-pip git + - pip install --no-cache-dir docker-compose==1.16.0 + - docker info + - cd tests + +stages: + - travis + - test + - cleanup + +test: + stage: test + script: + - docker-compose up --build -d + - docker-compose run --rm php vendor/bin/phpunit -v --exclude caching,db,data --log-junit tests/_junit/test.xml + +caching: + stage: test + only: + - tests/caching + - tests/full + script: + - export COMPOSE_FILE=docker-compose.yml:docker-compose.${CI_BUILD_NAME}.yml + - docker-compose up --build -d + - docker-compose run --rm php vendor/bin/phpunit -v --group caching --exclude db + +db: + stage: test + only: + - tests/mysql + - tests/full + script: + - docker-compose up --build -d + - docker-compose run --rm php vendor/bin/phpunit -v --group db --exclude caching,mysql,pgsql + + +mysql: + stage: test + only: + - tests/mysql + - tests/full + script: + - export COMPOSE_FILE=docker-compose.yml:docker-compose.${CI_BUILD_NAME}.yml + - docker-compose up --build -d + # wait for db (retry X times) + - docker-compose run --rm php bash -c "while ! curl mysql:3306; do ((c++)) && ((c==30)) && break; sleep 2; done" + - docker-compose run --rm php vendor/bin/phpunit -v --group mysql + + +pgsql: + stage: test + only: + - tests/pgsql + - tests/full + script: + - export COMPOSE_FILE=docker-compose.yml:docker-compose.${CI_BUILD_NAME}.yml + - docker-compose up --build -d + # wait for db (retry X times) + - docker-compose run --rm php bash -c 'while [ true ]; do curl postgres:5432; if [ $? == 52 ]; then break; fi; ((c++)) && ((c==25)) && break; sleep 2; done' + - docker-compose run --rm php vendor/bin/phpunit -v --group pgsql + + +mssql: + stage: test + only: + - tests/mssql + - tests/extra + script: + - cd mssql + - docker-compose up --build -d + # wait for db (retry X times) + - docker-compose run --rm php bash -c 'while [ true ]; do curl mssql:1433; if [ $? == 52 ]; then break; fi; ((c++)) && ((c==15)) && break; sleep 5; done' + - sleep 3 + # Note: Password has to be the last parameter + - docker-compose run --rm sqlcmd sh -c 'sqlcmd -S mssql -U sa -Q "CREATE DATABASE yii2test" -P Microsoft-12345' + - docker-compose run --rm php vendor/bin/phpunit -v --group mssql + + +travis: + stage: travis + only: + - travis + script: + - export COMPOSE_FILE=docker-compose.yml:docker-compose.mysql.yml:docker-compose.pgsql.yml + - docker-compose up --build -d + # wait for dbs ... + - sleep 10 + - docker-compose run --rm php vendor/bin/phpunit -v --exclude wincache,xcache + diff --git a/.php_cs b/.php_cs new file mode 100644 index 000000000..855023909 --- /dev/null +++ b/.php_cs @@ -0,0 +1,27 @@ +setCacheFile(__DIR__ . '/tests/runtime/php_cs.cache') + ->mergeRules([ + 'braces' => [ + 'allow_single_line_closure' => true, + ], + ]) + ->setFinder( + PhpCsFixer\Finder::create() + ->in(__DIR__) + ->exclude('docs') + ->exclude('apps') + ->exclude('extensions') + // requirement checker should work even on PHP 4.3, so it needs special treatment + ->exclude('framework/requirements') + ->notPath('framework/classes.php') + ->notPath('framework/helpers/mimeTypes.php') + ->notPath('framework/views/messageConfig.php') + ); diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 000000000..08f5200d9 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,10 @@ +imports: + - php + +tools: + external_code_coverage: + timeout: 2100 # Timeout in seconds. + # disable copy paste detector and similarity analyzer as they have no real value + # and a huge bunch of false-positives + php_sim: false + php_cpd: false diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..a1120711e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,110 @@ +# +# Travis Setup +# + +# use ubuntu trusty for newer version of nodejs, used for JS testing +dist: trusty + +# faster builds on new travis setup not using sudo +# temporary disable, see https://github.com/travis-ci/travis-ci/issues/6842 +#sudo: false +sudo: required +group: edge + +# build only on master branches +# commented as this prevents people from running builds on their forks: +# https://github.com/yiisoft/yii2/commit/bd87be990fa238c6d5e326d0a171f38d02dc253a +#branches: +# only: +# - master +# - 3.0 + + +# +# Test Matrix +# + +language: php + +env: + global: + - DEFAULT_COMPOSER_FLAGS="--prefer-dist --no-interaction --no-progress --optimize-autoloader" + - TASK_TESTS_PHP=1 + - TASK_TESTS_COVERAGE=0 + - TRAVIS_SECOND_USER=travis_two + +# cache vendor dirs +cache: + directories: + - vendor + - $HOME/.composer/cache + - $HOME/.npm + +addons: + code_climate: + repo_token: 2935307212620b0e2228ab67eadd92c9f5501ddb60549d0d86007a354d56915b + +matrix: + fast_finish: true + include: + - php: 7.3 + env: DEFAULT_COMPOSER_FLAGS="$DEFAULT_COMPOSER_FLAGS --ignore-platform-reqs" + - php: 7.2 + env: TASK_TESTS_COVERAGE=1 + - php: nightly + env: DEFAULT_COMPOSER_FLAGS="$DEFAULT_COMPOSER_FLAGS --ignore-platform-reqs" + + allow_failures: + - php: nightly + +install: + - | + if [[ $TASK_TESTS_COVERAGE != 1 ]]; then + # disable xdebug for performance reasons when code coverage is not needed + phpenv config-rm xdebug.ini || echo "xdebug is not installed" + fi + + # install composer dependencies + - travis_retry composer self-update + - export PATH="$HOME/.composer/vendor/bin:$PATH" + - travis_retry composer install $DEFAULT_COMPOSER_FLAGS + +before_script: + # show some versions and env information + - php --version + - composer --version + - | + if [ $TASK_TESTS_PHP == 1 ]; then + php -r "echo INTL_ICU_VERSION . \"\n\";" + php -r "echo INTL_ICU_DATA_VERSION . \"\n\";" + fi + + # enable code coverage + - | + if [ $TASK_TESTS_COVERAGE == 1 ]; then + PHPUNIT_FLAGS="--coverage-clover=coverage.clover" + fi + + # Disable DEPRECATE messages during PHPUnit initialization on PHP 7.2. To fix them, PHPUnit should be updated to 6.* + # For Yii2 tests, messages will be enabled by tests/bootstrap.php + - | + if [[ $TRAVIS_PHP_VERSION == 7.2 || $TRAVIS_PHP_VERSION = nightly ]]; then + echo 'Disabled DEPRECATED notifications for PHP >= 7.2'; + echo 'error_reporting = E_ALL & ~E_DEPRECATED' >> /tmp/php-config.ini; + phpenv config-add /tmp/php-config.ini; + fi + + +script: + # PHP tests + - | + if [ $TASK_TESTS_PHP == 1 ]; then + vendor/bin/phpunit --verbose $PHPUNIT_FLAGS --exclude-group wincache,xcache + fi + +after_script: + - | + if [ $TASK_TESTS_COVERAGE == 1 ]; then + travis_retry wget https://scrutinizer-ci.com/ocular.phar + php ocular.phar code-coverage:upload --format=php-clover coverage.clover + fi diff --git a/README.md b/README.md new file mode 100644 index 000000000..c24b5003b --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +

+ + + + + + +

Yii DataBase SQLite Extension

+
+

+ +This package provides [SQLite] extension for [Yii DataBase] library. +It is used in [Yii Framework] but is supposed to be usable separately. + +[SQLite]: https://www.sqlite.org/ +[Yii DataBase]: https://github.com/yiisoft/db +[Yii Framework]: https://github.com/yiisoft/core + +[![Latest Stable Version](https://poser.pugx.org/yiisoft/db-sqlite/v/stable.png)](https://packagist.org/packages/yiisoft/db-sqlite) +[![Total Downloads](https://poser.pugx.org/yiisoft/db-sqlite/downloads.png)](https://packagist.org/packages/yiisoft/db-sqlite) +[![Build Status](https://travis-ci.com/yiisoft/db-sqlite.svg?branch=master)](https://travis-ci.com/yiisoft/db-sqlite) diff --git a/code-of-conduct.md b/code-of-conduct.md new file mode 100644 index 000000000..82fdef144 --- /dev/null +++ b/code-of-conduct.md @@ -0,0 +1,68 @@ +Yii Contributor Code of Conduct +======================= + +## Our Pledge + +As contributors and maintainers of this project, and in order to keep Yii community open and welcoming, we ask to respect all community members. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Personal attacks +* Trolling or insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing other's private information, such as physical or electronic + addresses, without explicit permission +* Other conduct which could reasonably be considered inappropriate in + a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in response +to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, +commits, code, wiki edits, issues, and other contributions that are not aligned to this +Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors +that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when +an individual is representing the project or its community. Examples of representing +a project or community include posting via an official social media account, +within project GitHub, official forum or acting as an appointed representative at +an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported +by contacting core team members. All complaints will be reviewed and investigated +and will result in a response that is deemed necessary and appropriate to the circumstances. +The project team is obligated to maintain confidentiality with regard to the reporter of +an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith +may face temporary or permanent repercussions as determined by other members of +the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 1.4.0, available at +[http://contributor-covenant.org/version/1/4/][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/composer.json b/composer.json new file mode 100644 index 000000000..958440ae6 --- /dev/null +++ b/composer.json @@ -0,0 +1,104 @@ +{ + "name": "yiisoft/db-sqlite", + "type": "library", + "description": "Yii DataBase SQLite Extension", + "keywords": [ + "yii", + "sqlite" + ], + "homepage": "http://www.yiiframework.com/", + "license": "BSD-3-Clause", + "authors": [ + { + "name": "Qiang Xue", + "email": "qiang.xue@gmail.com", + "homepage": "http://www.yiiframework.com/", + "role": "Founder and project lead" + }, + { + "name": "Alexander Makarov", + "email": "sam@rmcreative.ru", + "homepage": "http://rmcreative.ru/", + "role": "Core framework development" + }, + { + "name": "Maurizio Domba", + "homepage": "http://mdomba.info/", + "role": "Core framework development" + }, + { + "name": "Carsten Brandt", + "email": "mail@cebe.cc", + "homepage": "http://cebe.cc/", + "role": "Core framework development" + }, + { + "name": "Timur Ruziev", + "email": "resurtm@gmail.com", + "homepage": "http://resurtm.com/", + "role": "Core framework development" + }, + { + "name": "Paul Klimov", + "email": "klimov.paul@gmail.com", + "role": "Core framework development" + }, + { + "name": "Dmitry Naumenko", + "email": "d.naumenko.a@gmail.com", + "role": "Core framework development" + }, + { + "name": "Boudewijn Vahrmeijer", + "email": "info@dynasource.eu", + "homepage": "http://dynasource.eu", + "role": "Core framework development" + } + ], + "support": { + "source": "https://github.com/yiisoft/db-sqlite", + "issues": "https://github.com/yiisoft/db-sqlite/issues", + "forum": "http://www.yiiframework.com/forum/", + "wiki": "http://www.yiiframework.com/wiki/", + "irc": "irc://irc.freenode.net/yii" + }, + "minimum-stability": "dev", + "prefer-stable": true, + "require": { + "yiisoft/arrays": "^3.0@dev", + "yiisoft/db": "^3.0@dev", + "yiisoft/strings": "^3.0@dev" + }, + "require-dev": { + "phpunit/phpunit": "^7.3", + "yiisoft/yii-core": "^3.0@dev", + "yiisoft/yii-console": "^3.0@dev", + "yiisoft/yii-web": "^3.0@dev", + "yiisoft/di": "^3.0@dev", + "yiisoft/log": "^3.0@dev", + "yiisoft/view": "^3.0@dev", + "yiisoft/cache": "^3.0@dev", + "yiisoft/active-record": "^3.0@dev", + "hiqdev/composer-config-plugin": "^1.0@dev" + }, + "autoload": { + "psr-4": { + "Yiisoft\\Db\\Sqlite\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Yiisoft\\Db\\Sqlite\\Tests\\": "tests" + } + }, + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + }, + "config-plugin": { + "params": "config/params.php", + "common": "config/common.php", + "tests": ["$common", "config/tests.php"] + } + } +} diff --git a/config/common.php b/config/common.php new file mode 100644 index 000000000..b62512838 --- /dev/null +++ b/config/common.php @@ -0,0 +1,4 @@ + + + + + ./tests/unit + + + + + + ./ + + ./tests + ./vendor + + + + diff --git a/src/ColumnSchemaBuilder.php b/src/ColumnSchemaBuilder.php new file mode 100644 index 000000000..0014764e9 --- /dev/null +++ b/src/ColumnSchemaBuilder.php @@ -0,0 +1,48 @@ + + * + * @since 2.0.8 + */ +class ColumnSchemaBuilder extends AbstractColumnSchemaBuilder +{ + /** + * {@inheritdoc} + */ + protected function buildUnsignedString() + { + return $this->isUnsigned ? ' UNSIGNED' : ''; + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + switch ($this->getTypeCategory()) { + case self::CATEGORY_PK: + $format = '{type}{check}{append}'; + break; + case self::CATEGORY_NUMERIC: + $format = '{type}{length}{unsigned}{notnull}{unique}{check}{default}{append}'; + break; + default: + $format = '{type}{length}{notnull}{unique}{check}{default}{append}'; + } + + return $this->buildCompleteString($format); + } +} diff --git a/src/Command.php b/src/Command.php new file mode 100644 index 000000000..4edf9d9c9 --- /dev/null +++ b/src/Command.php @@ -0,0 +1,126 @@ + + * + * @since 2.0.14 + */ +class Command extends \Yiisoft\Db\Command +{ + /** + * {@inheritdoc} + */ + public function execute() + { + $sql = $this->getSql(); + $params = $this->params; + $statements = $this->splitStatements($sql, $params); + if ($statements === false) { + return parent::execute(); + } + + $result = null; + foreach ($statements as $statement) { + [$statementSql, $statementParams] = $statement; + $this->setSql($statementSql)->bindValues($statementParams); + $result = parent::execute(); + } + $this->setSql($sql)->bindValues($params); + + return $result; + } + + /** + * {@inheritdoc} + */ + protected function queryInternal($method, $fetchMode = null) + { + $sql = $this->getSql(); + $params = $this->params; + $statements = $this->splitStatements($sql, $params); + if ($statements === false) { + return parent::queryInternal($method, $fetchMode); + } + + [$lastStatementSql, $lastStatementParams] = array_pop($statements); + foreach ($statements as $statement) { + [$statementSql, $statementParams] = $statement; + $this->setSql($statementSql)->bindValues($statementParams); + parent::execute(); + } + $this->setSql($lastStatementSql)->bindValues($lastStatementParams); + $result = parent::queryInternal($method, $fetchMode); + $this->setSql($sql)->bindValues($params); + + return $result; + } + + /** + * Splits the specified SQL code into individual SQL statements and returns them + * or `false` if there's a single statement. + * + * @param string $sql + * @param array $params + * + * @return string[]|false + */ + private function splitStatements($sql, $params) + { + $semicolonIndex = strpos($sql, ';'); + if ($semicolonIndex === false || $semicolonIndex === StringHelper::byteLength($sql) - 1) { + return false; + } + + $tokenizer = new SqlTokenizer($sql); + $codeToken = $tokenizer->tokenize(); + if (count($codeToken->getChildren()) === 1) { + return false; + } + + $statements = []; + foreach ($codeToken->getChildren() as $statement) { + $statements[] = [$statement->getSql(), $this->extractUsedParams($statement, $params)]; + } + + return $statements; + } + + /** + * Returns named bindings used in the specified statement token. + * + * @param SqlToken $statement + * @param array $params + * + * @return array + */ + private function extractUsedParams(SqlToken $statement, $params) + { + preg_match_all('/(?P[:][a-zA-Z0-9_]+)/', $statement->getSql(), $matches, PREG_SET_ORDER); + $result = []; + foreach ($matches as $match) { + $phName = ltrim($match['placeholder'], ':'); + if (isset($params[$phName])) { + $result[$phName] = $params[$phName]; + } elseif (isset($params[':'.$phName])) { + $result[':'.$phName] = $params[':'.$phName]; + } + } + + return $result; + } +} diff --git a/src/Conditions/InConditionBuilder.php b/src/Conditions/InConditionBuilder.php new file mode 100644 index 000000000..f69c74281 --- /dev/null +++ b/src/Conditions/InConditionBuilder.php @@ -0,0 +1,61 @@ + + * + * @since 2.0.14 + */ +class InConditionBuilder extends \Yiisoft\Db\Conditions\InConditionBuilder +{ + /** + * {@inheritdoc} + * + * @throws NotSupportedException if `$columns` is an array + */ + protected function buildSubqueryInCondition($operator, $columns, $values, &$params) + { + if (is_array($columns)) { + throw new NotSupportedException(__METHOD__.' is not supported by SQLite.'); + } + + return parent::buildSubqueryInCondition($operator, $columns, $values, $params); + } + + /** + * {@inheritdoc} + */ + protected function buildCompositeInCondition($operator, $columns, $values, &$params) + { + $quotedColumns = []; + foreach ($columns as $i => $column) { + $quotedColumns[$i] = strpos($column, '(') === false ? $this->queryBuilder->db->quoteColumnName($column) : $column; + } + $vss = []; + foreach ($values as $value) { + $vs = []; + foreach ($columns as $i => $column) { + if (isset($value[$column])) { + $phName = $this->queryBuilder->bindParam($value[$column], $params); + $vs[] = $quotedColumns[$i].($operator === 'IN' ? ' = ' : ' != ').$phName; + } else { + $vs[] = $quotedColumns[$i].($operator === 'IN' ? ' IS' : ' IS NOT').' NULL'; + } + } + $vss[] = '('.implode($operator === 'IN' ? ' AND ' : ' OR ', $vs).')'; + } + + return '('.implode($operator === 'IN' ? ' OR ' : ' AND ', $vss).')'; + } +} diff --git a/src/Conditions/LikeConditionBuilder.php b/src/Conditions/LikeConditionBuilder.php new file mode 100644 index 000000000..ec080a223 --- /dev/null +++ b/src/Conditions/LikeConditionBuilder.php @@ -0,0 +1,20 @@ + + * + * @since 2.0 + */ +class QueryBuilder extends \Yiisoft\Db\QueryBuilder +{ + /** + * @var array mapping from abstract column types (keys) to physical column types (values). + */ + public $typeMap = [ + Schema::TYPE_PK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', + Schema::TYPE_UPK => 'integer UNSIGNED PRIMARY KEY AUTOINCREMENT NOT NULL', + Schema::TYPE_BIGPK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', + Schema::TYPE_UBIGPK => 'integer UNSIGNED PRIMARY KEY AUTOINCREMENT NOT NULL', + Schema::TYPE_CHAR => 'char(1)', + Schema::TYPE_STRING => 'varchar(255)', + Schema::TYPE_TEXT => 'text', + Schema::TYPE_TINYINT => 'tinyint', + Schema::TYPE_SMALLINT => 'smallint', + Schema::TYPE_INTEGER => 'integer', + Schema::TYPE_BIGINT => 'bigint', + Schema::TYPE_FLOAT => 'float', + Schema::TYPE_DOUBLE => 'double', + Schema::TYPE_DECIMAL => 'decimal(10,0)', + Schema::TYPE_DATETIME => 'datetime', + Schema::TYPE_TIMESTAMP => 'timestamp', + Schema::TYPE_TIME => 'time', + Schema::TYPE_DATE => 'date', + Schema::TYPE_BINARY => 'blob', + Schema::TYPE_BOOLEAN => 'boolean', + Schema::TYPE_MONEY => 'decimal(19,4)', + ]; + + /** + * {@inheritdoc} + */ + protected function defaultExpressionBuilders() + { + return array_merge(parent::defaultExpressionBuilders(), [ + \Yiisoft\Db\Conditions\LikeCondition::class => Conditions\LikeConditionBuilder::class, + \Yiisoft\Db\Conditions\InCondition::class => Conditions\InConditionBuilder::class, + ]); + } + + /** + * {@inheritdoc} + * + * @see https://stackoverflow.com/questions/15277373/sqlite-upsert-update-or-insert/15277374#15277374 + */ + public function upsert($table, $insertColumns, $updateColumns, &$params) + { + /* @var Constraint[] $constraints */ + [$uniqueNames, $insertNames, $updateNames] = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns, $constraints); + if (empty($uniqueNames)) { + return $this->insert($table, $insertColumns, $params); + } + + [, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params); + $insertSql = 'INSERT OR IGNORE INTO '.$this->db->quoteTableName($table) + .(!empty($insertNames) ? ' ('.implode(', ', $insertNames).')' : '') + .(!empty($placeholders) ? ' VALUES ('.implode(', ', $placeholders).')' : $values); + if ($updateColumns === false) { + return $insertSql; + } + + $updateCondition = ['or']; + $quotedTableName = $this->db->quoteTableName($table); + foreach ($constraints as $constraint) { + $constraintCondition = ['and']; + foreach ($constraint->columnNames as $name) { + $quotedName = $this->db->quoteColumnName($name); + $constraintCondition[] = "$quotedTableName.$quotedName=(SELECT $quotedName FROM `EXCLUDED`)"; + } + $updateCondition[] = $constraintCondition; + } + if ($updateColumns === true) { + $updateColumns = []; + foreach ($updateNames as $name) { + $quotedName = $this->db->quoteColumnName($name); + if (strrpos($quotedName, '.') === false) { + $quotedName = "(SELECT $quotedName FROM `EXCLUDED`)"; + } + $updateColumns[$name] = new Expression($quotedName); + } + } + $updateSql = 'WITH "EXCLUDED" ('.implode(', ', $insertNames) + .') AS ('.(!empty($placeholders) ? 'VALUES ('.implode(', ', $placeholders).')' : ltrim($values, ' ')).') ' + .$this->update($table, $updateColumns, $updateCondition, $params); + + return "$updateSql; $insertSql;"; + } + + /** + * Generates a batch INSERT SQL statement. + * + * For example, + * + * ```php + * $connection->createCommand()->batchInsert('user', ['name', 'age'], [ + * ['Tom', 30], + * ['Jane', 20], + * ['Linda', 25], + * ])->execute(); + * ``` + * + * Note that the values in each row must match the corresponding column names. + * + * @param string $table the table that new rows will be inserted into. + * @param array $columns the column names + * @param array|\Generator $rows the rows to be batch inserted into the table + * + * @return string the batch INSERT SQL statement + */ + public function batchInsert($table, $columns, $rows, &$params = []) + { + if (empty($rows)) { + return ''; + } + + // SQLite supports batch insert natively since 3.7.11 + // http://www.sqlite.org/releaselog/3_7_11.html + $this->db->open(); // ensure pdo is not null + if (version_compare($this->db->getServerVersion(), '3.7.11', '>=')) { + return parent::batchInsert($table, $columns, $rows, $params); + } + + $schema = $this->db->getSchema(); + if (($tableSchema = $schema->getTableSchema($table)) !== null) { + $columnSchemas = $tableSchema->columns; + } else { + $columnSchemas = []; + } + + $values = []; + foreach ($rows as $row) { + $vs = []; + foreach ($row as $i => $value) { + if (isset($columnSchemas[$columns[$i]])) { + $value = $columnSchemas[$columns[$i]]->dbTypecast($value); + } + if (is_string($value)) { + $value = $schema->quoteValue($value); + } elseif (is_float($value)) { + // ensure type cast always has . as decimal separator in all locales + $value = StringHelper::floatToString($value); + } elseif ($value === false) { + $value = 0; + } elseif ($value === null) { + $value = 'NULL'; + } elseif ($value instanceof ExpressionInterface) { + $value = $this->buildExpression($value, $params); + } + $vs[] = $value; + } + $values[] = implode(', ', $vs); + } + if (empty($values)) { + return ''; + } + + foreach ($columns as $i => $name) { + $columns[$i] = $schema->quoteColumnName($name); + } + + return 'INSERT INTO '.$schema->quoteTableName($table) + .' ('.implode(', ', $columns).') SELECT '.implode(' UNION SELECT ', $values); + } + + /** + * Creates a SQL statement for resetting the sequence value of a table's primary key. + * The sequence will be reset such that the primary key of the next new row inserted + * will have the specified value or 1. + * + * @param string $tableName the name of the table whose primary key sequence will be reset + * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, + * the next new row's primary key will have a value 1. + * + * @throws InvalidArgumentException if the table does not exist or there is no sequence associated with the table. + * + * @return string the SQL statement for resetting sequence + */ + public function resetSequence($tableName, $value = null) + { + $db = $this->db; + $table = $db->getTableSchema($tableName); + if ($table !== null && $table->sequenceName !== null) { + $tableName = $db->quoteTableName($tableName); + if ($value === null) { + $key = $this->db->quoteColumnName(reset($table->primaryKey)); + $value = $this->db->useMaster(function (Connection $db) use ($key, $tableName) { + return $db->createCommand("SELECT MAX($key) FROM $tableName")->queryScalar(); + }); + } else { + $value = (int) $value - 1; + } + + return "UPDATE sqlite_sequence SET seq='$value' WHERE name='{$table->name}'"; + } elseif ($table === null) { + throw new InvalidArgumentException("Table not found: $tableName"); + } + + throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.'"); + } + + /** + * Enables or disables integrity check. + * + * @param bool $check whether to turn on or off the integrity check. + * @param string $schema the schema of the tables. Meaningless for SQLite. + * @param string $table the table name. Meaningless for SQLite. + * + * @throws NotSupportedException this is not supported by SQLite + * + * @return string the SQL statement for checking integrity + */ + public function checkIntegrity($check = true, $schema = '', $table = '') + { + return 'PRAGMA foreign_keys='.(int) $check; + } + + /** + * Builds a SQL statement for truncating a DB table. + * + * @param string $table the table to be truncated. The name will be properly quoted by the method. + * + * @return string the SQL statement for truncating a DB table. + */ + public function truncateTable($table) + { + return 'DELETE FROM '.$this->db->quoteTableName($table); + } + + /** + * Builds a SQL statement for dropping an index. + * + * @param string $name the name of the index to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method. + * + * @return string the SQL statement for dropping an index. + */ + public function dropIndex($name, $table) + { + return 'DROP INDEX '.$this->db->quoteTableName($name); + } + + /** + * Builds a SQL statement for dropping a DB column. + * + * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method. + * @param string $column the name of the column to be dropped. The name will be properly quoted by the method. + * + * @throws NotSupportedException this is not supported by SQLite + * + * @return string the SQL statement for dropping a DB column. + */ + public function dropColumn($table, $column) + { + throw new NotSupportedException(__METHOD__.' is not supported by SQLite.'); + } + + /** + * Builds a SQL statement for renaming a column. + * + * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method. + * @param string $oldName the old name of the column. The name will be properly quoted by the method. + * @param string $newName the new name of the column. The name will be properly quoted by the method. + * + * @throws NotSupportedException this is not supported by SQLite + * + * @return string the SQL statement for renaming a DB column. + */ + public function renameColumn($table, $oldName, $newName) + { + throw new NotSupportedException(__METHOD__.' is not supported by SQLite.'); + } + + /** + * Builds a SQL statement for adding a foreign key constraint to an existing table. + * The method will properly quote the table and column names. + * + * @param string $name the name of the foreign key constraint. + * @param string $table the table that the foreign key constraint will be added to. + * @param string|array $columns the name of the column to that the constraint will be added on. + * If there are multiple columns, separate them with commas or use an array to represent them. + * @param string $refTable the table that the foreign key references to. + * @param string|array $refColumns the name of the column that the foreign key references to. + * If there are multiple columns, separate them with commas or use an array to represent them. + * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL + * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL + * + * @throws NotSupportedException this is not supported by SQLite + * + * @return string the SQL statement for adding a foreign key constraint to an existing table. + */ + public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null) + { + throw new NotSupportedException(__METHOD__.' is not supported by SQLite.'); + } + + /** + * Builds a SQL statement for dropping a foreign key constraint. + * + * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method. + * + * @throws NotSupportedException this is not supported by SQLite + * + * @return string the SQL statement for dropping a foreign key constraint. + */ + public function dropForeignKey($name, $table) + { + throw new NotSupportedException(__METHOD__.' is not supported by SQLite.'); + } + + /** + * Builds a SQL statement for renaming a DB table. + * + * @param string $table the table to be renamed. The name will be properly quoted by the method. + * @param string $newName the new table name. The name will be properly quoted by the method. + * + * @return string the SQL statement for renaming a DB table. + */ + public function renameTable($table, $newName) + { + return 'ALTER TABLE '.$this->db->quoteTableName($table).' RENAME TO '.$this->db->quoteTableName($newName); + } + + /** + * Builds a SQL statement for changing the definition of a column. + * + * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. + * @param string $column the name of the column to be changed. The name will be properly quoted by the method. + * @param string $type the new column type. The [[getColumnType()]] method will be invoked to convert abstract + * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept + * in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null' + * will become 'varchar(255) not null'. + * + * @throws NotSupportedException this is not supported by SQLite + * + * @return string the SQL statement for changing the definition of a column. + */ + public function alterColumn($table, $column, $type) + { + throw new NotSupportedException(__METHOD__.' is not supported by SQLite.'); + } + + /** + * Builds a SQL statement for adding a primary key constraint to an existing table. + * + * @param string $name the name of the primary key constraint. + * @param string $table the table that the primary key constraint will be added to. + * @param string|array $columns comma separated string or array of columns that the primary key will consist of. + * + * @throws NotSupportedException this is not supported by SQLite + * + * @return string the SQL statement for adding a primary key constraint to an existing table. + */ + public function addPrimaryKey($name, $table, $columns) + { + throw new NotSupportedException(__METHOD__.' is not supported by SQLite.'); + } + + /** + * Builds a SQL statement for removing a primary key constraint to an existing table. + * + * @param string $name the name of the primary key constraint to be removed. + * @param string $table the table that the primary key constraint will be removed from. + * + * @throws NotSupportedException this is not supported by SQLite + * + * @return string the SQL statement for removing a primary key constraint from an existing table. + */ + public function dropPrimaryKey($name, $table) + { + throw new NotSupportedException(__METHOD__.' is not supported by SQLite.'); + } + + /** + * {@inheritdoc} + * + * @throws NotSupportedException this is not supported by SQLite. + */ + public function addUnique($name, $table, $columns) + { + throw new NotSupportedException(__METHOD__.' is not supported by SQLite.'); + } + + /** + * {@inheritdoc} + * + * @throws NotSupportedException this is not supported by SQLite. + */ + public function dropUnique($name, $table) + { + throw new NotSupportedException(__METHOD__.' is not supported by SQLite.'); + } + + /** + * {@inheritdoc} + * + * @throws NotSupportedException this is not supported by SQLite. + */ + public function addCheck($name, $table, $expression) + { + throw new NotSupportedException(__METHOD__.' is not supported by SQLite.'); + } + + /** + * {@inheritdoc} + * + * @throws NotSupportedException this is not supported by SQLite. + */ + public function dropCheck($name, $table) + { + throw new NotSupportedException(__METHOD__.' is not supported by SQLite.'); + } + + /** + * {@inheritdoc} + * + * @throws NotSupportedException this is not supported by SQLite. + */ + public function addDefaultValue($name, $table, $column, $value) + { + throw new NotSupportedException(__METHOD__.' is not supported by SQLite.'); + } + + /** + * {@inheritdoc} + * + * @throws NotSupportedException this is not supported by SQLite. + */ + public function dropDefaultValue($name, $table) + { + throw new NotSupportedException(__METHOD__.' is not supported by SQLite.'); + } + + /** + * {@inheritdoc} + * + * @throws NotSupportedException + * + * @since 2.0.8 + */ + public function addCommentOnColumn($table, $column, $comment) + { + throw new NotSupportedException(__METHOD__.' is not supported by SQLite.'); + } + + /** + * {@inheritdoc} + * + * @throws NotSupportedException + * + * @since 2.0.8 + */ + public function addCommentOnTable($table, $comment) + { + throw new NotSupportedException(__METHOD__.' is not supported by SQLite.'); + } + + /** + * {@inheritdoc} + * + * @throws NotSupportedException + * + * @since 2.0.8 + */ + public function dropCommentFromColumn($table, $column) + { + throw new NotSupportedException(__METHOD__.' is not supported by SQLite.'); + } + + /** + * {@inheritdoc} + * + * @throws NotSupportedException + * + * @since 2.0.8 + */ + public function dropCommentFromTable($table) + { + throw new NotSupportedException(__METHOD__.' is not supported by SQLite.'); + } + + /** + * {@inheritdoc} + */ + public function buildLimit($limit, $offset) + { + $sql = ''; + if ($this->hasLimit($limit)) { + $sql = 'LIMIT '.$limit; + if ($this->hasOffset($offset)) { + $sql .= ' OFFSET '.$offset; + } + } elseif ($this->hasOffset($offset)) { + // limit is not optional in SQLite + // http://www.sqlite.org/syntaxdiagrams.html#select-stmt + $sql = "LIMIT 9223372036854775807 OFFSET $offset"; // 2^63-1 + } + + return $sql; + } + + /** + * {@inheritdoc} + */ + public function build($query, $params = []) + { + $query = $query->prepare($this); + + $params = empty($params) ? $query->params : array_merge($params, $query->params); + + $clauses = [ + $this->buildSelect($query->select, $params, $query->distinct, $query->selectOption), + $this->buildFrom($query->from, $params), + $this->buildJoin($query->join, $params), + $this->buildWhere($query->where, $params), + $this->buildGroupBy($query->groupBy, $params), + $this->buildHaving($query->having, $params), + ]; + + $sql = implode($this->separator, array_filter($clauses)); + $sql = $this->buildOrderByAndLimit($sql, $query->orderBy, $query->limit, $query->offset, $params); + + $union = $this->buildUnion($query->union, $params); + if ($union !== '') { + $sql = "$sql{$this->separator}$union"; + } + + return [$sql, $params]; + } + + /** + * {@inheritdoc} + */ + public function buildUnion($unions, &$params) + { + if (empty($unions)) { + return ''; + } + + $result = ''; + + foreach ($unions as $i => $union) { + $query = $union['query']; + if ($query instanceof Query) { + [$unions[$i]['query'], $params] = $this->build($query, $params); + } + + $result .= ' UNION '.($union['all'] ? 'ALL ' : '').' '.$unions[$i]['query']; + } + + return trim($result); + } +} diff --git a/src/Schema.php b/src/Schema.php new file mode 100644 index 000000000..73892aa5b --- /dev/null +++ b/src/Schema.php @@ -0,0 +1,506 @@ + + * + * @since 2.0 + */ +class Schema extends \Yiisoft\Db\Schema implements ConstraintFinderInterface +{ + use ConstraintFinderTrait; + + /** + * @var array mapping from physical column types (keys) to abstract column types (values) + */ + public $typeMap = [ + 'tinyint' => self::TYPE_TINYINT, + 'bit' => self::TYPE_SMALLINT, + 'boolean' => self::TYPE_BOOLEAN, + 'bool' => self::TYPE_BOOLEAN, + 'smallint' => self::TYPE_SMALLINT, + 'mediumint' => self::TYPE_INTEGER, + 'int' => self::TYPE_INTEGER, + 'integer' => self::TYPE_INTEGER, + 'bigint' => self::TYPE_BIGINT, + 'float' => self::TYPE_FLOAT, + 'double' => self::TYPE_DOUBLE, + 'real' => self::TYPE_FLOAT, + 'decimal' => self::TYPE_DECIMAL, + 'numeric' => self::TYPE_DECIMAL, + 'tinytext' => self::TYPE_TEXT, + 'mediumtext' => self::TYPE_TEXT, + 'longtext' => self::TYPE_TEXT, + 'text' => self::TYPE_TEXT, + 'varchar' => self::TYPE_STRING, + 'string' => self::TYPE_STRING, + 'char' => self::TYPE_CHAR, + 'blob' => self::TYPE_BINARY, + 'datetime' => self::TYPE_DATETIME, + 'year' => self::TYPE_DATE, + 'date' => self::TYPE_DATE, + 'time' => self::TYPE_TIME, + 'timestamp' => self::TYPE_TIMESTAMP, + 'enum' => self::TYPE_STRING, + ]; + + /** + * {@inheritdoc} + */ + protected $tableQuoteCharacter = '`'; + /** + * {@inheritdoc} + */ + protected $columnQuoteCharacter = '`'; + + /** + * {@inheritdoc} + */ + protected function findTableNames($schema = '') + { + $sql = "SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name<>'sqlite_sequence' ORDER BY tbl_name"; + + return $this->db->createCommand($sql)->queryColumn(); + } + + /** + * {@inheritdoc} + */ + protected function loadTableSchema($name) + { + $table = new TableSchema(); + $table->name = $name; + $table->fullName = $name; + + if ($this->findColumns($table)) { + $this->findConstraints($table); + + return $table; + } + } + + /** + * {@inheritdoc} + */ + protected function loadTablePrimaryKey($tableName) + { + return $this->loadTableConstraints($tableName, 'primaryKey'); + } + + /** + * {@inheritdoc} + */ + protected function loadTableForeignKeys($tableName) + { + $foreignKeys = $this->db->createCommand('PRAGMA FOREIGN_KEY_LIST ('.$this->quoteValue($tableName).')')->queryAll(); + $foreignKeys = $this->normalizePdoRowKeyCase($foreignKeys, true); + $foreignKeys = ArrayHelper::index($foreignKeys, null, 'table'); + ArrayHelper::multisort($foreignKeys, 'seq', SORT_ASC, SORT_NUMERIC); + $result = []; + foreach ($foreignKeys as $table => $foreignKey) { + $result[] = new ForeignKeyConstraint([ + 'columnNames' => ArrayHelper::getColumn($foreignKey, 'from'), + 'foreignTableName' => $table, + 'foreignColumnNames' => ArrayHelper::getColumn($foreignKey, 'to'), + 'onDelete' => $foreignKey[0]['on_delete'] ?? null, + 'onUpdate' => $foreignKey[0]['on_update'] ?? null, + ]); + } + + return $result; + } + + /** + * {@inheritdoc} + */ + protected function loadTableIndexes($tableName) + { + return $this->loadTableConstraints($tableName, 'indexes'); + } + + /** + * {@inheritdoc} + */ + protected function loadTableUniques($tableName) + { + return $this->loadTableConstraints($tableName, 'uniques'); + } + + /** + * {@inheritdoc} + */ + protected function loadTableChecks($tableName) + { + $sql = $this->db->createCommand('SELECT `sql` FROM `sqlite_master` WHERE name = :tableName', [ + ':tableName' => $tableName, + ])->queryScalar(); + /** @var $code SqlToken[]|SqlToken[][]|SqlToken[][][] */ + $code = (new SqlTokenizer($sql))->tokenize(); + $pattern = (new SqlTokenizer('any CREATE any TABLE any()'))->tokenize(); + if (!$code[0]->matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) { + return []; + } + + $createTableToken = $code[0][$lastMatchIndex - 1]; + $result = []; + $offset = 0; + while (true) { + $pattern = (new SqlTokenizer('any CHECK()'))->tokenize(); + if (!$createTableToken->matches($pattern, $offset, $firstMatchIndex, $offset)) { + break; + } + + $checkSql = $createTableToken[$offset - 1]->getSql(); + $name = null; + $pattern = (new SqlTokenizer('CONSTRAINT any'))->tokenize(); + if (isset($createTableToken[$firstMatchIndex - 2]) && $createTableToken->matches($pattern, $firstMatchIndex - 2)) { + $name = $createTableToken[$firstMatchIndex - 1]->content; + } + $result[] = new CheckConstraint([ + 'name' => $name, + 'expression' => $checkSql, + ]); + } + + return $result; + } + + /** + * {@inheritdoc} + * + * @throws NotSupportedException if this method is called. + */ + protected function loadTableDefaultValues($tableName) + { + throw new NotSupportedException('SQLite does not support default value constraints.'); + } + + /** + * Creates a query builder for the MySQL database. + * This method may be overridden by child classes to create a DBMS-specific query builder. + * + * @return QueryBuilder query builder instance + */ + public function createQueryBuilder() + { + return new QueryBuilder($this->db); + } + + /** + * {@inheritdoc} + * + * @return ColumnSchemaBuilder column schema builder instance + */ + public function createColumnSchemaBuilder($type, $length = null) + { + return new ColumnSchemaBuilder($type, $length); + } + + /** + * Collects the table column metadata. + * + * @param TableSchema $table the table metadata + * + * @return bool whether the table exists in the database + */ + protected function findColumns($table) + { + $sql = 'PRAGMA table_info('.$this->quoteSimpleTableName($table->name).')'; + $columns = $this->db->createCommand($sql)->queryAll(); + if (empty($columns)) { + return false; + } + + foreach ($columns as $info) { + $column = $this->loadColumnSchema($info); + $table->columns[$column->name] = $column; + if ($column->isPrimaryKey) { + $table->primaryKey[] = $column->name; + } + } + if (count($table->primaryKey) === 1 && !strncasecmp($table->columns[$table->primaryKey[0]]->dbType, 'int', 3)) { + $table->sequenceName = ''; + $table->columns[$table->primaryKey[0]]->autoIncrement = true; + } + + return true; + } + + /** + * Collects the foreign key column details for the given table. + * + * @param TableSchema $table the table metadata + */ + protected function findConstraints($table) + { + $sql = 'PRAGMA foreign_key_list('.$this->quoteSimpleTableName($table->name).')'; + $keys = $this->db->createCommand($sql)->queryAll(); + foreach ($keys as $key) { + $id = (int) $key['id']; + if (!isset($table->foreignKeys[$id])) { + $table->foreignKeys[$id] = [$key['table'], $key['from'] => $key['to']]; + } else { + // composite FK + $table->foreignKeys[$id][$key['from']] = $key['to']; + } + } + } + + /** + * Returns all unique indexes for the given table. + * + * Each array element is of the following structure: + * + * ```php + * [ + * 'IndexName1' => ['col1' [, ...]], + * 'IndexName2' => ['col2' [, ...]], + * ] + * ``` + * + * @param TableSchema $table the table metadata + * + * @return array all unique indexes for the given table. + */ + public function findUniqueIndexes($table) + { + $sql = 'PRAGMA index_list('.$this->quoteSimpleTableName($table->name).')'; + $indexes = $this->db->createCommand($sql)->queryAll(); + $uniqueIndexes = []; + + foreach ($indexes as $index) { + $indexName = $index['name']; + $indexInfo = $this->db->createCommand('PRAGMA index_info('.$this->quoteValue($index['name']).')')->queryAll(); + + if ($index['unique']) { + $uniqueIndexes[$indexName] = []; + foreach ($indexInfo as $row) { + $uniqueIndexes[$indexName][] = $row['name']; + } + } + } + + return $uniqueIndexes; + } + + /** + * Loads the column information into a [[ColumnSchema]] object. + * + * @param array $info column information + * + * @return ColumnSchema the column schema object + */ + protected function loadColumnSchema($info) + { + $column = $this->createColumnSchema(); + $column->name = $info['name']; + $column->allowNull = !$info['notnull']; + $column->isPrimaryKey = $info['pk'] != 0; + + $column->dbType = strtolower($info['type']); + $column->unsigned = strpos($column->dbType, 'unsigned') !== false; + + $column->type = self::TYPE_STRING; + if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) { + $type = strtolower($matches[1]); + if (isset($this->typeMap[$type])) { + $column->type = $this->typeMap[$type]; + } + + if (!empty($matches[2])) { + $values = explode(',', $matches[2]); + $column->size = $column->precision = (int) $values[0]; + if (isset($values[1])) { + $column->scale = (int) $values[1]; + } + if ($column->size === 1 && ($type === 'tinyint' || $type === 'bit')) { + $column->type = 'boolean'; + } elseif ($type === 'bit') { + if ($column->size > 32) { + $column->type = 'bigint'; + } elseif ($column->size === 32) { + $column->type = 'integer'; + } + } + } + } + $column->phpType = $this->getColumnPhpType($column); + + if (!$column->isPrimaryKey) { + if ($info['dflt_value'] === 'null' || $info['dflt_value'] === '' || $info['dflt_value'] === null) { + $column->defaultValue = null; + } elseif ($column->type === 'timestamp' && $info['dflt_value'] === 'CURRENT_TIMESTAMP') { + $column->defaultValue = new Expression('CURRENT_TIMESTAMP'); + } else { + $value = trim($info['dflt_value'], "'\""); + $column->defaultValue = $column->phpTypecast($value); + } + } + + return $column; + } + + /** + * Sets the isolation level of the current transaction. + * + * @param string $level The transaction isolation level to use for this transaction. + * This can be either [[Transaction::READ_UNCOMMITTED]] or [[Transaction::SERIALIZABLE]]. + * + * @throws NotSupportedException when unsupported isolation levels are used. + * SQLite only supports SERIALIZABLE and READ UNCOMMITTED. + * + * @see http://www.sqlite.org/pragma.html#pragma_read_uncommitted + */ + public function setTransactionIsolationLevel($level) + { + switch ($level) { + case Transaction::SERIALIZABLE: + $this->db->createCommand('PRAGMA read_uncommitted = False;')->execute(); + break; + case Transaction::READ_UNCOMMITTED: + $this->db->createCommand('PRAGMA read_uncommitted = True;')->execute(); + break; + default: + throw new NotSupportedException(get_class($this).' only supports transaction isolation levels READ UNCOMMITTED and SERIALIZABLE.'); + } + } + + /** + * Returns table columns info. + * + * @param string $tableName table name + * + * @return array + */ + private function loadTableColumnsInfo($tableName) + { + $tableColumns = $this->db->createCommand('PRAGMA TABLE_INFO ('.$this->quoteValue($tableName).')')->queryAll(); + $tableColumns = $this->normalizePdoRowKeyCase($tableColumns, true); + + return ArrayHelper::index($tableColumns, 'cid'); + } + + /** + * Loads multiple types of constraints and returns the specified ones. + * + * @param string $tableName table name. + * @param string $returnType return type: + * - primaryKey + * - indexes + * - uniques + * + * @return mixed constraints. + */ + private function loadTableConstraints($tableName, $returnType) + { + $indexes = $this->db->createCommand('PRAGMA INDEX_LIST ('.$this->quoteValue($tableName).')')->queryAll(); + $indexes = $this->normalizePdoRowKeyCase($indexes, true); + $tableColumns = null; + if (!empty($indexes) && !isset($indexes[0]['origin'])) { + /* + * SQLite may not have an "origin" column in INDEX_LIST + * See https://www.sqlite.org/src/info/2743846cdba572f6 + */ + $tableColumns = $this->loadTableColumnsInfo($tableName); + } + $result = [ + 'primaryKey' => null, + 'indexes' => [], + 'uniques' => [], + ]; + foreach ($indexes as $index) { + $columns = $this->db->createCommand('PRAGMA INDEX_INFO ('.$this->quoteValue($index['name']).')')->queryAll(); + $columns = $this->normalizePdoRowKeyCase($columns, true); + ArrayHelper::multisort($columns, 'seqno', SORT_ASC, SORT_NUMERIC); + if ($tableColumns !== null) { + // SQLite may not have an "origin" column in INDEX_LIST + $index['origin'] = 'c'; + if (!empty($columns) && $tableColumns[$columns[0]['cid']]['pk'] > 0) { + $index['origin'] = 'pk'; + } elseif ($index['unique'] && $this->isSystemIdentifier($index['name'])) { + $index['origin'] = 'u'; + } + } + $result['indexes'][] = new IndexConstraint([ + 'isPrimary' => $index['origin'] === 'pk', + 'isUnique' => (bool) $index['unique'], + 'name' => $index['name'], + 'columnNames' => ArrayHelper::getColumn($columns, 'name'), + ]); + if ($index['origin'] === 'u') { + $result['uniques'][] = new Constraint([ + 'name' => $index['name'], + 'columnNames' => ArrayHelper::getColumn($columns, 'name'), + ]); + } elseif ($index['origin'] === 'pk') { + $result['primaryKey'] = new Constraint([ + 'columnNames' => ArrayHelper::getColumn($columns, 'name'), + ]); + } + } + + if ($result['primaryKey'] === null) { + /* + * Additional check for PK in case of INTEGER PRIMARY KEY with ROWID + * See https://www.sqlite.org/lang_createtable.html#primkeyconst + */ + if ($tableColumns === null) { + $tableColumns = $this->loadTableColumnsInfo($tableName); + } + foreach ($tableColumns as $tableColumn) { + if ($tableColumn['pk'] > 0) { + $result['primaryKey'] = new Constraint([ + 'columnNames' => [$tableColumn['name']], + ]); + break; + } + } + } + + foreach ($result as $type => $data) { + $this->setTableMetadata($tableName, $type, $data); + } + + return $result[$returnType]; + } + + /** + * Return whether the specified identifier is a SQLite system identifier. + * + * @param string $identifier + * + * @return bool + * + * @see https://www.sqlite.org/src/artifact/74108007d286232f + */ + private function isSystemIdentifier($identifier) + { + return strncmp($identifier, 'sqlite_', 7) === 0; + } +} diff --git a/src/SqlTokenizer.php b/src/SqlTokenizer.php new file mode 100644 index 000000000..df34cb3e1 --- /dev/null +++ b/src/SqlTokenizer.php @@ -0,0 +1,296 @@ + + * + * @since 2.0.13 + */ +class SqlTokenizer extends \Yiisoft\Db\SqlTokenizer +{ + /** + * {@inheritdoc} + */ + protected function isWhitespace(&$length) + { + static $whitespaces = [ + "\f" => true, + "\n" => true, + "\r" => true, + "\t" => true, + ' ' => true, + ]; + + $length = 1; + + return isset($whitespaces[$this->substring($length)]); + } + + /** + * {@inheritdoc} + */ + protected function isComment(&$length) + { + static $comments = [ + '--' => true, + '/*' => true, + ]; + + $length = 2; + if (!isset($comments[$this->substring($length)])) { + return false; + } + + if ($this->substring($length) === '--') { + $length = $this->indexAfter("\n") - $this->offset; + } else { + $length = $this->indexAfter('*/') - $this->offset; + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function isOperator(&$length, &$content) + { + static $operators = [ + '!=', + '%', + '&', + '(', + ')', + '*', + '+', + ',', + '-', + '.', + '/', + ';', + '<', + '<<', + '<=', + '<>', + '=', + '==', + '>', + '>=', + '>>', + '|', + '||', + '~', + ]; + + return $this->startsWithAnyLongest($operators, true, $length); + } + + /** + * {@inheritdoc} + */ + protected function isIdentifier(&$length, &$content) + { + static $identifierDelimiters = [ + '"' => '"', + '[' => ']', + '`' => '`', + ]; + + if (!isset($identifierDelimiters[$this->substring(1)])) { + return false; + } + + $delimiter = $identifierDelimiters[$this->substring(1)]; + $offset = $this->offset; + while (true) { + $offset = $this->indexAfter($delimiter, $offset + 1); + if ($delimiter === ']' || $this->substring(1, true, $offset) !== $delimiter) { + break; + } + } + $length = $offset - $this->offset; + $content = $this->substring($length - 2, true, $this->offset + 1); + if ($delimiter !== ']') { + $content = strtr($content, ["$delimiter$delimiter" => $delimiter]); + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function isStringLiteral(&$length, &$content) + { + if ($this->substring(1) !== "'") { + return false; + } + + $offset = $this->offset; + while (true) { + $offset = $this->indexAfter("'", $offset + 1); + if ($this->substring(1, true, $offset) !== "'") { + break; + } + } + $length = $offset - $this->offset; + $content = strtr($this->substring($length - 2, true, $this->offset + 1), ["''" => "'"]); + + return true; + } + + /** + * {@inheritdoc} + */ + protected function isKeyword($string, &$content) + { + static $keywords = [ + 'ABORT' => true, + 'ACTION' => true, + 'ADD' => true, + 'AFTER' => true, + 'ALL' => true, + 'ALTER' => true, + 'ANALYZE' => true, + 'AND' => true, + 'AS' => true, + 'ASC' => true, + 'ATTACH' => true, + 'AUTOINCREMENT' => true, + 'BEFORE' => true, + 'BEGIN' => true, + 'BETWEEN' => true, + 'BY' => true, + 'CASCADE' => true, + 'CASE' => true, + 'CAST' => true, + 'CHECK' => true, + 'COLLATE' => true, + 'COLUMN' => true, + 'COMMIT' => true, + 'CONFLICT' => true, + 'CONSTRAINT' => true, + 'CREATE' => true, + 'CROSS' => true, + 'CURRENT_DATE' => true, + 'CURRENT_TIME' => true, + 'CURRENT_TIMESTAMP' => true, + 'DATABASE' => true, + 'DEFAULT' => true, + 'DEFERRABLE' => true, + 'DEFERRED' => true, + 'DELETE' => true, + 'DESC' => true, + 'DETACH' => true, + 'DISTINCT' => true, + 'DROP' => true, + 'EACH' => true, + 'ELSE' => true, + 'END' => true, + 'ESCAPE' => true, + 'EXCEPT' => true, + 'EXCLUSIVE' => true, + 'EXISTS' => true, + 'EXPLAIN' => true, + 'FAIL' => true, + 'FOR' => true, + 'FOREIGN' => true, + 'FROM' => true, + 'FULL' => true, + 'GLOB' => true, + 'GROUP' => true, + 'HAVING' => true, + 'IF' => true, + 'IGNORE' => true, + 'IMMEDIATE' => true, + 'IN' => true, + 'INDEX' => true, + 'INDEXED' => true, + 'INITIALLY' => true, + 'INNER' => true, + 'INSERT' => true, + 'INSTEAD' => true, + 'INTERSECT' => true, + 'INTO' => true, + 'IS' => true, + 'ISNULL' => true, + 'JOIN' => true, + 'KEY' => true, + 'LEFT' => true, + 'LIKE' => true, + 'LIMIT' => true, + 'MATCH' => true, + 'NATURAL' => true, + 'NO' => true, + 'NOT' => true, + 'NOTNULL' => true, + 'NULL' => true, + 'OF' => true, + 'OFFSET' => true, + 'ON' => true, + 'OR' => true, + 'ORDER' => true, + 'OUTER' => true, + 'PLAN' => true, + 'PRAGMA' => true, + 'PRIMARY' => true, + 'QUERY' => true, + 'RAISE' => true, + 'RECURSIVE' => true, + 'REFERENCES' => true, + 'REGEXP' => true, + 'REINDEX' => true, + 'RELEASE' => true, + 'RENAME' => true, + 'REPLACE' => true, + 'RESTRICT' => true, + 'RIGHT' => true, + 'ROLLBACK' => true, + 'ROW' => true, + 'SAVEPOINT' => true, + 'SELECT' => true, + 'SET' => true, + 'TABLE' => true, + 'TEMP' => true, + 'TEMPORARY' => true, + 'THEN' => true, + 'TO' => true, + 'TRANSACTION' => true, + 'TRIGGER' => true, + 'UNION' => true, + 'UNIQUE' => true, + 'UPDATE' => true, + 'USING' => true, + 'VACUUM' => true, + 'VALUES' => true, + 'VIEW' => true, + 'VIRTUAL' => true, + 'WHEN' => true, + 'WHERE' => true, + 'WITH' => true, + 'WITHOUT' => true, + ]; + + $string = mb_strtoupper($string, 'UTF-8'); + if (!isset($keywords[$string])) { + return false; + } + + $content = $string; + + return true; + } +} diff --git a/tests/ActiveDataProviderTest.php b/tests/ActiveDataProviderTest.php new file mode 100644 index 000000000..8c9dafdff --- /dev/null +++ b/tests/ActiveDataProviderTest.php @@ -0,0 +1,19 @@ +getConnection()); + } + + /** + * @return array + */ + public function typesProvider() + { + return [ + ['integer UNSIGNED', Schema::TYPE_INTEGER, null, [ + ['unsigned'], + ]], + ['integer(10) UNSIGNED', Schema::TYPE_INTEGER, 10, [ + ['unsigned'], + ]], + // comments are ignored + ['integer(10)', Schema::TYPE_INTEGER, 10, [ + ['comment', 'test'], + ]], + ]; + } +} diff --git a/tests/CommandTest.php b/tests/CommandTest.php new file mode 100644 index 000000000..6cc518911 --- /dev/null +++ b/tests/CommandTest.php @@ -0,0 +1,115 @@ +getConnection(false); + + $sql = 'SELECT [[id]], [[t.name]] FROM {{customer}} t'; + $command = $db->createCommand($sql); + $this->assertEquals('SELECT `id`, `t`.`name` FROM `customer` t', $command->sql); + } + + /** + * @dataProvider upsertProvider + * + * @param array $firstData + * @param array $secondData + */ + public function testUpsert(array $firstData, array $secondData) + { + if (version_compare($this->getConnection(false)->getServerVersion(), '3.8.3', '<')) { + $this->markTestSkipped('SQLite < 3.8.3 does not support "WITH" keyword.'); + + return; + } + + parent::testUpsert($firstData, $secondData); + } + + public function testAddDropPrimaryKey() + { + $this->markTestSkipped('SQLite does not support adding/dropping primary keys.'); + } + + public function testAddDropForeignKey() + { + $this->markTestSkipped('SQLite does not support adding/dropping foreign keys.'); + } + + public function testAddDropUnique() + { + $this->markTestSkipped('SQLite does not support adding/dropping unique constraints.'); + } + + public function testAddDropCheck() + { + $this->markTestSkipped('SQLite does not support adding/dropping check constraints.'); + } + + public function testMultiStatementSupport() + { + $db = $this->getConnection(false); + $sql = <<<'SQL' +DROP TABLE IF EXISTS {{T_multistatement}}; +CREATE TABLE {{T_multistatement}} ( + [[intcol]] INTEGER, + [[textcol]] TEXT +); +INSERT INTO {{T_multistatement}} VALUES(41, :val1); +INSERT INTO {{T_multistatement}} VALUES(42, :val2); +SQL; + $db->createCommand($sql, [ + 'val1' => 'foo', + 'val2' => 'bar', + ])->execute(); + $this->assertSame([ + [ + 'intcol' => '41', + 'textcol' => 'foo', + ], + [ + 'intcol' => '42', + 'textcol' => 'bar', + ], + ], $db->createCommand('SELECT * FROM {{T_multistatement}}')->queryAll()); + $sql = <<<'SQL' +UPDATE {{T_multistatement}} SET [[intcol]] = :newInt WHERE [[textcol]] = :val1; +DELETE FROM {{T_multistatement}} WHERE [[textcol]] = :val2; +SELECT * FROM {{T_multistatement}} +SQL; + $this->assertSame([ + [ + 'intcol' => '410', + 'textcol' => 'foo', + ], + ], $db->createCommand($sql, [ + 'newInt' => 410, + 'val1' => 'foo', + 'val2' => 'bar', + ])->queryAll()); + } + + public function batchInsertSqlProvider() + { + $parent = parent::batchInsertSqlProvider(); + unset($parent['wrongBehavior']); // Produces SQL syntax error: General error: 1 near ".": syntax error + + return $parent; + } +} diff --git a/tests/ConnectionTest.php b/tests/ConnectionTest.php new file mode 100644 index 000000000..442275ff4 --- /dev/null +++ b/tests/ConnectionTest.php @@ -0,0 +1,225 @@ +getConnection(false); + $params = $this->database; + + $this->assertEquals($params['dsn'], $connection->dsn); + } + + public function testQuoteValue() + { + $connection = $this->getConnection(false); + $this->assertEquals(123, $connection->quoteValue(123)); + $this->assertEquals("'string'", $connection->quoteValue('string')); + $this->assertEquals("'It''s interesting'", $connection->quoteValue("It's interesting")); + } + + public function testTransactionIsolation() + { + $connection = $this->getConnection(true); + + $transaction = $connection->beginTransaction(Transaction::READ_UNCOMMITTED); + $transaction->rollBack(); + + $transaction = $connection->beginTransaction(Transaction::SERIALIZABLE); + $transaction->rollBack(); + + $this->assertTrue(true); // No exceptions means test is passed. + } + + public function testMasterSlave() + { + $counts = [[0, 2], [1, 2], [2, 2]]; + + foreach ($counts as [$masterCount, $slaveCount]) { + $db = $this->prepareMasterSlave($masterCount, $slaveCount); + + $this->assertInstanceOf(Connection::class, $db->getSlave()); + $this->assertTrue($db->getSlave()->isActive); + $this->assertFalse($db->isActive); + + // test SELECT uses slave + $this->assertEquals(2, $db->createCommand('SELECT COUNT(*) FROM profile')->queryScalar()); + $this->assertFalse($db->isActive); + + // test UPDATE uses master + $db->createCommand("UPDATE profile SET description='test' WHERE id=1")->execute(); + $this->assertTrue($db->isActive); + if ($masterCount > 0) { + $this->assertInstanceOf(Connection::class, $db->getMaster()); + $this->assertTrue($db->getMaster()->isActive); + } else { + $this->assertNull($db->getMaster()); + } + $this->assertNotEquals('test', $db->createCommand('SELECT description FROM profile WHERE id=1')->queryScalar()); + $result = $db->useMaster(function (Connection $db) { + return $db->createCommand('SELECT description FROM profile WHERE id=1')->queryScalar(); + }); + $this->assertEquals('test', $result); + + // test ActiveRecord read/write split + ActiveRecord::$db = $db = $this->prepareMasterSlave($masterCount, $slaveCount); + $this->assertFalse($db->isActive); + + $customer = Customer::findOne(1); + $this->assertInstanceOf(Customer::class, $customer); + $this->assertEquals('user1', $customer->name); + $this->assertFalse($db->isActive); + + $customer->name = 'test'; + $customer->save(); + $this->assertTrue($db->isActive); + $customer = Customer::findOne(1); + $this->assertInstanceOf(Customer::class, $customer); + $this->assertEquals('user1', $customer->name); + $result = $db->useMaster(function () { + return Customer::findOne(1)->name; + }); + $this->assertEquals('test', $result); + } + } + + public function testMastersShuffled() + { + $mastersCount = 2; + $slavesCount = 2; + $retryPerNode = 10; + + $nodesCount = $mastersCount + $slavesCount; + + $hit_slaves = $hit_masters = []; + + for ($i = $nodesCount * $retryPerNode; $i-- > 0;) { + $db = $this->prepareMasterSlave($mastersCount, $slavesCount); + $db->shuffleMasters = true; + + $hit_slaves[$db->getSlave()->dsn] = true; + $hit_masters[$db->getMaster()->dsn] = true; + if (\count($hit_slaves) === $slavesCount && \count($hit_masters) === $mastersCount) { + break; + } + } + + $this->assertCount($mastersCount, $hit_masters, 'all masters hit'); + $this->assertCount($slavesCount, $hit_slaves, 'all slaves hit'); + } + + public function testMastersSequential() + { + $mastersCount = 2; + $slavesCount = 2; + $retryPerNode = 10; + + $nodesCount = $mastersCount + $slavesCount; + + $hit_slaves = $hit_masters = []; + + for ($i = $nodesCount * $retryPerNode; $i-- > 0;) { + $db = $this->prepareMasterSlave($mastersCount, $slavesCount); + $db->shuffleMasters = false; + + $hit_slaves[$db->getSlave()->dsn] = true; + $hit_masters[$db->getMaster()->dsn] = true; + if (\count($hit_slaves) === $slavesCount) { + break; + } + } + + $this->assertCount(1, $hit_masters, 'same master hit'); + // slaves are always random + $this->assertCount($slavesCount, $hit_slaves, 'all slaves hit'); + } + + public function testRestoreMasterAfterException() + { + $db = $this->prepareMasterSlave(1, 1); + $this->assertTrue($db->enableSlaves); + + try { + $db->useMaster(function (Connection $db) { + throw new \Exception('fail'); + }); + $this->fail('Exception was caught somewhere'); + } catch (\Exception $e) { + // ok + } + $this->assertTrue($db->enableSlaves); + } + + /** + * @param int $masterCount + * @param int $slaveCount + * + * @return Connection + */ + protected function prepareMasterSlave($masterCount, $slaveCount) + { + $databases = self::getParam('databases'); + $fixture = $databases[$this->driverName]['fixture']; + $basePath = Yii::getAlias('@yii/tests/runtime'); + + $config = [ + '__class' => \Yiisoft\Db\Connection::class, + 'dsn' => "sqlite:$basePath/yii2test.sq3", + ]; + $this->prepareDatabase($config, $fixture)->close(); + + for ($i = 0; $i < $masterCount; $i++) { + $master = ['dsn' => "sqlite:$basePath/yii2test_master{$i}.sq3"]; + $db = $this->prepareDatabase($master, $fixture); + $db->close(); + $config['masters'][] = $master; + } + + for ($i = 0; $i < $slaveCount; $i++) { + $slave = ['dsn' => "sqlite:$basePath/yii2test_slave{$i}.sq3"]; + $db = $this->prepareDatabase($slave, $fixture); + $db->close(); + $config['slaves'][] = $slave; + } + + return Yii::createObject($config); + } + + public function testAliasDbPath() + { + $config = [ + 'dsn' => 'sqlite:@yii/tests/runtime/yii2aliastest.sq3', + ]; + $connection = new Connection($config); + $connection->open(); + $this->assertTrue($connection->isActive); + $this->assertEquals($config['dsn'], $connection->dsn); + + $connection->close(); + } + + public function testExceptionContainsRawQuery() + { + $this->markTestSkipped('This test does not work on sqlite because preparing the failing query fails'); + } +} diff --git a/tests/ExistValidatorTest.php b/tests/ExistValidatorTest.php new file mode 100644 index 000000000..2b4b8db2d --- /dev/null +++ b/tests/ExistValidatorTest.php @@ -0,0 +1,19 @@ +primaryKey()->first()->after('col_before'), + 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', + ], + ]); + } + + public function conditionProvider() + { + return array_merge(parent::conditionProvider(), [ + 'composite in using array objects' => [ + ['in', new TraversableObject(['id', 'name']), new TraversableObject([ + ['id' => 1, 'name' => 'oy'], + ['id' => 2, 'name' => 'yo'], + ])], + '(([[id]] = :qp0 AND [[name]] = :qp1) OR ([[id]] = :qp2 AND [[name]] = :qp3))', + [':qp0' => 1, ':qp1' => 'oy', ':qp2' => 2, ':qp3' => 'yo'], + ], + 'composite in' => [ + ['in', ['id', 'name'], [['id' => 1, 'name' => 'oy']]], + '(([[id]] = :qp0 AND [[name]] = :qp1))', + [':qp0' => 1, ':qp1' => 'oy'], + ], + ]); + } + + public function primaryKeysProvider() + { + $this->markTestSkipped('Adding/dropping primary keys is not supported in SQLite.'); + } + + public function foreignKeysProvider() + { + $this->markTestSkipped('Adding/dropping foreign keys is not supported in SQLite.'); + } + + public function indexesProvider() + { + $result = parent::indexesProvider(); + $result['drop'][0] = 'DROP INDEX [[CN_constraints_2_single]]'; + + return $result; + } + + public function uniquesProvider() + { + $this->markTestSkipped('Adding/dropping unique constraints is not supported in SQLite.'); + } + + public function checksProvider() + { + $this->markTestSkipped('Adding/dropping check constraints is not supported in SQLite.'); + } + + public function defaultValuesProvider() + { + $this->markTestSkipped('Adding/dropping default constraints is not supported in SQLite.'); + } + + public function testCommentColumn() + { + $this->markTestSkipped('Comments are not supported in SQLite'); + } + + public function testCommentTable() + { + $this->markTestSkipped('Comments are not supported in SQLite'); + } + + public function batchInsertProvider() + { + $data = parent::batchInsertProvider(); + $data['escape-danger-chars']['expected'] = "INSERT INTO `customer` (`address`) VALUES ('SQL-danger chars are escaped: ''); --')"; + + return $data; + } + + public function testBatchInsertOnOlderVersions() + { + $db = $this->getConnection(); + if (version_compare($db->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '3.7.11', '>=')) { + $this->markTestSkipped('This test is only relevant for SQLite < 3.7.11'); + } + $sql = $this->getQueryBuilder()->batchInsert('{{customer}} t', ['t.id', 't.name'], [[1, 'a'], [2, 'b']]); + $this->assertEquals("INSERT INTO {{customer}} t (`t`.`id`, `t`.`name`) SELECT 1, 'a' UNION SELECT 2, 'b'", $sql); + } + + public function testRenameTable() + { + $sql = $this->getQueryBuilder()->renameTable('table_from', 'table_to'); + $this->assertEquals('ALTER TABLE `table_from` RENAME TO `table_to`', $sql); + } + + /** + * {@inheritdoc} + */ + public function testBuildUnion() + { + $expectedQuerySql = $this->replaceQuotes( + 'SELECT `id` FROM `TotalExample` `t1` WHERE (w > 0) AND (x < 2) UNION SELECT `id` FROM `TotalTotalExample` `t2` WHERE w > 5 UNION ALL SELECT `id` FROM `TotalTotalExample` `t3` WHERE w = 3' + ); + $query = new Query(); + $secondQuery = new Query(); + $secondQuery->select('id') + ->from('TotalTotalExample t2') + ->where('w > 5'); + $thirdQuery = new Query(); + $thirdQuery->select('id') + ->from('TotalTotalExample t3') + ->where('w = 3'); + $query->select('id') + ->from('TotalExample t1') + ->where(['and', 'w > 0', 'x < 2']) + ->union($secondQuery) + ->union($thirdQuery, true); + [$actualQuerySql, $queryParams] = $this->getQueryBuilder()->build($query); + $this->assertEquals($expectedQuerySql, $actualQuerySql); + $this->assertEquals([], $queryParams); + } + + public function testResetSequence() + { + $qb = $this->getQueryBuilder(true, true); + + $expected = "UPDATE sqlite_sequence SET seq='5' WHERE name='item'"; + $sql = $qb->resetSequence('item'); + $this->assertEquals($expected, $sql); + + $expected = "UPDATE sqlite_sequence SET seq='3' WHERE name='item'"; + $sql = $qb->resetSequence('item', 4); + $this->assertEquals($expected, $sql); + } + + public function upsertProvider() + { + $concreteData = [ + 'regular values' => [ + 3 => 'WITH "EXCLUDED" (`email`, `address`, `status`, `profile_id`) AS (VALUES (:qp0, :qp1, :qp2, :qp3)) UPDATE `T_upsert` SET `address`=(SELECT `address` FROM `EXCLUDED`), `status`=(SELECT `status` FROM `EXCLUDED`), `profile_id`=(SELECT `profile_id` FROM `EXCLUDED`) WHERE `T_upsert`.`email`=(SELECT `email` FROM `EXCLUDED`); INSERT OR IGNORE INTO `T_upsert` (`email`, `address`, `status`, `profile_id`) VALUES (:qp0, :qp1, :qp2, :qp3);', + ], + 'regular values with update part' => [ + 3 => 'WITH "EXCLUDED" (`email`, `address`, `status`, `profile_id`) AS (VALUES (:qp0, :qp1, :qp2, :qp3)) UPDATE `T_upsert` SET `address`=:qp4, `status`=:qp5, `orders`=T_upsert.orders + 1 WHERE `T_upsert`.`email`=(SELECT `email` FROM `EXCLUDED`); INSERT OR IGNORE INTO `T_upsert` (`email`, `address`, `status`, `profile_id`) VALUES (:qp0, :qp1, :qp2, :qp3);', + ], + 'regular values without update part' => [ + 3 => 'INSERT OR IGNORE INTO `T_upsert` (`email`, `address`, `status`, `profile_id`) VALUES (:qp0, :qp1, :qp2, :qp3)', + ], + 'query' => [ + 3 => 'WITH "EXCLUDED" (`email`, `status`) AS (SELECT `email`, 2 AS `status` FROM `customer` WHERE `name`=:qp0 LIMIT 1) UPDATE `T_upsert` SET `status`=(SELECT `status` FROM `EXCLUDED`) WHERE `T_upsert`.`email`=(SELECT `email` FROM `EXCLUDED`); INSERT OR IGNORE INTO `T_upsert` (`email`, `status`) SELECT `email`, 2 AS `status` FROM `customer` WHERE `name`=:qp0 LIMIT 1;', + ], + 'query with update part' => [ + 3 => 'WITH "EXCLUDED" (`email`, `status`) AS (SELECT `email`, 2 AS `status` FROM `customer` WHERE `name`=:qp0 LIMIT 1) UPDATE `T_upsert` SET `address`=:qp1, `status`=:qp2, `orders`=T_upsert.orders + 1 WHERE `T_upsert`.`email`=(SELECT `email` FROM `EXCLUDED`); INSERT OR IGNORE INTO `T_upsert` (`email`, `status`) SELECT `email`, 2 AS `status` FROM `customer` WHERE `name`=:qp0 LIMIT 1;', + ], + 'query without update part' => [ + 3 => 'INSERT OR IGNORE INTO `T_upsert` (`email`, `status`) SELECT `email`, 2 AS `status` FROM `customer` WHERE `name`=:qp0 LIMIT 1', + ], + 'values and expressions' => [ + 3 => 'INSERT INTO {{%T_upsert}} ({{%T_upsert}}.[[email]], [[ts]]) VALUES (:qp0, now())', + ], + 'values and expressions with update part' => [ + 3 => 'INSERT INTO {{%T_upsert}} ({{%T_upsert}}.[[email]], [[ts]]) VALUES (:qp0, now())', + ], + 'values and expressions without update part' => [ + 3 => 'INSERT INTO {{%T_upsert}} ({{%T_upsert}}.[[email]], [[ts]]) VALUES (:qp0, now())', + ], + 'query, values and expressions with update part' => [ + 3 => 'WITH "EXCLUDED" (`email`, [[time]]) AS (SELECT :phEmail AS `email`, now() AS [[time]]) UPDATE {{%T_upsert}} SET `ts`=:qp1, [[orders]]=T_upsert.orders + 1 WHERE {{%T_upsert}}.`email`=(SELECT `email` FROM `EXCLUDED`); INSERT OR IGNORE INTO {{%T_upsert}} (`email`, [[time]]) SELECT :phEmail AS `email`, now() AS [[time]];', + ], + 'query, values and expressions without update part' => [ + 3 => 'WITH "EXCLUDED" (`email`, [[time]]) AS (SELECT :phEmail AS `email`, now() AS [[time]]) UPDATE {{%T_upsert}} SET `ts`=:qp1, [[orders]]=T_upsert.orders + 1 WHERE {{%T_upsert}}.`email`=(SELECT `email` FROM `EXCLUDED`); INSERT OR IGNORE INTO {{%T_upsert}} (`email`, [[time]]) SELECT :phEmail AS `email`, now() AS [[time]];', + ], + ]; + $newData = parent::upsertProvider(); + foreach ($concreteData as $testName => $data) { + $newData[$testName] = array_replace($newData[$testName], $data); + } + + return $newData; + } +} diff --git a/tests/QueryTest.php b/tests/QueryTest.php new file mode 100644 index 000000000..e13cd4d41 --- /dev/null +++ b/tests/QueryTest.php @@ -0,0 +1,36 @@ +getConnection(); + $query = new Query(); + $query->select(['id', 'name']) + ->from('item') + ->union( + (new Query()) + ->select(['id', 'name']) + ->from(['category']) + ); + $result = $query->all($connection); + $this->assertNotEmpty($result); + $this->assertCount(7, $result); + } +} diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php new file mode 100644 index 000000000..15d7534d8 --- /dev/null +++ b/tests/SchemaTest.php @@ -0,0 +1,86 @@ +markTestSkipped('Schemas are not supported in SQLite.'); + } + + public function getExpectedColumns() + { + $columns = parent::getExpectedColumns(); + unset($columns['enum_col']); + unset($columns['bit_col']); + unset($columns['json_col']); + $columns['int_col']['dbType'] = 'integer'; + $columns['int_col']['size'] = null; + $columns['int_col']['precision'] = null; + $columns['int_col2']['dbType'] = 'integer'; + $columns['int_col2']['size'] = null; + $columns['int_col2']['precision'] = null; + $columns['bool_col']['type'] = 'boolean'; + $columns['bool_col']['phpType'] = 'boolean'; + $columns['bool_col2']['type'] = 'boolean'; + $columns['bool_col2']['phpType'] = 'boolean'; + $columns['bool_col2']['defaultValue'] = true; + + return $columns; + } + + public function testCompositeFk() + { + $schema = $this->getConnection()->schema; + + $table = $schema->getTableSchema('composite_fk'); + + $this->assertCount(1, $table->foreignKeys); + $this->assertTrue(isset($table->foreignKeys[0])); + $this->assertEquals('order_item', $table->foreignKeys[0][0]); + $this->assertEquals('order_id', $table->foreignKeys[0]['order_id']); + $this->assertEquals('item_id', $table->foreignKeys[0]['item_id']); + } + + public function constraintsProvider() + { + $result = parent::constraintsProvider(); + $result['1: primary key'][2]->name = null; + $result['1: check'][2][0]->columnNames = null; + $result['1: check'][2][0]->expression = '"C_check" <> \'\''; + $result['1: unique'][2][0]->name = AnyValue::getInstance(); + $result['1: index'][2][1]->name = AnyValue::getInstance(); + + $result['2: primary key'][2]->name = null; + $result['2: unique'][2][0]->name = AnyValue::getInstance(); + $result['2: index'][2][2]->name = AnyValue::getInstance(); + + $result['3: foreign key'][2][0]->name = null; + $result['3: index'][2] = []; + + $result['4: primary key'][2]->name = null; + $result['4: unique'][2][0]->name = AnyValue::getInstance(); + + $result['5: primary key'] = ['T_upsert', 'primaryKey', new Constraint([ + 'name' => AnyValue::getInstance(), + 'columnNames' => ['id'], + ])]; + + return $result; + } +} diff --git a/tests/SqlTokenizerTest.php b/tests/SqlTokenizerTest.php new file mode 100644 index 000000000..169957fc3 --- /dev/null +++ b/tests/SqlTokenizerTest.php @@ -0,0 +1,1149 @@ + [ + <<<'SQL' +CREATE TABLE `constraints_test_1` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `col_1` integer, + `col_2` integer NOT NULL, `chktest` text NOT NULL DEFAULT 'none' CHECK([chktest] <> '''' and not(chktest=='foo')), + -- CONSTRAINT `ch` CHECK -- (`col_1` <> 0 and col_2 <> 1)) +CONSTRAINT `ch2` CHECK -- (`col_1` <> 0 and col_2 <> -1)) +(`col_1` <> 41 and not (col_2 == 'ั‚ะตั''ั‚'))); +CREATE TABLE t300(id INTEGER PRIMARY KEY); +CREATE TABLE t301( + id INTEGER PRIMARY KEY, + c1 INTEGER NOT NULL, + c2 INTEGER NOT NULL, + c3 BOOLEAN NOT NULL DEFAULT 0, + FOREIGN KEY(c1) REFERENCES t300(id) ON DELETE CASCADE ON UPDATE RESTRICT + /* no comma */ + FOREIGN KEY(c2) REFERENCES t300(id) ON DELETE CASCADE ON UPDATE RESTRICT + /* no comma */ + UNIQUE(c1, c2) +); +PRAGMA foreign_key_list(t301); +SELECT*from/*foo*/`T_constraints_1`WHERE not`C_check`='foo''bar'--bar +;;;;;;;;;/* +SQL +, + new SqlToken([ + 'type' => SqlToken::TYPE_CODE, + 'content' => 'CREATE TABLE `constraints_test_1` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `col_1` integer, + `col_2` integer NOT NULL, `chktest` text NOT NULL DEFAULT \'none\' CHECK([chktest] <> \'\'\'\' and not(chktest==\'foo\')), + -- CONSTRAINT `ch` CHECK -- (`col_1` <> 0 and col_2 <> 1)) +CONSTRAINT `ch2` CHECK -- (`col_1` <> 0 and col_2 <> -1)) +(`col_1` <> 41 and not (col_2 == \'ั‚ะตั\'\'ั‚\'))); +CREATE TABLE t300(id INTEGER PRIMARY KEY); +CREATE TABLE t301( + id INTEGER PRIMARY KEY, + c1 INTEGER NOT NULL, + c2 INTEGER NOT NULL, + c3 BOOLEAN NOT NULL DEFAULT 0, + FOREIGN KEY(c1) REFERENCES t300(id) ON DELETE CASCADE ON UPDATE RESTRICT + /* no comma */ + FOREIGN KEY(c2) REFERENCES t300(id) ON DELETE CASCADE ON UPDATE RESTRICT + /* no comma */ + UNIQUE(c1, c2) +); +PRAGMA foreign_key_list(t301); +SELECT*from/*foo*/`T_constraints_1`WHERE not`C_check`=\'foo\'\'bar\'--bar +;;;;;;;;;/*', + 'startOffset' => 0, + 'endOffset' => 875, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_STATEMENT, + 'content' => null, + 'startOffset' => 0, + 'endOffset' => 383, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'CREATE', + 'startOffset' => 0, + 'endOffset' => 6, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'TABLE', + 'startOffset' => 7, + 'endOffset' => 12, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_IDENTIFIER, + 'content' => 'constraints_test_1', + 'startOffset' => 13, + 'endOffset' => 33, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 34, + 'endOffset' => 35, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 37, + 'endOffset' => 381, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_IDENTIFIER, + 'content' => 'id', + 'startOffset' => 37, + 'endOffset' => 41, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'integer', + 'startOffset' => 42, + 'endOffset' => 49, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'PRIMARY', + 'startOffset' => 50, + 'endOffset' => 57, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'KEY', + 'startOffset' => 58, + 'endOffset' => 61, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'AUTOINCREMENT', + 'startOffset' => 62, + 'endOffset' => 75, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NOT', + 'startOffset' => 76, + 'endOffset' => 79, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NULL', + 'startOffset' => 80, + 'endOffset' => 84, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ',', + 'startOffset' => 84, + 'endOffset' => 85, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_IDENTIFIER, + 'content' => 'col_1', + 'startOffset' => 87, + 'endOffset' => 94, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'integer', + 'startOffset' => 95, + 'endOffset' => 102, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ',', + 'startOffset' => 102, + 'endOffset' => 103, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_IDENTIFIER, + 'content' => 'col_2', + 'startOffset' => 105, + 'endOffset' => 112, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'integer', + 'startOffset' => 113, + 'endOffset' => 120, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NOT', + 'startOffset' => 121, + 'endOffset' => 124, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NULL', + 'startOffset' => 125, + 'endOffset' => 129, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ',', + 'startOffset' => 129, + 'endOffset' => 130, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_IDENTIFIER, + 'content' => 'chktest', + 'startOffset' => 131, + 'endOffset' => 140, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'text', + 'startOffset' => 141, + 'endOffset' => 145, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NOT', + 'startOffset' => 146, + 'endOffset' => 149, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NULL', + 'startOffset' => 150, + 'endOffset' => 154, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'DEFAULT', + 'startOffset' => 155, + 'endOffset' => 162, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_STRING_LITERAL, + 'content' => 'none', + 'startOffset' => 163, + 'endOffset' => 169, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'CHECK', + 'startOffset' => 170, + 'endOffset' => 175, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 175, + 'endOffset' => 176, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 176, + 'endOffset' => 217, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_IDENTIFIER, + 'content' => 'chktest', + 'startOffset' => 176, + 'endOffset' => 185, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '<>', + 'startOffset' => 186, + 'endOffset' => 188, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_STRING_LITERAL, + 'content' => '\'', + 'startOffset' => 189, + 'endOffset' => 193, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'AND', + 'startOffset' => 194, + 'endOffset' => 197, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NOT', + 'startOffset' => 198, + 'endOffset' => 201, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 201, + 'endOffset' => 202, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 202, + 'endOffset' => 216, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'chktest', + 'startOffset' => 202, + 'endOffset' => 209, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '==', + 'startOffset' => 209, + 'endOffset' => 211, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_STRING_LITERAL, + 'content' => 'foo', + 'startOffset' => 211, + 'endOffset' => 216, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 216, + 'endOffset' => 217, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 217, + 'endOffset' => 218, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ',', + 'startOffset' => 218, + 'endOffset' => 219, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'CONSTRAINT', + 'startOffset' => 280, + 'endOffset' => 290, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_IDENTIFIER, + 'content' => 'ch2', + 'startOffset' => 291, + 'endOffset' => 296, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'CHECK', + 'startOffset' => 297, + 'endOffset' => 302, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 338, + 'endOffset' => 339, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 339, + 'endOffset' => 380, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_IDENTIFIER, + 'content' => 'col_1', + 'startOffset' => 339, + 'endOffset' => 346, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '<>', + 'startOffset' => 347, + 'endOffset' => 349, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => '41', + 'startOffset' => 350, + 'endOffset' => 352, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'AND', + 'startOffset' => 353, + 'endOffset' => 356, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NOT', + 'startOffset' => 357, + 'endOffset' => 360, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 361, + 'endOffset' => 362, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 362, + 'endOffset' => 379, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'col_2', + 'startOffset' => 362, + 'endOffset' => 367, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '==', + 'startOffset' => 368, + 'endOffset' => 370, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_STRING_LITERAL, + 'content' => 'ั‚ะตั\'ั‚', + 'startOffset' => 371, + 'endOffset' => 379, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 379, + 'endOffset' => 380, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 380, + 'endOffset' => 381, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 381, + 'endOffset' => 382, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ';', + 'startOffset' => 382, + 'endOffset' => 383, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_STATEMENT, + 'content' => null, + 'startOffset' => 384, + 'endOffset' => 426, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'CREATE', + 'startOffset' => 384, + 'endOffset' => 390, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'TABLE', + 'startOffset' => 391, + 'endOffset' => 396, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 't300', + 'startOffset' => 397, + 'endOffset' => 401, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 401, + 'endOffset' => 402, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 402, + 'endOffset' => 424, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'id', + 'startOffset' => 402, + 'endOffset' => 404, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'INTEGER', + 'startOffset' => 405, + 'endOffset' => 412, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'PRIMARY', + 'startOffset' => 413, + 'endOffset' => 420, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'KEY', + 'startOffset' => 421, + 'endOffset' => 424, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 424, + 'endOffset' => 425, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ';', + 'startOffset' => 425, + 'endOffset' => 426, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_STATEMENT, + 'content' => null, + 'startOffset' => 427, + 'endOffset' => 772, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'CREATE', + 'startOffset' => 427, + 'endOffset' => 433, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'TABLE', + 'startOffset' => 434, + 'endOffset' => 439, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 't301', + 'startOffset' => 440, + 'endOffset' => 444, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 444, + 'endOffset' => 445, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 450, + 'endOffset' => 769, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'id', + 'startOffset' => 450, + 'endOffset' => 452, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'INTEGER', + 'startOffset' => 453, + 'endOffset' => 460, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'PRIMARY', + 'startOffset' => 461, + 'endOffset' => 468, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'KEY', + 'startOffset' => 469, + 'endOffset' => 472, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ',', + 'startOffset' => 472, + 'endOffset' => 473, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'c1', + 'startOffset' => 478, + 'endOffset' => 480, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'INTEGER', + 'startOffset' => 481, + 'endOffset' => 488, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NOT', + 'startOffset' => 489, + 'endOffset' => 492, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NULL', + 'startOffset' => 493, + 'endOffset' => 497, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ',', + 'startOffset' => 497, + 'endOffset' => 498, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'c2', + 'startOffset' => 503, + 'endOffset' => 505, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'INTEGER', + 'startOffset' => 506, + 'endOffset' => 513, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NOT', + 'startOffset' => 514, + 'endOffset' => 517, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NULL', + 'startOffset' => 518, + 'endOffset' => 522, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ',', + 'startOffset' => 522, + 'endOffset' => 523, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'c3', + 'startOffset' => 528, + 'endOffset' => 530, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'BOOLEAN', + 'startOffset' => 531, + 'endOffset' => 538, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NOT', + 'startOffset' => 539, + 'endOffset' => 542, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NULL', + 'startOffset' => 543, + 'endOffset' => 547, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'DEFAULT', + 'startOffset' => 548, + 'endOffset' => 555, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => '0', + 'startOffset' => 556, + 'endOffset' => 557, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ',', + 'startOffset' => 557, + 'endOffset' => 558, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'FOREIGN', + 'startOffset' => 563, + 'endOffset' => 570, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'KEY', + 'startOffset' => 571, + 'endOffset' => 574, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 574, + 'endOffset' => 575, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 575, + 'endOffset' => 577, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'c1', + 'startOffset' => 575, + 'endOffset' => 577, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 577, + 'endOffset' => 578, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'REFERENCES', + 'startOffset' => 579, + 'endOffset' => 589, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 't300', + 'startOffset' => 590, + 'endOffset' => 594, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 594, + 'endOffset' => 595, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 595, + 'endOffset' => 597, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'id', + 'startOffset' => 595, + 'endOffset' => 597, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 597, + 'endOffset' => 598, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'ON', + 'startOffset' => 599, + 'endOffset' => 601, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'DELETE', + 'startOffset' => 602, + 'endOffset' => 608, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'CASCADE', + 'startOffset' => 609, + 'endOffset' => 616, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'ON', + 'startOffset' => 617, + 'endOffset' => 619, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'UPDATE', + 'startOffset' => 620, + 'endOffset' => 626, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'RESTRICT', + 'startOffset' => 627, + 'endOffset' => 635, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'FOREIGN', + 'startOffset' => 659, + 'endOffset' => 666, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'KEY', + 'startOffset' => 667, + 'endOffset' => 670, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 670, + 'endOffset' => 671, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 671, + 'endOffset' => 673, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'c2', + 'startOffset' => 671, + 'endOffset' => 673, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 673, + 'endOffset' => 674, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'REFERENCES', + 'startOffset' => 675, + 'endOffset' => 685, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 't300', + 'startOffset' => 686, + 'endOffset' => 690, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 690, + 'endOffset' => 691, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 691, + 'endOffset' => 693, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'id', + 'startOffset' => 691, + 'endOffset' => 693, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 693, + 'endOffset' => 694, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'ON', + 'startOffset' => 695, + 'endOffset' => 697, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'DELETE', + 'startOffset' => 698, + 'endOffset' => 704, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'CASCADE', + 'startOffset' => 705, + 'endOffset' => 712, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'ON', + 'startOffset' => 713, + 'endOffset' => 715, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'UPDATE', + 'startOffset' => 716, + 'endOffset' => 722, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'RESTRICT', + 'startOffset' => 723, + 'endOffset' => 731, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'UNIQUE', + 'startOffset' => 755, + 'endOffset' => 761, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 761, + 'endOffset' => 762, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 762, + 'endOffset' => 768, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'c1', + 'startOffset' => 762, + 'endOffset' => 764, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ',', + 'startOffset' => 764, + 'endOffset' => 765, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'c2', + 'startOffset' => 766, + 'endOffset' => 768, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 768, + 'endOffset' => 769, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 770, + 'endOffset' => 771, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ';', + 'startOffset' => 771, + 'endOffset' => 772, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_STATEMENT, + 'content' => null, + 'startOffset' => 773, + 'endOffset' => 803, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'PRAGMA', + 'startOffset' => 773, + 'endOffset' => 779, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 'foreign_key_list', + 'startOffset' => 780, + 'endOffset' => 796, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '(', + 'startOffset' => 796, + 'endOffset' => 797, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_PARENTHESIS, + 'content' => null, + 'startOffset' => 797, + 'endOffset' => 801, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_TOKEN, + 'content' => 't301', + 'startOffset' => 797, + 'endOffset' => 801, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ')', + 'startOffset' => 801, + 'endOffset' => 802, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ';', + 'startOffset' => 802, + 'endOffset' => 803, + ]), + ], + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_STATEMENT, + 'content' => null, + 'startOffset' => 804, + 'endOffset' => 875, + 'children' => [ + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'SELECT', + 'startOffset' => 804, + 'endOffset' => 810, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '*', + 'startOffset' => 810, + 'endOffset' => 811, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'FROM', + 'startOffset' => 811, + 'endOffset' => 815, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_IDENTIFIER, + 'content' => 'T_constraints_1', + 'startOffset' => 822, + 'endOffset' => 839, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'WHERE', + 'startOffset' => 839, + 'endOffset' => 844, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_KEYWORD, + 'content' => 'NOT', + 'startOffset' => 845, + 'endOffset' => 848, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_IDENTIFIER, + 'content' => 'C_check', + 'startOffset' => 848, + 'endOffset' => 857, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => '=', + 'startOffset' => 857, + 'endOffset' => 858, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_STRING_LITERAL, + 'content' => 'foo\'bar', + 'startOffset' => 858, + 'endOffset' => 868, + ]), + new SqlToken([ + 'type' => SqlToken::TYPE_OPERATOR, + 'content' => ';', + 'startOffset' => 874, + 'endOffset' => 875, + ]), + ], + ]), + ], + ]), + ], + ]; + } + + /** + * @dataProvider sqlProvider + * + * @param string $sql + */ + public function testTokenizer($sql, SqlToken $expectedToken) + { + $actualToken = (new SqlTokenizer($sql))->tokenize(); + $this->assertEquals($expectedToken, $actualToken); + } + + /** + * Use this to export SqlToken for tests. + * + * @param SqlToken $token + * + * @return array + */ + private function exportToken(SqlToken $token) + { + $result = get_object_vars($token); + unset($result['parent']); + $result['children'] = array_map(function (SqlToken $token) { + return $this->exportToken($token); + }, $token->children); + + return $result; + } +} diff --git a/tests/UniqueValidatorTest.php b/tests/UniqueValidatorTest.php new file mode 100644 index 000000000..81196bd15 --- /dev/null +++ b/tests/UniqueValidatorTest.php @@ -0,0 +1,19 @@ +