├── .buildkite ├── hooks │ └── pre-command └── pipeline.yml ├── .github ├── dependabot.yml ├── entrypoint.sh └── workflows │ └── ci.yml ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── activerecord-tidb-adapter.gemspec ├── bin ├── console └── setup ├── config.toml ├── lib ├── active_record │ ├── connection_adapters │ │ ├── tidb │ │ │ ├── database_statements.rb │ │ │ ├── database_tasks.rb │ │ │ ├── savepoints.rb │ │ │ ├── schema_statements.rb │ │ │ ├── setup.rb │ │ │ └── type.rb │ │ └── tidb_adapter.rb │ ├── sequence.rb │ └── sequence │ │ ├── adapter.rb │ │ ├── command_recorder.rb │ │ ├── model_methods.rb │ │ └── schema_dumper.rb ├── activerecord-tidb-adapter.rb └── version.rb ├── test ├── activerecord_tidb_adapter_test.rb ├── cases │ ├── helper.rb │ ├── helper_tidb.rb │ └── sequence_test.rb ├── config.example.yml ├── excludes │ ├── ActiveRecord │ │ ├── Encryption │ │ │ ├── ContextsTest.rb │ │ │ ├── StoragePerformanceTest.rb │ │ │ └── UniquenessValidationsTest.rb │ │ ├── Migration │ │ │ └── ColumnsTest.rb │ │ └── StatementCacheTest.rb │ ├── BasicsTest.rb │ ├── CalculationsTest.rb │ ├── CommentTest.rb │ ├── LegacyPrimaryKeyTest │ │ ├── V4_2.rb │ │ └── V5_0.rb │ ├── LogSubscriberTest.rb │ ├── MarshalSerializationTest.rb │ ├── Mysql2DefaultEngineOptionTest.rb │ ├── Mysql2OptimizerHintsTest.rb │ ├── ReservedWordTest.rb │ └── SchemaDumperTest.rb ├── models │ └── tidb_order.rb ├── schema │ └── tidb_specific_schema.rb ├── support │ ├── config.rb │ ├── paths_tidb.rb │ └── rake_helpers.rb └── test_helper.rb └── testing.sh /.buildkite/hooks/pre-command: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Place this in /etc/buildkite-agent/hooks/pre-command 4 | # 5 | # Needs to be `chown buildkite-agent` and `chmod +x` 6 | 7 | # RVM uses unbound variables and will fail without this 8 | set +u 9 | 10 | source /var/lib/buildkite-agent/.rvm/scripts/rvm -------------------------------------------------------------------------------- /.buildkite/pipeline.yml: -------------------------------------------------------------------------------- 1 | env: 2 | MYSQL_HOST: 127.0.0.1 3 | MYSQL_USER: root 4 | 5 | steps: 6 | - command: "testing.sh" 7 | label: "Run TiDB ActiveRecord Adapter testing" -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | # Check for updates to GitHub Actions every weekday 8 | interval: "daily" 9 | -------------------------------------------------------------------------------- /.github/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | service supervisor start 4 | 5 | while sleep 60; do 6 | ps aux |grep tidb |grep -q -v grep 7 | TIDB_STATUS=$? 8 | ps aux |grep tikv |grep -q -v grep 9 | TIKV_STATUS=$? 10 | # If the greps above find anything, they exit with 0 status 11 | # If they are not both 0, then something is wrong 12 | if [ $TIDB_STATUS -ne 0 -o $TIKV_STATUS -ne 0 ]; then 13 | echo "One of the processes has already exited." 14 | exit 1 15 | fi 16 | done -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - 7-0-stable 8 | paths-ignore: 9 | - 'README*.md' 10 | - 'docs/**' 11 | pull_request: 12 | branches: 13 | - main 14 | - 7-0-stable 15 | paths-ignore: 16 | - 'README*.md' 17 | - 'docs/**' 18 | 19 | jobs: 20 | tidb510: 21 | if: ${{ !contains(github.event.commits[0].message, '[skip ci]') }} 22 | runs-on: ubuntu-latest 23 | services: 24 | tidb: 25 | image: hooopo/tidb-playground:v5.1.0 26 | env: 27 | TIDB_VERSION: v5.1.0 28 | ports: ["4000:4000"] 29 | steps: 30 | - uses: actions/checkout@v3 31 | - uses: ruby/setup-ruby@v1 32 | with: 33 | ruby-version: 2.7 34 | bundler-cache: true 35 | - run: sleep 30 && MYSQL_USER=root MYSQL_HOST=127.0.0.1 MYSQL_PORT=4000 tidb=1 ARCONN=tidb bundle exec rake db:tidb:build 36 | - run: sleep 10 && MYSQL_USER=root MYSQL_HOST=127.0.0.1 MYSQL_PORT=4000 tidb=1 ARCONN=tidb bundle exec rake test:tidb 37 | 38 | tidb503: 39 | if: ${{ !contains(github.event.commits[0].message, '[skip ci]') }} 40 | runs-on: ubuntu-latest 41 | services: 42 | tidb: 43 | image: hooopo/tidb-playground:v5.0.3 44 | env: 45 | TIDB_VERSION: v5.0.3 46 | ports: ["4000:4000"] 47 | steps: 48 | - uses: actions/checkout@v3 49 | - uses: ruby/setup-ruby@v1 50 | with: 51 | ruby-version: 2.7 52 | bundler-cache: true 53 | - run: sleep 30 && MYSQL_USER=root MYSQL_HOST=127.0.0.1 MYSQL_PORT=4000 tidb=1 ARCONN=tidb bundle exec rake db:tidb:build 54 | - run: mysql --host 127.0.0.1 --database activerecord_unittest --port 4000 -u root -e 'set @@global.tidb_enable_change_column_type = 1' 55 | - run: mysql --host 127.0.0.1 --database activerecord_unittest2 --port 4000 -u root -e 'set @@global.tidb_enable_change_column_type = 1' 56 | - run: sleep 10 && MYSQL_USER=root MYSQL_HOST=127.0.0.1 MYSQL_PORT=4000 tidb=1 ARCONN=tidb bundle exec rake test:tidb 57 | 58 | tidb511: 59 | if: ${{ !contains(github.event.commits[0].message, '[skip ci]') }} 60 | runs-on: ubuntu-latest 61 | services: 62 | tidb: 63 | image: hooopo/tidb-playground:v5.1.1 64 | env: 65 | TIDB_VERSION: v5.1.1 66 | ports: ["4000:4000"] 67 | steps: 68 | - uses: actions/checkout@v3 69 | - uses: ruby/setup-ruby@v1 70 | with: 71 | ruby-version: 2.7 72 | bundler-cache: true 73 | - run: sleep 30 && MYSQL_USER=root MYSQL_HOST=127.0.0.1 MYSQL_PORT=4000 tidb=1 ARCONN=tidb bundle exec rake db:tidb:build 74 | - run: sleep 10 && MYSQL_USER=root MYSQL_HOST=127.0.0.1 MYSQL_PORT=4000 tidb=1 ARCONN=tidb bundle exec rake test:tidb 75 | 76 | tidb520: 77 | if: ${{ !contains(github.event.commits[0].message, '[skip ci]') }} 78 | runs-on: ubuntu-latest 79 | services: 80 | tidb: 81 | image: hooopo/tidb-playground:v5.2.0 82 | env: 83 | TIDB_VERSION: v5.2.0 84 | ports: ["4000:4000"] 85 | steps: 86 | - uses: actions/checkout@v3 87 | - uses: ruby/setup-ruby@v1 88 | with: 89 | ruby-version: 2.7 90 | bundler-cache: true 91 | - run: sleep 30 && MYSQL_USER=root MYSQL_HOST=127.0.0.1 MYSQL_PORT=4000 tidb=1 ARCONN=tidb bundle exec rake db:tidb:build 92 | - run: sleep 10 && MYSQL_USER=root MYSQL_HOST=127.0.0.1 MYSQL_PORT=4000 tidb=1 ARCONN=tidb bundle exec rake test:tidb 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | /sqlnet.log 10 | /test/config.yml 11 | /test/db/ 12 | /test/fixtures/*.sqlite* 13 | /debug.log 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in activerecord-tidb-adapter.gemspec 6 | gemspec 7 | 8 | gem 'rake', '~> 13.0' 9 | 10 | gem 'minitest', '~> 5.0' 11 | gem 'minitest-excludes', '~> 2.0' 12 | 13 | gem 'pry' 14 | 15 | gem 'rubocop', '~> 1.18' 16 | 17 | gem 'rails', git: 'https://github.com/pingcap/rails.git', branch: '7-0-stable' 18 | 19 | gem 'byebug', '~> 11.1' 20 | 21 | gem 'sqlite3', '~> 1.4' 22 | 23 | gem 'pg', '~> 1.2' 24 | 25 | gem "benchmark-ips", "~> 2.9" 26 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/pingcap/rails.git 3 | revision: 8e88c0ecd838d642c542e8246a0b7c6e9df9c806 4 | branch: 7-0-stable 5 | specs: 6 | actioncable (7.0.2.3) 7 | actionpack (= 7.0.2.3) 8 | activesupport (= 7.0.2.3) 9 | nio4r (~> 2.0) 10 | websocket-driver (>= 0.6.1) 11 | actionmailbox (7.0.2.3) 12 | actionpack (= 7.0.2.3) 13 | activejob (= 7.0.2.3) 14 | activerecord (= 7.0.2.3) 15 | activestorage (= 7.0.2.3) 16 | activesupport (= 7.0.2.3) 17 | mail (>= 2.7.1) 18 | net-imap 19 | net-pop 20 | net-smtp 21 | actionmailer (7.0.2.3) 22 | actionpack (= 7.0.2.3) 23 | actionview (= 7.0.2.3) 24 | activejob (= 7.0.2.3) 25 | activesupport (= 7.0.2.3) 26 | mail (~> 2.5, >= 2.5.4) 27 | net-imap 28 | net-pop 29 | net-smtp 30 | rails-dom-testing (~> 2.0) 31 | actionpack (7.0.2.3) 32 | actionview (= 7.0.2.3) 33 | activesupport (= 7.0.2.3) 34 | rack (~> 2.0, >= 2.2.0) 35 | rack-test (>= 0.6.3) 36 | rails-dom-testing (~> 2.0) 37 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 38 | actiontext (7.0.2.3) 39 | actionpack (= 7.0.2.3) 40 | activerecord (= 7.0.2.3) 41 | activestorage (= 7.0.2.3) 42 | activesupport (= 7.0.2.3) 43 | globalid (>= 0.6.0) 44 | nokogiri (>= 1.8.5) 45 | actionview (7.0.2.3) 46 | activesupport (= 7.0.2.3) 47 | builder (~> 3.1) 48 | erubi (~> 1.4) 49 | rails-dom-testing (~> 2.0) 50 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 51 | activejob (7.0.2.3) 52 | activesupport (= 7.0.2.3) 53 | globalid (>= 0.3.6) 54 | activemodel (7.0.2.3) 55 | activesupport (= 7.0.2.3) 56 | activerecord (7.0.2.3) 57 | activemodel (= 7.0.2.3) 58 | activesupport (= 7.0.2.3) 59 | activestorage (7.0.2.3) 60 | actionpack (= 7.0.2.3) 61 | activejob (= 7.0.2.3) 62 | activerecord (= 7.0.2.3) 63 | activesupport (= 7.0.2.3) 64 | marcel (~> 1.0) 65 | mini_mime (>= 1.1.0) 66 | activesupport (7.0.2.3) 67 | concurrent-ruby (~> 1.0, >= 1.0.2) 68 | i18n (>= 1.6, < 2) 69 | minitest (>= 5.1) 70 | tzinfo (~> 2.0) 71 | rails (7.0.2.3) 72 | actioncable (= 7.0.2.3) 73 | actionmailbox (= 7.0.2.3) 74 | actionmailer (= 7.0.2.3) 75 | actionpack (= 7.0.2.3) 76 | actiontext (= 7.0.2.3) 77 | actionview (= 7.0.2.3) 78 | activejob (= 7.0.2.3) 79 | activemodel (= 7.0.2.3) 80 | activerecord (= 7.0.2.3) 81 | activestorage (= 7.0.2.3) 82 | activesupport (= 7.0.2.3) 83 | bundler (>= 1.15.0) 84 | railties (= 7.0.2.3) 85 | railties (7.0.2.3) 86 | actionpack (= 7.0.2.3) 87 | activesupport (= 7.0.2.3) 88 | method_source 89 | rake (>= 12.2) 90 | thor (~> 1.0) 91 | zeitwerk (~> 2.5) 92 | 93 | PATH 94 | remote: . 95 | specs: 96 | activerecord-tidb-adapter (7.0.0) 97 | activerecord (~> 7.0.0) 98 | mysql2 99 | 100 | GEM 101 | remote: https://rubygems.org/ 102 | specs: 103 | ast (2.4.2) 104 | benchmark-ips (2.9.1) 105 | builder (3.2.4) 106 | byebug (11.1.3) 107 | coderay (1.1.3) 108 | concurrent-ruby (1.1.10) 109 | crass (1.0.6) 110 | digest (3.1.0) 111 | erubi (1.10.0) 112 | globalid (1.0.0) 113 | activesupport (>= 5.0) 114 | i18n (1.10.0) 115 | concurrent-ruby (~> 1.0) 116 | io-wait (0.2.1) 117 | loofah (2.15.0) 118 | crass (~> 1.0.2) 119 | nokogiri (>= 1.5.9) 120 | mail (2.7.1) 121 | mini_mime (>= 0.1.1) 122 | marcel (1.0.2) 123 | method_source (1.0.0) 124 | mini_mime (1.1.2) 125 | minitest (5.14.4) 126 | minitest-excludes (2.0.1) 127 | minitest (~> 5.0) 128 | mysql2 (0.5.3) 129 | net-imap (0.2.3) 130 | digest 131 | net-protocol 132 | strscan 133 | net-pop (0.1.1) 134 | digest 135 | net-protocol 136 | timeout 137 | net-protocol (0.1.2) 138 | io-wait 139 | timeout 140 | net-smtp (0.3.1) 141 | digest 142 | net-protocol 143 | timeout 144 | nio4r (2.5.8) 145 | nokogiri (1.13.3-x86_64-darwin) 146 | racc (~> 1.4) 147 | nokogiri (1.13.3-x86_64-linux) 148 | racc (~> 1.4) 149 | parallel (1.20.1) 150 | parser (3.0.2.0) 151 | ast (~> 2.4.1) 152 | pg (1.2.3) 153 | pry (0.14.1) 154 | coderay (~> 1.1) 155 | method_source (~> 1.0) 156 | racc (1.6.0) 157 | rack (2.2.3) 158 | rack-test (1.1.0) 159 | rack (>= 1.0, < 3) 160 | rails-dom-testing (2.0.3) 161 | activesupport (>= 4.2.0) 162 | nokogiri (>= 1.6) 163 | rails-html-sanitizer (1.4.2) 164 | loofah (~> 2.3) 165 | rainbow (3.0.0) 166 | rake (13.0.6) 167 | regexp_parser (2.1.1) 168 | rexml (3.2.5) 169 | rubocop (1.19.0) 170 | parallel (~> 1.10) 171 | parser (>= 3.0.0.0) 172 | rainbow (>= 2.2.2, < 4.0) 173 | regexp_parser (>= 1.8, < 3.0) 174 | rexml 175 | rubocop-ast (>= 1.9.1, < 2.0) 176 | ruby-progressbar (~> 1.7) 177 | unicode-display_width (>= 1.4.0, < 3.0) 178 | rubocop-ast (1.10.0) 179 | parser (>= 3.0.1.1) 180 | ruby-progressbar (1.11.0) 181 | sqlite3 (1.4.2) 182 | strscan (3.0.1) 183 | thor (1.2.1) 184 | timeout (0.2.0) 185 | tzinfo (2.0.4) 186 | concurrent-ruby (~> 1.0) 187 | unicode-display_width (2.0.0) 188 | websocket-driver (0.7.5) 189 | websocket-extensions (>= 0.1.0) 190 | websocket-extensions (0.1.5) 191 | zeitwerk (2.5.4) 192 | 193 | PLATFORMS 194 | x86_64-darwin-18 195 | x86_64-darwin-20 196 | x86_64-darwin-21 197 | x86_64-linux 198 | 199 | DEPENDENCIES 200 | activerecord-tidb-adapter! 201 | benchmark-ips (~> 2.9) 202 | byebug (~> 11.1) 203 | minitest (~> 5.0) 204 | minitest-excludes (~> 2.0) 205 | pg (~> 1.2) 206 | pry 207 | rails! 208 | rake (~> 13.0) 209 | rubocop (~> 1.18) 210 | sqlite3 (~> 1.4) 211 | 212 | BUNDLED WITH 213 | 2.2.32 214 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ActiveRecord TiDB Adapter 2 | 3 | [![Gem Version](https://badge.fury.io/rb/activerecord-tidb-adapter.svg)](https://badge.fury.io/rb/activerecord-tidb-adapter) 4 | [![activerecord-tidb-adapter 7.0](https://github.com/pingcap/activerecord-tidb-adapter/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/pingcap/activerecord-tidb-adapter/actions/workflows/ci.yml) 5 | 6 | ⚠️ This Gem is not currently maintained. If you're looking to connect to TiDB serverless with a Rails application, follow our [TiDB serverless ruby connect example](https://github.com/hooopo/tidb-serverless-ruby-connect-example)  7 | 8 | ⚠️ Since this Gem is a solution for the compatibility issues left by the older versions (prior to 6.0) of TiDB, TiDB Cloud and TiDB serverless will always use the latest versions, so there is no need for additional adapters for integration. You can directly use ActiveRecord or the mysql2 gem. 9 | 10 | ------ 11 | 12 | TiDB adapter for ActiveRecord 5.2, 6.1 and 7.0 13 | This is a lightweight extension of the mysql2 adapter that establishes compatibility with [TiDB](https://github.com/pingcap/tidb). 14 | 15 | 16 | ## Installation 17 | 18 | Add this line to your application's Gemfile: 19 | 20 | ```ruby 21 | gem 'activerecord-tidb-adapter', '~> 7.0.0' 22 | ``` 23 | 24 | If you're using Rails 5.2, use the 5.2.x versions of this gem. 25 | 26 | If you're using Rails 6.1, use the 6.1.x versions of this gem. 27 | 28 | If you're using Rails 7.0, use the 7.0.x versions of this gem. 29 | 30 | And then execute: 31 | 32 | $ bundle install 33 | 34 | Or install it yourself as: 35 | 36 | $ gem install activerecord-tidb-adapter 37 | 38 | ## Usage 39 | 40 | config/database.yml 41 | 42 | ```yml 43 | default: &default 44 | adapter: tidb 45 | encoding: utf8mb4 46 | collation: utf8mb4_general_ci 47 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 48 | host: 127.0.0.1 49 | port: 4000 50 | variables: 51 | tidb_enable_noop_functions: ON 52 | username: root 53 | password: 54 | 55 | development: 56 | <<: *default 57 | database: activerecord_tidb_adapter_demo_development 58 | 59 | ``` 60 | 61 | * demo repo with rails 6.1.4: https://github.com/hooopo/activerecord-tidb-adapter-demo 62 | 63 | ## TiDB features 64 | 65 | **[Sequence](https://docs.pingcap.com/tidb/stable/sql-statement-create-sequence)** 66 | 67 | Sequence as primary key 68 | 69 | ```ruby 70 | class TestSeq < ActiveRecord::Migration[6.1] 71 | def up 72 | # more options: increment, min_value, cycle, cache 73 | create_sequence :orders_seq, start: 1024 74 | create_table :orders, id: false do |t| 75 | t.primary_key :id, default: -> { "nextval(orders_seq)" } 76 | t.string :name 77 | end 78 | end 79 | 80 | def down 81 | drop_table :orders 82 | drop_sequence :orders_seq 83 | end 84 | end 85 | ``` 86 | 87 | This gem also adds a few helpers to interact with `SEQUENCE` 88 | 89 | ```ruby 90 | # Advance sequence and return new value 91 | ActiveRecord::Base.nextval("numbers") 92 | 93 | # Return value most recently obtained with nextval for specified sequence. 94 | ActiveRecord::Base.lastval("numbers") 95 | 96 | # Set sequence's current value 97 | ActiveRecord::Base.setval("numbers", 1234) 98 | ``` 99 | 100 | **[CTE](https://docs.pingcap.com/tidb/dev/sql-statement-with#with)** 101 | 102 | ```bash 103 | $ bundle add activerecord-cte 104 | 105 | ``` 106 | 107 | ```ruby 108 | require 'activerecord/cte' 109 | 110 | Post 111 | .with(posts_with_tags: "SELECT * FROM posts WHERE tags_count > 0") 112 | .from("posts_with_tags AS posts") 113 | # WITH posts_with_tags AS ( 114 | # SELECT * FROM posts WHERE (tags_count > 0) 115 | # ) 116 | # SELECT * FROM posts_with_tags AS posts 117 | 118 | Post 119 | .with(posts_with_tags: "SELECT * FROM posts WHERE tags_count > 0") 120 | .from("posts_with_tags AS posts") 121 | .count 122 | 123 | # WITH posts_with_tags AS ( 124 | # SELECT * FROM posts WHERE (tags_count > 0) 125 | # ) 126 | # SELECT COUNT(*) FROM posts_with_tags AS posts 127 | 128 | Post 129 | .with(posts_with_tags: Post.where("tags_count > 0")) 130 | .from("posts_with_tags AS posts") 131 | .count 132 | 133 | ``` 134 | 135 | 136 | ## Setting up local TiDB server 137 | 138 | Install [tiup](https://github.com/pingcap/tiup) 139 | 140 | ```shell 141 | $ curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh 142 | ``` 143 | Starting TiDB playground 144 | 145 | ```shell 146 | $ tiup playground nightly 147 | ``` 148 | 149 | ## Tutorials 150 | 151 | * [Build a Rails App with TiDB and the ActiveRecord TiDB Adapter](https://gist.github.com/hooopo/83db933ab07a054f70e23da0ff945747) 152 | 153 | ## Development 154 | 155 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 156 | 157 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). 158 | 159 | ## Testing 160 | 161 | install gems 162 | 163 | ``` 164 | bundle install 165 | ``` 166 | 167 | start tidb server 168 | 169 | ``` 170 | tiup playground nightly 171 | ``` 172 | 173 | create database for testing 174 | 175 | ``` 176 | MYSQL_USER=root MYSQL_HOST=127.0.0.1 MYSQL_PORT=4000 tidb=1 ARCONN=tidb bundle exec rake db:tidb:rebuild 177 | 178 | ``` 179 | 180 | run tidb adapter tests and activerecord buildin tests 181 | 182 | ``` 183 | MYSQL_USER=root MYSQL_HOST=127.0.0.1 MYSQL_PORT=4000 tidb=1 ARCONN=tidb bundle exec rake test:tidb 184 | 185 | ``` 186 | 187 | run **ONLY** tidb adapter tests using `ONLY_TEST_TIDB` env: 188 | 189 | ``` 190 | ONLY_TEST_TIDB=1 MYSQL_USER=root MYSQL_HOST=127.0.0.1 MYSQL_PORT=4000 tidb=1 ARCONN=tidb bundle exec rake test:tidb 191 | ``` 192 | 193 | ## Contributing 194 | 195 | Bug reports and pull requests are welcome on GitHub at https://github.com/pingcap/activerecord-tidb-adapter. 196 | 197 | ## License 198 | 199 | Apache 2.0 200 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rake/testtask' 5 | require 'pry' 6 | 7 | require_relative 'test/support/paths_tidb' 8 | require_relative 'test/support/rake_helpers' 9 | require_relative 'test/support/config' 10 | 11 | task test: ['test:tidb'] 12 | task default: [:test] 13 | 14 | namespace :test do 15 | Rake::TestTask.new('tidb') do |t| 16 | t.libs = ARTest::TiDB.test_load_paths 17 | t.test_files = test_files 18 | t.warning = !ENV['WARNING'].nil? 19 | t.verbose = false 20 | end 21 | 22 | task 'tidb:env' do 23 | ENV['ARCONN'] = 'tidb' 24 | ENV['tidb'] = '1' 25 | end 26 | end 27 | 28 | task 'test:tidb' => 'test:tidb:env' 29 | 30 | namespace :db do 31 | namespace :tidb do 32 | connection_arguments = lambda do |connection_name| 33 | config = ARTest.config['connections']['tidb'][connection_name] 34 | ["--user=#{config['username']}", "--password=#{config['password']}", "--port=#{config['port']}", 35 | ("--host=#{config['host']}" if config['host'])].join(' ') 36 | end 37 | 38 | desc 'Build the TiDB test databases' 39 | task :build do 40 | config = ARTest.config['connections']['tidb'] 41 | `mysql #{connection_arguments['arunit']} -e "create DATABASE #{config['arunit']['database']} DEFAULT CHARACTER SET utf8mb4 COLLATE #{config['arunit']['collation']}"` 42 | `mysql #{connection_arguments['arunit2']} -e "create DATABASE #{config['arunit2']['database']} DEFAULT CHARACTER SET utf8mb4 COLLATE #{config['arunit2']['collation']}"` 43 | end 44 | 45 | desc 'Drop the TiDB test databases' 46 | task :drop do 47 | config = ARTest.config['connections']['tidb'] 48 | `mysqladmin #{connection_arguments['arunit']} -f drop #{config['arunit']['database']}` 49 | `mysqladmin #{connection_arguments['arunit2']} -f drop #{config['arunit2']['database']}` 50 | end 51 | 52 | desc 'Rebuild the TiDB test databases' 53 | task rebuild: %i[drop build] 54 | end 55 | end 56 | 57 | task build_mysql_databases: 'db:mysql:build' 58 | -------------------------------------------------------------------------------- /activerecord-tidb-adapter.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'activerecord-tidb-adapter' 7 | spec.version = ActiveRecord::TIDB_ADAPTER_VERSION 8 | spec.authors = ['Hooopo Wang'] 9 | spec.email = ['hoooopo@gmail.com'] 10 | 11 | spec.summary = 'TiDB adapter for ActiveRecord.' 12 | spec.description = 'Allows the use of TiDB as a backend for ActiveRecord and Rails apps.' 13 | spec.homepage = 'https://github.com/pingcap/activerecord-tidb-adapter' 14 | spec.license = 'Apache-2.0' 15 | spec.required_ruby_version = '>= 2.7.0' 16 | 17 | # spec.metadata["allowed_push_host"] = "TODO: Set to 'https://mygemserver.com'" 18 | 19 | spec.metadata['homepage_uri'] = spec.homepage 20 | spec.metadata['source_code_uri'] = 'https://github.com/pingcap/activerecord-tidb-adapter' 21 | # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." 22 | 23 | # Specify which files should be added to the gem when it is released. 24 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 25 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 26 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) } 27 | end 28 | spec.bindir = 'exe' 29 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 30 | spec.require_paths = ['lib'] 31 | 32 | spec.add_dependency 'activerecord', '~> 7.0.0' 33 | spec.add_dependency 'mysql2' 34 | 35 | # Uncomment to register a new dependency of your gem 36 | # spec.add_dependency "example-gem", "~> 1.0" 37 | 38 | # For more information and examples about making a new gem, checkout our 39 | # guide at: https://bundler.io/guides/creating_gem.html 40 | end 41 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'activerecord/tidb/adapter' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require 'irb' 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | new_collations_enabled_on_first_bootstrap = true 2 | allow-expression-index = true 3 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/tidb/database_statements.rb: -------------------------------------------------------------------------------- 1 | require 'active_record/connection_adapters/abstract/database_statements' 2 | 3 | ActiveRecord::ConnectionAdapters::DatabaseStatements.class_eval do 4 | def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = []) 5 | sql, binds = to_sql_and_binds(arel, binds) 6 | value = exec_insert(sql, name, binds, pk, sequence_name) 7 | return id_value if id_value.present? 8 | return last_inserted_id(value) if arel.is_a?(String) 9 | model = arel.ast.relation.instance_variable_get(:@klass) 10 | pk_def = schema_cache.columns_hash(model.table_name)[pk] 11 | if pk_def&.default_function && pk_def.default_function =~ /nextval/ 12 | query_value("SELECT #{pk_def.default_function.sub('nextval', 'lastval')}") 13 | else 14 | last_inserted_id(value) 15 | end 16 | end 17 | alias create insert 18 | 19 | def transaction_isolation_levels 20 | { 21 | read_committed: 'READ COMMITTED', 22 | repeatable_read: 'REPEATABLE READ' 23 | } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/tidb/database_tasks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_record/base' 4 | 5 | module ActiveRecord 6 | module ConnectionAdapters 7 | module Tidb 8 | class DatabaseTasks < ActiveRecord::Tasks::MySQLDatabaseTasks 9 | end 10 | end 11 | end 12 | end 13 | 14 | ActiveRecord::Tasks::DatabaseTasks.register_task(/tidb/, ActiveRecord::ConnectionAdapters::Tidb::DatabaseTasks) 15 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/tidb/savepoints.rb: -------------------------------------------------------------------------------- 1 | require 'active_record/connection_adapters/abstract/savepoints' 2 | 3 | ActiveRecord::ConnectionAdapters::Savepoints.class_eval do 4 | 5 | def current_savepoint_name 6 | raise NotImplementedError, "Savepoint is not supported https://github.com/pingcap/tidb/issues/6840" 7 | end 8 | 9 | def create_savepoint(name = current_savepoint_name) 10 | raise NotImplementedError, "Savepoint is not supported https://github.com/pingcap/tidb/issues/6840" 11 | end 12 | 13 | def exec_rollback_to_savepoint(name = current_savepoint_name) 14 | raise NotImplementedError, "Savepoint is not supported https://github.com/pingcap/tidb/issues/6840" 15 | end 16 | 17 | def release_savepoint(name = current_savepoint_name) 18 | raise NotImplementedError, "Savepoint is not supported https://github.com/pingcap/tidb/issues/6840" 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/tidb/schema_statements.rb: -------------------------------------------------------------------------------- 1 | require 'active_record/connection_adapters/mysql/schema_statements' 2 | 3 | ActiveRecord::ConnectionAdapters::MySQL::SchemaStatements.class_eval do 4 | def indexes(table_name) 5 | indexes = [] 6 | current_index = nil 7 | execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result| 8 | each_hash(result) do |row| 9 | if current_index != row[:Key_name] 10 | next if row[:Key_name] == "PRIMARY" # skip the primary key 11 | current_index = row[:Key_name] 12 | 13 | mysql_index_type = row[:Index_type].downcase.to_sym 14 | case mysql_index_type 15 | when :fulltext, :spatial 16 | index_type = mysql_index_type 17 | when :btree, :hash 18 | index_using = mysql_index_type 19 | end 20 | 21 | indexes << [ 22 | row[:Table], 23 | row[:Key_name], 24 | row[:Non_unique].to_i == 0, 25 | [], 26 | lengths: {}, 27 | orders: {}, 28 | type: index_type, 29 | using: index_using, 30 | comment: row[:Index_comment].presence 31 | ] 32 | end 33 | 34 | # FIX https://github.com/pingcap/tidb/issues/26110 for older version of TiDB 35 | row[:Expression] = nil if row[:Expression] == 'NULL' 36 | 37 | if row[:Expression] 38 | expression = row[:Expression] 39 | expression = +"(#{expression})" unless expression.start_with?("(") 40 | indexes.last[-2] << expression 41 | indexes.last[-1][:expressions] ||= {} 42 | indexes.last[-1][:expressions][expression] = expression 43 | indexes.last[-1][:orders][expression] = :desc if row[:Collation] == "D" 44 | else 45 | indexes.last[-2] << row[:Column_name] 46 | indexes.last[-1][:lengths][row[:Column_name]] = row[:Sub_part].to_i if row[:Sub_part] 47 | indexes.last[-1][:orders][row[:Column_name]] = :desc if row[:Collation] == "D" 48 | end 49 | end 50 | end 51 | 52 | indexes.map do |index| 53 | options = index.pop 54 | 55 | if expressions = options.delete(:expressions) 56 | orders = options.delete(:orders) 57 | lengths = options.delete(:lengths) 58 | 59 | columns = index[-1].map { |name| 60 | [ name.to_sym, expressions[name] || +quote_column_name(name) ] 61 | }.to_h 62 | 63 | index[-1] = add_options_for_index_columns( 64 | columns, order: orders, length: lengths 65 | ).values.join(", ") 66 | end 67 | 68 | ActiveRecord::ConnectionAdapters::IndexDefinition.new(*index, **options) 69 | end 70 | end 71 | end -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/tidb/setup.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module ConnectionAdapters 5 | module Tidb 6 | def self.initial_setup 7 | ::ActiveRecord::SchemaDumper.ignore_tables |= %w[] 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/tidb/type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module Type 5 | class << self 6 | def adapter_name_from(_model) 7 | :mysql 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/tidb_adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_record/connection_adapters' 4 | require 'active_record/connection_adapters/mysql2_adapter' 5 | require 'active_record/connection_adapters/tidb/setup' 6 | require_relative '../../version' 7 | require_relative '../sequence' 8 | require_relative 'tidb/schema_statements' 9 | require_relative 'tidb/database_statements' 10 | require_relative 'tidb/savepoints' 11 | 12 | ActiveRecord::ConnectionAdapters::Tidb.initial_setup 13 | 14 | module ActiveRecord 15 | module ConnectionHandling # :nodoc: 16 | # Establishes a connection to the database that's used by all Active Record objects. 17 | def tidb_connection(config) # :nodoc: 18 | config = config.symbolize_keys 19 | config[:flags] ||= 0 20 | 21 | if config[:flags].is_a? Array 22 | config[:flags].push 'FOUND_ROWS' 23 | else 24 | config[:flags] |= Mysql2::Client::FOUND_ROWS 25 | end 26 | 27 | ConnectionAdapters::TidbAdapter.new( 28 | ConnectionAdapters::Mysql2Adapter.new_client(config), 29 | logger, 30 | nil, 31 | config 32 | ) 33 | end 34 | end 35 | 36 | module ConnectionAdapters 37 | class TidbAdapter < Mysql2Adapter 38 | include ActiveRecord::Sequence::Adapter 39 | ADAPTER_NAME = 'Tidb' 40 | 41 | def supports_savepoints? 42 | # https://github.com/pingcap/tidb/issues/6840 support is required 43 | false 44 | end 45 | 46 | def supports_foreign_keys? 47 | # https://github.com/pingcap/tidb/issues/18209 support is required 48 | false 49 | end 50 | 51 | def supports_bulk_alter? 52 | # https://github.com/pingcap/tidb/issues/14766 support is required 53 | false 54 | end 55 | 56 | def supports_advisory_locks? 57 | # https://github.com/pingcap/tidb/issues/14994 support is required 58 | false 59 | end 60 | 61 | def supports_optimizer_hints? 62 | true 63 | end 64 | 65 | def supports_json? 66 | true 67 | end 68 | 69 | def supports_index_sort_order? 70 | # https://github.com/pingcap/tidb/issues/2519 support is required 71 | false 72 | end 73 | 74 | def supports_expression_index? 75 | sql = <<~SQL 76 | SELECT VALUE 77 | FROM INFORMATION_SCHEMA.CLUSTER_CONFIG 78 | WHERE `KEY` = 'experimental.allow-expression-index' AND `TYPE` = 'tidb' 79 | SQL 80 | query_value(sql) == 'true' 81 | end 82 | 83 | def supports_common_table_expressions? 84 | tidb_version >= '5.1.0' 85 | end 86 | 87 | def initialize(connection, logger, conn_params, config) 88 | super(connection, logger, conn_params, config) 89 | 90 | tidb_version_string = query_value('select version()') 91 | @tidb_version = tidb_version_string[/TiDB-v([0-9.]+)/, 1] 92 | end 93 | 94 | def tidb_version_string 95 | @tidb_version 96 | end 97 | 98 | def tidb_version 99 | Version.new(tidb_version_string) 100 | end 101 | 102 | def self.database_exists?(config) 103 | !ActiveRecord::Base.tidb_connection(config).nil? 104 | rescue ActiveRecord::NoDatabaseError 105 | false 106 | end 107 | 108 | def new_column_from_field(_table_name, field) 109 | type_metadata = fetch_type_metadata(field[:Type], field[:Extra]) 110 | default = field[:Default] 111 | default_function = nil 112 | 113 | if type_metadata.type == :datetime && /\ACURRENT_TIMESTAMP(?:\([0-6]?\))?\z/i.match?(default) 114 | default = "#{default} ON UPDATE #{default}" if /on update CURRENT_TIMESTAMP/i.match?(field[:Extra]) 115 | default_function = default 116 | default = nil 117 | elsif type_metadata.extra == 'DEFAULT_GENERATED' 118 | default = +"(#{default})" unless default.start_with?('(') 119 | default_function = default 120 | default = nil 121 | elsif default.to_s =~ /nextval/i 122 | default_function = default 123 | default = nil 124 | end 125 | 126 | MySQL::Column.new( 127 | field[:Field], 128 | default, 129 | type_metadata, 130 | field[:Null] == 'YES', 131 | default_function, 132 | collation: field[:Collation], 133 | comment: field[:Comment].presence 134 | ) 135 | end 136 | end 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /lib/active_record/sequence.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/all' 4 | require 'active_record' 5 | require 'active_record/connection_adapters/mysql/schema_dumper' 6 | require 'active_record/migration/command_recorder' 7 | require 'active_record/schema_dumper' 8 | 9 | module ActiveRecord 10 | module Sequence 11 | require 'active_record/sequence/command_recorder' 12 | require 'active_record/sequence/adapter' 13 | require 'active_record/sequence/schema_dumper' 14 | require 'active_record/sequence/model_methods' 15 | end 16 | end 17 | 18 | ActiveRecord::Migration::CommandRecorder.include(ActiveRecord::Sequence::CommandRecorder) 19 | 20 | ActiveRecord::SchemaDumper.prepend(ActiveRecord::Sequence::SchemaDumper) 21 | ActiveRecord::Base.extend(ActiveRecord::Sequence::ModelMethods) 22 | -------------------------------------------------------------------------------- /lib/active_record/sequence/adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module Sequence 5 | module Adapter 6 | def check_sequences 7 | select_all( 8 | 'SELECT * FROM information_schema.sequences ORDER BY sequence_name' 9 | ).to_a 10 | end 11 | 12 | def create_sequence(name, options = {}) 13 | increment = options[:increment] || options[:step] 14 | name = quote_column_name(name) 15 | 16 | sql = ["CREATE SEQUENCE IF NOT EXISTS #{name}"] 17 | sql << "INCREMENT BY #{increment}" if increment 18 | sql << "START WITH #{options[:start]}" if options[:start] 19 | sql << if options[:cache] 20 | "CACHE #{options[:cache]}" 21 | else 22 | 'NOCACHE' 23 | end 24 | 25 | sql << if options[:cycle] 26 | 'CYCLE' 27 | else 28 | 'NOCYCLE' 29 | end 30 | 31 | sql << "MIN_VALUE #{options[:min_value]}" if options[:min_value] 32 | 33 | sql << "COMMENT #{quote(options[:comment].to_s)}" if options[:comment] 34 | 35 | execute(sql.join("\n")) 36 | end 37 | 38 | def drop_sequence(name) 39 | name = quote_column_name(name) 40 | sql = "DROP SEQUENCE IF EXISTS #{name}" 41 | execute(sql) 42 | end 43 | 44 | def recreate_sequence(name, options = {}) 45 | drop_sequence(name) 46 | create_sequence(name, options) 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/active_record/sequence/command_recorder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module Sequence 5 | module CommandRecorder 6 | def create_sequence(name, options = {}) 7 | record(__method__, [name, options]) 8 | end 9 | 10 | def drop_sequence(name) 11 | record(__method__, [name]) 12 | end 13 | 14 | def invert_create_sequence(args) 15 | name, = args 16 | [:drop_sequence, [name]] 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/active_record/sequence/model_methods.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module Sequence 5 | module ModelMethods 6 | def nextval(name) 7 | name = connection.quote_column_name(name) 8 | connection.query_value("SELECT nextval(#{name})") 9 | end 10 | 11 | def lastval(name) 12 | name = connection.quote_column_name(name) 13 | connection.query_value("SELECT lastval(#{name})") 14 | end 15 | 16 | def setval(name, value) 17 | name = connection.quote_column_name(name) 18 | connection.query_value("SELECT setval(#{name}, #{value})") 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/active_record/sequence/schema_dumper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module Sequence 5 | module SchemaDumper 6 | def header(stream) 7 | super 8 | sequences(stream) 9 | end 10 | 11 | def sequences(stream) 12 | sequences = @connection.check_sequences 13 | return if sequences.empty? 14 | 15 | sequences.each do |seq| 16 | start_value = seq['START'] 17 | increment = seq['INCREMENT'] 18 | cache = seq['CACHE'] 19 | cache_value = seq['CACHE_VALUE'] 20 | min_value = seq['MIN_VALUE'] 21 | cycle = seq['CYCLE'] 22 | comment = seq['COMMENT'] 23 | 24 | options = [] 25 | 26 | options << "start: #{start_value}" if start_value && Integer(start_value) != 1 27 | 28 | options << "increment: #{increment}" if increment && Integer(increment) != 1 29 | 30 | options << "cache: #{cache_value}" if cache_value && Integer(cache_value) != 0 31 | 32 | options << "min_value: #{min_value}" if min_value 33 | 34 | options << 'cycle: true' if cycle.to_i != 0 35 | 36 | options << "comment: #{comment.inspect}" if comment.present? 37 | 38 | statement = [ 39 | 'create_sequence', 40 | seq['SEQUENCE_NAME'].inspect 41 | ].join(' ') 42 | 43 | if options.any? 44 | statement << (options.any? ? ", #{options.join(', ')}" : '') 45 | end 46 | 47 | stream.puts " #{statement}" 48 | end 49 | 50 | stream.puts 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/activerecord-tidb-adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if defined?(Rails) 4 | module ActiveRecord 5 | module ConnectionAdapters 6 | class TidbRailtie < ::Rails::Railtie 7 | rake_tasks do 8 | load 'active_record/connection_adapters/tidb/database_tasks.rb' 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | TIDB_ADAPTER_VERSION = '7.0.0' 5 | end 6 | -------------------------------------------------------------------------------- /test/activerecord_tidb_adapter_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class ActiveRecordTidbAdapterTest < Minitest::Test 6 | def test_that_it_has_a_version_number 7 | refute_nil ::ActiveRecord::TIDB_ADAPTER_VERSION 8 | end 9 | 10 | def test_it_does_something_useful 11 | assert true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/cases/helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "config" 4 | 5 | require "stringio" 6 | 7 | require "active_record" 8 | require "cases/test_case" 9 | require "active_support/dependencies" 10 | require "active_support/logger" 11 | require "active_support/core_ext/kernel/singleton_class" 12 | 13 | require "support/config" 14 | require "support/connection" 15 | require 'minitest/excludes' 16 | 17 | # TODO: Move all these random hacks into the ARTest namespace and into the support/ dir 18 | 19 | Thread.abort_on_exception = true 20 | 21 | # Show backtraces for deprecated behavior for quicker cleanup. 22 | ActiveSupport::Deprecation.debug = true 23 | 24 | # Disable available locale checks to avoid warnings running the test suite. 25 | I18n.enforce_available_locales = false 26 | 27 | # Connect to the database 28 | ARTest.connect 29 | 30 | # Quote "type" if it's a reserved word for the current connection. 31 | QUOTED_TYPE = ActiveRecord::Base.connection.quote_column_name("type") 32 | 33 | def current_adapter?(*types) 34 | types.any? do |type| 35 | ActiveRecord::ConnectionAdapters.const_defined?(type) && 36 | ActiveRecord::Base.connection.is_a?(ActiveRecord::ConnectionAdapters.const_get(type)) 37 | end 38 | end 39 | 40 | def in_memory_db? 41 | current_adapter?(:SQLite3Adapter) && 42 | ActiveRecord::Base.connection_pool.db_config.database == ":memory:" 43 | end 44 | 45 | def mysql_enforcing_gtid_consistency? 46 | current_adapter?(:Mysql2Adapter) && "ON" == ActiveRecord::Base.connection.show_variable("enforce_gtid_consistency") 47 | end 48 | 49 | def supports_default_expression? 50 | if current_adapter?(:PostgreSQLAdapter) 51 | true 52 | elsif current_adapter?(:Mysql2Adapter) 53 | conn = ActiveRecord::Base.connection 54 | !conn.mariadb? && conn.database_version >= "8.0.13" 55 | end 56 | end 57 | 58 | def supports_non_unique_constraint_name? 59 | if current_adapter?(:Mysql2Adapter) 60 | conn = ActiveRecord::Base.connection 61 | conn.mariadb? 62 | else 63 | false 64 | end 65 | end 66 | 67 | def supports_text_column_with_default? 68 | if current_adapter?(:Mysql2Adapter) 69 | conn = ActiveRecord::Base.connection 70 | conn.mariadb? && conn.database_version >= "10.2.1" 71 | else 72 | true 73 | end 74 | end 75 | 76 | %w[ 77 | supports_savepoints? 78 | supports_partial_index? 79 | supports_partitioned_indexes? 80 | supports_expression_index? 81 | supports_insert_returning? 82 | supports_insert_on_duplicate_skip? 83 | supports_insert_on_duplicate_update? 84 | supports_insert_conflict_target? 85 | supports_optimizer_hints? 86 | supports_datetime_with_precision? 87 | ].each do |method_name| 88 | define_method method_name do 89 | ActiveRecord::Base.connection.public_send(method_name) 90 | end 91 | end 92 | 93 | def with_env_tz(new_tz = "US/Eastern") 94 | old_tz, ENV["TZ"] = ENV["TZ"], new_tz 95 | yield 96 | ensure 97 | old_tz ? ENV["TZ"] = old_tz : ENV.delete("TZ") 98 | end 99 | 100 | def with_timezone_config(cfg) 101 | verify_default_timezone_config 102 | 103 | old_default_zone = ActiveRecord.default_timezone 104 | old_awareness = ActiveRecord::Base.time_zone_aware_attributes 105 | old_zone = Time.zone 106 | 107 | if cfg.has_key?(:default) 108 | ActiveRecord.default_timezone = cfg[:default] 109 | end 110 | if cfg.has_key?(:aware_attributes) 111 | ActiveRecord::Base.time_zone_aware_attributes = cfg[:aware_attributes] 112 | end 113 | if cfg.has_key?(:zone) 114 | Time.zone = cfg[:zone] 115 | end 116 | yield 117 | ensure 118 | ActiveRecord.default_timezone = old_default_zone 119 | ActiveRecord::Base.time_zone_aware_attributes = old_awareness 120 | Time.zone = old_zone 121 | end 122 | 123 | # This method makes sure that tests don't leak global state related to time zones. 124 | EXPECTED_ZONE = nil 125 | EXPECTED_DEFAULT_TIMEZONE = :utc 126 | EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES = false 127 | def verify_default_timezone_config 128 | if Time.zone != EXPECTED_ZONE 129 | $stderr.puts <<-MSG 130 | \n#{self} 131 | Global state `Time.zone` was leaked. 132 | Expected: #{EXPECTED_ZONE} 133 | Got: #{Time.zone} 134 | MSG 135 | end 136 | if ActiveRecord.default_timezone != EXPECTED_DEFAULT_TIMEZONE 137 | $stderr.puts <<-MSG 138 | \n#{self} 139 | Global state `ActiveRecord.default_timezone` was leaked. 140 | Expected: #{EXPECTED_DEFAULT_TIMEZONE} 141 | Got: #{ActiveRecord.default_timezone} 142 | MSG 143 | end 144 | if ActiveRecord::Base.time_zone_aware_attributes != EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES 145 | $stderr.puts <<-MSG 146 | \n#{self} 147 | Global state `ActiveRecord::Base.time_zone_aware_attributes` was leaked. 148 | Expected: #{EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES} 149 | Got: #{ActiveRecord::Base.time_zone_aware_attributes} 150 | MSG 151 | end 152 | end 153 | 154 | def enable_extension!(extension, connection) 155 | return false unless connection.supports_extensions? 156 | return connection.reconnect! if connection.extension_enabled?(extension) 157 | 158 | connection.enable_extension extension 159 | connection.commit_db_transaction if connection.transaction_open? 160 | connection.reconnect! 161 | end 162 | 163 | def disable_extension!(extension, connection) 164 | return false unless connection.supports_extensions? 165 | return true unless connection.extension_enabled?(extension) 166 | 167 | connection.disable_extension extension 168 | connection.reconnect! 169 | end 170 | 171 | def clean_up_legacy_connection_handlers 172 | handler = ActiveRecord::Base.default_connection_handler 173 | assert_deprecated do 174 | ActiveRecord::Base.connection_handlers = {} 175 | end 176 | 177 | handler.connection_pool_names.each do |name| 178 | next if ["ActiveRecord::Base", "ARUnit2Model", "Contact", "ContactSti", "FirstAbstractClass", "SecondAbstractClass"].include?(name) 179 | 180 | handler.send(:owner_to_pool_manager).delete(name) 181 | end 182 | end 183 | 184 | def clean_up_connection_handler 185 | handler = ActiveRecord::Base.connection_handler 186 | handler.instance_variable_get(:@owner_to_pool_manager).each do |owner, pool_manager| 187 | pool_manager.role_names.each do |role_name| 188 | next if role_name == ActiveRecord::Base.default_role 189 | pool_manager.remove_role(role_name) 190 | end 191 | end 192 | end 193 | 194 | def load_schema 195 | # silence verbose schema loading 196 | original_stdout = $stdout 197 | $stdout = StringIO.new 198 | 199 | adapter_name = ActiveRecord::Base.connection.adapter_name.downcase 200 | p adapter_name 201 | adapter_specific_schema_file = SCHEMA_ROOT + "/#{adapter_name}_specific_schema.rb" 202 | 203 | load "#{SCHEMA_ROOT}/schema.rb" 204 | load "#{SCHEMA_ROOT}/mysql2_specific_schema.rb" 205 | 206 | load adapter_specific_schema_file if File.exist?(adapter_specific_schema_file) 207 | 208 | ActiveRecord::FixtureSet.reset_cache 209 | ensure 210 | $stdout = original_stdout 211 | end 212 | 213 | load_schema 214 | 215 | class SQLSubscriber 216 | attr_reader :logged 217 | attr_reader :payloads 218 | 219 | def initialize 220 | @logged = [] 221 | @payloads = [] 222 | end 223 | 224 | def start(name, id, payload) 225 | @payloads << payload 226 | @logged << [payload[:sql].squish, payload[:name], payload[:binds]] 227 | end 228 | 229 | def finish(name, id, payload); end 230 | end 231 | 232 | module InTimeZone 233 | private 234 | def in_time_zone(zone) 235 | old_zone = Time.zone 236 | old_tz = ActiveRecord::Base.time_zone_aware_attributes 237 | 238 | Time.zone = zone ? ActiveSupport::TimeZone[zone] : nil 239 | ActiveRecord::Base.time_zone_aware_attributes = !zone.nil? 240 | yield 241 | ensure 242 | Time.zone = old_zone 243 | ActiveRecord::Base.time_zone_aware_attributes = old_tz 244 | end 245 | end 246 | 247 | # Encryption 248 | 249 | ActiveRecord::Encryption.configure \ 250 | primary_key: "test master key", 251 | deterministic_key: "test deterministic key", 252 | key_derivation_salt: "testing key derivation salt" 253 | 254 | ActiveRecord::Encryption::ExtendedDeterministicQueries.install_support 255 | ActiveRecord::Encryption::ExtendedDeterministicUniquenessValidator.install_support -------------------------------------------------------------------------------- /test/cases/helper_tidb.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | Bundler.require :development 5 | 6 | # Turn on debugging for the test environment 7 | ENV['DEBUG_TIDB_ADAPTER'] = '1' 8 | 9 | # Load ActiveRecord test helper 10 | require 'cases/helper' 11 | require 'minitest/excludes' 12 | 13 | def load_tidb_specific_schema 14 | # silence verbose schema loading 15 | original_stdout = $stdout 16 | $stdout = StringIO.new 17 | 18 | load 'schema/tidb_specific_schema.rb' 19 | 20 | ActiveRecord::FixtureSet.reset_cache 21 | ensure 22 | $stdout = original_stdout 23 | end 24 | 25 | load_tidb_specific_schema 26 | -------------------------------------------------------------------------------- /test/cases/sequence_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'cases/helper_tidb' 4 | require 'models/tidb_order' 5 | require 'pry' 6 | 7 | module TiDB 8 | class SequenceTest < ActiveRecord::TestCase 9 | self.use_transactional_tests = false 10 | 11 | def setup 12 | @connection = ActiveRecord::Base.connection 13 | end 14 | 15 | def tearndown 16 | ActiveRecord::Base.connection.recreate_sequence(:orders_seq, start: 1000) 17 | end 18 | 19 | def test_sequence_as_pk 20 | @order = TidbOrder.create!(name: 'name') 21 | assert_equal @order.id, 1000 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/config.example.yml: -------------------------------------------------------------------------------- 1 | connections: 2 | tidb: 3 | arunit: 4 | username: <%= ENV["MYSQL_USER"] || 'root' %> 5 | port: <%= ENV["MYSQL_PORT"] || 4000 %> 6 | encoding: utf8mb4 7 | collation: utf8mb4_unicode_ci 8 | <% if ENV['MYSQL_PREPARED_STATEMENTS'] %> 9 | prepared_statements: true 10 | <% end %> 11 | <% if ENV['MYSQL_HOST'] %> 12 | host: <%= ENV['MYSQL_HOST'] || '127.0.0.1' %> 13 | <% end %> 14 | <% if ENV['tidb'] %> 15 | variables: 16 | tidb_enable_noop_functions: ON 17 | tidb_skip_isolation_level_check: 1 18 | <% end %> 19 | arunit2: 20 | username: <%= ENV["MYSQL_USER"] || 'root' %> 21 | port: <%= ENV["MYSQL_PORT"] || 4000 %> 22 | encoding: utf8mb4 23 | collation: utf8mb4_general_ci 24 | <% if ENV['MYSQL_PREPARED_STATEMENTS'] %> 25 | prepared_statements: true 26 | <% end %> 27 | <% if ENV['MYSQL_HOST'] %> 28 | host: <%= ENV['MYSQL_HOST'] || '127.0.0.1' %> 29 | <% end %> 30 | <% if ENV['tidb'] %> 31 | variables: 32 | tidb_enable_noop_functions: ON 33 | tidb_skip_isolation_level_check: 1 34 | <% end %> -------------------------------------------------------------------------------- /test/excludes/ActiveRecord/Encryption/ContextsTest.rb: -------------------------------------------------------------------------------- 1 | exclude "test_.protecting_encrypted_data_allows_db-queries_on_deterministic_attributes".to_sym, 'skip' -------------------------------------------------------------------------------- /test/excludes/ActiveRecord/Encryption/StoragePerformanceTest.rb: -------------------------------------------------------------------------------- 1 | exclude :test_storage_overload_storing_keys_is_acceptable_for_DerivedSecretKeyProvider, 'skip' -------------------------------------------------------------------------------- /test/excludes/ActiveRecord/Encryption/UniquenessValidationsTest.rb: -------------------------------------------------------------------------------- 1 | exclude :test_uniqueness_validations_work_when_using_old_encryption_schemes, 'skip' 2 | exclude :test_uniqueness_validations_work, 'skip' 3 | exclude :test_uniqueness_validations_work_when_mixing_encrypted_an_unencrypted_data, 'skip' 4 | -------------------------------------------------------------------------------- /test/excludes/ActiveRecord/Migration/ColumnsTest.rb: -------------------------------------------------------------------------------- 1 | exclude :test_change_column, 'skip' -------------------------------------------------------------------------------- /test/excludes/ActiveRecord/StatementCacheTest.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | exclude :test_find_does_not_use_statement_cache_if_table_name_is_changed, 'skip' 4 | -------------------------------------------------------------------------------- /test/excludes/BasicsTest.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | exclude :test_column_names_are_escaped, 'skip' 4 | -------------------------------------------------------------------------------- /test/excludes/CalculationsTest.rb: -------------------------------------------------------------------------------- 1 | exclude :test_pluck_auto_table_name_prefix, 'skip' -------------------------------------------------------------------------------- /test/excludes/CommentTest.rb: -------------------------------------------------------------------------------- 1 | exclude :test_remove_comment_from_column, 'skip' 2 | exclude :test_schema_dump_with_comments, 'skip' -------------------------------------------------------------------------------- /test/excludes/LegacyPrimaryKeyTest/V4_2.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | exclude :test_legacy_bigint_primary_key_should_be_auto_incremented, 'skip' 4 | -------------------------------------------------------------------------------- /test/excludes/LegacyPrimaryKeyTest/V5_0.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | exclude :test_legacy_bigint_primary_key_should_be_auto_incremented, 'skip' 4 | -------------------------------------------------------------------------------- /test/excludes/LogSubscriberTest.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | exclude :test_vebose_query_logs, 'skip' 4 | exclude :test_verbose_query_logs, 'skip' -------------------------------------------------------------------------------- /test/excludes/MarshalSerializationTest.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | exclude :test_deserializing_rails_6_0_marshal_basic, 'skip' 4 | exclude :test_deserializing_rails_6_0_marshal_with_loaded_association_cache, 'skip' 5 | exclude :test_deserializing_rails_6_1_marshal_basic, 'skip' 6 | exclude :test_deserializing_rails_6_1_marshal_with_loaded_association_cache, 'skip' -------------------------------------------------------------------------------- /test/excludes/Mysql2DefaultEngineOptionTest.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | exclude 'test_legacy_migrations_contain_default_ENGINE=InnoDB_option'.to_sym, 'skip' 4 | -------------------------------------------------------------------------------- /test/excludes/Mysql2OptimizerHintsTest.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | exclude :test_optimizer_hints, 'skip' 4 | exclude :test_optimizer_hints_is_sanitized, 'skip' 5 | -------------------------------------------------------------------------------- /test/excludes/ReservedWordTest.rb: -------------------------------------------------------------------------------- 1 | exclude :test_change_columns, 'skip' -------------------------------------------------------------------------------- /test/excludes/SchemaDumperTest.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | exclude :test_schema_dump_expression_indices, 'skip' 4 | -------------------------------------------------------------------------------- /test/models/tidb_order.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class TidbOrder < ActiveRecord::Base 4 | end 5 | -------------------------------------------------------------------------------- /test/schema/tidb_specific_schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ActiveRecord::Schema.define do 4 | create_table :tidb_table, force: true do |t| 5 | t.string :name 6 | end 7 | 8 | recreate_sequence :orders_seq, start: 1000 9 | create_table :tidb_orders, force: true, id: false do |t| 10 | t.primary_key :id, null: false, default: -> { 'nextval(orders_seq)' } 11 | t.string :name 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/support/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'fileutils' 4 | require 'pathname' 5 | require 'active_support/configuration_file' 6 | require_relative 'paths_tidb' 7 | 8 | module ARTest 9 | class << self 10 | def config 11 | @config ||= read_config 12 | end 13 | 14 | private 15 | 16 | def config_file 17 | Pathname.new(ENV['ARCONFIG'] || "#{ARTest::TiDB.test_root_tidb}/config.yml") 18 | end 19 | 20 | def read_config 21 | FileUtils.cp "#{ARTest::TiDB.test_root_tidb}/config.example.yml", config_file unless config_file.exist? 22 | 23 | expand_config ActiveSupport::ConfigurationFile.parse(config_file) 24 | end 25 | 26 | def expand_config(config) 27 | config['connections'].each do |adapter, connection| 28 | dbs = [%w[arunit activerecord_unittest], %w[arunit2 activerecord_unittest2], 29 | %w[arunit_without_prepared_statements activerecord_unittest]] 30 | dbs.each do |name, dbname| 31 | connection[name] = { 'database' => connection[name] } unless connection[name].is_a?(Hash) 32 | 33 | connection[name]['database'] ||= dbname 34 | connection[name]['adapter'] ||= adapter 35 | end 36 | end 37 | 38 | config 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/support/paths_tidb.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ARTest 4 | module TiDB 5 | module_function 6 | 7 | def root_tidb 8 | File.expand_path File.join(File.dirname(__FILE__), '..', '..') 9 | end 10 | 11 | def test_root_tidb 12 | File.join root_tidb, 'test' 13 | end 14 | 15 | def root_activerecord 16 | File.join Gem.loaded_specs['rails'].full_gem_path, 'activerecord' 17 | end 18 | 19 | def root_activerecord_lib 20 | File.join root_activerecord, 'lib' 21 | end 22 | 23 | def root_activerecord_test 24 | File.join root_activerecord, 'test' 25 | end 26 | 27 | def test_load_paths 28 | ['lib', 'test', root_activerecord_lib, root_activerecord_test] 29 | end 30 | 31 | def add_to_load_paths! 32 | test_load_paths.each { |p| $LOAD_PATH.unshift(p) unless $LOAD_PATH.include?(p) } 33 | end 34 | 35 | def arconfig_file 36 | File.join test_root_tidb, 'config.yml' 37 | end 38 | 39 | def arconfig_file_env! 40 | ENV['ARCONFIG'] = arconfig_file 41 | end 42 | end 43 | end 44 | 45 | ARTest::TiDB.add_to_load_paths! 46 | ARTest::TiDB.arconfig_file_env! 47 | -------------------------------------------------------------------------------- /test/support/rake_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | TIDB_TEST_HELPER = 'test/cases/helper_tidb.rb' 4 | 5 | def test_files 6 | env_activerecord_test_files || 7 | env_tidb_test_files || 8 | only_activerecord_test_files || 9 | only_tidb_test_files || 10 | all_test_files 11 | end 12 | 13 | def env_activerecord_test_files 14 | return unless ENV['TEST_FILES_AR'] && !ENV['TEST_FILES_AR'].empty? 15 | 16 | @env_ar_test_files ||= ENV['TEST_FILES_AR'] 17 | .split(',') 18 | .map { |file| File.join ARTest::TiDB.root_activerecord, file.strip } 19 | .sort 20 | .prepend(TIDB_TEST_HELPER) 21 | end 22 | 23 | def env_tidb_test_files 24 | return unless ENV['TEST_FILES'] && !ENV['TEST_FILES'].empty? 25 | 26 | @env_test_files ||= ENV['TEST_FILES'].split(',').map(&:strip) 27 | end 28 | 29 | def only_activerecord_test_files 30 | return unless ENV['ONLY_TEST_AR'] 31 | 32 | activerecord_test_files 33 | end 34 | 35 | def only_tidb_test_files 36 | return unless ENV['ONLY_TEST_TIDB'] 37 | 38 | tidb_test_files 39 | end 40 | 41 | def all_test_files 42 | activerecord_test_files + tidb_test_files 43 | end 44 | 45 | def activerecord_test_files 46 | Dir 47 | .glob("#{ARTest::TiDB.root_activerecord}/test/cases/**/*_test.rb") 48 | .reject { |x| x =~ %r{/adapters/postgresql/} } 49 | .reject { |x| x =~ %r{/adapters/sqlite3/} } 50 | .sort 51 | .prepend(TIDB_TEST_HELPER) 52 | end 53 | 54 | def tidb_test_files 55 | Dir.glob('test/cases/**/*_test.rb') 56 | end 57 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path('../lib', __dir__) 4 | require 'activerecord-tidb-adapter' 5 | 6 | require 'minitest/autorun' 7 | -------------------------------------------------------------------------------- /testing.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | bundle config set --local path '/tmp/buildkite-cache' 6 | 7 | echo "Setup gem mirror" 8 | bundle config mirror.https://rubygems.org https://gems.ruby-china.com 9 | 10 | echo "Bundle install" 11 | bundle install 12 | 13 | echo "Setup database for testing" 14 | tidb=1 ARCONN=tidb bundle exec rake db:tidb:rebuild && tidb=1 ARCONN=tidb bundle exec rake test:tidb 15 | --------------------------------------------------------------------------------