fx 0.9.0 → 0.10.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -2
  3. data/CHANGELOG.md +27 -1
  4. data/Gemfile +0 -1
  5. data/bin/rake +2 -3
  6. data/bin/rspec +13 -3
  7. data/bin/yard +13 -3
  8. data/fx.gemspec +3 -3
  9. data/lib/fx/adapters/postgres/connection.rb +12 -4
  10. data/lib/fx/adapters/postgres/functions.rb +11 -28
  11. data/lib/fx/adapters/postgres/query_executor.rb +34 -0
  12. data/lib/fx/adapters/postgres/triggers.rb +14 -29
  13. data/lib/fx/adapters/postgres.rb +10 -10
  14. data/lib/fx/configuration.rb +2 -2
  15. data/lib/fx/schema_dumper.rb +6 -14
  16. data/lib/fx/statements.rb +61 -58
  17. data/lib/fx/version.rb +1 -1
  18. data/lib/generators/fx/function/function_generator.rb +50 -53
  19. data/lib/generators/fx/function/templates/db/migrate/create_function.erb +1 -1
  20. data/lib/generators/fx/function/templates/db/migrate/update_function.erb +1 -1
  21. data/lib/generators/fx/migration_helper.rb +53 -0
  22. data/lib/generators/fx/name_helper.rb +33 -0
  23. data/lib/generators/fx/trigger/templates/db/migrate/create_trigger.erb +1 -1
  24. data/lib/generators/fx/trigger/templates/db/migrate/update_trigger.erb +1 -1
  25. data/lib/generators/fx/trigger/trigger_generator.rb +45 -64
  26. data/lib/generators/fx/version_helper.rb +55 -0
  27. data/spec/acceptance/user_manages_functions_spec.rb +6 -6
  28. data/spec/acceptance/user_manages_triggers_spec.rb +10 -10
  29. data/spec/acceptance_helper.rb +2 -2
  30. data/spec/features/functions/migrations_spec.rb +4 -4
  31. data/spec/features/functions/revert_spec.rb +4 -4
  32. data/spec/features/triggers/migrations_spec.rb +6 -6
  33. data/spec/features/triggers/revert_spec.rb +8 -8
  34. data/spec/fx/adapters/postgres/functions_spec.rb +12 -5
  35. data/spec/fx/adapters/postgres/query_executor_spec.rb +75 -0
  36. data/spec/fx/adapters/postgres/triggers_spec.rb +14 -7
  37. data/spec/fx/adapters/postgres_spec.rb +62 -20
  38. data/spec/fx/definition_spec.rb +6 -6
  39. data/spec/fx/schema_dumper_spec.rb +60 -14
  40. data/spec/fx_spec.rb +1 -1
  41. data/spec/generators/fx/function/function_generator_spec.rb +10 -10
  42. data/spec/generators/fx/migration_helper_spec.rb +133 -0
  43. data/spec/generators/fx/name_helper_spec.rb +114 -0
  44. data/spec/generators/fx/trigger/trigger_generator_spec.rb +42 -19
  45. data/spec/generators/fx/version_helper_spec.rb +157 -0
  46. data/spec/spec_helper.rb +2 -0
  47. data/spec/support/generator_setup.rb +46 -5
  48. metadata +28 -11
data/lib/fx/statements.rb CHANGED
@@ -4,13 +4,13 @@ module Fx
4
4
  # Create a new database function.
5
5
  #
6
6
  # @param name [String, Symbol] The name of the database function.
7
- # @param version [Fixnum] The version number of the function, used to
7
+ # @param version [Integer] The version number of the function, used to
8
8
  # find the definition file in `db/functions`. This defaults to `1` if
9
9
  # not provided.
10
10
  # @param sql_definition [String] The SQL query for the function schema.
11
11
  # If both `sql_definition` and `version` are provided,
12
- # `sql_definition` takes prescedence.
13
- # @return The database response from executing the create statement.
12
+ # `sql_definition` takes precedence.
13
+ # @return [void] The database response from executing the create statement.
14
14
  #
15
15
  # @example Create from `db/functions/uppercase_users_name_v02.sql`
16
16
  # create_function(:uppercase_users_name, version: 2)
@@ -30,14 +30,8 @@ module Fx
30
30
  version = options.fetch(:version, 1)
31
31
  sql_definition = options[:sql_definition]
32
32
 
33
- if version.nil? && sql_definition.nil?
34
- raise(
35
- ArgumentError,
36
- "version or sql_definition must be specified"
37
- )
38
- end
39
- sql_definition = sql_definition.strip_heredoc if sql_definition
40
- sql_definition ||= Fx::Definition.function(name: name, version: version).to_sql
33
+ validate_version_or_sql_definition_present!(version, sql_definition)
34
+ sql_definition = resolve_sql_definition(sql_definition, name, version, :function)
41
35
 
42
36
  Fx.database.create_function(sql_definition)
43
37
  end
@@ -45,10 +39,10 @@ module Fx
45
39
  # Drop a database function by name.
46
40
  #
47
41
  # @param name [String, Symbol] The name of the database function.
48
- # @param revert_to_version [Fixnum] Used to reverse the `drop_function`
42
+ # @param revert_to_version [Integer] Used to reverse the `drop_function`
49
43
  # command on `rake db:rollback`. The provided version will be passed as
50
44
  # the `version` argument to {#create_function}.
51
- # @return The database response from executing the drop statement.
45
+ # @return [void] The database response from executing the drop statement.
52
46
  #
53
47
  # @example Drop a function, rolling back to version 2 on rollback
54
48
  # drop_function(:uppercase_users_name, revert_to_version: 2)
@@ -60,13 +54,13 @@ module Fx
60
54
  # Update a database function.
61
55
  #
62
56
  # @param name [String, Symbol] The name of the database function.
63
- # @param version [Fixnum] The version number of the function, used to
57
+ # @param version [Integer] The version number of the function, used to
64
58
  # find the definition file in `db/functions`. This defaults to `1` if
65
59
  # not provided.
66
60
  # @param sql_definition [String] The SQL query for the function schema.
67
61
  # If both `sql_definition` and `version` are provided,
68
- # `sql_definition` takes prescedence.
69
- # @return The database response from executing the create statement.
62
+ # `sql_definition` takes precedence.
63
+ # @return [void] The database response from executing the create statement.
70
64
  #
71
65
  # @example Update function to a given version
72
66
  # update_function(
@@ -90,15 +84,9 @@ module Fx
90
84
  version = options[:version]
91
85
  sql_definition = options[:sql_definition]
92
86
 
93
- if version.nil? && sql_definition.nil?
94
- raise(
95
- ArgumentError,
96
- "version or sql_definition must be specified"
97
- )
98
- end
87
+ validate_version_or_sql_definition_present!(version, sql_definition)
99
88
 
100
- sql_definition = sql_definition.strip_heredoc if sql_definition
101
- sql_definition ||= Fx::Definition.function(name: name, version: version).to_sql
89
+ sql_definition = resolve_sql_definition(sql_definition, name, version, :function)
102
90
 
103
91
  Fx.database.update_function(name, sql_definition)
104
92
  end
@@ -106,13 +94,13 @@ module Fx
106
94
  # Create a new database trigger.
107
95
  #
108
96
  # @param name [String, Symbol] The name of the database trigger.
109
- # @param version [Fixnum] The version number of the trigger, used to
97
+ # @param version [Integer] The version number of the trigger, used to
110
98
  # find the definition file in `db/triggers`. This defaults to `1` if
111
99
  # not provided.
112
100
  # @param sql_definition [String] The SQL query for the function. An error
113
101
  # will be raised if `sql_definition` and `version` are both set,
114
102
  # as they are mutually exclusive.
115
- # @return The database response from executing the create statement.
103
+ # @return [void] The database response from executing the create statement.
116
104
  #
117
105
  # @example Create trigger from `db/triggers/uppercase_users_name_v01.sql`
118
106
  # create_trigger(:uppercase_users_name, version: 1)
@@ -127,22 +115,13 @@ module Fx
127
115
  #
128
116
  def create_trigger(name, options = {})
129
117
  version = options[:version]
130
- _on = options[:on]
131
118
  sql_definition = options[:sql_definition]
132
119
 
133
- if version.present? && sql_definition.present?
134
- raise(
135
- ArgumentError,
136
- "sql_definition and version cannot both be set"
137
- )
138
- end
120
+ validate_version_and_sql_definition_exclusive!(version, sql_definition)
139
121
 
140
- if version.nil?
141
- version = 1
142
- end
122
+ version ||= 1
143
123
 
144
- sql_definition = sql_definition.strip_heredoc if sql_definition
145
- sql_definition ||= Fx::Definition.trigger(name: name, version: version).to_sql
124
+ sql_definition = resolve_sql_definition(sql_definition, name, version, :trigger)
146
125
 
147
126
  Fx.database.create_trigger(sql_definition)
148
127
  end
@@ -152,10 +131,10 @@ module Fx
152
131
  # @param name [String, Symbol] The name of the database trigger.
153
132
  # @param on [String, Symbol] The name of the table the database trigger
154
133
  # is associated with.
155
- # @param revert_to_version [Fixnum] Used to reverse the `drop_trigger`
134
+ # @param revert_to_version [Integer] Used to reverse the `drop_trigger`
156
135
  # command on `rake db:rollback`. The provided version will be passed as
157
136
  # the `version` argument to {#create_trigger}.
158
- # @return The database response from executing the drop statement.
137
+ # @return [void] The database response from executing the drop statement.
159
138
  #
160
139
  # @example Drop a trigger, rolling back to version 3 on rollback
161
140
  # drop_trigger(:log_inserts, on: :users, revert_to_version: 3)
@@ -171,15 +150,15 @@ module Fx
171
150
  # and `version` parameter.
172
151
  #
173
152
  # @param name [String, Symbol] The name of the database trigger.
174
- # @param version [Fixnum] The version number of the trigger.
153
+ # @param version [Integer] The version number of the trigger.
175
154
  # @param on [String, Symbol] The name of the table the database trigger
176
155
  # is associated with.
177
156
  # @param sql_definition [String] The SQL query for the function. An error
178
157
  # will be raised if `sql_definition` and `version` are both set,
179
158
  # as they are mutually exclusive.
180
- # @param revert_to_version [Fixnum] The version number to rollback to on
159
+ # @param revert_to_version [Integer] The version number to rollback to on
181
160
  # `rake db rollback`
182
- # @return The database response from executing the create statement.
161
+ # @return [void] The database response from executing the create statement.
183
162
  #
184
163
  # @example Update trigger to a given version
185
164
  # update_trigger(
@@ -202,26 +181,14 @@ module Fx
202
181
  on = options[:on]
203
182
  sql_definition = options[:sql_definition]
204
183
 
205
- if version.nil? && sql_definition.nil?
206
- raise(
207
- ArgumentError,
208
- "version or sql_definition must be specified"
209
- )
210
- end
211
-
212
- if version.present? && sql_definition.present?
213
- raise(
214
- ArgumentError,
215
- "sql_definition and version cannot both be set"
216
- )
217
- end
184
+ validate_version_or_sql_definition_present!(version, sql_definition)
185
+ validate_version_and_sql_definition_exclusive!(version, sql_definition)
218
186
 
219
187
  if on.nil?
220
188
  raise ArgumentError, "on is required"
221
189
  end
222
190
 
223
- sql_definition = sql_definition.strip_heredoc if sql_definition
224
- sql_definition ||= Fx::Definition.trigger(name: name, version: version).to_sql
191
+ sql_definition = resolve_sql_definition(sql_definition, name, version, :trigger)
225
192
 
226
193
  Fx.database.update_trigger(
227
194
  name,
@@ -229,5 +196,41 @@ module Fx
229
196
  sql_definition: sql_definition
230
197
  )
231
198
  end
199
+
200
+ private
201
+
202
+ VERSION_OR_SQL_DEFINITION_REQUIRED = "version or sql_definition must be specified".freeze
203
+ private_constant :VERSION_OR_SQL_DEFINITION_REQUIRED
204
+
205
+ VERSION_AND_SQL_DEFINITION_EXCLUSIVE = "sql_definition and version cannot both be set".freeze
206
+ private_constant :VERSION_AND_SQL_DEFINITION_EXCLUSIVE
207
+
208
+ def validate_version_or_sql_definition_present!(version, sql_definition)
209
+ if version.nil? && sql_definition.nil?
210
+ raise ArgumentError, VERSION_OR_SQL_DEFINITION_REQUIRED, caller
211
+ end
212
+ end
213
+
214
+ def validate_version_and_sql_definition_exclusive!(version, sql_definition)
215
+ if version.present? && sql_definition.present?
216
+ raise ArgumentError, VERSION_AND_SQL_DEFINITION_EXCLUSIVE, caller
217
+ end
218
+ end
219
+
220
+ def resolve_sql_definition(sql_definition, name, version, type)
221
+ return sql_definition.strip_heredoc if sql_definition
222
+
223
+ definition =
224
+ case type
225
+ when :function
226
+ Fx::Definition.function(name: name, version: version)
227
+ when :trigger
228
+ Fx::Definition.trigger(name: name, version: version)
229
+ else
230
+ raise ArgumentError, "Unknown type: #{type}. Must be :function or :trigger", caller
231
+ end
232
+
233
+ definition.to_sql
234
+ end
232
235
  end
233
236
  end
data/lib/fx/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Fx
2
2
  # @api private
3
- VERSION = "0.9.0"
3
+ VERSION = "0.10.0"
4
4
  end
@@ -1,42 +1,49 @@
1
1
  require "rails/generators"
2
2
  require "rails/generators/active_record"
3
+ require "generators/fx/version_helper"
4
+ require "generators/fx/migration_helper"
5
+ require "generators/fx/name_helper"
3
6
 
4
7
  module Fx
5
8
  module Generators
6
9
  # @api private
7
10
  class FunctionGenerator < Rails::Generators::NamedBase
8
11
  include Rails::Generators::Migration
12
+
9
13
  source_root File.expand_path("../templates", __FILE__)
10
14
 
15
+ DEFINITION_PATH = %w[db functions].freeze
16
+
11
17
  class_option :migration, type: :boolean
12
18
 
13
19
  def create_functions_directory
14
- unless function_definition_path.exist?
15
- empty_directory(function_definition_path)
16
- end
20
+ return if function_definition_path.exist?
21
+
22
+ empty_directory(function_definition_path)
17
23
  end
18
24
 
19
25
  def create_function_definition
20
- if creating_new_function?
21
- create_file definition.path
26
+ if version_helper.creating_new?
27
+ create_file(definition.path)
22
28
  else
23
- copy_file previous_definition.full_path, definition.full_path
29
+ copy_file(previous_definition.full_path, definition.full_path)
24
30
  end
25
31
  end
26
32
 
27
33
  def create_migration_file
28
- return if skip_migration_creation?
29
- if updating_existing_function?
30
- migration_template(
31
- "db/migrate/update_function.erb",
32
- "db/migrate/update_function_#{file_name}_to_version_#{version}.rb"
33
- )
34
- else
35
- migration_template(
36
- "db/migrate/create_function.erb",
37
- "db/migrate/create_function_#{file_name}.rb"
38
- )
39
- end
34
+ return if migration_helper.skip_creation?
35
+
36
+ template_info = migration_helper.migration_template_info(
37
+ object_type: :function,
38
+ file_name: file_name,
39
+ updating_existing: version_helper.updating_existing?,
40
+ version: version_helper.current_version
41
+ )
42
+
43
+ migration_template(
44
+ template_info.fetch(:template),
45
+ template_info.fetch(:filename)
46
+ )
40
47
  end
41
48
 
42
49
  def self.next_migration_number(dir)
@@ -45,75 +52,65 @@ module Fx
45
52
 
46
53
  no_tasks do
47
54
  def previous_version
48
- @_previous_version ||= Dir.entries(function_definition_path)
49
- .map { |name| version_regex.match(name).try(:[], "version").to_i }
50
- .max
55
+ version_helper.previous_version
51
56
  end
52
57
 
53
58
  def version
54
- @_version ||= previous_version.next
59
+ version_helper.current_version
55
60
  end
56
61
 
57
62
  def migration_class_name
58
- if updating_existing_function?
59
- "UpdateFunction#{class_name}ToVersion#{version}"
63
+ if version_helper.updating_existing?
64
+ migration_helper.update_migration_class_name(
65
+ object_type: :function,
66
+ class_name: class_name,
67
+ version: version
68
+ )
60
69
  else
61
70
  super
62
71
  end
63
72
  end
64
73
 
65
- def activerecord_migration_class
66
- if ActiveRecord::Migration.respond_to?(:current_version)
67
- "ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]"
68
- else
69
- "ActiveRecord::Migration"
70
- end
74
+ def active_record_migration_class
75
+ migration_helper.active_record_migration_class
71
76
  end
72
77
 
73
78
  def formatted_name
74
- if singular_name.include?(".")
75
- "\"#{singular_name}\""
76
- else
77
- ":#{singular_name}"
78
- end
79
+ NameHelper.format_for_migration(singular_name)
79
80
  end
80
81
  end
81
82
 
82
83
  private
83
84
 
84
85
  def function_definition_path
85
- @_function_definition_path ||= Rails.root.join(*%w[db functions])
86
- end
87
-
88
- def version_regex
89
- /\A#{file_name}_v(?<version>\d+)\.sql\z/
86
+ @_function_definition_path ||= Rails.root.join(*DEFINITION_PATH)
90
87
  end
91
88
 
92
- def updating_existing_function?
93
- previous_version > 0
89
+ def version_helper
90
+ @_version_helper ||= Fx::Generators::VersionHelper.new(
91
+ file_name: file_name,
92
+ definition_path: function_definition_path
93
+ )
94
94
  end
95
95
 
96
- def creating_new_function?
97
- previous_version == 0
96
+ def migration_helper
97
+ @_migration_helper ||= Fx::Generators::MigrationHelper.new(options)
98
98
  end
99
99
 
100
100
  def definition
101
- Fx::Definition.function(name: file_name, version: version)
101
+ version_helper.definition_for_version(version: version, type: :function)
102
102
  end
103
103
 
104
104
  def previous_definition
105
- Fx::Definition.function(name: file_name, version: previous_version)
105
+ version_helper.definition_for_version(version: previous_version, type: :function)
106
106
  end
107
107
 
108
- # Skip creating migration file if:
109
- # - migrations option is nil or false
110
- def skip_migration_creation?
111
- !migration
108
+ def updating_existing_function?
109
+ version_helper.updating_existing?
112
110
  end
113
111
 
114
- # True unless explicitly false
115
- def migration
116
- options[:migration] != false
112
+ def creating_new_function?
113
+ version_helper.creating_new?
117
114
  end
118
115
  end
119
116
  end
@@ -1,4 +1,4 @@
1
- class <%= migration_class_name %> < <%= activerecord_migration_class %>
1
+ class <%= migration_class_name %> < <%= active_record_migration_class %>
2
2
  def change
3
3
  create_function <%= formatted_name %>
4
4
  end
@@ -1,4 +1,4 @@
1
- class <%= migration_class_name %> < <%= activerecord_migration_class %>
1
+ class <%= migration_class_name %> < <%= active_record_migration_class %>
2
2
  def change
3
3
  update_function <%= formatted_name %>, version: <%= version %>, revert_to_version: <%= previous_version %>
4
4
  end
@@ -0,0 +1,53 @@
1
+ module Fx
2
+ module Generators
3
+ # @api private
4
+ class MigrationHelper
5
+ def initialize(options)
6
+ @options = options
7
+ end
8
+
9
+ def skip_creation?
10
+ !should_create_migration?
11
+ end
12
+
13
+ def active_record_migration_class
14
+ if ActiveRecord::Migration.respond_to?(:current_version)
15
+ "ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]"
16
+ else
17
+ "ActiveRecord::Migration"
18
+ end
19
+ end
20
+
21
+ def update_migration_class_name(object_type:, class_name:, version:)
22
+ "Update#{object_type.capitalize}#{class_name}ToVersion#{version}"
23
+ end
24
+
25
+ def migration_template_info(
26
+ object_type:,
27
+ file_name:,
28
+ updating_existing:,
29
+ version:
30
+ )
31
+ if updating_existing
32
+ {
33
+ template: "db/migrate/update_#{object_type}.erb",
34
+ filename: "db/migrate/update_#{object_type}_#{file_name}_to_version_#{version}.rb"
35
+ }
36
+ else
37
+ {
38
+ template: "db/migrate/create_#{object_type}.erb",
39
+ filename: "db/migrate/create_#{object_type}_#{file_name}.rb"
40
+ }
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :options
47
+
48
+ def should_create_migration?
49
+ options.fetch(:migration, true)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,33 @@
1
+ module Fx
2
+ module Generators
3
+ # @api private
4
+ class NameHelper
5
+ def self.format_for_migration(name)
6
+ if name.include?(".")
7
+ "\"#{name}\""
8
+ else
9
+ ":#{name}"
10
+ end
11
+ end
12
+
13
+ def self.format_table_name_from_hash(table_hash)
14
+ name = table_hash["table_name"] || table_hash["on"]
15
+
16
+ if name.nil?
17
+ raise(
18
+ ArgumentError,
19
+ "Either `table_name:NAME` or `on:NAME` must be specified"
20
+ )
21
+ end
22
+
23
+ format_for_migration(name)
24
+ end
25
+
26
+ def self.validate_and_format(name)
27
+ raise ArgumentError, "Name cannot be blank" if name.blank?
28
+
29
+ format_for_migration(name)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,4 +1,4 @@
1
- class <%= migration_class_name %> < <%= activerecord_migration_class %>
1
+ class <%= migration_class_name %> < <%= active_record_migration_class %>
2
2
  def change
3
3
  create_trigger <%= formatted_name %>, on: <%= formatted_table_name %>
4
4
  end
@@ -1,4 +1,4 @@
1
- class <%= migration_class_name %> < <%= activerecord_migration_class %>
1
+ class <%= migration_class_name %> < <%= active_record_migration_class %>
2
2
  def change
3
3
  update_trigger <%= formatted_name %>, on: <%= formatted_table_name %>, version: <%= version %>, revert_to_version: <%= previous_version %>
4
4
  end