fx 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +27 -0
- data/.yardopts +4 -0
- data/Appraisals +21 -0
- data/CONTRIBUTING.md +15 -0
- data/Gemfile +4 -0
- data/README.md +81 -0
- data/Rakefile +23 -0
- data/bin/appraisal +17 -0
- data/bin/console +14 -0
- data/bin/rake +17 -0
- data/bin/rspec +17 -0
- data/bin/setup +12 -0
- data/bin/yard +17 -0
- data/fx.gemspec +38 -0
- data/gemfiles/rails40.gemfile +8 -0
- data/gemfiles/rails40.gemfile.lock +111 -0
- data/gemfiles/rails41.gemfile +8 -0
- data/gemfiles/rails41.gemfile.lock +113 -0
- data/gemfiles/rails42.gemfile +8 -0
- data/gemfiles/rails42.gemfile.lock +130 -0
- data/gemfiles/rails50.gemfile +8 -0
- data/gemfiles/rails50.gemfile.lock +126 -0
- data/lib/fx.rb +21 -0
- data/lib/fx/adapters/postgres.rb +142 -0
- data/lib/fx/adapters/postgres/connection.rb +16 -0
- data/lib/fx/adapters/postgres/functions.rb +55 -0
- data/lib/fx/adapters/postgres/triggers.rb +56 -0
- data/lib/fx/command_recorder.rb +29 -0
- data/lib/fx/command_recorder/arguments.rb +43 -0
- data/lib/fx/command_recorder/function.rb +30 -0
- data/lib/fx/command_recorder/trigger.rb +30 -0
- data/lib/fx/configuration.rb +38 -0
- data/lib/fx/definition.rb +36 -0
- data/lib/fx/function.rb +24 -0
- data/lib/fx/schema_dumper.rb +15 -0
- data/lib/fx/schema_dumper/function.rb +29 -0
- data/lib/fx/schema_dumper/trigger.rb +29 -0
- data/lib/fx/statements.rb +16 -0
- data/lib/fx/statements/function.rb +105 -0
- data/lib/fx/statements/trigger.rb +133 -0
- data/lib/fx/trigger.rb +24 -0
- data/lib/fx/version.rb +4 -0
- data/lib/generators.rb +11 -0
- data/lib/generators/fx/function/USAGE +9 -0
- data/lib/generators/fx/function/function_generator.rb +98 -0
- data/lib/generators/fx/function/templates/db/migrate/create_function.erb +5 -0
- data/lib/generators/fx/function/templates/db/migrate/update_function.erb +5 -0
- data/lib/generators/fx/trigger/USAGE +18 -0
- data/lib/generators/fx/trigger/templates/db/migrate/create_trigger.erb +5 -0
- data/lib/generators/fx/trigger/templates/db/migrate/update_trigger.erb +5 -0
- data/lib/generators/fx/trigger/trigger_generator.rb +108 -0
- data/spec/acceptance/user_manages_functions_spec.rb +37 -0
- data/spec/acceptance/user_manages_triggers_spec.rb +51 -0
- data/spec/acceptance_helper.rb +61 -0
- data/spec/dummy/.gitignore +16 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +15 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +9 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/db/migrate/.keep +0 -0
- data/spec/features/functions/migrations_spec.rb +65 -0
- data/spec/features/functions/revert_spec.rb +75 -0
- data/spec/features/triggers/migrations_spec.rb +56 -0
- data/spec/features/triggers/revert_spec.rb +95 -0
- data/spec/fx/adapters/postgres_spec.rb +149 -0
- data/spec/fx/command_recorder/arguments_spec.rb +41 -0
- data/spec/fx/command_recorder_spec.rb +171 -0
- data/spec/fx/configuration_spec.rb +21 -0
- data/spec/fx/definition_spec.rb +111 -0
- data/spec/fx/schema_dumper/function_spec.rb +22 -0
- data/spec/fx/schema_dumper/trigger_spec.rb +40 -0
- data/spec/fx/statements/function_spec.rb +103 -0
- data/spec/fx/statements/trigger_spec.rb +132 -0
- data/spec/generators/fx/function/function_generator_spec.rb +34 -0
- data/spec/generators/fx/trigger/trigger_generator_spec.rb +47 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/support/definition_helpers.rb +37 -0
- data/spec/support/generator_setup.rb +11 -0
- data/spec/support/migration_helpers.rb +17 -0
- 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
|
data/lib/fx/function.rb
ADDED
@@ -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
|