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.
- 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
|