arql 0.3.31 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,369 +2,221 @@ require 'arql/concerns'
2
2
  require 'arql/vd'
3
3
 
4
4
  module Arql
5
- module Extension
6
- extend ActiveSupport::Concern
7
5
 
8
- def t(**options)
9
- puts Terminal::Table.new { |t|
10
- v(**options).each { |row| t << (row || :separator) }
11
- }
6
+ class BaseModel < ::ActiveRecord::Base
7
+ self.abstract_class = true
8
+
9
+ define_singleton_method(:indexes) do
10
+ connection.indexes(table_name).map do |idx|
11
+ {
12
+ Table: idx.table,
13
+ Name: idx.name,
14
+ Columns: idx.columns.join(', '),
15
+ Unique: idx.unique,
16
+ Comment: idx.comment
17
+ }
18
+ end.t
12
19
  end
20
+ end
13
21
 
14
- def vd
15
- VD.new do |vd|
16
- vd << ['Attribute Name', 'Attribute Value', 'SQL Type', 'Comment']
17
- self.class.connection.columns(self.class.table_name).each do |column|
18
- vd << [column.name, read_attribute(column.name), column.sql_type, column.comment || '']
19
- end
20
- end
21
- end
22
+ class Definition
23
+ attr :connection, :ssh_proxy, :options, :models, :namespace_module, :namespace
22
24
 
23
- def v(**options)
24
- t = []
25
- t << ['Attribute Name', 'Attribute Value', 'SQL Type', 'Comment']
26
- t << nil
27
- compact_mode = options[:compact] || false
28
- self.class.connection.columns(self.class.table_name).each do |column|
29
- value = read_attribute(column.name)
30
- if compact_mode && value.blank?
31
- next
32
- end
33
- t << [column.name, value, column.sql_type, column.comment || '']
25
+ def redefine
26
+ @models.each do |model|
27
+ @namespace_module.send :remove_const, model[:model].class_name.to_sym if model[:model]
28
+ @namespace_module.send :remove_const, model[:abbr].sub(/^#{@namespace}::/, '').to_sym if model[:abbr]
34
29
  end
35
- t
36
- end
30
+ @models = []
31
+ @connection.tables.each do |table_name|
32
+ model = define_model_from_table(table_name, @primary_keys[table_name])
33
+ next unless model
37
34
 
38
- def to_insert_sql
39
- self.class.to_insert_sql([self])
35
+ model[:comment] = @comments[table_name]
36
+ @models << model
37
+ end
38
+ App.instance&.load_initializer!
40
39
  end
41
40
 
42
- def to_upsert_sql
43
- self.class.to_upsert_sql([self])
44
- end
41
+ def initialize(options)
42
+ @models = []
43
+ @options = options
44
+ @classify_method = @options[:singularized_table_names] ? :camelize : :classify
45
+ @ssh_proxy = start_ssh_proxy if options[:ssh].present?
46
+ create_connection
45
47
 
46
- def write_csv(filename, *fields, **options)
47
- [self].write_csv(filename, *fields, **options)
48
- end
48
+ tables = @connection.tables
49
+ if @connection.adapter_name == 'Mysql2'
50
+ require 'arql/ext/active_record/connection_adapters/abstract_mysql_adapter'
51
+ @comments = @connection.table_comment_of_tables(tables)
52
+ @primary_keys = @connection.primary_keys_of_tables(tables)
53
+ else
54
+ @comments = tables.map { |t| [t, @connection.table_comment(t)] }.to_h
55
+ @primary_keys = tables.map { |t| [t, @connection.primary_keys(t)] }.to_h
56
+ end
49
57
 
50
- def write_excel(filename, *fields, **options)
51
- [self].write_excel(filename, *fields, **options)
52
- end
58
+ tables.each do |table_name|
59
+ model = define_model_from_table(table_name, @primary_keys[table_name])
60
+ next unless model
53
61
 
54
- def dump(filename, batch_size=500)
55
- [self].dump(filename, batch_size)
62
+ model[:comment] = @comments[table_name]
63
+ @models << model
64
+ end
56
65
  end
57
66
 
58
- included do
67
+ def model_names_mapping
68
+ @model_names_mapping ||= @options[:model_names] || {}
59
69
  end
60
70
 
61
- class_methods do
62
- def t
63
- table_name = Commands::Table::get_table_name(name)
64
- puts "\nTable: #{table_name}"
65
- puts Commands::Table::table_info_table(table_name)
66
- end
67
-
68
- def vd
69
- table_name = Commands::Table::get_table_name(name)
70
- Commands::VD::table_info_vd(table_name)
71
- nil
72
- end
73
-
74
- def v
75
- table_name = Commands::Table::get_table_name(name)
76
- Commands::Table::table_info(table_name)
77
- end
78
- def to_insert_sql(records, batch_size=1)
79
- to_sql(records, :skip, batch_size)
80
- end
81
-
82
- def to_upsert_sql(records, batch_size=1)
83
- to_sql(records, :update, batch_size)
84
- end
71
+ def define_model_from_table(table_name, primary_keys)
72
+ model_name = make_model_name(table_name)
73
+ return unless model_name
85
74
 
86
- def to_sql(records, on_duplicate, batch_size)
87
- records.in_groups_of(batch_size, false).map do |group|
88
- ActiveRecord::InsertAll.new(self, group.map(&:attributes), on_duplicate: on_duplicate).send(:to_sql) + ';'
89
- end.join("\n")
90
- end
75
+ model_class = make_model_class(table_name, primary_keys)
76
+ @namespace_module.const_set(model_name, model_class)
77
+ abbr_name = make_model_abbr_name(model_name, table_name)
78
+ @namespace_module.const_set(abbr_name, model_class)
91
79
 
92
- def to_create_sql
93
- ActiveRecord::Base.connection.exec_query("show create table #{table_name}").rows.last.last
94
- end
80
+ # if Arql::App.instance.environments&.size == 1
81
+ # Object.const_set(model_name, model_class)
82
+ # Object.const_set(abbr_name, model_class)
83
+ # end
95
84
 
96
- def dump(filename, no_create_table=false)
97
- Arql::Mysqldump.new.dump_table(filename, table_name, no_create_table)
98
- end
85
+ { model: model_class, abbr: "#@namespace::#{abbr_name}", table: table_name }
99
86
  end
100
- end
101
-
102
- class Definition
103
- class << self
104
- def models
105
- @@models ||= []
106
- end
107
87
 
108
- def redefine
109
- options = @@options
110
- @@models.each do |model|
111
- Object.send :remove_const, model[:model].name.to_sym if model[:model]
112
- Object.send :remove_const, model[:abbr].to_sym if model[:abbr]
88
+ def make_model_abbr_name(model_name, table_name)
89
+ mapping = model_names_mapping[table_name]
90
+ return mapping[1] if mapping.present? && mapping.is_a?(Array) && mapping.size > 1
91
+
92
+ bare_abbr = model_name.gsub(/[a-z]*/, '')
93
+ model_abbr_name = bare_abbr
94
+ 1000.times do |idx|
95
+ abbr = idx.zero? ? bare_abbr : "#{bare_abbr}#{idx + 1}"
96
+ unless @namespace_module.const_defined?(abbr)
97
+ model_abbr_name = abbr
98
+ break
113
99
  end
114
- @@models = []
115
- ActiveRecord::Base.connection.tap do |conn|
116
- tables = conn.tables
117
- comments = conn.table_comment_of_tables(tables)
118
- primary_keys = conn.primary_keys_of_tables(tables)
119
- tables.each do |table_name|
120
- table_comment = comments[table_name]
121
- primary_keys[table_name].tap do |pkey|
122
- table_name_prefixes = options[:table_name_prefixes] || []
123
- normalized_table_name = table_name_prefixes.each_with_object(table_name.dup) do |prefix, name|
124
- name.sub!(/^#{prefix}/, '')
125
- end
126
- normalized_table_name.send(@@classify_method).tap do |const_name|
127
- const_name = 'Modul' if const_name == 'Module'
128
- const_name = 'Clazz' if const_name == 'Class'
129
- Class.new(::ArqlModel) do
130
- include Arql::Extension
131
- if pkey.is_a?(Array) && pkey.size > 1
132
- self.primary_keys = pkey
133
- else
134
- self.primary_key = pkey&.first
135
- end
136
- self.table_name = table_name
137
- self.inheritance_column = nil
138
- ActiveRecord.default_timezone = :local
139
- if options[:created_at].present?
140
- define_singleton_method :timestamp_attributes_for_create do
141
- options[:created_at]
142
- end
143
- end
144
-
145
- if options[:updated_at].present?
146
- define_singleton_method :timestamp_attributes_for_update do
147
- options[:updated_at]
148
- end
149
- end
150
- end.tap do |clazz|
151
- Object.const_set(const_name, clazz).tap do |const|
152
- const_name.gsub(/[a-z]*/, '').tap do |bare_abbr|
153
- abbr_const = nil
154
- 9.times do |idx|
155
- abbr = idx.zero? ? bare_abbr : "#{bare_abbr}#{idx+1}"
156
- unless Object.const_defined?(abbr)
157
- Object.const_set abbr, const
158
- abbr_const = abbr
159
- break
160
- end
161
- end
162
-
163
- @@models << {
164
- model: const,
165
- abbr: abbr_const,
166
- table: table_name,
167
- comment: table_comment
168
- }
169
- end
170
- end
171
- end
172
- end
173
- end
174
- end
175
- end
176
-
177
- App.instance&.load_initializer!
178
100
  end
101
+ model_abbr_name
179
102
  end
180
103
 
181
- def initialize(options)
182
- @@options = options
183
- @@models = []
184
- @@classify_method = if @@options[:singularized_table_names]
185
- :camelize
186
- else
187
- :classify
188
- end
189
- ActiveRecord::Base.connection.tap do |conn|
190
- Object.const_set('ArqlModel', Class.new(ActiveRecord::Base) do
191
- include ::Arql::Concerns::TableDataDefinition
192
- self.abstract_class = true
193
-
194
- define_singleton_method(:indexes) do
195
- conn.indexes(table_name).map do |idx|
196
- {
197
- Table: idx.table,
198
- Name: idx.name,
199
- Columns: idx.columns.join(', '),
200
- Unique: idx.unique,
201
- Comment: idx.comment
202
- }
203
- end.t
204
- end
205
- end)
206
-
207
- tables = conn.tables
208
- if conn.adapter_name == 'Mysql2'
209
- require 'arql/ext/active_record/connection_adapters/abstract_mysql_adapter'
210
- comments = conn.table_comment_of_tables(tables)
211
- primary_keys = conn.primary_keys_of_tables(tables)
104
+ def make_model_name(table_name)
105
+ mapping = model_names_mapping[table_name]
106
+ if mapping.present?
107
+ return mapping if mapping.is_a?(String)
108
+ return mapping.first if mapping.is_a?(Array) && mapping.size >= 1
109
+ end
110
+ table_name_prefixes = @options[:table_name_prefixes] || []
111
+ model_name = table_name_prefixes.each_with_object(table_name.dup) do |prefix, name|
112
+ name.sub!(/^#{prefix}/, '')
113
+ end.send(@classify_method)
114
+ model_name.gsub!(/^Module|Class|BaseModel$/, '$&0')
115
+ if model_name !~ /^[A-Z][A-Za-z0-9_]*$/
116
+ warn "Could not make model name from table name: #{table_name}"
117
+ return
118
+ end
119
+ model_name
120
+ end
121
+
122
+ def make_model_class(table_name, primary_keys)
123
+ options = @options
124
+ Class.new(@base_model) do
125
+ include Arql::Extension
126
+ if primary_keys.is_a?(Array) && primary_keys.size > 1
127
+ self.primary_keys = primary_keys
212
128
  else
213
- comments = tables.map { |t| [t, conn.table_comment(t)] }.to_h
214
- primary_keys = tables.map { |t| [t, conn.primary_keys(t)] }.to_h
129
+ self.primary_key = primary_keys&.first
130
+ end
131
+ self.table_name = table_name
132
+ self.inheritance_column = nil
133
+ ActiveRecord.default_timezone = :local
134
+ if options[:created_at].present?
135
+ define_singleton_method :timestamp_attributes_for_create do
136
+ options[:created_at]
137
+ end
215
138
  end
216
139
 
217
- tables.each do |table_name|
218
- table_comment = comments[table_name]
219
- primary_keys[table_name].tap do |pkey|
220
- table_name_prefixes = options[:table_name_prefixes] || []
221
- normalized_table_name = table_name_prefixes.each_with_object(table_name.dup) do |prefix, name|
222
- name.sub!(/^#{prefix}/, '')
223
- end
224
- normalized_table_name.send(@@classify_method).tap do |const_name|
225
- const_name = 'Modul' if const_name == 'Module'
226
- const_name = 'Clazz' if const_name == 'Class'
227
- if const_name !~ /^[A-Z][A-Za-z0-9_]*$/
228
- puts "Invalid class name: #{const_name}, skipped."
229
- next
230
- end
231
- Class.new(::ArqlModel) do
232
- include Arql::Extension
233
- if pkey.is_a?(Array) && pkey.size > 1
234
- self.primary_keys = pkey
235
- else
236
- self.primary_key = pkey&.first
237
- end
238
- self.table_name = table_name
239
- self.inheritance_column = nil
240
- ActiveRecord.default_timezone = :local
241
- if options[:created_at].present?
242
- define_singleton_method :timestamp_attributes_for_create do
243
- options[:created_at]
244
- end
245
- end
246
-
247
- if options[:updated_at].present?
248
- define_singleton_method :timestamp_attributes_for_update do
249
- options[:updated_at]
250
- end
251
- end
252
- end.tap do |clazz|
253
- Object.const_set(const_name, clazz).tap do |const|
254
- const_name.gsub(/[a-z]*/, '').tap do |bare_abbr|
255
- abbr_const = nil
256
- 9.times do |idx|
257
- abbr = idx.zero? ? bare_abbr : "#{bare_abbr}#{idx+1}"
258
- unless Object.const_defined?(abbr)
259
- Object.const_set abbr, const
260
- abbr_const = abbr
261
- break
262
- end
263
- end
264
-
265
- @@models << {
266
- model: const,
267
- abbr: abbr_const,
268
- table: table_name,
269
- comment: table_comment
270
- }
271
- end
272
- end
273
- end
274
- end
140
+ if options[:updated_at].present?
141
+ define_singleton_method :timestamp_attributes_for_update do
142
+ options[:updated_at]
275
143
  end
276
144
  end
277
145
  end
278
146
  end
279
147
 
280
- ::ActiveRecord::Relation.class_eval do
281
- def t(*attrs, **options)
282
- records.t(*attrs, **options)
283
- end
284
-
285
- def vd(*attrs, **options)
286
- records.vd(*attrs, **options)
287
- end
288
-
289
- def v
290
- records.v
291
- end
292
-
293
- def a
294
- to_a
295
- end
296
-
297
- def write_csv(filename, *fields, **options)
298
- records.write_csv(filename, *fields, **options)
299
- end
300
-
301
- def write_excel(filename, *fields, **options)
302
- records.write_excel(filename, *fields, **options)
303
- end
304
-
305
- def dump(filename, batch_size=500)
306
- records.dump(filename, batch_size)
307
- end
148
+ def start_ssh_proxy
149
+ ssh_config = @options[:ssh]
150
+ Arql::SSHProxy.new(
151
+ ssh_config.slice(:host, :user, :port, :password)
152
+ .merge(forward_host: @options[:host],
153
+ forward_port: @options[:port],
154
+ local_port: ssh_config[:local_port]))
308
155
  end
309
156
 
310
- ::ActiveRecord::Result.class_eval do
311
- def t(*attrs, **options)
312
- to_a.t(*attrs, **options)
313
- end
314
-
315
- def vd(*attrs, **options)
316
- to_a.vd(*attrs, **options)
317
- end
318
-
319
- def v
320
- to_a.v
321
- end
157
+ def get_connection_options
158
+ connect_conf = @options.slice(:adapter, :host, :username, :password,
159
+ :database, :encoding, :pool, :port, :socket)
160
+ connect_conf.merge(@ssh_proxy.database_host_port) if @ssh_proxy
161
+ connect_conf
162
+ end
322
163
 
323
- def a
324
- to_a
164
+ def create_connection
165
+ @namespace = @options[:namespace]
166
+ connection_opts = get_connection_options
167
+ print "Establishing DB connection to #{connection_opts[:host]}:#{connection_opts[:port]}"
168
+ @namespace_module = create_namespace_module(@namespace)
169
+ @base_model = @namespace_module.const_set('BaseModel', Class.new(BaseModel))
170
+ @base_model.class_eval do
171
+ include ::Arql::Concerns::TableDataDefinition
172
+ self.abstract_class = true
173
+ establish_connection(connection_opts)
174
+ class << self
175
+ attr_accessor :definition
176
+ end
325
177
  end
326
-
327
- def write_csv(filename, *fields, **options)
328
- to_a.write_csv(filename, *fields, **options)
178
+ print "\u001b[2K"
179
+ puts "\rDB connection to #{connection_opts[:host]}:#{connection_opts[:port]} established\n"
180
+ @connection = @base_model.connection
181
+ @base_model.define_singleton_method(:dump) do |filename, no_create_db = false|
182
+ Arql::Mysqldump.new(options).dump_database(filename, no_create_db)
329
183
  end
184
+ @base_model.definition = self
185
+ end
330
186
 
331
- def write_excel(filename, *fields, **options)
332
- to_a.write_excel(filename, *fields, **options)
333
- end
187
+ def create_namespace_module(namespace)
188
+ definition = self
334
189
 
335
- def dump(filename, batch_size=500)
336
- to_a.dump(filename, batch_size)
337
- end
338
- end
190
+ Object.const_set(namespace, Module.new {
339
191
 
340
- ::Ransack::Search.class_eval do
341
- def t(*attrs, **options)
342
- result.t(*attrs, **options)
343
- end
192
+ define_singleton_method(:config) do
193
+ definition.options
194
+ end
344
195
 
345
- def vd(*attrs, **options)
346
- result.vd(*attrs, **options)
347
- end
196
+ define_singleton_method(:models) do
197
+ definition.models.map { |m| m[:model] }
198
+ end
348
199
 
349
- def v
350
- result.v
351
- end
200
+ define_singleton_method(:tables) do
201
+ definition.models.map { |m| m[:table] }
202
+ end
352
203
 
353
- def a
354
- result.a
355
- end
204
+ define_singleton_method(:model_names) do
205
+ models.map(&:name)
206
+ end
356
207
 
357
- def write_csv(filename, *fields, **options)
358
- result.write_csv(filename, *fields, **options)
359
- end
208
+ define_singleton_method(:q) do |sql|
209
+ definition.connection.exec_query(sql)
210
+ end
360
211
 
361
- def write_excel(filename, *fields, **options)
362
- result.write_excel(filename, *fields, **options)
363
- end
212
+ define_singleton_method(:create_table) do |table_name, **options, &blk|
213
+ definition.connection.create_table(table_name, **options, &blk)
214
+ end
364
215
 
365
- def dump(filename, batch_size=500)
366
- result.dump(filename, batch_size)
367
- end
216
+ define_singleton_method(:dump) do |filename, no_create_db = false|
217
+ Arql::Mysqldump.new(definition.options).dump_database(filename, no_create_db)
218
+ end
219
+ })
368
220
  end
369
221
  end
370
222
  end
@@ -0,0 +1,29 @@
1
+ ActiveRecord::Relation.class_eval do
2
+ def t(*attrs, **options)
3
+ records.t(*attrs, **options)
4
+ end
5
+
6
+ def vd(*attrs, **options)
7
+ records.vd(*attrs, **options)
8
+ end
9
+
10
+ def v
11
+ records.v
12
+ end
13
+
14
+ def a
15
+ to_a
16
+ end
17
+
18
+ def write_csv(filename, *fields, **options)
19
+ records.write_csv(filename, *fields, **options)
20
+ end
21
+
22
+ def write_excel(filename, *fields, **options)
23
+ records.write_excel(filename, *fields, **options)
24
+ end
25
+
26
+ def dump(filename, batch_size=500)
27
+ records.dump(filename, batch_size)
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ ActiveRecord::Result.class_eval do
2
+ def t(*attrs, **options)
3
+ to_a.t(*attrs, **options)
4
+ end
5
+
6
+ def vd(*attrs, **options)
7
+ to_a.vd(*attrs, **options)
8
+ end
9
+
10
+ def v
11
+ to_a.v
12
+ end
13
+
14
+ def a
15
+ to_a
16
+ end
17
+
18
+ def write_csv(filename, *fields, **options)
19
+ to_a.write_csv(filename, *fields, **options)
20
+ end
21
+
22
+ def write_excel(filename, *fields, **options)
23
+ to_a.write_excel(filename, *fields, **options)
24
+ end
25
+
26
+ def dump(filename, batch_size=500)
27
+ to_a.dump(filename, batch_size)
28
+ end
29
+ end
@@ -17,6 +17,7 @@ class Array
17
17
  end
18
18
 
19
19
  def t(*attrs, **options)
20
+ format = options[:format] || :terminal
20
21
  if (attrs.present? || options.present? && options[:except]) && present? && first.is_a?(ActiveRecord::Base)
21
22
  column_names = first.attribute_names.map(&:to_sym)
22
23
  attrs = attrs.flat_map { |e| e.is_a?(Regexp) ? column_names.grep(e) : e }.uniq
@@ -32,16 +33,36 @@ class Array
32
33
  # attrs = attrs.select { |e| any { |r| r.attributes[e.to_s]&.present? } }
33
34
  # end
34
35
  puts Terminal::Table.new { |t|
36
+ t.style = table_style_for_format(format)
35
37
  t << attrs
36
38
  t << :separator
37
39
  each do |e|
38
40
  t << e.attributes.values_at(*attrs.map(&:to_s))
39
41
  end
42
+ }.try { |e|
43
+ case format
44
+ when 'md'
45
+ e.to_s.lines.map { |l| ' ' + l }.join
46
+ when 'org'
47
+ e.to_s.lines.map { |l| ' ' + l.gsub(/^\+|\+$/, '|') }.join
48
+ else
49
+ e.to_s
50
+ end
40
51
  }
41
52
  else
42
53
  table = Terminal::Table.new { |t|
54
+ t.style = table_style_for_format(format)
43
55
  v(**options).each { |row| t << (row || :separator)}
44
- }.to_s
56
+ }.try { |e|
57
+ case format
58
+ when 'md'
59
+ e.to_s.lines.map { |l| ' ' + l }.join
60
+ when 'org'
61
+ e.to_s.lines.map { |l| ' ' + l.gsub(/^\+|\+$/, '|') }.join
62
+ else
63
+ e.to_s
64
+ end
65
+ }
45
66
 
46
67
  terminal_width = `tput cols`.to_i
47
68
  if table.lines.first.size > terminal_width
@@ -220,4 +241,22 @@ class Array
220
241
  plot.render(STDOUT)
221
242
  end
222
243
  end
244
+
245
+ def table_style_for_format(format)
246
+ case format.to_s
247
+ when 'md'
248
+ {
249
+ border_top: false,
250
+ border_bottom: false,
251
+ border_i: '|'
252
+ }
253
+ when 'org'
254
+ {
255
+ border_top: false,
256
+ border_bottom: false,
257
+ }
258
+ else
259
+ {}
260
+ end
261
+ end
223
262
  end