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 @@
+