arfi 0.4.0 → 0.5.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 +4 -4
- data/.rspec +3 -0
- data/.rubocop.yml +9 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +267 -0
- data/Rakefile +12 -0
- data/Steepfile +35 -0
- data/exe/arfi +7 -0
- data/lib/arfi/Rakefile +6 -0
- data/lib/arfi/cli.rb +23 -0
- data/lib/arfi/commands/f_idx.rb +238 -0
- data/lib/arfi/commands/project.rb +59 -0
- data/lib/arfi/errors.rb +30 -0
- data/lib/arfi/extensions/active_record/base.rb +36 -0
- data/lib/arfi/extensions/active_record/connection_adapters/postgresql/database_statements.rb +32 -0
- data/lib/arfi/extensions/extensions.rb +7 -0
- data/lib/arfi/railtie.rb +15 -0
- data/lib/arfi/sql_function_loader.rb +153 -0
- data/lib/arfi/tasks/db.rake +38 -0
- data/lib/arfi/version.rb +5 -0
- data/lib/arfi.rb +11 -0
- data/rakelib/yard_docs_generator.rake +29 -0
- data/rbs_collection.lock.yaml +452 -0
- data/rbs_collection.yaml +19 -0
- data/sig/arfi.rbs +2 -0
- data/sig/lib/arfi/cli.rbs +6 -0
- data/sig/lib/arfi/commands/f_idx.rbs +107 -0
- data/sig/lib/arfi/commands/project.rbs +28 -0
- data/sig/lib/arfi/errors.rbs +21 -0
- data/sig/lib/arfi/extensions/active_record/base.rbs +14 -0
- data/sig/lib/arfi/railtie.rbs +4 -0
- data/sig/lib/arfi/sql_function_loader.rbs +94 -0
- data/sig/lib/arfi/version.rbs +3 -0
- metadata +61 -10
@@ -0,0 +1,238 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
require 'rails'
|
5
|
+
require File.expand_path('config/environment', Dir.pwd)
|
6
|
+
|
7
|
+
module Arfi
|
8
|
+
module Commands
|
9
|
+
# +Arfi::Commands::FIdx+ module contains commands for manipulating functional index in Rails project.
|
10
|
+
class FIdx < Thor
|
11
|
+
ADAPTERS = %i[postgresql mysql].freeze
|
12
|
+
|
13
|
+
# steep:ignore:start
|
14
|
+
desc 'create FUNCTION_NAME [--template=template_file --adapter=adapter]', 'Initialize the functional index'
|
15
|
+
option :template, type: :string, banner: 'template_file',
|
16
|
+
desc: 'Path to the template file. See `README.md` for details.'
|
17
|
+
option :adapter, type: :string,
|
18
|
+
desc: 'Specify database adapter, used for projects with multiple database architecture. ' \
|
19
|
+
"Available adapters: #{ADAPTERS.join(', ')}",
|
20
|
+
banner: 'adapter'
|
21
|
+
# steep:ignore:end
|
22
|
+
|
23
|
+
# +Arfi::Commands::FIdx#create+ -> void
|
24
|
+
#
|
25
|
+
# This command is used to create the functional index.
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
# bundle exec arfi f_idx create some_function
|
29
|
+
#
|
30
|
+
# ARFI also supports the use of custom templates for SQL functions, but now there are some restrictions and rules
|
31
|
+
# according to which it is necessary to describe the function. First, the function must be written in a
|
32
|
+
# Ruby-compatible syntax: the file name is not so important, but the name for the function name must be
|
33
|
+
# interpolated with the +index_name+ variable name, and the function itself must be placed in the HEREDOC
|
34
|
+
# statement. Below is an example file.
|
35
|
+
#
|
36
|
+
# @example
|
37
|
+
# # ./template/my_custom_template
|
38
|
+
# <<~SQL
|
39
|
+
# CREATE OR REPLACE FUNCTION #{index_name}() RETURNS TEXT[]
|
40
|
+
# LANGUAGE SQL
|
41
|
+
# IMMUTABLE AS
|
42
|
+
# $$
|
43
|
+
# -- Function body here
|
44
|
+
# $$
|
45
|
+
# SQL
|
46
|
+
#
|
47
|
+
# To use a custom template, add the --template flag.
|
48
|
+
#
|
49
|
+
# @example
|
50
|
+
# bundle exec arfi f_idx create some_function --template ./template/my_custom_template
|
51
|
+
#
|
52
|
+
# @param index_name [String] Name of the index.
|
53
|
+
# @return [void]
|
54
|
+
# @raise [Arfi::Errors::InvalidSchemaFormat] if ActiveRecord.schema_format is not :ruby
|
55
|
+
# @raise [Arfi::Errors::NoFunctionsDir] if there is no `db/functions` directory
|
56
|
+
# @see Arfi::Commands::FIdx#validate_schema_format!
|
57
|
+
def create(index_name)
|
58
|
+
validate_schema_format!
|
59
|
+
content = build_sql_function(index_name)
|
60
|
+
create_function_file(index_name, content)
|
61
|
+
end
|
62
|
+
|
63
|
+
# steep:ignore:start
|
64
|
+
desc 'destroy INDEX_NAME [--revision=revision --adapter=adapter]', 'Delete the functional index.'
|
65
|
+
option :revision, type: :string, banner: 'revision', desc: 'Revision of the function.'
|
66
|
+
option :adapter, type: :string,
|
67
|
+
desc: 'Specify database adapter, used for projects with multiple database architecture. ' \
|
68
|
+
"Available adapters: #{ADAPTERS.join(', ')}",
|
69
|
+
banner: 'adapter'
|
70
|
+
# steep:ignore:end
|
71
|
+
|
72
|
+
# +Arfi::Commands::FIdx#destroy+ -> void
|
73
|
+
#
|
74
|
+
# This command is used to delete the functional index.
|
75
|
+
#
|
76
|
+
# @example
|
77
|
+
# bundle exec arfi f_idx destroy some_function [revision index (just an integer, 1 is by default)]
|
78
|
+
# @param index_name [String] Name of the index.
|
79
|
+
# @return [void]
|
80
|
+
# @raise [Arfi::Errors::InvalidSchemaFormat] if ActiveRecord.schema_format is not :ruby
|
81
|
+
def destroy(index_name)
|
82
|
+
validate_schema_format!
|
83
|
+
|
84
|
+
revision = Integer(options[:revision] || '01') # steep:ignore NoMethod
|
85
|
+
revision = "0#{revision}"
|
86
|
+
FileUtils.rm("#{functions_dir}/#{index_name}_v#{revision}.sql")
|
87
|
+
puts "Deleted: #{functions_dir}/#{index_name}_v#{revision}.sql"
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# +Arfi::Commands::FIdx#validate_schema_format!+ -> void
|
93
|
+
#
|
94
|
+
# Helper method to validate the schema format.
|
95
|
+
#
|
96
|
+
# @!visibility private
|
97
|
+
# @private
|
98
|
+
# @raise [Arfi::Errors::InvalidSchemaFormat] if ActiveRecord.schema_format is not :ruby.
|
99
|
+
# @return [nil] if the schema format is valid.
|
100
|
+
def validate_schema_format!
|
101
|
+
raise Arfi::Errors::InvalidSchemaFormat unless ActiveRecord.schema_format == :ruby # steep:ignore NoMethod
|
102
|
+
end
|
103
|
+
|
104
|
+
# +Arfi::Commands::FIdx#build_sql_function+ -> String
|
105
|
+
#
|
106
|
+
# Helper method to build the SQL function.
|
107
|
+
#
|
108
|
+
# @!visibility private
|
109
|
+
# @private
|
110
|
+
# @param index_name [String] Name of the index.
|
111
|
+
# @return [String] SQL function body.
|
112
|
+
def build_sql_function(index_name) # rubocop:disable Metrics/MethodLength
|
113
|
+
return build_from_file(index_name) if options[:template] # steep:ignore NoMethod
|
114
|
+
|
115
|
+
unless options[:adapter] # steep:ignore NoMethod
|
116
|
+
return <<~SQL
|
117
|
+
CREATE OR REPLACE FUNCTION #{index_name}() RETURNS TEXT[]
|
118
|
+
LANGUAGE SQL
|
119
|
+
IMMUTABLE AS
|
120
|
+
$$
|
121
|
+
-- Function body here
|
122
|
+
$$
|
123
|
+
SQL
|
124
|
+
end
|
125
|
+
|
126
|
+
case options[:adapter] # steep:ignore NoMethod
|
127
|
+
when 'postgresql'
|
128
|
+
<<~SQL
|
129
|
+
CREATE OR REPLACE FUNCTION #{index_name}() RETURNS TEXT[]
|
130
|
+
LANGUAGE SQL
|
131
|
+
IMMUTABLE AS
|
132
|
+
$$
|
133
|
+
-- Function body here
|
134
|
+
$$
|
135
|
+
SQL
|
136
|
+
when 'mysql'
|
137
|
+
<<~SQL
|
138
|
+
CREATE FUNCTION #{index_name} ()
|
139
|
+
RETURNS return_type
|
140
|
+
BEGIN
|
141
|
+
-- Function body here
|
142
|
+
END;
|
143
|
+
SQL
|
144
|
+
else
|
145
|
+
# steep:ignore:start
|
146
|
+
raise "Unknown adapter: #{options[:adapter]}. Supported adapters: #{ADAPTERS.join(', ')}"
|
147
|
+
# steep:ignore:end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# +Arfi::Commands::FIdx#build_from_file+ -> String
|
152
|
+
#
|
153
|
+
# Helper method to build the SQL function. Used with flag `--template`.
|
154
|
+
#
|
155
|
+
# @!visibility private
|
156
|
+
# @private
|
157
|
+
# @param index_name [String] Name of the index.
|
158
|
+
# @return [String] SQL function body.
|
159
|
+
# @see Arfi::Commands::FIdx#create
|
160
|
+
# @see Arfi::Commands::FIdx#build_sql_function
|
161
|
+
def build_from_file(index_name)
|
162
|
+
# steep:ignore:start
|
163
|
+
RubyVM::InstructionSequence.compile("index_name = '#{index_name}'; #{File.read(options[:template])}").eval
|
164
|
+
# steep:ignore:end
|
165
|
+
end
|
166
|
+
|
167
|
+
# +Arfi::Commands::FIdx#create_function_file+ -> void
|
168
|
+
#
|
169
|
+
# Helper method to create the index file.
|
170
|
+
#
|
171
|
+
# @!visibility private
|
172
|
+
# @private
|
173
|
+
# @param index_name [String] Name of the index.
|
174
|
+
# @param content [String] SQL function body.
|
175
|
+
# @return [void]
|
176
|
+
def create_function_file(index_name, content)
|
177
|
+
existing_files = Dir.glob("#{functions_dir}/#{index_name}*.sql")
|
178
|
+
|
179
|
+
return write_file(index_name, content, 1) if existing_files.empty?
|
180
|
+
|
181
|
+
latest_version = extract_latest_version(existing_files)
|
182
|
+
write_file(index_name, content, latest_version.succ)
|
183
|
+
end
|
184
|
+
|
185
|
+
# +Arfi::Commands::FIdx#extract_latest_version+ -> Integer
|
186
|
+
#
|
187
|
+
# Helper method to extract the latest version of the index.
|
188
|
+
#
|
189
|
+
# @!visibility private
|
190
|
+
# @private
|
191
|
+
# @param files [Array<String>] List of files.
|
192
|
+
# @return [String] Latest version of the index.
|
193
|
+
def extract_latest_version(files)
|
194
|
+
version_numbers = files.map do |file|
|
195
|
+
File.basename(file)[/\w+_v(\d+)\.sql/, 1]
|
196
|
+
end.compact
|
197
|
+
|
198
|
+
version_numbers.max
|
199
|
+
end
|
200
|
+
|
201
|
+
# +Arfi::Commands::FIdx#write_file+ -> void
|
202
|
+
#
|
203
|
+
# Helper method to write the index file.
|
204
|
+
#
|
205
|
+
# @!visibility private
|
206
|
+
# @private
|
207
|
+
# @param index_name [String] Name of the index.
|
208
|
+
# @param content [String] SQL function body.
|
209
|
+
# @param version [String|Integer] Version of the index.
|
210
|
+
# @return [void]
|
211
|
+
def write_file(index_name, content, version)
|
212
|
+
version_str = format('%02d', version)
|
213
|
+
path = "#{functions_dir}/#{index_name}_v#{version_str}.sql"
|
214
|
+
File.write(path, content.to_s)
|
215
|
+
puts "Created: #{path}"
|
216
|
+
end
|
217
|
+
|
218
|
+
# +Arfi::Commands::FIdx#functions_dir+ -> Pathname
|
219
|
+
#
|
220
|
+
# Helper method to get path to `db/functions` directory.
|
221
|
+
#
|
222
|
+
# @!visibility private
|
223
|
+
# @private
|
224
|
+
# @return [Pathname] Path to `db/functions` directory
|
225
|
+
def functions_dir
|
226
|
+
# steep:ignore:start
|
227
|
+
if options[:adapter]
|
228
|
+
raise Arfi::Errors::AdapterNotSupported unless ADAPTERS.include?(options[:adapter].to_sym)
|
229
|
+
|
230
|
+
Rails.root.join("db/functions/#{options[:adapter]}")
|
231
|
+
# steep:ignore:end
|
232
|
+
else
|
233
|
+
Rails.root.join('db/functions')
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'rails'
|
6
|
+
|
7
|
+
module Arfi
|
8
|
+
module Commands
|
9
|
+
# +Arfi::Commands::Project+ class is used to create `db/functions` directory.
|
10
|
+
class Project < Thor
|
11
|
+
ADAPTERS = %i[postgresql mysql].freeze
|
12
|
+
|
13
|
+
# steep:ignore:start
|
14
|
+
desc 'create', 'Initialize project by creating db/functions directory'
|
15
|
+
option :adapter, type: :string,
|
16
|
+
desc: 'Specify database adapter, used for projects with multiple database architecture. ' \
|
17
|
+
"Available adapters: #{ADAPTERS.join(', ')}",
|
18
|
+
banner: 'adapter'
|
19
|
+
# steep:ignore:end
|
20
|
+
|
21
|
+
# +Arfi::Commands::Project#create+ -> void
|
22
|
+
#
|
23
|
+
# This command is used to create `db/functions` directory.
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
# bundle exec arfi project create
|
27
|
+
# @return [void]
|
28
|
+
# @raise [Arfi::Errors::InvalidSchemaFormat] if ActiveRecord.schema_format is not :ruby.
|
29
|
+
def create
|
30
|
+
raise Arfi::Errors::InvalidSchemaFormat unless ActiveRecord.schema_format == :ruby # steep:ignore NoMethod
|
31
|
+
return puts "Directory #{functions_dir} already exists" if Dir.exist?(functions_dir)
|
32
|
+
|
33
|
+
FileUtils.mkdir_p(functions_dir)
|
34
|
+
puts "Created: #{functions_dir}"
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# +Arfi::Commands::Project#functions_dir+ -> Pathname
|
40
|
+
#
|
41
|
+
# Helper method to get path to `db/functions` directory.
|
42
|
+
#
|
43
|
+
# @!visibility private
|
44
|
+
# @private
|
45
|
+
# @return [Pathname] Path to `db/functions` directory
|
46
|
+
def functions_dir
|
47
|
+
# steep:ignore:start
|
48
|
+
if options[:adapter]
|
49
|
+
raise Arfi::Errors::AdapterNotSupported unless ADAPTERS.include?(options[:adapter].to_sym)
|
50
|
+
|
51
|
+
Rails.root.join("db/functions/#{options[:adapter]}")
|
52
|
+
# steep:ignore:end
|
53
|
+
else
|
54
|
+
Rails.root.join('db/functions')
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/arfi/errors.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Arfi
|
4
|
+
module Errors
|
5
|
+
# This error is raised when there is no `db/functions` directory
|
6
|
+
class NoFunctionsDir < StandardError
|
7
|
+
def initialize(message =
|
8
|
+
'There is no such directory: db/functions. Did you run `bundle exec arfi project:create`?')
|
9
|
+
@message = message
|
10
|
+
super
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# This error is raised when Rails project schema format is not +schema.rb+
|
15
|
+
class InvalidSchemaFormat < StandardError
|
16
|
+
def initialize(message = 'Invalid schema format. ARFI supports only ruby format schemas.')
|
17
|
+
@message = message
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# This error is raised when database adapter is not supported (for e.g., SQLite3).
|
23
|
+
class AdapterNotSupported < StandardError
|
24
|
+
def initialize(message = 'Adapter not supported')
|
25
|
+
@message = message
|
26
|
+
super
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record'
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
class Base # :nodoc:
|
7
|
+
# +ActiveRecord::Base.function_exists?+ -> bool
|
8
|
+
#
|
9
|
+
# This method checks if a custom SQL function exists in the database.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# ActiveRecord::Base.function_exists?('my_function') #=> true
|
13
|
+
# ActiveRecord::Base.function_exists?('my_function123') #=> false
|
14
|
+
# @param [String] function_name The name of the function to check.
|
15
|
+
# @return [Boolean] Returns true if the function exists, false otherwise.
|
16
|
+
def self.function_exists?(function_name) # rubocop:disable Metrics/MethodLength
|
17
|
+
case connection
|
18
|
+
when ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
|
19
|
+
connection.execute("SELECT * FROM pg_proc WHERE proname = '#{function_name}'").any?
|
20
|
+
when ActiveRecord::ConnectionAdapters::Mysql2Adapter
|
21
|
+
sql = <<~SQL
|
22
|
+
SELECT 1
|
23
|
+
FROM information_schema.ROUTINES
|
24
|
+
WHERE ROUTINE_TYPE = 'FUNCTION'
|
25
|
+
AND ROUTINE_SCHEMA = '#{connection.current_database}'
|
26
|
+
AND ROUTINE_NAME = '#{function_name}'
|
27
|
+
LIMIT 1;
|
28
|
+
SQL
|
29
|
+
|
30
|
+
!!connection.execute(sql).first
|
31
|
+
else
|
32
|
+
raise ActiveRecord::AdapterNotFound, "adapter #{connection.class.name} is not supported"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record/connection_adapters/postgresql/database_statements'
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
module ConnectionAdapters
|
7
|
+
module PostgreSQL
|
8
|
+
module DatabaseStatements
|
9
|
+
# This patch is used for db:prepare task.
|
10
|
+
def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
|
11
|
+
log(sql, name, async: async) do |notification_payload|
|
12
|
+
with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
|
13
|
+
result = conn.async_exec(sql)
|
14
|
+
verified!
|
15
|
+
handle_warnings(result)
|
16
|
+
notification_payload[:row_count] = result.count
|
17
|
+
result
|
18
|
+
rescue => e
|
19
|
+
if e.message.match?(/ERROR:\s{2}function (\S+\(\w+\)) does not exist/)
|
20
|
+
function_name = e.message.match(/ERROR:\s{2}function (\S+\(\w+\)) does not exist/)[1][/^[^(]*/]
|
21
|
+
if (function_file = Dir.glob(Rails.root.join('db', 'functions', "#{function_name}_v*.sql").to_s).first)
|
22
|
+
conn.async_exec(File.read(function_file).chop)
|
23
|
+
retry
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/arfi/railtie.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'arfi'
|
4
|
+
require 'rails'
|
5
|
+
|
6
|
+
module Arfi
|
7
|
+
class Railtie < ::Rails::Railtie # :nodoc:
|
8
|
+
railtie_name :arfi
|
9
|
+
|
10
|
+
rake_tasks do
|
11
|
+
path = File.expand_path(__dir__ || '.')
|
12
|
+
Dir.glob("#{path}/tasks/**/*.rake").each { |f| load f }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Arfi
|
4
|
+
# +Arfi::SqlFunctionLoader+ is a class which loads user defined SQL functions into database.
|
5
|
+
class SqlFunctionLoader
|
6
|
+
class << self
|
7
|
+
# +Arfi::SqlFunctionLoader.load!+ -> (nil | void)
|
8
|
+
#
|
9
|
+
# Loads user defined SQL functions into database.
|
10
|
+
#
|
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?
|
17
|
+
|
18
|
+
raise_unless_supported_adapter
|
19
|
+
handle_db_population
|
20
|
+
conn.close
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_accessor :task_name
|
26
|
+
|
27
|
+
# +Arfi::SqlFunctionLoader#raise_unless_supported_adapter+ -> void
|
28
|
+
#
|
29
|
+
# Checks if the database adapter is supported.
|
30
|
+
#
|
31
|
+
# @!visibility private
|
32
|
+
# @private
|
33
|
+
# @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
|
39
|
+
|
40
|
+
raise Arfi::Errors::AdapterNotSupported
|
41
|
+
end
|
42
|
+
|
43
|
+
# +Arfi::SqlFunctionLoader#handle_db_population+ -> void
|
44
|
+
#
|
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
|
+
# @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]
|
67
|
+
def multi_db?
|
68
|
+
ActiveRecord::Base.configurations.configurations.count { _1.env_name == Rails.env } > 1 # steep:ignore NoMethod
|
69
|
+
end
|
70
|
+
|
71
|
+
# +Arfi::SqlFunctionLoader#populate_multiple_db+ -> void
|
72
|
+
#
|
73
|
+
# Loads user defined SQL functions into all databases.
|
74
|
+
#
|
75
|
+
# @!visibility private
|
76
|
+
# @private
|
77
|
+
# @return [void]
|
78
|
+
# @see Arfi::SqlFunctionLoader#multi_db?
|
79
|
+
# @see Arfi::SqlFunctionLoader#populate_db
|
80
|
+
def populate_multiple_db
|
81
|
+
# steep:ignore:start
|
82
|
+
ActiveRecord::Base.configurations.configurations.select { _1.env_name == Rails.env }.each do |config|
|
83
|
+
ActiveRecord::Base.establish_connection(config)
|
84
|
+
populate_db
|
85
|
+
end
|
86
|
+
# steep:ignore:end
|
87
|
+
end
|
88
|
+
|
89
|
+
# +Arfi::SqlFunctionLoader#populate_db+ -> void
|
90
|
+
#
|
91
|
+
# Loads user defined SQL functions into database.
|
92
|
+
#
|
93
|
+
# @!visibility private
|
94
|
+
# @private
|
95
|
+
# @return [void]
|
96
|
+
def populate_db
|
97
|
+
sql_files.each do |file|
|
98
|
+
sql = File.read(file).strip
|
99
|
+
conn.execute(sql)
|
100
|
+
puts "[ARFI] Loaded: #{File.basename(file)} into #{conn.pool.db_config.env_name} #{conn.pool.db_config.name}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# +Arfi::SqlFunctionLoader#sql_files+ -> Array<String>
|
105
|
+
#
|
106
|
+
# Helper method to get list of SQL files. Here we check if we need to populate all databases or just one.
|
107
|
+
#
|
108
|
+
# @!visibility private
|
109
|
+
# @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
|
117
|
+
else
|
118
|
+
Dir.glob(Rails.root.join('db', 'functions').join('*.sql'))
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# +Arfi::SqlFunctionLoader#sql_functions_by_adapter+ -> Array<String>
|
123
|
+
#
|
124
|
+
# Helper method to get list of SQL files for specific database adapter.
|
125
|
+
#
|
126
|
+
# @!visibility private
|
127
|
+
# @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'))
|
136
|
+
else
|
137
|
+
raise Arfi::Errors::AdapterNotSupported
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# +Arfi::SqlFunctionLoader#conn+ -> ActiveRecord::ConnectionAdapters::AbstractAdapter
|
142
|
+
#
|
143
|
+
# Helper method to get database connection.
|
144
|
+
#
|
145
|
+
# @!visibility private
|
146
|
+
# @private
|
147
|
+
# @return [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection.
|
148
|
+
def conn
|
149
|
+
ActiveRecord::Base.lease_connection
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rake'
|
4
|
+
require 'arfi/sql_function_loader'
|
5
|
+
|
6
|
+
namespace :_db do
|
7
|
+
task :arfi_enhance do
|
8
|
+
Arfi::SqlFunctionLoader.load!
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
Rake::Task.define_task(:environment) unless Rake::Task.task_defined?(:environment)
|
13
|
+
|
14
|
+
# Enhancing single db tasks
|
15
|
+
%w[db:migrate db:schema:load db:setup].each do |task|
|
16
|
+
Rake::Task[task].enhance(['_db:arfi_enhance']) if Rake::Task.task_defined?(task)
|
17
|
+
end
|
18
|
+
|
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+)$/)
|
24
|
+
end
|
25
|
+
possible_tasks = possible_tasks.select do |task|
|
26
|
+
rdbms_configs.any? { |n| task.name.include?(n) }
|
27
|
+
end
|
28
|
+
|
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.
|
33
|
+
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}"])
|
38
|
+
end
|
data/lib/arfi/version.rb
ADDED
data/lib/arfi.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'arfi/version'
|
4
|
+
require_relative 'arfi/errors'
|
5
|
+
require 'arfi/extensions/extensions'
|
6
|
+
require 'rails' if defined?(Rails)
|
7
|
+
|
8
|
+
# Top level module
|
9
|
+
module Arfi
|
10
|
+
require_relative 'arfi/railtie' if defined?(Rails)
|
11
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yard'
|
4
|
+
|
5
|
+
# run as `rm -rf arfi_docs && bundle exec rake docs:generate && bundle exec rake docs:push`
|
6
|
+
|
7
|
+
namespace :docs do
|
8
|
+
desc 'Generate new docs and push them repo'
|
9
|
+
task :generate do
|
10
|
+
puts 'Generating docs...'
|
11
|
+
args = %w[--no-cache --private --protected --readme README.md --no-progress --output-dir doc]
|
12
|
+
YARD::CLI::Yardoc.run(*args)
|
13
|
+
puts 'Docs generated'
|
14
|
+
end
|
15
|
+
|
16
|
+
desc 'Push docs to repo'
|
17
|
+
task :push do
|
18
|
+
puts 'Copying docs...'
|
19
|
+
`git clone git@github.com:unurgunite/arfi_docs.git`
|
20
|
+
Dir.chdir('arfi_docs') do
|
21
|
+
cp_r('../doc/.', '.', remove_destination: true)
|
22
|
+
`git add .`
|
23
|
+
`git commit -m "Update docs #{Time.now.utc}"`
|
24
|
+
`git push`
|
25
|
+
end
|
26
|
+
rm_rf('arfi_docs')
|
27
|
+
puts 'Docs pushed'
|
28
|
+
end
|
29
|
+
end
|