fx 0.2.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -0
  3. data/.hound.yml +2 -0
  4. data/.rubocop.yml +648 -0
  5. data/.travis.yml +18 -6
  6. data/Appraisals +21 -10
  7. data/LICENSE +18 -0
  8. data/README.md +45 -13
  9. data/bin/setup +1 -0
  10. data/gemfiles/rails42.gemfile +2 -1
  11. data/gemfiles/rails50.gemfile +1 -1
  12. data/gemfiles/rails51.gemfile +8 -0
  13. data/gemfiles/rails52.gemfile +8 -0
  14. data/gemfiles/rails60.gemfile +8 -0
  15. data/gemfiles/rails_edge.gemfile +8 -0
  16. data/lib/fx.rb +22 -0
  17. data/lib/fx/adapters/postgres.rb +26 -1
  18. data/lib/fx/adapters/postgres/functions.rb +5 -2
  19. data/lib/fx/adapters/postgres/triggers.rb +2 -2
  20. data/lib/fx/command_recorder.rb +0 -5
  21. data/lib/fx/configuration.rb +10 -0
  22. data/lib/fx/definition.rb +13 -3
  23. data/lib/fx/function.rb +2 -0
  24. data/lib/fx/railtie.rb +15 -0
  25. data/lib/fx/schema_dumper.rb +0 -5
  26. data/lib/fx/schema_dumper/function.rb +14 -5
  27. data/lib/fx/statements.rb +0 -5
  28. data/lib/fx/statements/function.rb +4 -2
  29. data/lib/fx/statements/trigger.rb +2 -0
  30. data/lib/fx/trigger.rb +2 -0
  31. data/lib/fx/version.rb +1 -1
  32. data/lib/generators/fx/function/USAGE +2 -0
  33. data/lib/generators/fx/function/function_generator.rb +14 -1
  34. data/lib/generators/fx/trigger/USAGE +2 -0
  35. data/lib/generators/fx/trigger/trigger_generator.rb +14 -1
  36. data/spec/acceptance/user_manages_functions_spec.rb +22 -2
  37. data/spec/acceptance/user_manages_triggers_spec.rb +6 -6
  38. data/spec/acceptance_helper.rb +2 -1
  39. data/spec/dummy/Rakefile +7 -0
  40. data/spec/features/functions/migrations_spec.rb +2 -2
  41. data/spec/features/functions/revert_spec.rb +2 -2
  42. data/spec/features/triggers/migrations_spec.rb +3 -3
  43. data/spec/features/triggers/revert_spec.rb +4 -4
  44. data/spec/fx/adapters/postgres/functions_spec.rb +37 -0
  45. data/spec/fx/adapters/postgres/triggers_spec.rb +45 -0
  46. data/spec/fx/adapters/postgres_spec.rb +46 -49
  47. data/spec/fx/definition_spec.rb +25 -2
  48. data/spec/fx/function_spec.rb +55 -0
  49. data/spec/fx/schema_dumper/function_spec.rb +58 -2
  50. data/spec/fx/schema_dumper/trigger_spec.rb +3 -3
  51. data/spec/fx/trigger_spec.rb +55 -0
  52. data/spec/generators/fx/function/function_generator_spec.rb +12 -0
  53. data/spec/generators/fx/trigger/trigger_generator_spec.rb +12 -0
  54. metadata +19 -11
  55. data/.ruby-version +0 -1
  56. data/gemfiles/rails40.gemfile +0 -8
  57. data/gemfiles/rails40.gemfile.lock +0 -111
  58. data/gemfiles/rails41.gemfile +0 -8
  59. data/gemfiles/rails41.gemfile.lock +0 -113
  60. data/gemfiles/rails42.gemfile.lock +0 -130
  61. data/gemfiles/rails50.gemfile.lock +0 -126
@@ -1,5 +1,9 @@
1
1
  addons:
2
- postgresql: "9.4"
2
+ postgresql: "10"
3
+ apt:
4
+ packages:
5
+ - postgresql-10
6
+ - postgresql-client-10
3
7
  before_install:
4
8
  - "echo '--colour' > ~/.rspec"
5
9
  - "echo 'gem: --no-document' > ~/.gemrc"
@@ -15,13 +19,21 @@ language:
15
19
  notifications:
16
20
  email: false
17
21
  rvm:
18
- - 2.3.1
22
+ - 2.6
23
+ - 2.5
24
+ - 2.4
19
25
  gemfile:
20
- - gemfiles/rails40.gemfile
21
- - gemfiles/rails41.gemfile
22
26
  - gemfiles/rails42.gemfile
23
27
  - gemfiles/rails50.gemfile
28
+ - gemfiles/rails51.gemfile
29
+ - gemfiles/rails52.gemfile
30
+ - gemfiles/rails60.gemfile
31
+ - gemfiles/rails_edge.gemfile
24
32
  matrix:
33
+ allow_failures:
34
+ - gemfile: gemfiles/rails_edge.gemfile
25
35
  exclude:
26
- - rvm: 2.1.8
27
- gemfile: gemfiles/rails50.gemfile
36
+ - rvm: 2.4
37
+ gemfile: gemfiles/rails_edge.gemfile
38
+ - rvm: 2.4
39
+ gemfile: gemfiles/rails60.gemfile
data/Appraisals CHANGED
@@ -1,16 +1,7 @@
1
- appraise "rails40" do
2
- gem "activerecord", "~> 4.0.0"
3
- gem "railties", "~> 4.0.0"
4
- end
5
-
6
- appraise "rails41" do
7
- gem "activerecord", "~> 4.1.0"
8
- gem "railties", "~> 4.1.0"
9
- end
10
-
11
1
  appraise "rails42" do
12
2
  gem "activerecord", "~> 4.2.0"
13
3
  gem "railties", "~> 4.2.0"
4
+ gem "pg", "~> 0.15"
14
5
  end
15
6
 
16
7
  if RUBY_VERSION > "2.2.0"
@@ -18,4 +9,24 @@ if RUBY_VERSION > "2.2.0"
18
9
  gem "activerecord", "~> 5.0"
19
10
  gem "railties", "~> 5.0"
20
11
  end
12
+
13
+ appraise "rails51" do
14
+ gem "activerecord", "~> 5.1"
15
+ gem "railties", "~> 5.1"
16
+ end
17
+
18
+ appraise "rails52" do
19
+ gem "activerecord", "~> 5.2"
20
+ gem "railties", "~> 5.2"
21
+ end
22
+
23
+ appraise "rails60" do
24
+ gem "activerecord", "~> 6.0"
25
+ gem "railties", "~> 6.0"
26
+ end
27
+
28
+ appraise "rails-edge" do
29
+ gem "rails", github: "rails/rails"
30
+ gem "arel", :github => "rails/arel"
31
+ end
21
32
  end
data/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Copyright 2016 Teo Ljungberg
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software is furnished to do so,
8
+ subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # F(x)
2
2
 
3
+ [![Build Status](https://travis-ci.com/teoljungberg/fx.svg?token=AgJn4nPeY6ue2Pvy23JQ&branch=master)](https://travis-ci.com/teoljungberg/fx)
4
+ [![Documentation Quality](http://inch-ci.org/github/teoljungberg/fx.svg?branch=master)](http://inch-ci.org/github/teoljungberg/fx)
5
+
3
6
  F(x) adds methods to `ActiveRecord::Migration` to create and manage database
4
7
  functions and triggers in Rails.
5
8
 
@@ -18,18 +21,29 @@ F(x) ships with support for PostgreSQL. The adapter is configurable (see
18
21
 
19
22
  ## Great, how do I create a trigger and a function?
20
23
 
21
- You've got this great idea for a trigger you'd like to call
24
+ You've got this great idea for a function you'd like to call
22
25
  `uppercase_users_name`. You can create the migration and the corresponding
23
26
  definition file with the following command:
24
27
 
25
28
  ```sh
26
- % rails generate fx:trigger uppercase_users_name
29
+ % rails generate fx:function uppercase_users_name
30
+ create db/functions/uppercase_users_name_v01.sql
31
+ create db/migrate/[TIMESTAMP]_create_function_uppercase_users_name.rb
32
+ ```
33
+
34
+ Edit the `db/functions/uppercase_users_name_v01.sql` file with the SQL statement
35
+ that defines your function.
36
+
37
+ Next, let's add a trigger called `uppercase_users_name` to call our new
38
+ function each time we `INSERT` on the `users` table.
39
+
40
+ ```sh
41
+ % rails generate fx:trigger uppercase_users_name table_name:users
27
42
  create db/triggers/uppercase_users_name_v01.sql
28
43
  create db/migrate/[TIMESTAMP]_create_trigger_uppercase_users_name.rb
29
44
  ```
30
45
 
31
- Edit the `db/triggers/uppercase_users_name_v01.sql` file with the SQL statement
32
- that defines your trigger. In our example, this might look something like this:
46
+ In our example, this might look something like this:
33
47
 
34
48
  ```sql
35
49
  CREATE TRIGGER uppercase_users_name
@@ -38,15 +52,6 @@ CREATE TRIGGER uppercase_users_name
38
52
  EXECUTE PROCEDURE uppercase_users_name();
39
53
  ```
40
54
 
41
- As you see, we execute a function called `uppercase_users_name` before each
42
- `INSERT` on the `users` table, which is a function we don't have yet.
43
-
44
- ```sh
45
- % rails generate fx:function uppercase_users_name
46
- create db/functions/uppercase_users_name_v01.sql
47
- create db/migrate/[TIMESTAMP]_create_function_uppercase_users_name.rb
48
- ```
49
-
50
55
  The generated migrations contains `create_function` and `create_trigger`
51
56
  statements. The migration is reversible and the schema will be dumped into your
52
57
  `schema.rb` file.
@@ -79,3 +84,30 @@ def change
79
84
  drop_function :uppercase_users_name, revert_to_version: 2
80
85
  end
81
86
  ```
87
+
88
+ ## What if I need to use a function as the default value of a column?
89
+
90
+ You need to set F(x) to dump the functions in the beginning of db/schema.rb in a
91
+ initializer:
92
+
93
+ ```ruby
94
+ # config/initializers/fx.rb
95
+ Fx.configure do |config|
96
+ config.dump_functions_at_beginning_of_schema = true
97
+ end
98
+ ```
99
+
100
+ And then you can use a lambda in your migration file:
101
+
102
+ ```ruby
103
+ create_table :my_table do |t|
104
+ t.string :my_column, default: -> { "my_function()" }
105
+ end
106
+ ```
107
+
108
+ That's how you tell Rails to use the default as a literal SQL for the default
109
+ column value instead of a plain string.
110
+
111
+ ## Contributing
112
+
113
+ See [contributing](CONTRIBUTING.md) for more details.
data/bin/setup CHANGED
@@ -9,4 +9,5 @@ if [ -z "$CI" ]; then
9
9
  bundle exec appraisal install
10
10
  fi
11
11
 
12
+ bundle exec rake dummy:db:drop
12
13
  bundle exec rake dummy:db:create
@@ -4,5 +4,6 @@ source "https://rubygems.org"
4
4
 
5
5
  gem "activerecord", "~> 4.2.0"
6
6
  gem "railties", "~> 4.2.0"
7
+ gem "pg", "~> 0.15"
7
8
 
8
- gemspec :path => "../"
9
+ gemspec path: "../"
@@ -5,4 +5,4 @@ source "https://rubygems.org"
5
5
  gem "activerecord", "~> 5.0"
6
6
  gem "railties", "~> 5.0"
7
7
 
8
- gemspec :path => "../"
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 5.1"
6
+ gem "railties", "~> 5.1"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 5.2"
6
+ gem "railties", "~> 5.2"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 6.0"
6
+ gem "railties", "~> 6.0"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", github: "rails/rails"
6
+ gem "arel", github: "rails/arel"
7
+
8
+ gemspec path: "../"
data/lib/fx.rb CHANGED
@@ -7,10 +7,32 @@ require "fx/function"
7
7
  require "fx/statements"
8
8
  require "fx/schema_dumper"
9
9
  require "fx/trigger"
10
+ require "fx/railtie"
10
11
 
11
12
  # F(x) adds methods `ActiveRecord::Migration` to create and manage database
12
13
  # triggers and functions in Rails applications.
13
14
  module Fx
15
+ # Hooks Fx into Rails.
16
+ #
17
+ # Enables fx migration methods, migration reversability, and `schema.rb`
18
+ # dumping.
19
+ def self.load
20
+ ActiveRecord::Migration::CommandRecorder.send(
21
+ :include,
22
+ Fx::CommandRecorder,
23
+ )
24
+
25
+ ActiveRecord::SchemaDumper.send(
26
+ :prepend,
27
+ Fx::SchemaDumper,
28
+ )
29
+
30
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.send(
31
+ :include,
32
+ Fx::Statements,
33
+ )
34
+ end
35
+
14
36
  # The current database adapter used by F(x).
15
37
  #
16
38
  # This defaults to {Fx::Adapters::Postgres} but can be overridden
@@ -23,6 +23,19 @@ module Fx
23
23
  # config.adapter = Fx::Adapters::Postgres.new
24
24
  # end
25
25
  class Postgres
26
+ # Creates an instance of the F(x) Postgres adapter.
27
+ #
28
+ # This is the default adapter for F(x). Configuring it via
29
+ # {Fx.configure} is not required, but the example below shows how one
30
+ # would explicitly set it.
31
+ #
32
+ # @param [#connection] connectable An object that returns the connection
33
+ # for F(x) to use. Defaults to `ActiveRecord::Base`.
34
+ #
35
+ # @example
36
+ # Fx.configure do |config|
37
+ # config.adapter = Fx::Adapters::Postgres.new
38
+ # end
26
39
  def initialize(connectable = ActiveRecord::Base)
27
40
  @connectable = connectable
28
41
  end
@@ -112,7 +125,11 @@ module Fx
112
125
  #
113
126
  # @return [void]
114
127
  def drop_function(name)
115
- execute "DROP FUNCTION #{name}();"
128
+ if support_drop_function_without_args
129
+ execute "DROP FUNCTION #{name};"
130
+ else
131
+ execute "DROP FUNCTION #{name}();"
132
+ end
116
133
  end
117
134
 
118
135
  # Drops the trigger from the database
@@ -137,6 +154,14 @@ module Fx
137
154
  def connection
138
155
  Connection.new(connectable.connection)
139
156
  end
157
+
158
+ def support_drop_function_without_args
159
+ # https://www.postgresql.org/docs/9.6/sql-dropfunction.html
160
+ # https://www.postgresql.org/docs/10/sql-dropfunction.html
161
+
162
+ pg_connection = connectable.connection.raw_connection
163
+ pg_connection.server_version >= 10_00_00
164
+ end
140
165
  end
141
166
  end
142
167
  end
@@ -8,7 +8,7 @@ module Fx
8
8
  class Functions
9
9
  # The SQL query used by F(x) to retrieve the functions considered
10
10
  # dumpable into `db/schema.rb`.
11
- FUNCTIONS_WITH_DEFINITIONS_QUERY = <<~SQL
11
+ FUNCTIONS_WITH_DEFINITIONS_QUERY = <<-EOS.freeze
12
12
  SELECT
13
13
  pp.proname AS name,
14
14
  pg_get_functiondef(pp.oid) AS definition
@@ -17,9 +17,12 @@ module Fx
17
17
  ON pn.oid = pp.pronamespace
18
18
  LEFT JOIN pg_depend pd
19
19
  ON pd.objid = pp.oid AND pd.deptype = 'e'
20
+ LEFT JOIN pg_aggregate pa
21
+ ON pa.aggfnoid = pp.oid
20
22
  WHERE pn.nspname = 'public' AND pd.objid IS NULL
23
+ AND pa.aggfnoid IS NULL
21
24
  ORDER BY pp.oid;
22
- SQL
25
+ EOS
23
26
 
24
27
  # Wraps #all as a static facade.
25
28
  #
@@ -8,7 +8,7 @@ module Fx
8
8
  class Triggers
9
9
  # The SQL query used by F(x) to retrieve the triggers considered
10
10
  # dumpable into `db/schema.rb`.
11
- TRIGGERS_WITH_DEFINITIONS_QUERY = <<~SQL
11
+ TRIGGERS_WITH_DEFINITIONS_QUERY = <<-EOS.freeze
12
12
  SELECT
13
13
  pt.tgname AS name,
14
14
  pg_get_triggerdef(pt.oid) AS definition
@@ -20,7 +20,7 @@ module Fx
20
20
  WHERE pt.tgname
21
21
  NOT ILIKE '%constraint%' AND pt.tgname NOT ILIKE 'pg%'
22
22
  ORDER BY pc.oid;
23
- SQL
23
+ EOS
24
24
 
25
25
  # Wraps #all as a static facade.
26
26
  #
@@ -22,8 +22,3 @@ module Fx
22
22
  end
23
23
  end
24
24
  end
25
-
26
- ActiveRecord::Migration::CommandRecorder.send(
27
- :include,
28
- Fx::CommandRecorder,
29
- )
@@ -17,6 +17,7 @@ module Fx
17
17
  # ```
18
18
  # Fx.configure do |config|
19
19
  # config.database = Fx::Adapters::Postgres
20
+ # config.dump_functions_at_beginning_of_schema = true
20
21
  # end
21
22
  # ```
22
23
  def self.configure
@@ -31,8 +32,17 @@ module Fx
31
32
  # @return Fx adapter
32
33
  attr_accessor :database
33
34
 
35
+ # Prioritizes the order in the schema.rb of functions before other
36
+ # statements in order to make directly schema load work when using functions
37
+ # in statements below, i.e.: default column values.
38
+ #
39
+ # Defaults to false
40
+ # @return Boolean
41
+ attr_accessor :dump_functions_at_beginning_of_schema
42
+
34
43
  def initialize
35
44
  @database = Fx::Adapters::Postgres.new
45
+ @dump_functions_at_beginning_of_schema = false
36
46
  end
37
47
  end
38
48
  end
@@ -8,7 +8,7 @@ module Fx
8
8
  end
9
9
 
10
10
  def to_sql
11
- File.read(full_path).tap do |content|
11
+ File.read(find_file || full_path).tap do |content|
12
12
  if content.empty?
13
13
  raise "Define #{@type} in #{path} before migrating."
14
14
  end
@@ -20,7 +20,7 @@ module Fx
20
20
  end
21
21
 
22
22
  def path
23
- File.join("db", @type.pluralize, filename)
23
+ @_path ||= File.join("db", @type.pluralize, filename)
24
24
  end
25
25
 
26
26
  def version
@@ -30,7 +30,17 @@ module Fx
30
30
  private
31
31
 
32
32
  def filename
33
- "#{@name}_v#{version}.sql"
33
+ @_filename ||= "#{@name}_v#{version}.sql"
34
+ end
35
+
36
+ def find_file
37
+ migration_paths.lazy
38
+ .map { |migration_path| File.expand_path(File.join("..", "..", path), migration_path) }
39
+ .find { |definition_path| File.exist?(definition_path) }
40
+ end
41
+
42
+ def migration_paths
43
+ Rails.application.config.paths["db/migrate"].expanded
34
44
  end
35
45
  end
36
46
  end
@@ -1,6 +1,8 @@
1
1
  module Fx
2
2
  # @api private
3
3
  class Function
4
+ include Comparable
5
+
4
6
  attr_reader :name, :definition
5
7
  delegate :<=>, to: :name
6
8