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
+ module Fx
2
+ module Adapters
3
+ class Postgres
4
+ # Decorates an ActiveRecord connection with methods that help determine
5
+ # the connections capabilities.
6
+ #
7
+ # Every attempt is made to use the versions of these methods defined by
8
+ # Rails where they are available and public before falling back to our own
9
+ # implementations for older Rails versions.
10
+ #
11
+ # @api private
12
+ class Connection < SimpleDelegator
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,55 @@
1
+ require "fx/function"
2
+
3
+ module Fx
4
+ module Adapters
5
+ class Postgres
6
+ # Fetches defined functions from the postgres connection.
7
+ # @api private
8
+ class Functions
9
+ # The SQL query used by F(x) to retrieve the functions considered
10
+ # dumpable into `db/schema.rb`.
11
+ FUNCTIONS_WITH_DEFINITIONS_QUERY = <<~SQL
12
+ SELECT
13
+ pp.proname AS name,
14
+ pg_get_functiondef(pp.oid) AS definition
15
+ FROM pg_proc pp
16
+ JOIN pg_namespace pn
17
+ ON pn.oid = pp.pronamespace
18
+ LEFT JOIN pg_depend pd
19
+ ON pd.objid = pp.oid AND pd.deptype = 'e'
20
+ WHERE pn.nspname = 'public' AND pd.objid IS NULL;
21
+ SQL
22
+
23
+ # Wraps #all as a static facade.
24
+ #
25
+ # @return [Array<Fx::Function>]
26
+ def self.all(*args)
27
+ new(*args).all
28
+ end
29
+
30
+ def initialize(connection)
31
+ @connection = connection
32
+ end
33
+
34
+ # All of the functions that this connection has defined.
35
+ #
36
+ # @return [Array<Fx::Function>]
37
+ def all
38
+ functions_from_postgres.map { |function| to_fx_function(function) }
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :connection
44
+
45
+ def functions_from_postgres
46
+ connection.execute(FUNCTIONS_WITH_DEFINITIONS_QUERY)
47
+ end
48
+
49
+ def to_fx_function(result)
50
+ Fx::Function.new(result)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,56 @@
1
+ require "fx/trigger"
2
+
3
+ module Fx
4
+ module Adapters
5
+ class Postgres
6
+ # Fetches defined triggers from the postgres connection.
7
+ # @api private
8
+ class Triggers
9
+ # The SQL query used by F(x) to retrieve the triggers considered
10
+ # dumpable into `db/schema.rb`.
11
+ TRIGGERS_WITH_DEFINITIONS_QUERY = <<~SQL
12
+ SELECT
13
+ pt.tgname AS name,
14
+ pg_get_triggerdef(pt.oid) AS definition
15
+ FROM pg_trigger pt
16
+ JOIN pg_class pc
17
+ ON (pc.oid = pt.tgrelid)
18
+ JOIN pg_proc pp
19
+ ON (pp.oid = pt.tgfoid)
20
+ WHERE pt.tgname
21
+ NOT ILIKE '%constraint%' AND pt.tgname NOT ILIKE 'pg%';
22
+ SQL
23
+
24
+ # Wraps #all as a static facade.
25
+ #
26
+ # @return [Array<Fx::Trigger>]
27
+ def self.all(*args)
28
+ new(*args).all
29
+ end
30
+
31
+ def initialize(connection)
32
+ @connection = connection
33
+ end
34
+
35
+ # All of the triggers that this connection has defined.
36
+ #
37
+ # @return [Array<Fx::Trigger>]
38
+ def all
39
+ triggers_from_postgres.map { |trigger| to_fx_trigger(trigger) }
40
+ end
41
+
42
+ private
43
+
44
+ attr_reader :connection
45
+
46
+ def triggers_from_postgres
47
+ connection.execute(TRIGGERS_WITH_DEFINITIONS_QUERY)
48
+ end
49
+
50
+ def to_fx_trigger(result)
51
+ Fx::Trigger.new(result)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,29 @@
1
+ require "fx/command_recorder/arguments"
2
+ require "fx/command_recorder/function"
3
+ require "fx/command_recorder/trigger"
4
+
5
+ module Fx
6
+ # @api private
7
+ module CommandRecorder
8
+ include Function
9
+ include Trigger
10
+
11
+ private
12
+
13
+ def perform_inversion(method, args)
14
+ arguments = Arguments.new(args)
15
+
16
+ if arguments.revert_to_version.nil?
17
+ message = "`#{method}` is reversible only if given a `revert_to_version`"
18
+ raise ActiveRecord::IrreversibleMigration, message
19
+ end
20
+
21
+ [method, arguments.invert_version.to_a]
22
+ end
23
+ end
24
+ end
25
+
26
+ ActiveRecord::Migration::CommandRecorder.send(
27
+ :include,
28
+ Fx::CommandRecorder,
29
+ )
@@ -0,0 +1,43 @@
1
+ module Fx
2
+ module CommandRecorder
3
+ # @api private
4
+ class Arguments
5
+ def initialize(args)
6
+ @args = args.freeze
7
+ end
8
+
9
+ def function
10
+ @args[0]
11
+ end
12
+
13
+ def version
14
+ options[:version]
15
+ end
16
+
17
+ def revert_to_version
18
+ options[:revert_to_version]
19
+ end
20
+
21
+ def invert_version
22
+ Arguments.new([function, options_for_revert])
23
+ end
24
+
25
+ def to_a
26
+ @args.to_a
27
+ end
28
+
29
+ private
30
+
31
+ def options
32
+ @options ||= @args[1] || {}
33
+ end
34
+
35
+ def options_for_revert
36
+ options.clone.tap do |revert_options|
37
+ revert_options[:version] = revert_to_version
38
+ revert_options.delete(:revert_to_version)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,30 @@
1
+ module Fx
2
+ module CommandRecorder
3
+ # @api private
4
+ module Function
5
+ def create_function(*args)
6
+ record(:create_function, args)
7
+ end
8
+
9
+ def drop_function(*args)
10
+ record(:drop_function, args)
11
+ end
12
+
13
+ def update_function(*args)
14
+ record(:update_function, args)
15
+ end
16
+
17
+ def invert_create_function(args)
18
+ [:drop_function, args]
19
+ end
20
+
21
+ def invert_drop_function(args)
22
+ perform_inversion(:create_function, args)
23
+ end
24
+
25
+ def invert_update_function(args)
26
+ perform_inversion(:update_function, args)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ module Fx
2
+ module CommandRecorder
3
+ # @api private
4
+ module Trigger
5
+ def create_trigger(*args)
6
+ record(:create_trigger, args)
7
+ end
8
+
9
+ def drop_trigger(*args)
10
+ record(:drop_trigger, args)
11
+ end
12
+
13
+ def update_trigger(*args)
14
+ record(:update_trigger, args)
15
+ end
16
+
17
+ def invert_create_trigger(args)
18
+ [:drop_trigger, args]
19
+ end
20
+
21
+ def invert_drop_trigger(args)
22
+ perform_inversion(:create_trigger, args)
23
+ end
24
+
25
+ def invert_update_trigger(args)
26
+ perform_inversion(:update_trigger, args)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,38 @@
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
+ # end
21
+ # ```
22
+ def self.configure
23
+ yield configuration
24
+ end
25
+
26
+ # F(x)'s configuration object.
27
+ class Configuration
28
+ # The F(x) database adapter instance to use when executing SQL.
29
+ #
30
+ # Defaults to an instance of {Fx::Adapters::Postgres}
31
+ # @return Fx adapter
32
+ attr_accessor :database
33
+
34
+ def initialize
35
+ @database = Fx::Adapters::Postgres.new
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,36 @@
1
+ module Fx
2
+ # @api private
3
+ class Definition
4
+ def initialize(name:, version:, type: "function")
5
+ @name = name
6
+ @version = version.to_i
7
+ @type = type
8
+ end
9
+
10
+ def to_sql
11
+ File.read(full_path).tap do |content|
12
+ if content.empty?
13
+ raise "Define #{@type} in #{path} before migrating."
14
+ end
15
+ end
16
+ end
17
+
18
+ def full_path
19
+ Rails.root.join(path)
20
+ end
21
+
22
+ def path
23
+ File.join("db", @type.pluralize, filename)
24
+ end
25
+
26
+ def version
27
+ @version.to_s.rjust(2, "0")
28
+ end
29
+
30
+ private
31
+
32
+ def filename
33
+ "#{@name}_v#{version}.sql"
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,24 @@
1
+ module Fx
2
+ # @api private
3
+ class Function
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.indent(2)
18
+ create_function :#{name}, sql_definition: <<-\SQL
19
+ #{definition.indent(4).rstrip}
20
+ SQL
21
+ SCHEMA
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,15 @@
1
+ require "fx/schema_dumper/function"
2
+ require "fx/schema_dumper/trigger"
3
+
4
+ module Fx
5
+ # @api private
6
+ module SchemaDumper
7
+ include Function
8
+ include Trigger
9
+ end
10
+ end
11
+
12
+ ActiveRecord::SchemaDumper.send(
13
+ :prepend,
14
+ Fx::SchemaDumper,
15
+ )
@@ -0,0 +1,29 @@
1
+ require "rails"
2
+
3
+ module Fx
4
+ module SchemaDumper
5
+ # @api private
6
+ module Function
7
+ def tables(stream)
8
+ super
9
+ functions(stream)
10
+ end
11
+
12
+ def functions(stream)
13
+ if dumpable_functions_in_database.any?
14
+ stream.puts
15
+ end
16
+
17
+ dumpable_functions_in_database.each do |function|
18
+ stream.puts(function.to_schema)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def dumpable_functions_in_database
25
+ @_dumpable_functions_in_database ||= Fx.database.functions
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ require "rails"
2
+
3
+ module Fx
4
+ module SchemaDumper
5
+ # @api private
6
+ module Trigger
7
+ def tables(stream)
8
+ super
9
+ triggers(stream)
10
+ end
11
+
12
+ def triggers(stream)
13
+ if dumpable_triggers_in_database.any?
14
+ stream.puts
15
+ end
16
+
17
+ dumpable_triggers_in_database.each do |trigger|
18
+ stream.puts(trigger.to_schema)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def dumpable_triggers_in_database
25
+ @_dumpable_triggers_in_database ||= Fx.database.triggers
26
+ end
27
+ end
28
+ end
29
+ end