fx 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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