fx 0.8.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -7
  3. data/CHANGELOG.md +150 -0
  4. data/CONTRIBUTING.md +3 -3
  5. data/Gemfile +11 -1
  6. data/README.md +2 -0
  7. data/bin/rake +2 -3
  8. data/bin/rspec +13 -3
  9. data/bin/standardrb +27 -0
  10. data/bin/yard +13 -3
  11. data/fx.gemspec +10 -15
  12. data/lib/fx/adapters/postgres/connection.rb +20 -0
  13. data/lib/fx/adapters/postgres/functions.rb +11 -28
  14. data/lib/fx/adapters/postgres/query_executor.rb +34 -0
  15. data/lib/fx/adapters/postgres/triggers.rb +14 -29
  16. data/lib/fx/adapters/postgres.rb +16 -24
  17. data/lib/fx/command_recorder.rb +87 -6
  18. data/lib/fx/configuration.rb +2 -27
  19. data/lib/fx/definition.rb +16 -6
  20. data/lib/fx/function.rb +3 -3
  21. data/lib/fx/schema_dumper.rb +37 -5
  22. data/lib/fx/statements.rb +231 -6
  23. data/lib/fx/trigger.rb +3 -3
  24. data/lib/fx/version.rb +1 -1
  25. data/lib/fx.rb +30 -12
  26. data/lib/generators/fx/function/function_generator.rb +50 -53
  27. data/lib/generators/fx/function/templates/db/migrate/create_function.erb +1 -1
  28. data/lib/generators/fx/function/templates/db/migrate/update_function.erb +1 -1
  29. data/lib/generators/fx/migration_helper.rb +53 -0
  30. data/lib/generators/fx/name_helper.rb +33 -0
  31. data/lib/generators/fx/trigger/templates/db/migrate/create_trigger.erb +1 -1
  32. data/lib/generators/fx/trigger/templates/db/migrate/update_trigger.erb +1 -1
  33. data/lib/generators/fx/trigger/trigger_generator.rb +44 -67
  34. data/lib/generators/fx/version_helper.rb +55 -0
  35. data/spec/acceptance/user_manages_functions_spec.rb +7 -7
  36. data/spec/acceptance/user_manages_triggers_spec.rb +11 -11
  37. data/spec/acceptance_helper.rb +4 -4
  38. data/spec/dummy/config/application.rb +5 -1
  39. data/spec/features/functions/migrations_spec.rb +5 -5
  40. data/spec/features/functions/revert_spec.rb +5 -5
  41. data/spec/features/triggers/migrations_spec.rb +7 -7
  42. data/spec/features/triggers/revert_spec.rb +9 -9
  43. data/spec/fx/adapters/postgres/functions_spec.rb +33 -30
  44. data/spec/fx/adapters/postgres/query_executor_spec.rb +75 -0
  45. data/spec/fx/adapters/postgres/triggers_spec.rb +41 -38
  46. data/spec/fx/adapters/postgres_spec.rb +155 -115
  47. data/spec/fx/command_recorder_spec.rb +27 -25
  48. data/spec/fx/configuration_spec.rb +20 -9
  49. data/spec/fx/definition_spec.rb +31 -39
  50. data/spec/fx/function_spec.rb +45 -48
  51. data/spec/fx/schema_dumper_spec.rb +169 -0
  52. data/spec/fx/statements_spec.rb +217 -0
  53. data/spec/fx/trigger_spec.rb +37 -40
  54. data/spec/fx_spec.rb +28 -0
  55. data/spec/generators/fx/function/function_generator_spec.rb +11 -11
  56. data/spec/generators/fx/migration_helper_spec.rb +133 -0
  57. data/spec/generators/fx/name_helper_spec.rb +114 -0
  58. data/spec/generators/fx/trigger/trigger_generator_spec.rb +45 -22
  59. data/spec/generators/fx/version_helper_spec.rb +157 -0
  60. data/spec/spec_helper.rb +7 -0
  61. data/spec/support/definition_helpers.rb +2 -6
  62. data/spec/support/generator_setup.rb +46 -5
  63. data/spec/support/warning_helper.rb +5 -0
  64. metadata +40 -165
  65. data/lib/fx/command_recorder/arguments.rb +0 -43
  66. data/lib/fx/command_recorder/function.rb +0 -30
  67. data/lib/fx/command_recorder/trigger.rb +0 -30
  68. data/lib/fx/schema_dumper/function.rb +0 -38
  69. data/lib/fx/schema_dumper/trigger.rb +0 -29
  70. data/lib/fx/statements/function.rb +0 -113
  71. data/lib/fx/statements/trigger.rb +0 -144
  72. data/spec/fx/command_recorder/arguments_spec.rb +0 -41
  73. data/spec/fx/schema_dumper/function_spec.rb +0 -78
  74. data/spec/fx/schema_dumper/trigger_spec.rb +0 -40
  75. data/spec/fx/statements/function_spec.rb +0 -103
  76. data/spec/fx/statements/trigger_spec.rb +0 -132
@@ -1,12 +1,53 @@
1
- require "fx/command_recorder/arguments"
2
- require "fx/command_recorder/function"
3
- require "fx/command_recorder/trigger"
4
-
5
1
  module Fx
6
2
  # @api private
7
3
  module CommandRecorder
8
- include Function
9
- include Trigger
4
+ def create_function(*args)
5
+ record(:create_function, args)
6
+ end
7
+
8
+ def drop_function(*args)
9
+ record(:drop_function, args)
10
+ end
11
+
12
+ def update_function(*args)
13
+ record(:update_function, args)
14
+ end
15
+
16
+ def invert_create_function(args)
17
+ [:drop_function, args]
18
+ end
19
+
20
+ def invert_drop_function(args)
21
+ perform_inversion(:create_function, args)
22
+ end
23
+
24
+ def invert_update_function(args)
25
+ perform_inversion(:update_function, args)
26
+ end
27
+
28
+ def create_trigger(*args)
29
+ record(:create_trigger, args)
30
+ end
31
+
32
+ def drop_trigger(*args)
33
+ record(:drop_trigger, args)
34
+ end
35
+
36
+ def update_trigger(*args)
37
+ record(:update_trigger, args)
38
+ end
39
+
40
+ def invert_create_trigger(args)
41
+ [:drop_trigger, args]
42
+ end
43
+
44
+ def invert_drop_trigger(args)
45
+ perform_inversion(:create_trigger, args)
46
+ end
47
+
48
+ def invert_update_trigger(args)
49
+ perform_inversion(:update_trigger, args)
50
+ end
10
51
 
11
52
  private
12
53
 
@@ -20,5 +61,45 @@ module Fx
20
61
 
21
62
  [method, arguments.invert_version.to_a]
22
63
  end
64
+
65
+ class Arguments
66
+ def initialize(args)
67
+ @args = args.freeze
68
+ end
69
+
70
+ def function
71
+ @args[0]
72
+ end
73
+
74
+ def version
75
+ options[:version]
76
+ end
77
+
78
+ def revert_to_version
79
+ options[:revert_to_version]
80
+ end
81
+
82
+ def invert_version
83
+ Arguments.new([function, options_for_revert])
84
+ end
85
+
86
+ def to_a
87
+ @args.to_a
88
+ end
89
+
90
+ private
91
+
92
+ def options
93
+ @options ||= @args[1] || {}
94
+ end
95
+
96
+ def options_for_revert
97
+ options.clone.tap do |revert_options|
98
+ revert_options[:version] = revert_to_version
99
+ revert_options.delete(:revert_to_version)
100
+ end
101
+ end
102
+ end
103
+ private_constant :Arguments
23
104
  end
24
105
  end
@@ -1,35 +1,10 @@
1
1
  module Fx
2
- # @return [Fx::Configuration] F(x)'s current configuration
3
- def self.configuration
4
- @_configuration ||= Configuration.new
5
- end
6
-
7
- # Set F(x)'s configuration
8
- #
9
- # @param config [Fx::Configuration]
10
- def self.configuration=(config)
11
- @_configuration = config
12
- end
13
-
14
- # Modify F(x)'s current configuration
15
- #
16
- # @yieldparam [Fx::Configuration] config current F(x) config
17
- # ```
18
- # Fx.configure do |config|
19
- # config.database = Fx::Adapters::Postgres
20
- # config.dump_functions_at_beginning_of_schema = true
21
- # end
22
- # ```
23
- def self.configure
24
- yield configuration
25
- end
26
-
27
2
  # F(x)'s configuration object.
28
3
  class Configuration
29
4
  # The F(x) database adapter instance to use when executing SQL.
30
5
  #
31
6
  # Defaults to an instance of {Fx::Adapters::Postgres}
32
- # @return Fx adapter
7
+ # @return [Fx::Adapters::Postgres] Fx adapter
33
8
  attr_accessor :database
34
9
 
35
10
  # Prioritizes the order in the schema.rb of functions before other
@@ -37,7 +12,7 @@ module Fx
37
12
  # in statements below, i.e.: default column values.
38
13
  #
39
14
  # Defaults to false
40
- # @return Boolean
15
+ # @return [Boolean] Boolean
41
16
  attr_accessor :dump_functions_at_beginning_of_schema
42
17
 
43
18
  def initialize
data/lib/fx/definition.rb CHANGED
@@ -1,18 +1,28 @@
1
1
  module Fx
2
2
  # @api private
3
3
  class Definition
4
- def initialize(name:, version:, type: "function")
4
+ FUNCTION = "function".freeze
5
+ TRIGGER = "trigger".freeze
6
+
7
+ def self.function(name:, version:)
8
+ new(name: name, version: version, type: FUNCTION)
9
+ end
10
+
11
+ def self.trigger(name:, version:)
12
+ new(name: name, version: version, type: TRIGGER)
13
+ end
14
+
15
+ def initialize(name:, version:, type:)
5
16
  @name = name
6
17
  @version = version.to_i
7
18
  @type = type
8
19
  end
9
20
 
10
21
  def to_sql
11
- File.read(find_file || full_path).tap do |content|
12
- if content.empty?
13
- raise "Define #{@type} in #{path} before migrating."
14
- end
15
- end
22
+ content = File.read(find_file || full_path)
23
+ raise "Define #{@type} in #{path} before migrating." if content.empty?
24
+
25
+ content
16
26
  end
17
27
 
18
28
  def full_path
data/lib/fx/function.rb CHANGED
@@ -6,9 +6,9 @@ module Fx
6
6
  attr_reader :name, :definition
7
7
  delegate :<=>, to: :name
8
8
 
9
- def initialize(function_row)
10
- @name = function_row.fetch("name")
11
- @definition = function_row.fetch("definition")
9
+ def initialize(row)
10
+ @name = row.fetch("name")
11
+ @definition = row.fetch("definition")
12
12
  end
13
13
 
14
14
  def ==(other)
@@ -1,10 +1,42 @@
1
- require "fx/schema_dumper/function"
2
- require "fx/schema_dumper/trigger"
3
-
4
1
  module Fx
5
2
  # @api private
6
3
  module SchemaDumper
7
- include Function
8
- include Trigger
4
+ def tables(stream)
5
+ if Fx.configuration.dump_functions_at_beginning_of_schema
6
+ functions(stream)
7
+ end
8
+
9
+ super
10
+
11
+ unless Fx.configuration.dump_functions_at_beginning_of_schema
12
+ functions(stream)
13
+ end
14
+
15
+ triggers(stream)
16
+ end
17
+
18
+ private
19
+
20
+ def functions(stream)
21
+ dumpable_functions_in_database = Fx.database.functions
22
+
23
+ dumpable_functions_in_database.each do |function|
24
+ stream.puts(function.to_schema)
25
+ end
26
+
27
+ stream.puts if dumpable_functions_in_database.any?
28
+ end
29
+
30
+ def triggers(stream)
31
+ dumpable_triggers_in_database = Fx.database.triggers
32
+
33
+ if dumpable_triggers_in_database.any?
34
+ stream.puts
35
+ end
36
+
37
+ dumpable_triggers_in_database.each do |trigger|
38
+ stream.puts(trigger.to_schema)
39
+ end
40
+ end
9
41
  end
10
42
  end
data/lib/fx/statements.rb CHANGED
@@ -1,11 +1,236 @@
1
- require "rails"
2
- require "fx/statements/function"
3
- require "fx/statements/trigger"
4
-
5
1
  module Fx
6
2
  # @api private
7
3
  module Statements
8
- include Function
9
- include Trigger
4
+ # Create a new database function.
5
+ #
6
+ # @param name [String, Symbol] The name of the database function.
7
+ # @param version [Integer] The version number of the function, used to
8
+ # find the definition file in `db/functions`. This defaults to `1` if
9
+ # not provided.
10
+ # @param sql_definition [String] The SQL query for the function schema.
11
+ # If both `sql_definition` and `version` are provided,
12
+ # `sql_definition` takes precedence.
13
+ # @return [void] The database response from executing the create statement.
14
+ #
15
+ # @example Create from `db/functions/uppercase_users_name_v02.sql`
16
+ # create_function(:uppercase_users_name, version: 2)
17
+ #
18
+ # @example Create from provided SQL string
19
+ # create_function(:uppercase_users_name, sql_definition: <<~SQL)
20
+ # CREATE OR REPLACE FUNCTION uppercase_users_name()
21
+ # RETURNS trigger AS $$
22
+ # BEGIN
23
+ # NEW.upper_name = UPPER(NEW.name);
24
+ # RETURN NEW;
25
+ # END;
26
+ # $$ LANGUAGE plpgsql;
27
+ # SQL
28
+ #
29
+ def create_function(name, options = {})
30
+ version = options.fetch(:version, 1)
31
+ sql_definition = options[:sql_definition]
32
+
33
+ validate_version_or_sql_definition_present!(version, sql_definition)
34
+ sql_definition = resolve_sql_definition(sql_definition, name, version, :function)
35
+
36
+ Fx.database.create_function(sql_definition)
37
+ end
38
+
39
+ # Drop a database function by name.
40
+ #
41
+ # @param name [String, Symbol] The name of the database function.
42
+ # @param revert_to_version [Integer] Used to reverse the `drop_function`
43
+ # command on `rake db:rollback`. The provided version will be passed as
44
+ # the `version` argument to {#create_function}.
45
+ # @return [void] The database response from executing the drop statement.
46
+ #
47
+ # @example Drop a function, rolling back to version 2 on rollback
48
+ # drop_function(:uppercase_users_name, revert_to_version: 2)
49
+ #
50
+ def drop_function(name, options = {})
51
+ Fx.database.drop_function(name)
52
+ end
53
+
54
+ # Update a database function.
55
+ #
56
+ # @param name [String, Symbol] The name of the database function.
57
+ # @param version [Integer] The version number of the function, used to
58
+ # find the definition file in `db/functions`. This defaults to `1` if
59
+ # not provided.
60
+ # @param sql_definition [String] The SQL query for the function schema.
61
+ # If both `sql_definition` and `version` are provided,
62
+ # `sql_definition` takes precedence.
63
+ # @return [void] The database response from executing the create statement.
64
+ #
65
+ # @example Update function to a given version
66
+ # update_function(
67
+ # :uppercase_users_name,
68
+ # version: 3,
69
+ # revert_to_version: 2,
70
+ # )
71
+ #
72
+ # @example Update function from provided SQL string
73
+ # update_function(:uppercase_users_name, sql_definition: <<~SQL)
74
+ # CREATE OR REPLACE FUNCTION uppercase_users_name()
75
+ # RETURNS trigger AS $$
76
+ # BEGIN
77
+ # NEW.upper_name = UPPER(NEW.name);
78
+ # RETURN NEW;
79
+ # END;
80
+ # $$ LANGUAGE plpgsql;
81
+ # SQL
82
+ #
83
+ def update_function(name, options = {})
84
+ version = options[:version]
85
+ sql_definition = options[:sql_definition]
86
+
87
+ validate_version_or_sql_definition_present!(version, sql_definition)
88
+
89
+ sql_definition = resolve_sql_definition(sql_definition, name, version, :function)
90
+
91
+ Fx.database.update_function(name, sql_definition)
92
+ end
93
+
94
+ # Create a new database trigger.
95
+ #
96
+ # @param name [String, Symbol] The name of the database trigger.
97
+ # @param version [Integer] The version number of the trigger, used to
98
+ # find the definition file in `db/triggers`. This defaults to `1` if
99
+ # not provided.
100
+ # @param sql_definition [String] The SQL query for the function. An error
101
+ # will be raised if `sql_definition` and `version` are both set,
102
+ # as they are mutually exclusive.
103
+ # @return [void] The database response from executing the create statement.
104
+ #
105
+ # @example Create trigger from `db/triggers/uppercase_users_name_v01.sql`
106
+ # create_trigger(:uppercase_users_name, version: 1)
107
+ #
108
+ # @example Create trigger from provided SQL string
109
+ # create_trigger(:uppercase_users_name, sql_definition: <<~SQL)
110
+ # CREATE TRIGGER uppercase_users_name
111
+ # BEFORE INSERT ON users
112
+ # FOR EACH ROW
113
+ # EXECUTE FUNCTION uppercase_users_name();
114
+ # SQL
115
+ #
116
+ def create_trigger(name, options = {})
117
+ version = options[:version]
118
+ sql_definition = options[:sql_definition]
119
+
120
+ validate_version_and_sql_definition_exclusive!(version, sql_definition)
121
+
122
+ version ||= 1
123
+
124
+ sql_definition = resolve_sql_definition(sql_definition, name, version, :trigger)
125
+
126
+ Fx.database.create_trigger(sql_definition)
127
+ end
128
+
129
+ # Drop a database trigger by name.
130
+ #
131
+ # @param name [String, Symbol] The name of the database trigger.
132
+ # @param on [String, Symbol] The name of the table the database trigger
133
+ # is associated with.
134
+ # @param revert_to_version [Integer] Used to reverse the `drop_trigger`
135
+ # command on `rake db:rollback`. The provided version will be passed as
136
+ # the `version` argument to {#create_trigger}.
137
+ # @return [void] The database response from executing the drop statement.
138
+ #
139
+ # @example Drop a trigger, rolling back to version 3 on rollback
140
+ # drop_trigger(:log_inserts, on: :users, revert_to_version: 3)
141
+ #
142
+ def drop_trigger(name, options = {})
143
+ on = options.fetch(:on)
144
+ Fx.database.drop_trigger(name, on: on)
145
+ end
146
+
147
+ # Update a database trigger to a new version.
148
+ #
149
+ # The existing trigger is dropped and recreated using the supplied `on`
150
+ # and `version` parameter.
151
+ #
152
+ # @param name [String, Symbol] The name of the database trigger.
153
+ # @param version [Integer] The version number of the trigger.
154
+ # @param on [String, Symbol] The name of the table the database trigger
155
+ # is associated with.
156
+ # @param sql_definition [String] The SQL query for the function. An error
157
+ # will be raised if `sql_definition` and `version` are both set,
158
+ # as they are mutually exclusive.
159
+ # @param revert_to_version [Integer] The version number to rollback to on
160
+ # `rake db rollback`
161
+ # @return [void] The database response from executing the create statement.
162
+ #
163
+ # @example Update trigger to a given version
164
+ # update_trigger(
165
+ # :log_inserts,
166
+ # on: :users,
167
+ # version: 3,
168
+ # revert_to_version: 2,
169
+ # )
170
+ #
171
+ # @example Update trigger from provided SQL string
172
+ # update_trigger(:uppercase_users_name, sql_definition: <<~SQL)
173
+ # CREATE TRIGGER uppercase_users_name
174
+ # BEFORE INSERT ON users
175
+ # FOR EACH ROW
176
+ # EXECUTE FUNCTION uppercase_users_name();
177
+ # SQL
178
+ #
179
+ def update_trigger(name, options = {})
180
+ version = options[:version]
181
+ on = options[:on]
182
+ sql_definition = options[:sql_definition]
183
+
184
+ validate_version_or_sql_definition_present!(version, sql_definition)
185
+ validate_version_and_sql_definition_exclusive!(version, sql_definition)
186
+
187
+ if on.nil?
188
+ raise ArgumentError, "on is required"
189
+ end
190
+
191
+ sql_definition = resolve_sql_definition(sql_definition, name, version, :trigger)
192
+
193
+ Fx.database.update_trigger(
194
+ name,
195
+ on: on,
196
+ sql_definition: sql_definition
197
+ )
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
10
235
  end
11
236
  end
data/lib/fx/trigger.rb CHANGED
@@ -6,9 +6,9 @@ module Fx
6
6
  attr_reader :name, :definition
7
7
  delegate :<=>, to: :name
8
8
 
9
- def initialize(function_row)
10
- @name = function_row.fetch("name")
11
- @definition = function_row.fetch("definition")
9
+ def initialize(row)
10
+ @name = row.fetch("name")
11
+ @definition = row.fetch("definition")
12
12
  end
13
13
 
14
14
  def ==(other)
data/lib/fx/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Fx
2
2
  # @api private
3
- VERSION = "0.8.0"
3
+ VERSION = "0.10.0"
4
4
  end
data/lib/fx.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require "rails"
2
+
1
3
  require "fx/version"
2
4
  require "fx/adapters/postgres"
3
5
  require "fx/command_recorder"
@@ -17,20 +19,36 @@ module Fx
17
19
  # Enables fx migration methods, migration reversability, and `schema.rb`
18
20
  # dumping.
19
21
  def self.load
20
- ActiveRecord::Migration::CommandRecorder.send(
21
- :include,
22
- Fx::CommandRecorder
23
- )
22
+ ActiveRecord::Migration::CommandRecorder.include(Fx::CommandRecorder)
23
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.include(Fx::Statements)
24
+ ActiveRecord::SchemaDumper.prepend(Fx::SchemaDumper)
25
+
26
+ true
27
+ end
28
+
29
+ # @return [Fx::Configuration] F(x)'s current configuration
30
+ def self.configuration
31
+ @_configuration ||= Configuration.new
32
+ end
24
33
 
25
- ActiveRecord::SchemaDumper.send(
26
- :prepend,
27
- Fx::SchemaDumper
28
- )
34
+ # Set F(x)'s configuration
35
+ #
36
+ # @param config [Fx::Configuration]
37
+ def self.configuration=(config)
38
+ @_configuration = config
39
+ end
29
40
 
30
- ActiveRecord::ConnectionAdapters::AbstractAdapter.send(
31
- :include,
32
- Fx::Statements
33
- )
41
+ # Modify F(x)'s current configuration
42
+ #
43
+ # @yieldparam [Fx::Configuration] config current F(x) config
44
+ # ```
45
+ # Fx.configure do |config|
46
+ # config.database = Fx::Adapters::Postgres
47
+ # config.dump_functions_at_beginning_of_schema = true
48
+ # end
49
+ # ```
50
+ def self.configure
51
+ yield configuration
34
52
  end
35
53
 
36
54
  # The current database adapter used by F(x).