arfi 0.5.0 → 1.0.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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.env.sample +2 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +8 -1
  5. data/.rubocop_todo.yml +12 -0
  6. data/CHANGELOG.md +94 -0
  7. data/CODE_OF_CONDUCT.md +5 -1
  8. data/CONTRIBUTING.md +26 -0
  9. data/README.md +327 -118
  10. data/SECURITY.md +17 -0
  11. data/Steepfile +1 -29
  12. data/compose.yml +33 -0
  13. data/docscribe.yml +92 -0
  14. data/gemfiles/rails_6_0.gemfile +11 -0
  15. data/gemfiles/rails_6_1.gemfile +11 -0
  16. data/gemfiles/rails_7_0.gemfile +9 -0
  17. data/gemfiles/rails_7_1.gemfile +10 -0
  18. data/gemfiles/rails_7_2.gemfile +10 -0
  19. data/gemfiles/rails_8_0.gemfile +10 -0
  20. data/gemfiles/rails_8_1.gemfile +10 -0
  21. data/lib/arfi/cli.rb +24 -9
  22. data/lib/arfi/commands/f_idx.rb +5 -230
  23. data/lib/arfi/commands/functions.rb +128 -0
  24. data/lib/arfi/commands/functions_candidates.rb +133 -0
  25. data/lib/arfi/commands/functions_creation.rb +157 -0
  26. data/lib/arfi/commands/functions_helpers.rb +181 -0
  27. data/lib/arfi/commands/functions_paths.rb +131 -0
  28. data/lib/arfi/commands/functions_rendering.rb +137 -0
  29. data/lib/arfi/commands/init.rb +88 -0
  30. data/lib/arfi/commands/project.rb +5 -51
  31. data/lib/arfi/errors.rb +15 -3
  32. data/lib/arfi/extensions/active_record/base.rb +33 -23
  33. data/lib/arfi/extensions/active_record/connection_adapters/postgresql/database_statements.rb +159 -24
  34. data/lib/arfi/sql_function_loader.rb +291 -88
  35. data/lib/arfi/tasks/db.rake +60 -18
  36. data/lib/arfi/version.rb +1 -1
  37. data/lib/arfi.rb +2 -0
  38. data/rbs_collection.lock.yaml +93 -61
  39. data/sig/compat/active_record_base_compat.rbs +17 -0
  40. data/sig/compat/thor_dsl.rbs +8 -0
  41. data/sig/lib/arfi/commands/f_idx.rbs +5 -103
  42. data/sig/lib/arfi/commands/functions.rbs +99 -0
  43. data/sig/lib/arfi/commands/functions_candidates.rbs +25 -0
  44. data/sig/lib/arfi/commands/functions_creation.rbs +29 -0
  45. data/sig/lib/arfi/commands/functions_helpers.rbs +41 -0
  46. data/sig/lib/arfi/commands/functions_paths.rbs +25 -0
  47. data/sig/lib/arfi/commands/functions_rendering.rbs +25 -0
  48. data/sig/lib/arfi/commands/init.rbs +22 -0
  49. data/sig/lib/arfi/commands/project.rbs +5 -24
  50. data/sig/lib/arfi/extensions/active_record/base.rbs +5 -10
  51. data/sig/lib/arfi/extensions/active_record/connection_adapters/postgresql/database_statements.rbs +28 -0
  52. data/sig/lib/arfi/sql_function_loader.rbs +45 -87
  53. data/sig/lib/arfi/tasks/db.rbs +3 -0
  54. data/sig/lib/arfi/version.rbs +1 -1
  55. metadata +125 -14
@@ -1,152 +1,355 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Arfi
4
- # +Arfi::SqlFunctionLoader+ is a class which loads user defined SQL functions into database.
4
+ # Loads user-defined SQL functions into the database by executing SQL files located under `db/functions`.
5
+ #
6
+ # Supported directory layout (Option B: explicit public):
7
+ #
8
+ # db/functions/public/*.sql (generic public)
9
+ # db/functions/postgresql/public/*.sql (postgres public)
10
+ # db/functions/postgresql/<schema>/*.sql (postgres schema)
11
+ # db/functions/mysql/public/*.sql (mysql/trilogy public)
12
+ #
13
+ # Backward-compatible legacy aliases:
14
+ #
15
+ # db/functions/*.sql (generic public legacy)
16
+ # db/functions/postgresql/*.sql (postgres public legacy)
17
+ # db/functions/mysql/*.sql (mysql public legacy)
18
+ #
19
+ # Rules:
20
+ # - underscore-prefixed files are ignored (_shared.sql)
21
+ # - adapter-specific overrides generic when schema+filename matches
22
+ #
23
+ # @api public
5
24
  class SqlFunctionLoader
6
25
  class << self
7
- # +Arfi::SqlFunctionLoader.load!+ -> (nil | void)
26
+ # Load all SQL function files into the database.
8
27
  #
9
- # Loads user defined SQL functions into database.
28
+ # Handles both single-DB and multi-DB setups. Uses the given connection or infers it.
10
29
  #
11
- # @param task_name [String|nil] Name of the task.
12
- # @return [nil] if there is no `db/functions` directory.
13
- # @return [void] if there is no errors.
14
- def load!(task_name: nil)
15
- self.task_name = task_name[/([^:]+$)/] if task_name
16
- return puts 'No SQL files found. Skipping db population with ARFI' unless sql_files.any?
30
+ # @param [String?] task_name Optional task name for logging (e.g. 'db:migrate')
31
+ # @param [ActiveRecord::ConnectionAdapters::AbstractAdapter?] connection Specific connection to load into
32
+ # @param [Boolean] clear_active_connections Whether to clear connections after loading
33
+ # @param [Boolean] verbose Whether to log each loaded file
34
+ # @return [void]
35
+ def load!(task_name: nil, connection: nil, clear_active_connections: true, verbose: true)
36
+ task_short = task_name ? task_name[/([^:]+$)/] : nil
37
+ conn = connection || default_connection
38
+
39
+ raise_unless_supported_adapter(conn)
17
40
 
18
- raise_unless_supported_adapter
19
- handle_db_population
20
- conn.close
41
+ if connection.nil? && multi_db? && task_name.nil?
42
+ populate_multiple_db(verbose: verbose, task_name: task_short)
43
+ else
44
+ populate_db(conn, verbose: verbose, task_name: task_short)
45
+ end
46
+ ensure
47
+ clear_active_connections_if_needed(clear_active_connections)
21
48
  end
22
49
 
23
50
  private
24
51
 
25
- attr_accessor :task_name
26
-
27
- # +Arfi::SqlFunctionLoader#raise_unless_supported_adapter+ -> void
28
- #
29
- # Checks if the database adapter is supported.
52
+ # Raise unless the connection adapter is one of the supported types.
30
53
  #
31
- # @!visibility private
32
54
  # @private
55
+ # @param [ActiveRecord::ConnectionAdapters::AbstractAdapter] conn Database connection
56
+ # @raise [Arfi::Errors::AdapterNotSupported] If adapter is not PostgreSQL, Mysql2, or Trilogy
33
57
  # @return [void]
34
- # @raise [Arfi::Errors::AdapterNotSupported]
35
- def raise_unless_supported_adapter
36
- allowed = %w[ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
37
- ActiveRecord::ConnectionAdapters::Mysql2Adapter].freeze
38
- return if allowed.include?(conn.class.to_s) # steep:ignore ArgumentTypeMismatch
58
+ def raise_unless_supported_adapter(conn) # rubocop:disable SortedMethodsByCall/Waterfall
59
+ allowed = %w[
60
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
61
+ ActiveRecord::ConnectionAdapters::Mysql2Adapter
62
+ ActiveRecord::ConnectionAdapters::TrilogyAdapter
63
+ ].freeze
39
64
 
40
- raise Arfi::Errors::AdapterNotSupported
65
+ raise Arfi::Errors::AdapterNotSupported unless allowed.include?(conn.class.name)
41
66
  end
42
67
 
43
- # +Arfi::SqlFunctionLoader#handle_db_population+ -> void
68
+ # Check whether the Rails app has multiple database configurations for the current environment.
44
69
  #
45
- # Loads user defined SQL functions into database. This conditional branch was written this way because if we
46
- # call db:migrate:db_name, then task_name will not be nil, but it will be zero if we call db:migrate. Then we
47
- # check that the application has been configured to work with multiple databases in order to populate all
48
- # databases, and only after this check can we populate the database in case the db:migrate (or any other) task
49
- # has been called for configuration with a single database. Go to `lib/arfi/tasks/db.rake` for additional info.
50
- #
51
- # @!visibility private
52
70
  # @private
53
- # @return [void]
54
- def handle_db_population
55
- if task_name || (task_name && multi_db?) || task_name.nil?
56
- populate_db
57
- elsif multi_db?
58
- populate_multiple_db
59
- end
60
- end
61
-
62
- # +Arfi::SqlFunctionLoader#multi_db?+ -> Boolean
63
- #
64
- # Checks if the application has been configured to work with multiple databases.
65
- #
66
- # @return [Boolean]
71
+ # @return [Boolean] Whether multi-DB is configured
67
72
  def multi_db?
68
73
  ActiveRecord::Base.configurations.configurations.count { _1.env_name == Rails.env } > 1 # steep:ignore NoMethod
69
74
  end
70
75
 
71
- # +Arfi::SqlFunctionLoader#populate_multiple_db+ -> void
76
+ # Load functions into all databases in a multi-DB setup.
72
77
  #
73
- # Loads user defined SQL functions into all databases.
78
+ # Saves the original connection and restores it after iterating all configs,
79
+ # matching the pattern used in `run_with_connection_switch` (db.rake).
74
80
  #
75
- # @!visibility private
76
81
  # @private
82
+ # @param [Boolean] verbose Whether to log each loaded file
83
+ # @param [String?] task_name Optional task name for logging
77
84
  # @return [void]
78
- # @see Arfi::SqlFunctionLoader#multi_db?
79
- # @see Arfi::SqlFunctionLoader#populate_db
80
- def populate_multiple_db
85
+ def populate_multiple_db(verbose:, task_name: nil)
86
+ original = current_db_config
81
87
  # steep:ignore:start
82
88
  ActiveRecord::Base.configurations.configurations.select { _1.env_name == Rails.env }.each do |config|
83
89
  ActiveRecord::Base.establish_connection(config)
84
- populate_db
90
+ populate_db(default_connection, verbose: verbose, task_name: task_name)
85
91
  end
86
92
  # steep:ignore:end
93
+ ensure
94
+ ActiveRecord::Base.establish_connection(original) if original
87
95
  end
88
96
 
89
- # +Arfi::SqlFunctionLoader#populate_db+ -> void
97
+ # Get the default database connection, handling Rails version differences.
90
98
  #
91
- # Loads user defined SQL functions into database.
99
+ # @private
100
+ # @return [ActiveRecord::ConnectionAdapters::AbstractAdapter] Default connection
101
+ def default_connection
102
+ if Rails::VERSION::MAJOR < 7 || (Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR < 2)
103
+ ActiveRecord::Base.connection
104
+ else
105
+ ActiveRecord::Base.lease_connection
106
+ end
107
+ end
108
+
109
+ # Get the current database config, or nil if no connection is established.
110
+ #
111
+ # @private
112
+ # @raise [StandardError]
113
+ # @return [ActiveRecord::DatabaseConfig, nil] Current database config, or nil if no connection is established
114
+ def current_db_config
115
+ ActiveRecord::Base.connection_db_config # steep:ignore NoMethod
116
+ rescue StandardError
117
+ nil
118
+ end
119
+
120
+ # Load SQL function files into a single database connection.
92
121
  #
93
- # @!visibility private
94
122
  # @private
123
+ # @param [ActiveRecord::ConnectionAdapters::AbstractAdapter] conn Database connection
124
+ # @param [Boolean] verbose Whether to log each loaded file
125
+ # @param [String?] task_name Optional task name for logging
95
126
  # @return [void]
96
- def populate_db
97
- sql_files.each do |file|
98
- sql = File.read(file).strip
127
+ def populate_db(conn, verbose:, task_name:)
128
+ files = sql_files(conn)
129
+ if files.empty?
130
+ log(conn, "No SQL files found for adapter #{conn.class}. Skipping db population with ARFI")
131
+ return
132
+ end
133
+
134
+ files.each { |file| load_sql_file(conn, file, verbose, task_name) }
135
+ end
136
+
137
+ # Clear all active database connections if requested, handling Rails version differences.
138
+ #
139
+ # @private
140
+ # @param [Boolean] clear Whether to clear connections
141
+ # @return [void]
142
+ def clear_active_connections_if_needed(clear)
143
+ return unless clear && defined?(ActiveRecord::Base)
144
+
145
+ if ActiveRecord::Base.respond_to?(:connection_handler) &&
146
+ ActiveRecord::Base.connection_handler.respond_to?(:clear_active_connections!)
147
+ ActiveRecord::Base.connection_handler.clear_active_connections!
148
+ elsif ActiveRecord::Base.respond_to?(:clear_active_connections!)
149
+ ActiveRecord::Base.clear_active_connections!
150
+ end
151
+ end
152
+
153
+ # Load a single SQL file into the database, wrapping errors with file context.
154
+ #
155
+ # @private
156
+ # @param [ActiveRecord::ConnectionAdapters::AbstractAdapter] conn Database connection
157
+ # @param [Pathname, String] file Path to the SQL file
158
+ # @param [Boolean] verbose Whether to log the loaded file
159
+ # @param [String?] task_name Optional task name for logging
160
+ # @raise [StandardError] If the SQL execution fails (re-raised with file context)
161
+ # @return [void]
162
+ def load_sql_file(conn, file, verbose, task_name)
163
+ sql = File.read(file.to_s).strip
164
+ return if sql.empty?
165
+
166
+ begin
99
167
  conn.execute(sql)
100
- puts "[ARFI] Loaded: #{File.basename(file)} into #{conn.pool.db_config.env_name} #{conn.pool.db_config.name}"
168
+ rescue StandardError => e
169
+ raise e.class, "#{e.message}\n[ARFI] while loading #{file}", e.backtrace
101
170
  end
171
+ return unless verbose
172
+
173
+ log_sql_load(conn, file, task_name)
102
174
  end
103
175
 
104
- # +Arfi::SqlFunctionLoader#sql_files+ -> Array<String>
176
+ # Log that a SQL file was successfully loaded into the database.
105
177
  #
106
- # Helper method to get list of SQL files. Here we check if we need to populate all databases or just one.
178
+ # @private
179
+ # @param [ActiveRecord::ConnectionAdapters::AbstractAdapter] conn Database connection
180
+ # @param [Pathname, String] file Path to the loaded SQL file
181
+ # @param [String?] task_name Optional task name for logging
182
+ # @return [void]
183
+ def log_sql_load(conn, file, task_name)
184
+ label = "[ARFI] Loaded: #{File.basename(file)} into #{safe_db_env(conn)} #{safe_db_name(conn)}"
185
+ label += " (#{task_name})" if task_name
186
+ log(conn, label)
187
+ end
188
+
189
+ # Get the database environment name safely, returning empty string on error.
107
190
  #
108
- # @!visibility private
109
191
  # @private
110
- # @return [Array<String>] List of SQL files.
111
- # @see Arfi::SqlFunctionLoader#load!
112
- # @see Arfi::SqlFunctionLoader#multi_db?
113
- # @see Arfi::SqlFunctionLoader#sql_functions_by_adapter
114
- def sql_files
115
- if task_name || multi_db?
116
- sql_functions_by_adapter
192
+ # @param [ActiveRecord::ConnectionAdapters::AbstractAdapter] conn Database connection
193
+ # @raise [StandardError]
194
+ # @return [String] Environment name, or empty string on error
195
+ # @return [String] if StandardError
196
+ def safe_db_env(conn)
197
+ conn.pool&.db_config&.env_name.to_s
198
+ rescue StandardError
199
+ ''
200
+ end
201
+
202
+ # Get the database name safely, returning empty string on error.
203
+ #
204
+ # @private
205
+ # @param [ActiveRecord::ConnectionAdapters::AbstractAdapter] conn Database connection
206
+ # @raise [StandardError]
207
+ # @return [String] Database name, or empty string on error
208
+ # @return [String] if StandardError
209
+ def safe_db_name(conn)
210
+ conn.pool&.db_config&.name.to_s
211
+ rescue StandardError
212
+ ''
213
+ end
214
+
215
+ # Log a message via Rails.logger or stdout.
216
+ #
217
+ # @private
218
+ # @param [ActiveRecord::ConnectionAdapters::AbstractAdapter] _conn Database connection (unused)
219
+ # @param [String] msg Message to log
220
+ # @return [void]
221
+ def log(_conn, msg)
222
+ if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
223
+ Rails.logger.info(msg)
117
224
  else
118
- Dir.glob(Rails.root.join('db', 'functions').join('*.sql'))
225
+ $stdout.puts(msg)
119
226
  end
120
227
  end
121
228
 
122
- # +Arfi::SqlFunctionLoader#sql_functions_by_adapter+ -> Array<String>
229
+ # Collect all SQL files to load for the given connection, applying override resolution.
123
230
  #
124
- # Helper method to get list of SQL files for specific database adapter.
231
+ # @private
232
+ # @param [ActiveRecord::ConnectionAdapters::AbstractAdapter] conn Database connection
233
+ # @return [Array<String>] Ordered list of SQL file paths
234
+ def sql_files(conn)
235
+ root = Rails.root.join('db', 'functions')
236
+ return [] unless root.directory?
237
+
238
+ generic = [
239
+ collect_sql(glob: root.join('*.sql'), schema: 'public', priority: 1),
240
+ collect_sql(glob: root.join('public', '*.sql'), schema: 'public', priority: 2)
241
+ ].flatten
242
+
243
+ adapter_root = adapter_root_for(conn, root)
244
+ return finalize_items(generic) unless adapter_root&.directory?
245
+
246
+ finalize_items(generic + collect_adapter_sql_files(conn, adapter_root))
247
+ end
248
+
249
+ # Collect adapter-specific SQL files, dispatching to the correct strategy based on adapter type.
125
250
  #
126
- # @!visibility private
127
251
  # @private
128
- # @return [Array<String>] List of SQL files.
129
- # @raise [Arfi::Errors::AdapterNotSupported] if database adapter is not supported.
130
- def sql_functions_by_adapter
131
- case conn
132
- when ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
133
- Dir.glob(Rails.root.join('db', 'functions', 'postgresql').join('*.sql'))
134
- when ActiveRecord::ConnectionAdapters::Mysql2Adapter
135
- Dir.glob(Rails.root.join('db', 'functions', 'mysql').join('*.sql'))
252
+ # @param [ActiveRecord::ConnectionAdapters::AbstractAdapter] conn Database connection
253
+ # @param [Pathname] adapter_root Adapter root directory (e.g. db/functions/postgresql)
254
+ # @raise [Arfi::Errors::AdapterNotSupported] If adapter is not supported
255
+ # @return [Array<Arfi::sql_file_item>] List of SQL file items
256
+ def collect_adapter_sql_files(conn, adapter_root)
257
+ case conn.class.name
258
+ when 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter'
259
+ collect_postgresql_sql_files(adapter_root)
260
+ when 'ActiveRecord::ConnectionAdapters::Mysql2Adapter', 'ActiveRecord::ConnectionAdapters::TrilogyAdapter'
261
+ collect_adapter_public_sql_files(adapter_root)
136
262
  else
137
263
  raise Arfi::Errors::AdapterNotSupported
138
264
  end
139
265
  end
140
266
 
141
- # +Arfi::SqlFunctionLoader#conn+ -> ActiveRecord::ConnectionAdapters::AbstractAdapter
267
+ # Collect SQL files for PostgreSQL, including schema subdirectories.
268
+ #
269
+ # @private
270
+ # @param [Pathname] adapter_root PostgreSQL adapter root directory
271
+ # @return [Array<Arfi::sql_file_item>] List of SQL file items
272
+ def collect_postgresql_sql_files(adapter_root)
273
+ items = collect_adapter_public_sql_files(adapter_root)
274
+
275
+ Dir.children(adapter_root).sort.each do |child|
276
+ next if child.start_with?('_')
277
+ next if child == 'public'
278
+
279
+ dir = adapter_root.join(child)
280
+ next unless dir.directory?
281
+
282
+ items.concat collect_sql(glob: dir.join('*.sql'), schema: child, priority: 10)
283
+ end
284
+
285
+ items
286
+ end
287
+
288
+ # Collect SQL files from the adapter's public directory (legacy + explicit).
289
+ #
290
+ # @private
291
+ # @param [Pathname] adapter_root Adapter root directory
292
+ # @return [Array<Arfi::sql_file_item>] List of SQL file items
293
+ def collect_adapter_public_sql_files(adapter_root)
294
+ items = [] # steep:ignore
295
+ items.concat collect_sql(glob: adapter_root.join('*.sql'), schema: 'public', priority: 8)
296
+ items.concat collect_sql(glob: adapter_root.join('public', '*.sql'), schema: 'public', priority: 9)
297
+ items
298
+ end
299
+
300
+ # Collect SQL files matching a glob pattern, skipping underscore-prefixed files.
142
301
  #
143
- # Helper method to get database connection.
302
+ # @private
303
+ # @param [Pathname, String] glob Glob pattern to match SQL files
304
+ # @param [String] schema Schema name to assign to matched files
305
+ # @param [Integer] priority Priority for override resolution (higher = preferred)
306
+ # @return [Array<Arfi::sql_file_item>] List of SQL file items
307
+ def collect_sql(glob:, schema:, priority:)
308
+ Dir.glob(glob.to_s).filter_map do |path|
309
+ base = File.basename(path)
310
+ next if base.start_with?('_')
311
+
312
+ { schema: schema, base: base, path: path, priority: priority }
313
+ end
314
+ end
315
+
316
+ # Finalize the file list by selecting the highest-priority item per schema/filename key.
144
317
  #
145
- # @!visibility private
146
318
  # @private
147
- # @return [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection.
148
- def conn
149
- ActiveRecord::Base.lease_connection
319
+ # @param [Array<Arfi::sql_file_item>] items All collected SQL file items
320
+ # @return [Array<String>] Ordered list of chosen file paths
321
+ def finalize_items(items)
322
+ # @type var chosen: Hash[String, { schema: String, base: String, path: String, priority: Integer }]
323
+ chosen = {}
324
+
325
+ items.each do |item|
326
+ key = "#{item[:schema]}/#{item[:base]}"
327
+ prev = chosen[key]
328
+ chosen[key] = item if prev.nil? || item[:priority] > prev[:priority]
329
+ end
330
+
331
+ chosen.values
332
+ .sort_by { |item| [item[:schema], item[:base]] }
333
+ .map { |item| item[:path] }
334
+ end
335
+
336
+ # Resolve the adapter-specific root directory for the given connection.
337
+ #
338
+ # @private
339
+ # @param [ActiveRecord::ConnectionAdapters::AbstractAdapter] conn Database connection
340
+ # @param [Pathname] root Project root (db/functions)
341
+ # @raise [Arfi::Errors::AdapterNotSupported] If adapter is not supported
342
+ # @return [Pathname] Adapter root directory
343
+ def adapter_root_for(conn, root)
344
+ case conn.class.name
345
+ when 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter'
346
+ root.join('postgresql')
347
+ when 'ActiveRecord::ConnectionAdapters::Mysql2Adapter',
348
+ 'ActiveRecord::ConnectionAdapters::TrilogyAdapter'
349
+ root.join('mysql')
350
+ else
351
+ raise Arfi::Errors::AdapterNotSupported
352
+ end
150
353
  end
151
354
  end
152
355
  end
@@ -1,38 +1,80 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rake'
4
+ require 'active_record'
4
5
  require 'arfi/sql_function_loader'
5
6
 
6
7
  namespace :_db do
7
8
  task :arfi_enhance do
8
- Arfi::SqlFunctionLoader.load!
9
+ # For non-suffixed tasks (db:migrate / db:prepare etc),
10
+ # loader will populate all DBs when multi-db and task_name is nil.
11
+ Arfi::SqlFunctionLoader.load!(verbose: true)
9
12
  end
10
13
  end
11
14
 
12
15
  Rake::Task.define_task(:environment) unless Rake::Task.task_defined?(:environment)
13
16
 
14
- # Enhancing single db tasks
15
- %w[db:migrate db:schema:load db:setup].each do |task|
17
+ # Enhance common single-db tasks (and multi-db "default" tasks)
18
+ %w[
19
+ db:migrate
20
+ db:schema:load
21
+ db:setup
22
+ db:prepare
23
+ db:test:prepare
24
+ ].each do |task|
16
25
  Rake::Task[task].enhance(['_db:arfi_enhance']) if Rake::Task.task_defined?(task)
17
26
  end
18
27
 
19
- # We remove user defined keys in database.yml and use only the ones about RDBMS connections.
20
- # Here we try to find tasks like db:migrate:your_db_name and enhance them as well as tasks for single db connections.
21
- rdbms_configs = Rails.configuration.database_configuration[Rails.env].select { |_k, v| v.is_a?(Hash) }.keys
22
- possible_tasks = Rake::Task.tasks.select do |task|
23
- task.name.match?(/^(db:migrate:|db:schema:load:|db:setup:|db:prepare:|db:test:prepare:)(\w+)$/)
28
+ def define_arfi_enhance_for(task, db_name)
29
+ Rake::Task.define_task("_db:arfi_enhance:#{task.name}") do
30
+ run_arfi_loader_for(task, arfi_db_config_for(db_name))
31
+ end
32
+ task.enhance(["_db:arfi_enhance:#{task.name}"])
33
+ end
34
+
35
+ def run_arfi_loader_for(task, config_hash)
36
+ if config_hash
37
+ run_with_connection_switch(task, config_hash)
38
+ else
39
+ run_arfi_loader(task)
40
+ end
41
+ end
42
+
43
+ def run_with_connection_switch(task, config_hash)
44
+ original = current_db_config
45
+ ActiveRecord::Base.establish_connection(config_hash)
46
+ run_arfi_loader(task)
47
+ ensure
48
+ ActiveRecord::Base.establish_connection(original) if original
49
+ end
50
+
51
+ def current_db_config
52
+ ActiveRecord::Base.connection_db_config
53
+ rescue StandardError
54
+ nil
55
+ end
56
+
57
+ def run_arfi_loader(task)
58
+ Arfi::SqlFunctionLoader.load!(
59
+ task_name: task.name,
60
+ connection: ActiveRecord::Base.connection,
61
+ verbose: true
62
+ )
24
63
  end
25
- possible_tasks = possible_tasks.select do |task|
26
- rdbms_configs.any? { |n| task.name.include?(n) }
64
+
65
+ def arfi_db_config_for(name)
66
+ return unless ActiveRecord::Base.respond_to?(:configurations) && ActiveRecord::Base.configurations
67
+
68
+ list = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: name)
69
+ cfg = Array(list).first
70
+ cfg&.configuration_hash
27
71
  end
28
72
 
29
- # In general, this is a small trick due to the fact that Rake does not allow you to view parent tasks from a task that
30
- # was called for enhancing. Moreover, the utility does not provide for passing parameters to the called task.
31
- # To get around this limitation, we dynamically create a task with the name we need, and then pass this name as
32
- # an argument to the method.
73
+ # Enhance suffixed tasks: db:migrate:<db>, db:schema:load:<db>, etc.
74
+ pattern = /^(db:migrate:|db:schema:load:|db:setup:|db:prepare:|db:test:prepare:)([^:]+)$/
75
+ possible_tasks = Rake::Task.tasks.select { |t| t.name.match?(pattern) }
76
+
33
77
  possible_tasks.each do |task|
34
- Rake::Task.define_task("_db:arfi_enhance:#{task.name}") do
35
- Arfi::SqlFunctionLoader.load!(task_name: task.name)
36
- end
37
- task.enhance(["_db:arfi_enhance:#{task.name}"])
78
+ db_name = task.name.match(pattern)[2]
79
+ define_arfi_enhance_for(task, db_name)
38
80
  end
data/lib/arfi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Arfi
4
- VERSION = '0.5.0'
4
+ VERSION = '1.0.0'
5
5
  end
data/lib/arfi.rb CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  require_relative 'arfi/version'
4
4
  require_relative 'arfi/errors'
5
+ require_relative 'arfi/cli'
6
+ require_relative 'arfi/commands/f_idx'
5
7
  require 'arfi/extensions/extensions'
6
8
  require 'rails' if defined?(Rails)
7
9