ridgepole-ext-tidb 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b369c3c9f3078db98f28341fe98396425d7f6b995d9bc931b8d48717c2a52335
4
- data.tar.gz: 78bf0c2a62ea2c22618bfb38a5477c6fb674531e765cbb6f2d835b0f013bb68c
3
+ metadata.gz: 35422c4801686a578e9871993e7ea61443110ef8ce39b2ba31510984824c2559
4
+ data.tar.gz: 4347c14f4e1990409c665ee39742111f68d214e3ad11fab549222564c5adcfec
5
5
  SHA512:
6
- metadata.gz: ee91cd5d10bb5d601499587ca3d88ecf5ff04b7eac44cecbccb6eeaed69670fe603c4ac5880958b8d636cc92a9ff09dd1f29ea6c9ebebe3fa817aa13f4cb2134
7
- data.tar.gz: f3956f87de294466ca03cf1b34247afbafdbbbaf8cfe6f7d5081bee0cb12cf6c8670ca7098aabc06bc23829ea8d8af41bdbc92b3fd9da2df8f682817fac8af2a
6
+ metadata.gz: 50330dcb777c17cc030d4569d03988b8343f50a4dbf1537bd21f8203a085cca3b681ff02d7b3ca0ff8158bb1ffb56ba41a1fd1b6fe02c939073b085de1c2bbcb
7
+ data.tar.gz: 8cf011251797a1d67bad7476dc1c374f00b630306314400dcdac5198d81a82374d41ad00b811e048106f1616eb10ac2daea163380f7ea51651bd95c0a70b6583
data/README.md CHANGED
@@ -1,19 +1,20 @@
1
1
  # Ridgepole::Ext::Tidb
2
2
 
3
3
  ![Tests](https://github.com/forgxisto/ridgepole-ext-tidb/actions/workflows/test.yml/badge.svg)
4
- ![Ruby Version](https://img.shields.io/badge/ruby-3.1%2B-red)
4
+ ![Ruby Version](https://img.shields.io/badge/ruby-3.2%2B-red)
5
5
  ![TiDB Compatibility](https://img.shields.io/badge/TiDB-v7.5.0%2B-blue)
6
6
 
7
7
  TiDBの`AUTO_RANDOM`カラム属性をサポートするRidgepole拡張機能です。この拡張により、TiDBの分散IDジェネレーション機能をSchemafile管理に統合できます。
8
8
 
9
9
  ## 主な機能
10
10
 
11
- - **AUTO_RANDOM検出**: TiDBのAUTO_RANDOMカラムを自動検出
12
- - **TiDB判定**: データベースがTiDBかどうかを自動判定
13
- - **MySQL互換**: mysql2とtrilogyアダプター両方に対応
14
- - **スキーマダンプ対応**: AUTO_RANDOM属性をRidgefileに正確に出力
15
- - **冪等性保証**: スキーマ適用の際の差分を正確に計算
16
- - **Ruby 3.1+ 対応**: 最新のRubyバージョンに完全対応
11
+ - **AUTO_RANDOM適用**: CREATE時に`AUTO_RANDOM(n)`を列定義へ付与し、`AUTO_INCREMENT`を抑止
12
+ - **AUTO_RANDOM_BASE**: テーブルオプションに`AUTO_RANDOM_BASE=<n>`を付与
13
+ - **スキーマダンプ対応**: `create_table`のオプションへ`auto_random:`/`auto_random_base:`を出力(往復一致)
14
+ - **冪等性**: apply→export→diff→applyの繰り返しでも差分ゼロを維持(ALTERは不使用)
15
+ - **TiDB判定**: 接続先がTiDBかどうかを自動判定
16
+ - **MySQL互換**: mysql2 / trilogy アダプターの両方に対応
17
+ - **Ruby 3.2+ 対応**
17
18
 
18
19
  ## インストール
19
20
 
@@ -41,10 +42,7 @@ $ gem install ridgepole-ext-tidb
41
42
 
42
43
  ```ruby
43
44
  require 'ridgepole'
44
- require 'ridgepole-ext-tidb'
45
-
46
- # TiDB拡張をセットアップ(ActiveRecord読み込み後)
47
- Ridgepole::Ext::Tidb.setup!
45
+ require 'ridgepole/ext_tidb'
48
46
 
49
47
  # Ridgepoleクライアントを設定
50
48
  client = Ridgepole::Client.new({
@@ -61,22 +59,25 @@ client = Ridgepole::Client.new({
61
59
 
62
60
  ```ruby
63
61
  # Schemafile
64
- create_table "users", id: { type: :bigint, auto_random: true } do |t|
62
+ require "ridgepole/ext_tidb"
63
+
64
+ # 1) テーブルレベルでAUTO_RANDOMとAUTO_RANDOM_BASEを指定
65
+ create_table "users",
66
+ id: :bigint,
67
+ auto_random: 5,
68
+ auto_random_base: 100_000,
69
+ options: "DEFAULT CHARSET=utf8mb4" do |t|
65
70
  t.string :name, null: false
66
- t.string :email, null: false
67
- t.timestamps
68
71
  end
69
72
 
70
- create_table "posts", force: :cascade do |t|
71
- t.bigint :id, auto_random: true, primary_key: true
72
- t.bigint :user_id, null: false
73
+ # 2) 手動PK(カラム側でAUTO_RANDOMを指定)
74
+ create_table "events", id: false, options: "DEFAULT CHARSET=utf8mb4" do |t|
75
+ t.bigint :id, primary_key: true, null: false, auto_random: 6
73
76
  t.string :title, null: false
74
- t.text :content
75
- t.timestamps
76
77
  end
77
78
  ```
78
79
 
79
- **注意**: 現在の実装では、`auto_random: true`オプションはスキーマダンプ時に出力されますが、`create_table`でのテーブル作成機能は基本実装のみです。実際のテーブル作成は標準のDDLを使用してください。
80
+ 出力(export)は`create_table`のオプションに`auto_random:`/`auto_random_base:`を含めて往復一致となります(ALTERは使用しません)。
80
81
 
81
82
  ### CLI使用例
82
83
 
@@ -102,11 +103,10 @@ $ bundle exec ridgepole -c config/database.yml -E development --export -o Schema
102
103
 
103
104
  ## 動作確認済み環境
104
105
 
105
- - **TiDB**: v7.5.0 (安定版)
106
- - **Ruby**: 3.1+
107
- - **ActiveRecord**: 7.0+
108
- - **Ridgepole**: 3.0.4+
109
- - **アダプター**: mysql2, trilogy
106
+ - TiDB: v7.5.0 以降
107
+ - ActiveRecord: 7 / 8 系
108
+ - Ridgepole: 3.0.4 以降
109
+ - アダプター: mysql2 / trilogy
110
110
 
111
111
  ## データベース設定
112
112
 
@@ -154,7 +154,8 @@ connection.auto_random_column?('users', 'id') # => true/false
154
154
 
155
155
  ### 3. スキーマダンプ対応
156
156
 
157
- 既存のAUTO_RANDOMテーブルからSchemafileを生成する際、`auto_random: true`オプションが正しく出力されます。
157
+ 既存テーブルからのダンプ時、SHOW CREATE を解析して `create_table` のオプションに
158
+ `auto_random:` と `auto_random_base:` を出力します。apply→export→diff→apply の繰り返しでも差分は発生しません。
158
159
 
159
160
  ## テスト結果例
160
161
 
@@ -213,7 +214,7 @@ CREATE TABLE users (
213
214
 
214
215
  ### 前提条件
215
216
 
216
- - Ruby 3.1 以上
217
+ - Ruby 3.2 以上
217
218
  - TiDB 4.0 以上 (テスト用)
218
219
  - Docker (テスト環境用)
219
220
 
@@ -227,31 +228,21 @@ $ bundle install
227
228
 
228
229
  ### テスト実行
229
230
 
230
- ```bash
231
- # 基本機能テスト(TiDBなしでも実行可能)
232
- $ SKIP_TIDB_TESTS=1 bundle exec rspec
233
-
234
- # TiDB統合テスト(Dockerが必要)
235
- $ docker compose up -d tidb
236
- $ bundle exec rspec
237
-
238
- # Docker環境でのテスト
239
- $ docker compose run --rm test
240
- ```
241
-
242
- ### TiDBテスト環境
231
+ このリポジトリのRSpecは、実行時に自動で TiDB コンテナ(docker compose)を起動・停止します。事前の手動起動は不要です。
243
232
 
244
- TiDB 7.5.0を使用したテスト環境が用意されています:
233
+ 前提: Docker と docker compose が使用可能で、ポート `14000` が空いていること。
245
234
 
246
235
  ```bash
247
- # TiDBを起動
248
- $ docker compose up -d tidb
236
+ # 統合テスト(TiDBを自動起動)
237
+ $ bundle exec rspec --format documentation
249
238
 
250
- # テストを実行
251
- $ docker compose run --rm test
252
- ```
239
+ # アダプタを切り替えたい場合(デフォルトは trilogy)
240
+ # mysql2 を使う場合は、別途 mysql2 をインストールしてください
241
+ $ AR_ADAPTER=mysql2 bundle exec rspec --format documentation
253
242
  ```
254
243
 
244
+ ヒント: コンテナを手動で起動しておきたい場合は `docker compose up -d tidb` を先に実行しても構いません(テストはそのまま動作します)。
245
+
255
246
  ## Contributing
256
247
 
257
248
  1. このリポジトリをフォーク
data/docker-compose.yml CHANGED
@@ -1,34 +1,9 @@
1
1
  services:
2
2
  tidb:
3
- image: pingcap/tidb:v7.5.0
4
- container_name: ridgepole-tidb-test
3
+ image: pingcap/tidb:latest
4
+ command: ["--store=mocktikv", "--log-file="]
5
5
  ports:
6
- - "14000:4000"
7
- - "14080:10080"
8
- command:
9
- - --store=unistore
10
- - --host=0.0.0.0
11
- - --path=""
12
- healthcheck:
13
- test: ["CMD-SHELL", "timeout 1 bash -c '</dev/tcp/127.0.0.1/4000' || exit 1"]
14
- interval: 10s
15
- timeout: 5s
16
- retries: 5
17
- start_period: 30s
18
- restart: unless-stopped
19
-
20
- test:
21
- build: .
22
- depends_on:
23
- tidb:
24
- condition: service_healthy
6
+ - "14000:4000" # MySQL protocol
25
7
  environment:
26
- - TIDB_HOST=tidb
27
- - TIDB_PORT=4000
28
- - TIDB_USER=root
29
- - TIDB_PASSWORD=
30
- - TIDB_DATABASE=ridgepole_test
31
- working_dir: /app
32
- volumes:
33
- - .:/app
34
- command: bundle exec rspec
8
+ # TiDB は root 無パスで入れる
9
+ - TZ=UTC
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ridgepole
4
+ module ExtTidb
5
+ module Detector
6
+ # TiDB接続の自動検出
7
+ def tidb?
8
+ return @tidb_detected if defined?(@tidb_detected)
9
+
10
+ @tidb_detected = begin
11
+ version_info = select_value('SELECT VERSION()')
12
+ result = version_info&.include?('TiDB') == true
13
+ Rails.logger.debug "TiDB detection: version=#{version_info}, result=#{result}" if defined?(Rails)
14
+ result
15
+ rescue => e
16
+ Rails.logger.debug "TiDB detection failed: #{e.message}" if defined?(Rails)
17
+ false
18
+ end
19
+ end
20
+
21
+ # AUTO_RANDOMカラムの検出
22
+ def auto_random_column?(table_name, column_name)
23
+ return false unless tidb?
24
+
25
+ # SHOW CREATE TABLEでAUTO_RANDOMを検出
26
+ result = execute("SHOW CREATE TABLE #{quote_table_name(table_name)}")
27
+ create_sql = result.first[1] if result.first
28
+
29
+ if create_sql
30
+ # TiDBでのAUTO_RANDOM検出パターン
31
+ patterns = [
32
+ /AUTO_RANDOM\(\d+\)/i,
33
+ /\/\*T!\[auto_rand\] AUTO_RANDOM\(\d+\) \*\//i
34
+ ]
35
+
36
+ patterns.any? { |pattern| create_sql.match?(pattern) }
37
+ else
38
+ # フォールバック: INFORMATION_SCHEMA.COLUMNSを確認
39
+ extra = select_value(<<~SQL)
40
+ SELECT EXTRA
41
+ FROM INFORMATION_SCHEMA.COLUMNS
42
+ WHERE TABLE_SCHEMA = DATABASE()
43
+ AND TABLE_NAME = #{quote(table_name)}
44
+ AND COLUMN_NAME = #{quote(column_name)}
45
+ SQL
46
+
47
+ extra&.downcase&.include?('auto_random') == true
48
+ end
49
+ rescue => e
50
+ Rails.logger.debug "AUTO_RANDOM detection failed: #{e.message}" if defined?(Rails)
51
+ false
52
+ end
53
+
54
+ # AUTO_RANDOM_BASEテーブルオプションの検出
55
+ def auto_random_base(table_name)
56
+ return nil unless tidb?
57
+
58
+ result = execute("SHOW CREATE TABLE #{quote_table_name(table_name)}")
59
+ create_sql = result.first[1] if result.first
60
+
61
+ if create_sql&.match(/AUTO_RANDOM_BASE=(\d+)/i)
62
+ $1.to_i
63
+ end
64
+ rescue
65
+ nil
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ridgepole
4
+ module ExtTidb
5
+ module DumpPatch
6
+ def dump(*args)
7
+ dsl = super
8
+
9
+ conn = ActiveRecord::Base.connection
10
+ return dsl unless conn.respond_to?(:tidb?) && conn.tidb?
11
+
12
+ begin
13
+ tables = conn.tables
14
+ rescue StandardError
15
+ return dsl
16
+ end
17
+
18
+ tables.each do |table|
19
+ extras = extract_table_auto_random_options_for_dump(conn, table)
20
+ next if extras.empty?
21
+
22
+ # inject into create_table line
23
+ dsl = inject_table_options_line(dsl, table, extras)
24
+ end
25
+
26
+ dsl
27
+ end
28
+
29
+ private
30
+
31
+ def extract_table_auto_random_options_for_dump(conn, table)
32
+ extras = {}
33
+ begin
34
+ row = conn.execute("SHOW CREATE TABLE #{conn.quote_table_name(table)}").first
35
+ create_sql = row[1] if row
36
+ return extras unless create_sql
37
+
38
+ if (m = create_sql.match(/AUTO_RANDOM\((\d+)\)/i))
39
+ extras[:auto_random] = m[1].to_i
40
+ end
41
+ if (m = create_sql.match(/AUTO_RANDOM_BASE=(\d+)/i))
42
+ extras[:auto_random_base] = m[1].to_i
43
+ end
44
+ rescue StandardError
45
+ end
46
+ extras
47
+ end
48
+
49
+ def inject_table_options_line(dsl, table, extras)
50
+ keyvals = []
51
+ keyvals << "auto_random: #{extras[:auto_random]}" if extras[:auto_random]
52
+ keyvals << "auto_random_base: #{extras[:auto_random_base]}" if extras[:auto_random_base]
53
+ return dsl if keyvals.empty?
54
+
55
+ pattern = /(^(\s*)create_table\s+"#{Regexp.escape(table)}",\s*)(.+?)(\s+do\s*\|t\|)/m
56
+ dsl.sub(pattern) do
57
+ head = Regexp.last_match(1)
58
+ indent = Regexp.last_match(2)
59
+ opts = Regexp.last_match(3)
60
+ tail = Regexp.last_match(4)
61
+
62
+ # 既に同キーがあるなら上書きはせずそのまま
63
+ already = keyvals.any? { |kv| opts.include?(kv.split(':').first + ':') }
64
+ if already
65
+ head + opts + tail
66
+ else
67
+ injected = keyvals.join(', ')
68
+ # id: が先頭にあるならその直後に差し込む。なければ先頭に追加。
69
+ if opts =~ /(id:\s*[^,]+,\s*)/i
70
+ head + opts.sub($1, "#{$1}#{injected}, ") + tail
71
+ else
72
+ head + "#{injected}, " + opts + tail
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ridgepole
4
+ module ExtTidb
5
+ module Install
6
+ # Ridgepole起動時にTiDB検出→各パッチをprepend
7
+ def self.apply_patches!
8
+ # Hash#assert_valid_keysを拡張してauto_randomキーを許可
9
+ extend_hash_assert_valid_keys
10
+
11
+ # ActiveRecordが読み込まれている場合は即座に適用
12
+ if defined?(ActiveRecord::Base)
13
+ apply_activerecord_patches
14
+ else
15
+ # ActiveRecordが後でロードされる場合に備えてフックを設定
16
+ ActiveSupport.on_load(:active_record) do
17
+ apply_activerecord_patches
18
+ end
19
+ end
20
+ end
21
+
22
+ def self.apply_activerecord_patches
23
+ extend_connection_adapters
24
+ # SchemaDumper への直接パッチは不要(dump は DumpPatch が担当)
25
+ # TableDefinition 拡張も不要(Hash#assert_valid_keys 拡張で回避)
26
+ install_connection_hook
27
+ extend_ridgepole_client
28
+ end
29
+
30
+ private
31
+
32
+ def self.extend_hash_assert_valid_keys
33
+ Hash.class_eval do
34
+ alias_method :assert_valid_keys_without_auto_random, :assert_valid_keys
35
+ def assert_valid_keys(*valid_keys)
36
+ # auto_random, auto_random_baseキーを有効なキーとして追加
37
+ auto_random_keys = [:auto_random, :auto_random_base]
38
+ auto_random_keys.each do |key|
39
+ if keys.include?(key) && !valid_keys.include?(key)
40
+ valid_keys = valid_keys + [key]
41
+ end
42
+ end
43
+ assert_valid_keys_without_auto_random(*valid_keys)
44
+ end
45
+ end
46
+ rescue NameError => e
47
+ Rails.logger.debug "Could not extend Hash#assert_valid_keys: #{e.message}" if defined?(Rails)
48
+ end
49
+
50
+ def self.extend_connection_adapters
51
+ return unless defined?(ActiveRecord::ConnectionAdapters)
52
+
53
+ # まずは抽象 MySQL アダプタにパッチ(mysql2/trilogy 双方を網羅)
54
+ begin
55
+ abstract_mysql = ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
56
+
57
+ abstract_mysql.prepend(Detector) unless abstract_mysql < Detector
58
+ abstract_mysql.prepend(TableOptions) unless abstract_mysql < TableOptions
59
+
60
+ if abstract_mysql.const_defined?(:SchemaCreation)
61
+ sc = abstract_mysql.const_get(:SchemaCreation)
62
+ sc.prepend(SchemaCreation) unless sc < SchemaCreation
63
+ end
64
+ rescue NameError
65
+ # 未ロードの環境もあるので無視(接続確立後にロードされる)
66
+ end
67
+
68
+ # 既に具体アダプタがロード済みなら、そちらにも適用(冪等)
69
+ %w[
70
+ ActiveRecord::ConnectionAdapters::Mysql2Adapter
71
+ ActiveRecord::ConnectionAdapters::TrilogyAdapter
72
+ ].each do |adapter_name|
73
+ begin
74
+ adapter_class = Object.const_get(adapter_name)
75
+ adapter_class.prepend(Detector) unless adapter_class < Detector
76
+ adapter_class.prepend(TableOptions) unless adapter_class < TableOptions
77
+ if adapter_class.const_defined?(:SchemaCreation)
78
+ sc = adapter_class.const_get(:SchemaCreation)
79
+ sc.prepend(SchemaCreation) unless sc < SchemaCreation
80
+ end
81
+ rescue NameError
82
+ # 未ロードならスキップ
83
+ end
84
+ end
85
+
86
+ # 名前空間の揺れに備え、ConnectionAdapters 配下の SchemaCreation すべてに適用
87
+ begin
88
+ ObjectSpace.each_object(Class) do |klass|
89
+ name = klass.name rescue nil
90
+ next unless name && name.start_with?("ActiveRecord::ConnectionAdapters")
91
+ next unless name.end_with?("::SchemaCreation")
92
+ klass.prepend(SchemaCreation) unless klass < SchemaCreation
93
+ end
94
+ rescue StandardError
95
+ # noop
96
+ end
97
+
98
+ # AbstractMysqlAdapter のサブクラスにも Detector/TableOptions を適用(ロード順対策)
99
+ begin
100
+ if defined?(ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter)
101
+ ObjectSpace.each_object(Class) do |klass|
102
+ next unless klass < ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
103
+ klass.prepend(Detector) unless klass < Detector
104
+ klass.prepend(TableOptions) unless klass < TableOptions
105
+ end
106
+ end
107
+ rescue StandardError
108
+ # noop
109
+ end
110
+ end
111
+
112
+ # 接続確立後(アダプタ読み込み後)にも確実にパッチを適用するためのフック
113
+ def self.install_connection_hook
114
+ return unless defined?(ActiveRecord::Base)
115
+ return if @establish_hook_installed
116
+
117
+ mod = Module.new do
118
+ def establish_connection(*args)
119
+ result = super
120
+ # アダプタが読み込まれた後に再度パッチ適用(冪等)
121
+ Ridgepole::ExtTidb::Install.extend_connection_adapters
122
+ result
123
+ end
124
+ end
125
+
126
+ # class << だとローカル変数がスコープ外になるため、singleton_class で prepend
127
+ ActiveRecord::Base.singleton_class.prepend(mod)
128
+
129
+ @establish_hook_installed = true
130
+ rescue StandardError => e
131
+ Rails.logger.debug "Could not install establish_connection hook: #{e.message}" if defined?(Rails)
132
+ end
133
+
134
+ # extend_schema_dumper: dump は DumpPatch に委譲するため不要
135
+ # extend_table_definition: Hash#assert_valid_keys で未知キーを許すため不要
136
+
137
+ def self.extend_ridgepole_client
138
+ return unless defined?(Ridgepole::Client)
139
+ return if Ridgepole::Client < Ridgepole::ExtTidb::DumpPatch
140
+ Ridgepole::Client.prepend(Ridgepole::ExtTidb::DumpPatch)
141
+ rescue StandardError => e
142
+ Rails.logger.debug "Could not extend Ridgepole::Client: #{e.message}" if defined?(Rails)
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ridgepole
4
+ module ExtTidb
5
+ module SchemaCreation
6
+ # この SchemaCreation インスタンスから接続の tidb? を参照できるように
7
+ def tidb?
8
+ if instance_variable_defined?(:@conn)
9
+ conn = instance_variable_get(:@conn)
10
+ return conn.respond_to?(:tidb?) && conn.tidb?
11
+ end
12
+ false
13
+ end
14
+ # デフォルト主キーの生成経路でも AUTO_RANDOM を反映
15
+ def visit_PrimaryKeyDefinition(o)
16
+ return super unless tidb?
17
+
18
+ auto_random_value = o.options.delete(:auto_random)
19
+ sql = super
20
+
21
+ if auto_random_value
22
+ sql.sub!(/\sAUTO_INCREMENT\b/i, "")
23
+ bits = (auto_random_value == true ? nil : Integer(auto_random_value) rescue nil)
24
+ sql << (bits ? " AUTO_RANDOM(#{bits})" : " AUTO_RANDOM")
25
+ end
26
+
27
+ sql
28
+ end
29
+
30
+ # APPLY: カラムに AUTO_RANDOM(n) を追加(PRIMARY KEY の有無に関わらず列側に付与)
31
+ def visit_ColumnDefinition(o)
32
+ return super unless tidb?
33
+
34
+ # AUTO_RANDOM を取り出し(未知キーassertは Hash 拡張で回避済み)
35
+ auto_random_value = o.options.delete(:auto_random)
36
+
37
+ sql = super
38
+
39
+ # テーブルレベル指定のフォールバック(id の自動生成経路で失われた場合に備える)
40
+ if !auto_random_value && o.respond_to?(:primary_key?) && o.primary_key?
41
+ if instance_variable_defined?(:@conn)
42
+ conn = instance_variable_get(:@conn)
43
+ if conn.instance_variable_defined?(:@tidb_pending_auto_random_pk_bits)
44
+ auto_random_value = conn.instance_variable_get(:@tidb_pending_auto_random_pk_bits)
45
+ begin
46
+ conn.remove_instance_variable(:@tidb_pending_auto_random_pk_bits)
47
+ rescue StandardError
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ if auto_random_value
54
+ # 念のため AUTO_INCREMENT を除去
55
+ sql.sub!(/\sAUTO_INCREMENT\b/i, "")
56
+
57
+ bits = (auto_random_value == true ? nil : Integer(auto_random_value) rescue nil)
58
+ sql << (bits ? " AUTO_RANDOM(#{bits})" : " AUTO_RANDOM")
59
+ end
60
+
61
+ sql
62
+ end
63
+
64
+ private
65
+
66
+ def add_column_options!(sql, options)
67
+ # 独自キーを先に取り出し
68
+ bits = options.delete(:auto_random)
69
+
70
+ # AUTO_RANDOM を付けるなら AUTO_INCREMENT を抑止
71
+ options[:auto_increment] = false if bits
72
+
73
+ super(sql, options)
74
+
75
+ if bits
76
+ unless sql.match?(/\bAUTO_RANDOM\b/i)
77
+ sql.sub!(/\sAUTO_INCREMENT\b/i, "")
78
+ sql << " AUTO_RANDOM(#{Integer(bits)})"
79
+ end
80
+ end
81
+
82
+ sql
83
+ end
84
+
85
+ # 一部の AR バージョンはこちらを呼ぶ場合があるため両方用意
86
+ def add_column_options(sql, options)
87
+ add_column_options!(sql, options)
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ridgepole
4
+ module ExtTidb
5
+ # TiDB 向け create_table 拡張(CREATE 時に完結)
6
+ # - テーブルレベル auto_random を id 定義へ伝播
7
+ # - AUTO_RANDOM_BASE をテーブル options に付与
8
+ module TableOptions
9
+ # CREATE 時に auto_random / auto_random_base を正しく反映
10
+ def create_table(table_name, **options, &block)
11
+ return super unless tidb?
12
+
13
+ # 独自キーを取り出す(AR の未知キー検証を回避)
14
+ auto_random_value = options.delete(:auto_random)
15
+ auto_random_base_value = options.delete(:auto_random_base)
16
+
17
+ # id: false でなければ、テーブルレベル auto_random を id に伝播
18
+ if auto_random_value && options[:id] != false
19
+ id_opt = options[:id]
20
+ id_opts = case id_opt
21
+ when Hash
22
+ id_opt.dup
23
+ when Symbol
24
+ { type: id_opt }
25
+ when true, nil
26
+ {}
27
+ else
28
+ { type: id_opt }
29
+ end
30
+ id_opts[:auto_random] = auto_random_value
31
+ id_opts[:auto_increment] = false
32
+ options[:id] = id_opts
33
+ end
34
+
35
+ # AUTO_RANDOM_BASE を options に付与
36
+ if auto_random_base_value
37
+ existing_options = options[:options] || ""
38
+ if existing_options.present?
39
+ options[:options] = "#{existing_options} AUTO_RANDOM_BASE=#{auto_random_base_value}"
40
+ else
41
+ options[:options] = "AUTO_RANDOM_BASE=#{auto_random_base_value}"
42
+ end
43
+ end
44
+
45
+ # 主キー経路でビット数が落ちる環境向けのフォールバック用に一時保存
46
+ if auto_random_value && options[:id] != false
47
+ begin
48
+ @tidb_pending_auto_random_pk_bits = auto_random_value
49
+ rescue StandardError
50
+ end
51
+ end
52
+
53
+ begin
54
+ super
55
+ ensure
56
+ remove_instance_variable(:@tidb_pending_auto_random_pk_bits) if instance_variable_defined?(:@tidb_pending_auto_random_pk_bits)
57
+ end
58
+ end
59
+
60
+
61
+ private
62
+
63
+ def build_create_table_options(options)
64
+ return super unless tidb?
65
+
66
+ # AUTO_RANDOM_BASE を options 文字列の先頭に寄せ、ノイズ差分を避ける
67
+ sql_options = super
68
+ if sql_options&.include?('AUTO_RANDOM_BASE')
69
+ parts = sql_options.split(/\s+/)
70
+ auto_random_parts = parts.select { |part| part.start_with?('AUTO_RANDOM_BASE=') }
71
+ other_parts = parts.reject { |part| part.start_with?('AUTO_RANDOM_BASE=') }
72
+ sql_options = (auto_random_parts + other_parts).join(' ')
73
+ end
74
+
75
+ sql_options
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ridgepole
4
+ module ExtTidb
5
+ VERSION = "0.3.0"
6
+ end
7
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ext_tidb/version'
4
+ require_relative 'ext_tidb/detector'
5
+ require_relative 'ext_tidb/schema_creation'
6
+ require_relative 'ext_tidb/table_options'
7
+ require_relative 'ext_tidb/dump_patch'
8
+ require_relative 'ext_tidb/install'
9
+
10
+ module Ridgepole
11
+ module ExtTidb
12
+ # エントリポイント - requireで自動的にインストール
13
+ def self.setup!
14
+ Install.apply_patches!
15
+ end
16
+ end
17
+ end
18
+
19
+ # 自動インストール
20
+ Ridgepole::ExtTidb.setup!
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'lib/ridgepole/ext/tidb/version'
3
+ require_relative 'lib/ridgepole/ext_tidb/version'
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = 'ridgepole-ext-tidb'
7
- spec.version = Ridgepole::Ext::Tidb::VERSION
7
+ spec.version = Ridgepole::ExtTidb::VERSION
8
8
  spec.authors = ['ikad']
9
9
  spec.email = ['info@forgxisto.com']
10
10
 
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
12
  spec.description = "Extends Ridgepole to support TiDB's AUTO_RANDOM column attribute for seamless schema management"
13
13
  spec.homepage = 'https://github.com/forgxisto/ridgepole-ext-tidb'
14
14
  spec.license = 'MIT'
15
- spec.required_ruby_version = '>= 3.1.0'
15
+ spec.required_ruby_version = '>= 3.2.0'
16
16
 
17
17
  spec.metadata['allowed_push_host'] = 'https://rubygems.org'
18
18
  spec.metadata['homepage_uri'] = spec.homepage
@@ -33,15 +33,15 @@ Gem::Specification.new do |spec|
33
33
  spec.require_paths = ['lib']
34
34
 
35
35
  # Dependencies
36
- spec.add_dependency 'ridgepole'
36
+ spec.add_dependency 'ridgepole', ">= 3.0"
37
37
 
38
38
  # Development dependencies
39
- spec.add_development_dependency 'activerecord-trilogy-adapter'
40
- spec.add_development_dependency 'mysql2'
41
39
  spec.add_development_dependency 'rake'
42
40
  spec.add_development_dependency 'rspec'
43
41
  spec.add_development_dependency 'rubocop'
44
42
  spec.add_development_dependency 'trilogy'
43
+ spec.add_development_dependency 'activerecord', '>= 8.0'
44
+ spec.add_development_dependency 'debug'
45
45
 
46
46
  # Ruby 3.4+ compatibility
47
47
  spec.add_development_dependency 'benchmark'
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ridgepole-ext-tidb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ikad
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-09-06 00:00:00.000000000 Z
10
+ date: 2025-09-08 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ridgepole
@@ -15,16 +15,16 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '0'
18
+ version: '3.0'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '0'
25
+ version: '3.0'
26
26
  - !ruby/object:Gem::Dependency
27
- name: activerecord-trilogy-adapter
27
+ name: rake
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - ">="
@@ -38,7 +38,7 @@ dependencies:
38
38
  - !ruby/object:Gem::Version
39
39
  version: '0'
40
40
  - !ruby/object:Gem::Dependency
41
- name: mysql2
41
+ name: rspec
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - ">="
@@ -52,7 +52,7 @@ dependencies:
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0'
54
54
  - !ruby/object:Gem::Dependency
55
- name: rake
55
+ name: rubocop
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - ">="
@@ -66,7 +66,7 @@ dependencies:
66
66
  - !ruby/object:Gem::Version
67
67
  version: '0'
68
68
  - !ruby/object:Gem::Dependency
69
- name: rspec
69
+ name: trilogy
70
70
  requirement: !ruby/object:Gem::Requirement
71
71
  requirements:
72
72
  - - ">="
@@ -80,21 +80,21 @@ dependencies:
80
80
  - !ruby/object:Gem::Version
81
81
  version: '0'
82
82
  - !ruby/object:Gem::Dependency
83
- name: rubocop
83
+ name: activerecord
84
84
  requirement: !ruby/object:Gem::Requirement
85
85
  requirements:
86
86
  - - ">="
87
87
  - !ruby/object:Gem::Version
88
- version: '0'
88
+ version: '8.0'
89
89
  type: :development
90
90
  prerelease: false
91
91
  version_requirements: !ruby/object:Gem::Requirement
92
92
  requirements:
93
93
  - - ">="
94
94
  - !ruby/object:Gem::Version
95
- version: '0'
95
+ version: '8.0'
96
96
  - !ruby/object:Gem::Dependency
97
- name: trilogy
97
+ name: debug
98
98
  requirement: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - ">="
@@ -179,9 +179,13 @@ files:
179
179
  - README.md
180
180
  - Rakefile
181
181
  - docker-compose.yml
182
- - lib/ridgepole-ext-tidb.rb
183
- - lib/ridgepole/ext/tidb.rb
184
- - lib/ridgepole/ext/tidb/version.rb
182
+ - lib/ridgepole/ext_tidb.rb
183
+ - lib/ridgepole/ext_tidb/detector.rb
184
+ - lib/ridgepole/ext_tidb/dump_patch.rb
185
+ - lib/ridgepole/ext_tidb/install.rb
186
+ - lib/ridgepole/ext_tidb/schema_creation.rb
187
+ - lib/ridgepole/ext_tidb/table_options.rb
188
+ - lib/ridgepole/ext_tidb/version.rb
185
189
  - ridgepole-ext-tidb.gemspec
186
190
  homepage: https://github.com/forgxisto/ridgepole-ext-tidb
187
191
  licenses:
@@ -198,7 +202,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
198
202
  requirements:
199
203
  - - ">="
200
204
  - !ruby/object:Gem::Version
201
- version: 3.1.0
205
+ version: 3.2.0
202
206
  required_rubygems_version: !ruby/object:Gem::Requirement
203
207
  requirements:
204
208
  - - ">="
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ridgepole
4
- module Ext
5
- module Tidb
6
- VERSION = '0.2.1'
7
- end
8
- end
9
- end
@@ -1,266 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'tidb/version'
4
-
5
- module Ridgepole
6
- module Ext
7
- module Tidb
8
- def self.setup!
9
- # SchemaDumperにもAUTO_RANDOM対応を追加
10
- extend_schema_dumper
11
- # Hash#assert_valid_keysを拡張してauto_randomキーを許可
12
- extend_hash_assert_valid_keys
13
- end # 手動で接続アダプタを拡張するメソッド(外部から呼び出し可能)
14
- def self.ensure_connection_extended!
15
- return unless ActiveRecord::Base.connected?
16
-
17
- connection = ActiveRecord::Base.connection
18
- extend_connection_adapter(connection)
19
- end
20
-
21
- def self.extend_connection_adapter(connection)
22
- return unless connection
23
-
24
- adapter_class = connection.class
25
-
26
- # 既に拡張済みかチェック
27
- return if adapter_class.method_defined?(:tidb?)
28
-
29
- # Hash#assert_valid_keysを拡張してauto_randomキーを許可
30
- extend_hash_assert_valid_keys
31
- # TableDefinitionを拡張して:auto_randomオプションをサポート
32
- extend_table_definition
33
-
34
- adapter_class.class_eval do
35
- # AUTO_RANDOMカラムの検出
36
- def auto_random_column?(table_name, column_name)
37
- return false unless tidb?
38
-
39
- # TiDB 7.5.0でのAUTO_RANDOM検出
40
- # SHOW CREATE TABLEでCREATE TABLE文を確認
41
- result = execute("SHOW CREATE TABLE #{quote_table_name(table_name)}")
42
- create_sql = result.first[1] if result.first
43
-
44
- if create_sql
45
- # TiDB 7.5.0では AUTO_RANDOM がコメント形式で表示される
46
- # 例: /*T![auto_rand] AUTO_RANDOM(5) */
47
- if create_sql.include?('AUTO_RANDOM') || create_sql.include?('auto_rand')
48
- return true
49
- end
50
-
51
- # テーブルオプションにAUTO_RANDOM_BASEが含まれているかチェック
52
- if create_sql.include?('AUTO_RANDOM_BASE')
53
- return true
54
- end
55
- end
56
-
57
- # INFORMATION_SCHEMA.COLUMNS の EXTRA を確認(フォールバック)
58
- extra = select_value(<<~SQL)
59
- SELECT EXTRA
60
- FROM INFORMATION_SCHEMA.COLUMNS
61
- WHERE TABLE_SCHEMA = DATABASE()
62
- AND TABLE_NAME = #{quote(table_name)}
63
- AND COLUMN_NAME = #{quote(column_name)}
64
- SQL
65
-
66
- if extra&.downcase&.include?('auto_random')
67
- return true
68
- end
69
-
70
- false
71
- rescue => e
72
- puts "AUTO_RANDOM detection failed: #{e.message}"
73
- false
74
- end
75
-
76
- # TiDBかどうかの判定
77
- def tidb?
78
- # VERSION()関数でTiDBを検出(キャッシュなし)
79
- version_info = select_value('SELECT VERSION()')
80
- result = version_info&.include?('TiDB') == true
81
- Rails.logger.debug "TiDB detection: version=#{version_info}, result=#{result}" if defined?(Rails)
82
- result
83
- rescue => e
84
- Rails.logger.debug "TiDB detection failed: #{e.message}" if defined?(Rails)
85
- false
86
- end
87
-
88
- # CREATE TABLE時のAUTO_RANDOM対応
89
- alias_method :create_table_without_auto_random, :create_table
90
- def create_table(table_name, **options, &block)
91
- # :auto_randomキーを処理する前に、idオプションから取り除く
92
- if options.dig(:id, :auto_random) && tidb?
93
- # auto_randomフラグを保存
94
- auto_random_enabled = options[:id].delete(:auto_random)
95
-
96
- # 通常のcreate_tableを呼び出してテーブル構造を作成
97
- create_table_without_auto_random(table_name, **options, &block)
98
-
99
- # AUTO_RANDOMを有効にするためにALTER TABLEを実行
100
- if auto_random_enabled
101
- execute("ALTER TABLE #{quote_table_name(table_name)} MODIFY COLUMN id BIGINT AUTO_RANDOM PRIMARY KEY")
102
- end
103
- else
104
- create_table_without_auto_random(table_name, **options, &block)
105
- end
106
- end
107
- end
108
-
109
- puts "✅ Methods added to #{adapter_class}"
110
- end
111
-
112
- def self.extend_activerecord_adapters
113
- puts "📦 Extending ActiveRecord adapters..."
114
- # Hash#assert_valid_keysを拡張してauto_randomキーを許可
115
- extend_hash_assert_valid_keys
116
- # TableDefinitionを拡張して:auto_randomオプションをサポート
117
- extend_table_definition
118
-
119
- # MySQL系アダプタにAUTO_RANDOMサポートを追加
120
- extend_adapter('ActiveRecord::ConnectionAdapters::Mysql2Adapter')
121
- extend_adapter('ActiveRecord::ConnectionAdapters::TrilogyAdapter')
122
-
123
- # SchemaDumperにもAUTO_RANDOM対応を追加
124
- extend_schema_dumper
125
- puts "📦 Adapter extension complete"
126
- end
127
-
128
- def self.extend_adapter(adapter_name)
129
- return unless defined?(ActiveRecord::ConnectionAdapters)
130
-
131
- begin
132
- adapter_class = Object.const_get(adapter_name)
133
- puts "🔧 Extending #{adapter_name}..."
134
- rescue NameError => e
135
- # アダプタが利用できない場合はスキップ
136
- puts "⚠️ Skipping #{adapter_name}: #{e.message}"
137
- return
138
- end
139
-
140
- # Hash#assert_valid_keysを拡張してauto_randomキーを許可
141
- extend_hash_assert_valid_keys
142
- # TableDefinitionを拡張して:auto_randomオプションをサポート
143
- extend_table_definition
144
-
145
- # 一時的にputsを外して動作確認
146
- adapter_class.class_eval do
147
- # AUTO_RANDOMカラムの検出
148
- def auto_random_column?(table_name, column_name)
149
- return false unless tidb?
150
-
151
- extra = select_value(<<~SQL)
152
- SELECT EXTRA
153
- FROM INFORMATION_SCHEMA.COLUMNS
154
- WHERE TABLE_SCHEMA = DATABASE()
155
- AND TABLE_NAME = #{quote(table_name)}
156
- AND COLUMN_NAME = #{quote(column_name)}
157
- SQL
158
-
159
- extra&.downcase&.include?('auto_random') == true
160
- rescue
161
- false
162
- end
163
-
164
- # TiDBかどうかの判定
165
- def tidb?
166
- # VERSION()関数でTiDBを検出(キャッシュなし)
167
- version_info = select_value('SELECT VERSION()')
168
- result = version_info&.include?('TiDB') == true
169
- Rails.logger.debug "TiDB detection: version=#{version_info}, result=#{result}" if defined?(Rails)
170
- result
171
- rescue => e
172
- Rails.logger.debug "TiDB detection failed: #{e.message}" if defined?(Rails)
173
- false
174
- end
175
-
176
- # CREATE TABLE時のAUTO_RANDOM対応
177
- alias_method :create_table_without_auto_random, :create_table
178
- def create_table(table_name, **options, &block)
179
- # :auto_randomキーを処理する前に、idオプションから取り除く
180
- if options.dig(:id, :auto_random) && tidb?
181
- # auto_randomフラグを保存
182
- auto_random_enabled = options[:id].delete(:auto_random)
183
-
184
- # 通常のcreate_tableを呼び出してテーブル構造を作成
185
- create_table_without_auto_random(table_name, **options, &block)
186
-
187
- # AUTO_RANDOMを有効にするためにALTER TABLEを実行
188
- if auto_random_enabled
189
- execute("ALTER TABLE #{quote_table_name(table_name)} MODIFY COLUMN id BIGINT AUTO_RANDOM PRIMARY KEY")
190
- end
191
- else
192
- create_table_without_auto_random(table_name, **options, &block)
193
- end
194
- end
195
- end
196
-
197
- puts "✅ Methods added to #{adapter_name}"
198
- end
199
-
200
- def self.extend_schema_dumper
201
- return unless defined?(ActiveRecord::SchemaDumper)
202
-
203
- ActiveRecord::SchemaDumper.class_eval do
204
- alias_method :prepare_column_options_without_auto_random, :prepare_column_options
205
- def prepare_column_options(column)
206
- spec = prepare_column_options_without_auto_random(column)
207
-
208
- # TiDB接続でAUTO_RANDOMカラムの場合、auto_randomオプションを追加
209
- if @connection.respond_to?(:tidb?) && @connection.tidb? &&
210
- @connection.respond_to?(:auto_random_column?) &&
211
- @connection.auto_random_column?(@table, column.name)
212
- spec[:auto_random] = true
213
- end
214
-
215
- spec
216
- end
217
- end
218
- rescue NameError
219
- # SchemaDumperが利用できない場合はスキップ
220
- end
221
-
222
- def self.extend_table_definition
223
- return unless defined?(ActiveRecord::ConnectionAdapters::TableDefinition)
224
-
225
- # TableDefinitionを拡張して:auto_randomオプションをサポート
226
- ActiveRecord::ConnectionAdapters::TableDefinition.class_eval do
227
- # カラム作成時のオプション検証を拡張
228
- alias_method :column_without_auto_random, :column
229
- def column(name, type, **options)
230
- # :auto_randomオプションが含まれている場合は、それを取り除いて後で処理
231
- if options.key?(:auto_random)
232
- auto_random_value = options.delete(:auto_random)
233
- # カラム定義にauto_randomの情報を保存(後でcreate_tableで使用)
234
- @auto_random_columns ||= {}
235
- @auto_random_columns[name.to_s] = auto_random_value
236
- end
237
- column_without_auto_random(name, type, **options)
238
- end
239
-
240
- # auto_randomカラムの情報を取得するメソッド
241
- def auto_random_columns
242
- @auto_random_columns ||= {}
243
- end
244
- end
245
- rescue NameError => e
246
- puts "⚠️ Could not extend TableDefinition: #{e.message}"
247
- end
248
-
249
- def self.extend_hash_assert_valid_keys
250
- # Hashクラスを拡張して、auto_randomキーを有効なキーとして認識させる
251
- Hash.class_eval do
252
- alias_method :assert_valid_keys_without_auto_random, :assert_valid_keys
253
- def assert_valid_keys(*valid_keys)
254
- # auto_randomキーが含まれている場合は、それを有効なキーとして追加
255
- if keys.include?(:auto_random) && !valid_keys.include?(:auto_random)
256
- valid_keys = valid_keys + [:auto_random]
257
- end
258
- assert_valid_keys_without_auto_random(*valid_keys)
259
- end
260
- end
261
- rescue NameError => e
262
- puts "⚠️ Could not extend Hash#assert_valid_keys: #{e.message}"
263
- end
264
- end
265
- end
266
- end
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'ridgepole/ext/tidb'
4
-
5
- # Ridgepoleが使用される前に拡張を適用
6
- Ridgepole::Ext::Tidb.setup!
7
-
8
- # ActiveRecordアダプタが利用可能になったときに自動拡張を実行
9
- if defined?(ActiveRecord::Base)
10
- Ridgepole::Ext::Tidb.extend_activerecord_adapters
11
- else
12
- # ActiveRecordが後でロードされる場合に備えてフックを設定
13
- ActiveSupport.on_load(:active_record) do
14
- Ridgepole::Ext::Tidb.extend_activerecord_adapters
15
- end
16
- end