fx 0.1.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 (89) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +27 -0
  6. data/.yardopts +4 -0
  7. data/Appraisals +21 -0
  8. data/CONTRIBUTING.md +15 -0
  9. data/Gemfile +4 -0
  10. data/README.md +81 -0
  11. data/Rakefile +23 -0
  12. data/bin/appraisal +17 -0
  13. data/bin/console +14 -0
  14. data/bin/rake +17 -0
  15. data/bin/rspec +17 -0
  16. data/bin/setup +12 -0
  17. data/bin/yard +17 -0
  18. data/fx.gemspec +38 -0
  19. data/gemfiles/rails40.gemfile +8 -0
  20. data/gemfiles/rails40.gemfile.lock +111 -0
  21. data/gemfiles/rails41.gemfile +8 -0
  22. data/gemfiles/rails41.gemfile.lock +113 -0
  23. data/gemfiles/rails42.gemfile +8 -0
  24. data/gemfiles/rails42.gemfile.lock +130 -0
  25. data/gemfiles/rails50.gemfile +8 -0
  26. data/gemfiles/rails50.gemfile.lock +126 -0
  27. data/lib/fx.rb +21 -0
  28. data/lib/fx/adapters/postgres.rb +142 -0
  29. data/lib/fx/adapters/postgres/connection.rb +16 -0
  30. data/lib/fx/adapters/postgres/functions.rb +55 -0
  31. data/lib/fx/adapters/postgres/triggers.rb +56 -0
  32. data/lib/fx/command_recorder.rb +29 -0
  33. data/lib/fx/command_recorder/arguments.rb +43 -0
  34. data/lib/fx/command_recorder/function.rb +30 -0
  35. data/lib/fx/command_recorder/trigger.rb +30 -0
  36. data/lib/fx/configuration.rb +38 -0
  37. data/lib/fx/definition.rb +36 -0
  38. data/lib/fx/function.rb +24 -0
  39. data/lib/fx/schema_dumper.rb +15 -0
  40. data/lib/fx/schema_dumper/function.rb +29 -0
  41. data/lib/fx/schema_dumper/trigger.rb +29 -0
  42. data/lib/fx/statements.rb +16 -0
  43. data/lib/fx/statements/function.rb +105 -0
  44. data/lib/fx/statements/trigger.rb +133 -0
  45. data/lib/fx/trigger.rb +24 -0
  46. data/lib/fx/version.rb +4 -0
  47. data/lib/generators.rb +11 -0
  48. data/lib/generators/fx/function/USAGE +9 -0
  49. data/lib/generators/fx/function/function_generator.rb +98 -0
  50. data/lib/generators/fx/function/templates/db/migrate/create_function.erb +5 -0
  51. data/lib/generators/fx/function/templates/db/migrate/update_function.erb +5 -0
  52. data/lib/generators/fx/trigger/USAGE +18 -0
  53. data/lib/generators/fx/trigger/templates/db/migrate/create_trigger.erb +5 -0
  54. data/lib/generators/fx/trigger/templates/db/migrate/update_trigger.erb +5 -0
  55. data/lib/generators/fx/trigger/trigger_generator.rb +108 -0
  56. data/spec/acceptance/user_manages_functions_spec.rb +37 -0
  57. data/spec/acceptance/user_manages_triggers_spec.rb +51 -0
  58. data/spec/acceptance_helper.rb +61 -0
  59. data/spec/dummy/.gitignore +16 -0
  60. data/spec/dummy/Rakefile +6 -0
  61. data/spec/dummy/bin/bundle +3 -0
  62. data/spec/dummy/bin/rails +4 -0
  63. data/spec/dummy/bin/rake +4 -0
  64. data/spec/dummy/config.ru +4 -0
  65. data/spec/dummy/config/application.rb +15 -0
  66. data/spec/dummy/config/boot.rb +5 -0
  67. data/spec/dummy/config/database.yml +9 -0
  68. data/spec/dummy/config/environment.rb +5 -0
  69. data/spec/dummy/db/migrate/.keep +0 -0
  70. data/spec/features/functions/migrations_spec.rb +65 -0
  71. data/spec/features/functions/revert_spec.rb +75 -0
  72. data/spec/features/triggers/migrations_spec.rb +56 -0
  73. data/spec/features/triggers/revert_spec.rb +95 -0
  74. data/spec/fx/adapters/postgres_spec.rb +149 -0
  75. data/spec/fx/command_recorder/arguments_spec.rb +41 -0
  76. data/spec/fx/command_recorder_spec.rb +171 -0
  77. data/spec/fx/configuration_spec.rb +21 -0
  78. data/spec/fx/definition_spec.rb +111 -0
  79. data/spec/fx/schema_dumper/function_spec.rb +22 -0
  80. data/spec/fx/schema_dumper/trigger_spec.rb +40 -0
  81. data/spec/fx/statements/function_spec.rb +103 -0
  82. data/spec/fx/statements/trigger_spec.rb +132 -0
  83. data/spec/generators/fx/function/function_generator_spec.rb +34 -0
  84. data/spec/generators/fx/trigger/trigger_generator_spec.rb +47 -0
  85. data/spec/spec_helper.rb +21 -0
  86. data/spec/support/definition_helpers.rb +37 -0
  87. data/spec/support/generator_setup.rb +11 -0
  88. data/spec/support/migration_helpers.rb +17 -0
  89. metadata +334 -0
@@ -0,0 +1,16 @@
1
+ require "rails"
2
+ require "fx/statements/function"
3
+ require "fx/statements/trigger"
4
+
5
+ module Fx
6
+ # @api private
7
+ module Statements
8
+ include Function
9
+ include Trigger
10
+ end
11
+ end
12
+
13
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.send(
14
+ :include,
15
+ Fx::Statements,
16
+ )
@@ -0,0 +1,105 @@
1
+ require "rails"
2
+
3
+ module Fx
4
+ module Statements
5
+ # Methods that are made available in migrations for managing Fx functions.
6
+ module Function
7
+ # Create a new database function.
8
+ #
9
+ # @param name [String, Symbol] The name of the database function.
10
+ # @param version [Fixnum] The version number of the function, used to
11
+ # find the definition file in `db/functions`. This defaults to `1` if
12
+ # not provided.
13
+ # @param sql_definition [String] The SQL query for the function schema.
14
+ # If both `sql_defintion` and `version` are provided,
15
+ # `sql_definition` takes prescedence.
16
+ # @return The database response from executing the create statement.
17
+ #
18
+ # @example Create from `db/functions/uppercase_users_name_v02.sql`
19
+ # create_function(:uppercase_users_name, version: 2)
20
+ #
21
+ # @example Create from provided SQL string
22
+ # create_function(:uppercase_users_name, sql_definition: <<-SQL)
23
+ # CREATE OR REPLACE FUNCTION uppercase_users_name()
24
+ # RETURNS trigger AS $$
25
+ # BEGIN
26
+ # NEW.upper_name = UPPER(NEW.name);
27
+ # RETURN NEW;
28
+ # END;
29
+ # $$ LANGUAGE plpgsql;
30
+ # SQL
31
+ #
32
+ def create_function(name, version: 1, sql_definition: nil)
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 ||= Fx::Definition.new(name: name, version: version).to_sql
40
+
41
+ Fx.database.create_function(sql_definition)
42
+ end
43
+
44
+ # Drop a database function by name.
45
+ #
46
+ # @param name [String, Symbol] The name of the database function.
47
+ # @param revert_to_version [Fixnum] Used to reverse the `drop_function`
48
+ # command on `rake db:rollback`. The provided version will be passed as
49
+ # the `version` argument to {#create_function}.
50
+ # @return The database response from executing the drop statement.
51
+ #
52
+ # @example Drop a function, rolling back to version 3 on rollback
53
+ # drop_function(:uppercase_users_name, on: :users, revert_to_version: 3)
54
+ #
55
+ def drop_function(name, revert_to_version: nil)
56
+ Fx.database.drop_function(name)
57
+ end
58
+
59
+ # Update a database function.
60
+ #
61
+ # @param name [String, Symbol] The name of the database function.
62
+ # @param version [Fixnum] The version number of the function, used to
63
+ # find the definition file in `db/functions`. This defaults to `1` if
64
+ # not provided.
65
+ # @param sql_definition [String] The SQL query for the function schema.
66
+ # If both `sql_defintion` and `version` are provided,
67
+ # `sql_definition` takes prescedence.
68
+ # @return The database response from executing the create statement.
69
+ #
70
+ # @example Update function to a given version
71
+ # update_function(
72
+ # :uppercase_users_name,
73
+ # version: 3,
74
+ # revert_to_version: 2,
75
+ # )
76
+ #
77
+ # @example Update function from provided SQL string
78
+ # update_function(:uppercase_users_name, sql_definition: <<-SQL)
79
+ # CREATE OR REPLACE FUNCTION uppercase_users_name()
80
+ # RETURNS trigger AS $$
81
+ # BEGIN
82
+ # NEW.upper_name = UPPER(NEW.name);
83
+ # RETURN NEW;
84
+ # END;
85
+ # $$ LANGUAGE plpgsql;
86
+ # SQL
87
+ #
88
+ def update_function(name, version: nil, sql_definition: nil, revert_to_version: nil)
89
+ if version.nil? && sql_definition.nil?
90
+ raise(
91
+ ArgumentError,
92
+ "version or sql_definition must be specified",
93
+ )
94
+ end
95
+
96
+ sql_definition ||= Fx::Definition.new(
97
+ name: name,
98
+ version: version,
99
+ ).to_sql
100
+
101
+ Fx.database.update_function(name, sql_definition)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,133 @@
1
+ module Fx
2
+ module Statements
3
+ # Methods that are made available in migrations for managing Fx triggers.
4
+ module Trigger
5
+ # @api private
6
+ DEFINTION_TYPE = "trigger".freeze
7
+
8
+ # Create a new database trigger.
9
+ #
10
+ # @param name [String, Symbol] The name of the database trigger.
11
+ # @param version [Fixnum] The version number of the trigger, used to
12
+ # find the definition file in `db/triggers`. This defaults to `1` if
13
+ # not provided.
14
+ # @param sql_definition [String] The SQL query for the function. An error
15
+ # will be raised if `sql_definition` and `version` are both set,
16
+ # as they are mutually exclusive.
17
+ # @return The database response from executing the create statement.
18
+ #
19
+ # @example Create trigger from `db/triggers/uppercase_users_name_v01.sql`
20
+ # create_trigger(:uppercase_users_name, version: 1)
21
+ #
22
+ # @example Create trigger from provided SQL string
23
+ # create_trigger(:uppercase_users_name, sql_definition: <<-SQL)
24
+ # CREATE TRIGGER uppercase_users_name
25
+ # BEFORE INSERT ON users
26
+ # FOR EACH ROW
27
+ # EXECUTE PROCEDURE uppercase_users_name();
28
+ # SQL
29
+ #
30
+ def create_trigger(name, version: nil, on: nil, sql_definition: nil)
31
+ if version.present? && sql_definition.present?
32
+ raise(
33
+ ArgumentError,
34
+ "sql_definition and version cannot both be set",
35
+ )
36
+ end
37
+
38
+ if version.nil?
39
+ version = 1
40
+ end
41
+
42
+ sql_definition ||= Fx::Definition.new(
43
+ name: name,
44
+ version: version,
45
+ type: DEFINTION_TYPE,
46
+ ).to_sql
47
+
48
+ Fx.database.create_trigger(sql_definition)
49
+ end
50
+
51
+ # Drop a database trigger by name.
52
+ #
53
+ # @param name [String, Symbol] The name of the database trigger.
54
+ # @param on [String, Symbol] The name of the table the database trigger
55
+ # is associated with.
56
+ # @param revert_to_version [Fixnum] Used to reverse the `drop_trigger`
57
+ # command on `rake db:rollback`. The provided version will be passed as
58
+ # the `version` argument to {#create_trigger}.
59
+ # @return The database response from executing the drop statement.
60
+ #
61
+ # @example Drop a trigger, rolling back to version 3 on rollback
62
+ # drop_trigger(:log_inserts, on: :users, revert_to_version: 3)
63
+ #
64
+ def drop_trigger(name, on:, revert_to_version: nil)
65
+ Fx.database.drop_trigger(name, on: on)
66
+ end
67
+
68
+ # Update a database trigger to a new version.
69
+ #
70
+ # The existing trigger is dropped and recreated using the supplied `on`
71
+ # and `version` parameter.
72
+ #
73
+ # @param name [String, Symbol] The name of the database trigger.
74
+ # @param version [Fixnum] The version number of the trigger.
75
+ # @param on [String, Symbol] The name of the table the database trigger
76
+ # is associated with.
77
+ # @param sql_definition [String] The SQL query for the function. An error
78
+ # will be raised if `sql_definition` and `version` are both set,
79
+ # as they are mutually exclusive.
80
+ # @param revert_to_version [Fixnum] The version number to rollback to on
81
+ # `rake db rollback`
82
+ # @return The database response from executing the create statement.
83
+ #
84
+ # @example Update trigger to a given version
85
+ # update_trigger(
86
+ # :log_inserts,
87
+ # on: :users,
88
+ # version: 3,
89
+ # revert_to_version: 2,
90
+ # )
91
+ #
92
+ # @example Update trigger from provided SQL string
93
+ # update_trigger(:uppercase_users_name, sql_definition: <<-SQL)
94
+ # CREATE TRIGGER uppercase_users_name
95
+ # BEFORE INSERT ON users
96
+ # FOR EACH ROW
97
+ # EXECUTE PROCEDURE uppercase_users_name();
98
+ # SQL
99
+ #
100
+ def update_trigger(name, version: nil, on: nil, sql_definition: nil, revert_to_version: nil)
101
+ if version.nil? && sql_definition.nil?
102
+ raise(
103
+ ArgumentError,
104
+ "version or sql_definition must be specified",
105
+ )
106
+ end
107
+
108
+ if version.present? && sql_definition.present?
109
+ raise(
110
+ ArgumentError,
111
+ "sql_definition and version cannot both be set",
112
+ )
113
+ end
114
+
115
+ if on.nil?
116
+ raise ArgumentError, "on is required"
117
+ end
118
+
119
+ sql_definition ||= Fx::Definition.new(
120
+ name: name,
121
+ version: version,
122
+ type: DEFINTION_TYPE,
123
+ ).to_sql
124
+
125
+ Fx.database.update_trigger(
126
+ name,
127
+ on: on,
128
+ sql_definition: sql_definition,
129
+ )
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,24 @@
1
+ module Fx
2
+ # @api private
3
+ class Trigger
4
+ attr_reader :name, :definition
5
+ delegate :<=>, to: :name
6
+
7
+ def initialize(function_row)
8
+ @name = function_row.fetch("name")
9
+ @definition = function_row.fetch("definition")
10
+ end
11
+
12
+ def ==(other)
13
+ name == other.name && definition == other.definition
14
+ end
15
+
16
+ def to_schema
17
+ <<-SCHEMA
18
+ create_trigger :#{name}, sql_definition: <<-\SQL
19
+ #{definition}
20
+ SQL
21
+ SCHEMA
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,4 @@
1
+ module Fx
2
+ # @api private
3
+ VERSION = "0.1.0"
4
+ end
@@ -0,0 +1,11 @@
1
+ module Fx
2
+ # Fx provides generators for creating and updating functions and triggers.
3
+ #
4
+ # See:
5
+ #
6
+ # * {file:lib/generators/fx/function/USAGE Function Generator}
7
+ # * {file:lib/generators/fx/trigger/USAGE Trigger Generator}
8
+ # * {file:README.md README}
9
+ module Generators
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ Description:
2
+ Create a new database function for your application. This will create a new
3
+ function definition file and the accompanying migration.
4
+
5
+ Examples:
6
+ rails generate fx:function test
7
+
8
+ create: db/functions/test_v01.sql
9
+ create: db/migrate/[TIMESTAMP]_create_test.rb
@@ -0,0 +1,98 @@
1
+ require "rails/generators"
2
+ require "rails/generators/active_record"
3
+
4
+ module Fx
5
+ module Generators
6
+ # @api private
7
+ class FunctionGenerator < Rails::Generators::NamedBase
8
+ include Rails::Generators::Migration
9
+ source_root File.expand_path("../templates", __FILE__)
10
+
11
+ def create_functions_directory
12
+ unless function_definition_path.exist?
13
+ empty_directory(function_definition_path)
14
+ end
15
+ end
16
+
17
+ def create_function_definition
18
+ if creating_new_function?
19
+ create_file definition.path
20
+ else
21
+ copy_file previous_definition.full_path, definition.full_path
22
+ end
23
+ end
24
+
25
+ def create_migration_file
26
+ if updating_existing_function?
27
+ migration_template(
28
+ "db/migrate/update_function.erb",
29
+ "db/migrate/update_function_#{file_name}_to_version_#{version}.rb",
30
+ )
31
+ else
32
+ migration_template(
33
+ "db/migrate/create_function.erb",
34
+ "db/migrate/create_function_#{file_name}.rb",
35
+ )
36
+ end
37
+ end
38
+
39
+ def self.next_migration_number(dir)
40
+ ::ActiveRecord::Generators::Base.next_migration_number(dir)
41
+ end
42
+
43
+ no_tasks do
44
+ def previous_version
45
+ @_previous_version ||= Dir.entries(function_definition_path).
46
+ map { |name| version_regex.match(name).try(:[], "version").to_i }.
47
+ max
48
+ end
49
+
50
+ def version
51
+ @_version ||= previous_version.next
52
+ end
53
+
54
+ def migration_class_name
55
+ if updating_existing_function?
56
+ "UpdateFunction#{class_name}ToVersion#{version}"
57
+ else
58
+ super
59
+ end
60
+ end
61
+
62
+ def formatted_name
63
+ if singular_name.include?(".")
64
+ "\"#{singular_name}\""
65
+ else
66
+ ":#{singular_name}"
67
+ end
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def function_definition_path
74
+ @_function_definition_path ||= Rails.root.join(*%w(db functions))
75
+ end
76
+
77
+ def version_regex
78
+ /\A#{file_name}_v(?<version>\d+)\.sql\z/
79
+ end
80
+
81
+ def updating_existing_function?
82
+ previous_version > 0
83
+ end
84
+
85
+ def creating_new_function?
86
+ previous_version == 0
87
+ end
88
+
89
+ def definition
90
+ Fx::Definition.new(name: file_name, version: version)
91
+ end
92
+
93
+ def previous_definition
94
+ Fx::Definition.new(name: file_name, version: previous_version)
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,5 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration
2
+ def change
3
+ create_function <%= formatted_name %>
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration
2
+ def change
3
+ update_function <%= formatted_name %>, version: <%= version %>, revert_to_version: <%= previous_version %>
4
+ end
5
+ end
@@ -0,0 +1,18 @@
1
+ Description:
2
+ Create a new database trigger for your application. This will create a new
3
+ trigger definition file and the accompanying migration.
4
+
5
+ If a trigger of the given name already exists, create a new version of the
6
+ trigger and a migration to replace the old version with the new.
7
+
8
+ Examples:
9
+
10
+ rails generate fx:trigger test
11
+
12
+ create: db/triggers/test_v01.sql
13
+ create: db/migrate/[TIMESTAMP]_create_trigger_test.rb
14
+
15
+ rails generate fx:trigger test
16
+
17
+ create: db/triggers/test_v02.sql
18
+ create: db/migrate/[TIMESTAMP]_update_trigger_test_to_version_2.rb
@@ -0,0 +1,5 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration
2
+ def change
3
+ create_trigger <%= formatted_name %>, on: <%= formatted_table_name %>
4
+ end
5
+ end