arql 0.4.13 → 0.4.15

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: 8b67b593bb1bb99cb10e911067353ee07d3e3bd63736d115bb449550b2c2e395
4
- data.tar.gz: 7e586dab61df6eb6c71ea37ac60b2e2dc76b00ccb363964d32b2e986972d637c
3
+ metadata.gz: d444bc5ecc11b4c15db01b52b413341e85525d71d82f24e994315418b728a989
4
+ data.tar.gz: 1e69014e37aafa3974c3aae3be52c37c7f3c27db2c7b59d169caa8b4de5ce6d7
5
5
  SHA512:
6
- metadata.gz: d21f517315f2abbecca08d19d0bce1879b8cd2a6eda19d2e70ccc6ee7ff8623c774b8e89211d85711fe1d991cb001235331a03b830c6bdb9ab27fd6a9e525e71
7
- data.tar.gz: 616dd1c78f257532c7999800ee2e966e6942e06eaafa2a519dde2c6d8938e780f8a7634c4ce29d260880ad26bf8fc70b4d8dd5306098d9a82bd51eb294535239
6
+ metadata.gz: 0dd50276ddff1a65d3d01bc7eb8ec05dbdd59d441eb8fe2555ca3ccaf2b951650ace6b7bf5908f4eccb2f38a0dec7000130f41c63f62df74e4a8ef4c6504897d
7
+ data.tar.gz: ada7756d3e50fbff30e8b6581b1eceaebff35fdacb7bf76bf504348410fd7152398515799de35f355e91c3d3cb87d87646a401901da3003bb4b16c2ead88bfd5
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- arql (0.4.13)
4
+ arql (0.4.15)
5
5
  activerecord (>= 6.1.5, < 7.1.0)
6
6
  activesupport (>= 6.1.5, < 7.1.0)
7
7
  caxlsx (~> 3.3.0)
@@ -0,0 +1,275 @@
1
+ # rename_columns Implementation Plan
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Add `rename_columns` config option that creates AR-level attribute aliases for columns with conflicting names (e.g., `model_name`).
6
+
7
+ **Architecture:** In `lib/arql/definition.rb`, add `rename_columns_mapping` accessor, modify `make_model_class` to merge renamed columns into `ignored_columns` and call `alias_attribute`, and add `validate_rename_columns!` for three-layer conflict detection at startup.
8
+
9
+ **Tech Stack:** Ruby, ActiveRecord `alias_attribute`, `ignored_columns`, `ActiveRecord::AttributeMethods.dangerous_attribute_methods`
10
+
11
+ ---
12
+
13
+ ### Task 1: Add `rename_columns_mapping` accessor method
14
+
15
+ **Files:**
16
+ - Modify: `lib/arql/definition.rb:95-97` (after `model_names_mapping`)
17
+
18
+ **Step 1: Add the method**
19
+
20
+ After line 97 (`end` of `model_names_mapping`), add:
21
+
22
+ ```ruby
23
+ def rename_columns_mapping
24
+ @rename_columns_mapping ||= @options[:rename_columns] || {}
25
+ end
26
+ ```
27
+
28
+ This follows the exact same pattern as `model_names_mapping` on line 95.
29
+
30
+ **Step 2: Verify syntax**
31
+
32
+ Run: `ruby -c lib/arql/definition.rb`
33
+ Expected: `Syntax OK`
34
+
35
+ **Step 3: Commit**
36
+
37
+ ```bash
38
+ git add lib/arql/definition.rb
39
+ git commit -m "feat: add rename_columns_mapping accessor"
40
+ ```
41
+
42
+ ---
43
+
44
+ ### Task 2: Modify `make_model_class` to handle column renaming
45
+
46
+ **Files:**
47
+ - Modify: `lib/arql/definition.rb:150-175` (the `make_model_class` method)
48
+
49
+ **Step 1: Implement the rename logic inside the Class.new block**
50
+
51
+ Replace lines 150-175 with:
52
+
53
+ ```ruby
54
+ def make_model_class(table_name, primary_keys)
55
+ options = @options
56
+ rename_columns = rename_columns_mapping
57
+ Class.new(@base_model) do
58
+ include Arql::Extension
59
+ if primary_keys.is_a?(Array) && primary_keys.size > 1
60
+ self.primary_keys = primary_keys
61
+ else
62
+ self.primary_key = primary_keys&.first
63
+ end
64
+ self.table_name = table_name
65
+ self.inheritance_column = nil
66
+
67
+ # Merge renamed column original names into ignored_columns
68
+ ignored = Array(options[:ignored_columns])
69
+ rename_columns.each do |old_name, new_name|
70
+ ignored << old_name.to_s
71
+ end
72
+ self.ignored_columns = ignored.uniq if ignored.present?
73
+
74
+ ActiveRecord.default_timezone = :local
75
+ if options[:created_at].present?
76
+ define_singleton_method :timestamp_attributes_for_create do
77
+ options[:created_at]
78
+ end
79
+ end
80
+
81
+ if options[:updated_at].present?
82
+ define_singleton_method :timestamp_attributes_for_update do
83
+ options[:updated_at]
84
+ end
85
+ end
86
+
87
+ # Create alias attributes for renamed columns
88
+ rename_columns.each do |old_name, new_name|
89
+ alias_attribute new_name.to_sym, old_name.to_sym
90
+ end
91
+ end
92
+ end
93
+ ```
94
+
95
+ Key changes from the original:
96
+ 1. `ignored_columns` now merges both `options[:ignored_columns]` and all `rename_columns` original names
97
+ 2. `alias_attribute` is called for each rename mapping inside the `Class.new` block
98
+
99
+ **Step 2: Verify syntax**
100
+
101
+ Run: `ruby -c lib/arql/definition.rb`
102
+ Expected: `Syntax OK`
103
+
104
+ **Step 3: Commit**
105
+
106
+ ```bash
107
+ git add lib/arql/definition.rb
108
+ git commit -m "feat: integrate rename_columns into make_model_class with ignored_columns merge and alias_attribute"
109
+ ```
110
+
111
+ ---
112
+
113
+ ### Task 3: Add `validate_rename_columns!` with three-layer conflict detection
114
+
115
+ **Files:**
116
+ - Modify: `lib/arql/definition.rb` (add new method after `rename_columns_mapping`)
117
+
118
+ **Step 1: Add the validation method**
119
+
120
+ After the `rename_columns_mapping` method, add:
121
+
122
+ ```ruby
123
+ ARQL_EXTENSION_INSTANCE_METHODS = %i[v t vd to_insert_sql to_upsert_sql dump write_csv write_excel].freeze
124
+
125
+ def validate_rename_columns!(table_name, model_class)
126
+ rename_columns = rename_columns_mapping
127
+ table_column_names = model_class.column_names.map(&:to_s)
128
+ ar_reserved = ActiveRecord::AttributeMethods.dangerous_attribute_methods
129
+
130
+ rename_columns.each do |old_name, new_name|
131
+ old_name = old_name.to_s
132
+ new_name = new_name.to_s
133
+
134
+ # Check: original column must exist in the table
135
+ unless table_column_names.include?(old_name)
136
+ warn "rename_columns: column '#{old_name}' not found in table '#{table_name}', skipping"
137
+ next
138
+ end
139
+
140
+ # Check: new name must not conflict with existing column names
141
+ if table_column_names.include?(new_name) && new_name != old_name
142
+ warn "rename_columns: new name '#{new_name}' conflicts with existing column in table '#{table_name}', skipping rename of '#{old_name}'"
143
+ next
144
+ end
145
+
146
+ # Check: new name must not be an ActiveRecord reserved attribute method
147
+ if ar_reserved.include?(new_name)
148
+ raise ActiveRecord::DangerousAttributeError,
149
+ "rename_columns: new name '#{new_name}' is a reserved ActiveRecord method, cannot use as column alias"
150
+ end
151
+
152
+ # Check: new name must not conflict with arql Extension instance methods
153
+ if ARQL_EXTENSION_INSTANCE_METHODS.include?(new_name.to_sym)
154
+ warn "rename_columns: new name '#{new_name}' conflicts with an arql method in table '#{table_name}', skipping rename of '#{old_name}'"
155
+ next
156
+ end
157
+ end
158
+ end
159
+ ```
160
+
161
+ **Step 2: Verify syntax**
162
+
163
+ Run: `ruby -c lib/arql/definition.rb`
164
+ Expected: `Syntax OK`
165
+
166
+ **Step 3: Commit**
167
+
168
+ ```bash
169
+ git add lib/arql/definition.rb
170
+ git commit -m "feat: add validate_rename_columns! with three-layer conflict detection"
171
+ ```
172
+
173
+ ---
174
+
175
+ ### Task 4: Wire up validation in `define_model_from_table` and `redefine`
176
+
177
+ **Files:**
178
+ - Modify: `lib/arql/definition.rb:99-114` (`define_model_from_table`)
179
+ - Modify: `lib/arql/definition.rb:39-53` (`redefine`)
180
+
181
+ **Step 1: Add validation call in `define_model_from_table`**
182
+
183
+ In `define_model_from_table`, after line 113 (the return hash), add the validation call. The method should become:
184
+
185
+ ```ruby
186
+ def define_model_from_table(table_name, primary_keys)
187
+ model_name = make_model_name(table_name)
188
+ return unless model_name
189
+
190
+ model_class = make_model_class(table_name, primary_keys)
191
+ validate_rename_columns!(table_name, model_class)
192
+ @namespace_module.const_set(model_name, model_class)
193
+ abbr_name = make_model_abbr_name(model_name, table_name)
194
+ @namespace_module.const_set(abbr_name, model_class)
195
+
196
+ { model: model_class, abbr: "#@namespace::#{abbr_name}", table: table_name }
197
+ end
198
+ ```
199
+
200
+ Only change: added `validate_rename_columns!(table_name, model_class)` after `make_model_class` and before `const_set`.
201
+
202
+ **Step 2: Verify `redefine` already works**
203
+
204
+ The `redefine` method (line 39) calls `define_model_from_table`, so it automatically picks up the validation. No changes needed to `redefine`.
205
+
206
+ **Step 3: Verify syntax**
207
+
208
+ Run: `ruby -c lib/arql/definition.rb`
209
+ Expected: `Syntax OK`
210
+
211
+ **Step 4: Commit**
212
+
213
+ ```bash
214
+ git add lib/arql/definition.rb
215
+ git commit -m "feat: wire up validate_rename_columns! in define_model_from_table"
216
+ ```
217
+
218
+ ---
219
+
220
+ ### Task 5: Manual verification
221
+
222
+ **Files:** None (testing only)
223
+
224
+ **Step 1: Verify with a real database**
225
+
226
+ Create a test SQLite database with a `model_name` column, configure `rename_columns`, and verify:
227
+
228
+ ```bash
229
+ # Create test DB
230
+ sqlite3 /tmp/arql_test.db "CREATE TABLE test_table (id INTEGER PRIMARY KEY, model_name TEXT, name TEXT); INSERT INTO test_table VALUES (1, 'TestModel', 'Alice');"
231
+
232
+ # Create config
233
+ cat > /tmp/arql_test.yml << 'EOF'
234
+ default:
235
+ adapter: sqlite3
236
+ database: /tmp/arql_test.db
237
+ rename_columns:
238
+ model_name: record_model_name
239
+ EOF
240
+
241
+ # Run arql
242
+ arql -c /tmp/arql_test.yml -E 'puts TestTable.last.record_model_name'
243
+ ```
244
+
245
+ Expected output: `TestModel`
246
+
247
+ **Step 2: Verify query aliasing**
248
+
249
+ ```bash
250
+ arql -c /tmp/arql_test.yml -S -E 'puts TestTable.where(record_model_name: "TestModel").to_sql'
251
+ ```
252
+
253
+ Expected output contains: `WHERE "test_table"."model_name" = 'TestModel'`
254
+
255
+ **Step 3: Verify conflict detection**
256
+
257
+ ```bash
258
+ cat > /tmp/arql_test_bad.yml << 'EOF'
259
+ default:
260
+ adapter: sqlite3
261
+ database: /tmp/arql_test.db
262
+ rename_columns:
263
+ model_name: save
264
+ EOF
265
+
266
+ arql -c /tmp/arql_test_bad.yml -E 'puts "hello"'
267
+ ```
268
+
269
+ Expected: `ActiveRecord::DangerousAttributeError` with message about `save` being reserved.
270
+
271
+ **Step 4: Cleanup**
272
+
273
+ ```bash
274
+ rm /tmp/arql_test.db /tmp/arql_test.yml /tmp/arql_test_bad.yml
275
+ ```
@@ -2,7 +2,6 @@ require 'arql/concerns'
2
2
  require 'arql/vd'
3
3
 
4
4
  module Arql
5
-
6
5
  class BaseModel < ::ActiveRecord::Base
7
6
  self.abstract_class = true
8
7
 
@@ -24,9 +23,10 @@ module Arql
24
23
 
25
24
  def self.method_missing(method_name, *args, &block)
26
25
  if method_name.to_s =~ /^(.+)_like$/
27
- attr_name = $1.to_sym
26
+ attr_name = ::Regexp.last_match(1).to_sym
28
27
  return super unless has_attribute?(attr_name)
29
- send(:like, $1 => args.first)
28
+
29
+ send(:like, ::Regexp.last_match(1) => args.first)
30
30
  else
31
31
  super
32
32
  end
@@ -34,7 +34,7 @@ module Arql
34
34
  end
35
35
 
36
36
  class Definition
37
- attr :connection, :ssh_proxy, :options, :models, :namespace_module, :namespace
37
+ attr_reader :connection, :ssh_proxy, :options, :models, :namespace_module, :namespace
38
38
 
39
39
  def redefine
40
40
  @models.each do |model|
@@ -78,16 +78,14 @@ module Arql
78
78
  end
79
79
 
80
80
  order_column = @options[:order_column]
81
- if order_column
82
- @models.each do |model|
83
- model_class = model[:model]
84
- model_class.define_singleton_method(:implicit_order_column) do
85
- if column_names.include?(order_column)
86
- order_column
87
- else
88
- nil
89
- end
90
- end
81
+ return unless order_column
82
+
83
+ @models.each do |model|
84
+ model_class = model[:model]
85
+ model_class.define_singleton_method(:implicit_order_column) do
86
+ return unless column_names.include?(order_column)
87
+
88
+ order_column
91
89
  end
92
90
  end
93
91
  end
@@ -96,11 +94,59 @@ module Arql
96
94
  @model_names_mapping ||= @options[:model_names] || {}
97
95
  end
98
96
 
97
+ def rename_columns_mapping
98
+ @rename_columns_mapping ||= @options[:rename_columns] || {}
99
+ end
100
+
101
+ ARQL_EXTENSION_INSTANCE_METHODS = %i[v t vd to_insert_sql to_upsert_sql dump write_csv write_excel].freeze
102
+
103
+ def validate_rename_columns!(table_name, model_class)
104
+ table_column_names_set = model_class.column_names.to_set
105
+ return unless table_column_names_set.intersect?(rename_columns_mapping.keys.map(&:to_s).to_set)
106
+
107
+ ar_reserved = ActiveRecord::AttributeMethods.dangerous_attribute_methods
108
+
109
+ rename_columns_mapping.each do |old_name, new_name|
110
+ next unless table_column_names_set.include?(old_name.to_s)
111
+
112
+ new_name = new_name.to_s
113
+
114
+ if table_column_names_set.include?(new_name) && new_name != old_name.to_s
115
+ warn "rename_columns: new name '#{new_name}' already exists in table '#{table_name}', skipping rename of '#{old_name}'"
116
+ next
117
+ end
118
+
119
+ if ar_reserved.include?(new_name)
120
+ raise ActiveRecord::DangerousAttributeError,
121
+ "rename_columns: new name '#{new_name}' is a reserved ActiveRecord method"
122
+ end
123
+
124
+ if ARQL_EXTENSION_INSTANCE_METHODS.include?(new_name.to_sym)
125
+ warn "rename_columns: new name '#{new_name}' conflicts with an arql method in table '#{table_name}', skipping rename of '#{old_name}'"
126
+ next
127
+ end
128
+ end
129
+ end
130
+
99
131
  def define_model_from_table(table_name, primary_keys)
100
132
  model_name = make_model_name(table_name)
101
133
  return unless model_name
102
134
 
103
135
  model_class = make_model_class(table_name, primary_keys)
136
+ if rename_columns_mapping.present?
137
+ validate_rename_columns!(table_name, model_class)
138
+ relevant_old_names = rename_columns_mapping.keys.select { |n| model_class.column_names.include?(n.to_s) }
139
+ if relevant_old_names.any?
140
+ model_class.define_attribute_methods
141
+ mod = model_class.send(:generated_attribute_methods)
142
+ relevant_old_names.each do |old_name|
143
+ old_name = old_name.to_s
144
+ [old_name.to_sym, :"#{old_name}=", :"#{old_name}?"].each do |m|
145
+ mod.send(:remove_method, m) if mod.method_defined?(m)
146
+ end
147
+ end
148
+ end
149
+ end
104
150
  @namespace_module.const_set(model_name, model_class)
105
151
  abbr_name = make_model_abbr_name(model_name, table_name)
106
152
  @namespace_module.const_set(abbr_name, model_class)
@@ -110,7 +156,7 @@ module Arql
110
156
  # Object.const_set(abbr_name, model_class)
111
157
  # end
112
158
 
113
- { model: model_class, abbr: "#@namespace::#{abbr_name}", table: table_name }
159
+ { model: model_class, abbr: "#{@namespace}::#{abbr_name}", table: table_name }
114
160
  end
115
161
 
116
162
  def make_model_abbr_name(model_name, table_name)
@@ -149,6 +195,8 @@ module Arql
149
195
 
150
196
  def make_model_class(table_name, primary_keys)
151
197
  options = @options
198
+ rename_columns = rename_columns_mapping
199
+ renamed_old_names = rename_columns.keys.map(&:to_s)
152
200
  Class.new(@base_model) do
153
201
  include Arql::Extension
154
202
  if primary_keys.is_a?(Array) && primary_keys.size > 1
@@ -160,6 +208,16 @@ module Arql
160
208
  self.inheritance_column = nil
161
209
  self.ignored_columns = options[:ignored_columns] if options[:ignored_columns].present?
162
210
  ActiveRecord.default_timezone = :local
211
+
212
+ if renamed_old_names.any?
213
+ define_singleton_method(:instance_method_already_implemented?) do |method_name|
214
+ name = method_name.to_s.sub(/[=?\z]/, '')
215
+ return false if renamed_old_names.include?(name)
216
+
217
+ super(method_name)
218
+ end
219
+ end
220
+
163
221
  if options[:created_at].present?
164
222
  define_singleton_method :timestamp_attributes_for_create do
165
223
  options[:created_at]
@@ -171,6 +229,26 @@ module Arql
171
229
  options[:updated_at]
172
230
  end
173
231
  end
232
+
233
+ if rename_columns.present?
234
+ rename_columns.each do |old_name, new_name|
235
+ new_sym = new_name.to_sym
236
+ define_method(new_sym) { read_attribute(old_name.to_s) }
237
+ define_method(:"#{new_sym}=") { |value| write_attribute(old_name.to_s, value) }
238
+ define_method(:"#{new_sym}?") { !!read_attribute(old_name.to_s) }
239
+ end
240
+ self.attribute_aliases = rename_columns.transform_keys(&:to_sym).invert
241
+
242
+ original_inspect = instance_method(:inspect)
243
+ rename_columns_for_inspect = rename_columns.dup
244
+ define_method(:inspect) do
245
+ result = original_inspect.bind(self).call
246
+ rename_columns_for_inspect.each do |old_name, new_name|
247
+ result = result.sub(/\b#{Regexp.escape(old_name.to_s)}(?=:)/, new_name.to_s)
248
+ end
249
+ result
250
+ end
251
+ end
174
252
  end
175
253
  end
176
254
 
@@ -179,13 +257,14 @@ module Arql
179
257
  Arql::SSHProxy.new(
180
258
  ssh_config.slice(:host, :user, :port, :password)
181
259
  .merge(forward_host: @options[:host],
182
- forward_port: @options[:port],
183
- local_port: ssh_config[:local_port]))
260
+ forward_port: @options[:port],
261
+ local_port: ssh_config[:local_port])
262
+ )
184
263
  end
185
264
 
186
265
  def get_connection_options
187
266
  connect_conf = @options.slice(:adapter, :host, :username, :password,
188
- :database, :encoding, :pool, :port, :socket)
267
+ :database, :encoding, :pool, :port, :socket)
189
268
  connect_conf.merge!(@ssh_proxy.database_host_port) if @ssh_proxy
190
269
  connect_conf
191
270
  end
@@ -216,8 +295,7 @@ module Arql
216
295
  def create_namespace_module(namespace)
217
296
  definition = self
218
297
 
219
- Object.const_set(namespace, Module.new {
220
-
298
+ Object.const_set(namespace, Module.new do
221
299
  define_singleton_method(:config) do
222
300
  definition.options
223
301
  end
@@ -245,7 +323,7 @@ module Arql
245
323
  define_singleton_method(:dump) do |filename, no_create_db = false|
246
324
  Arql::Mysqldump.new(definition.options).dump_database(filename, no_create_db)
247
325
  end
248
- })
326
+ end)
249
327
  end
250
328
  end
251
329
  end
data/lib/arql/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Arql
2
- VERSION = "0.4.13"
2
+ VERSION = "0.4.15"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: arql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.13
4
+ version: 0.4.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Liu Xiang
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-09-29 00:00:00.000000000 Z
11
+ date: 2026-04-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mysql2
@@ -330,6 +330,7 @@ files:
330
330
  - custom-configurations.org
331
331
  - define-associations-zh_CN.org
332
332
  - define-associations.org
333
+ - docs/plans/2026-04-02-rename-columns-design.md
333
334
  - exe/arql
334
335
  - exe/arql_setsid_wrapper
335
336
  - fuzzy-field-query-zh_CN.org