fx 0.10.1 → 0.11.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 +4 -4
- data/.github/scripts/check-eol +95 -0
- data/.github/workflows/ci.yml +11 -3
- data/.github/workflows/eol-check.yml +30 -0
- data/CHANGELOG.md +22 -2
- data/Gemfile +7 -2
- data/README.md +38 -7
- data/Rakefile +9 -0
- data/fx.gemspec +3 -3
- data/lib/fx/adapters/postgres/connection.rb +4 -18
- data/lib/fx/adapters/postgres/functions.rb +2 -1
- data/lib/fx/adapters/postgres.rb +1 -5
- data/lib/fx/command_recorder.rb +33 -20
- data/lib/fx/function.rb +14 -2
- data/lib/fx/statements.rb +6 -24
- data/lib/fx/version.rb +1 -1
- data/spec/fx/adapters/postgres_spec.rb +2 -35
- data/spec/fx/command_recorder_spec.rb +0 -16
- data/spec/fx/function_spec.rb +87 -9
- data/spec/fx/statements_spec.rb +2 -0
- metadata +6 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a1b25361cfa43280f81e493a4eb518864862dad321878048282ce8f9daad6e1a
|
|
4
|
+
data.tar.gz: ce20ce6b1bb9031b420e78b83ecf23416d50ca44668cf15e0951142e32a0c75e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2e8d5db85fb283586a9d713cdffb6fc50604e85498eeb2c16ac8f63708fabe01a29e03b4157b32f84fe061d4bedb996f4e44db90f5d53d2b655c730352c7c101
|
|
7
|
+
data.tar.gz: 70e6670c67e267f383fa7ed3001350fa3c6548d4bf6183625ee10bf1a5c53cfa5c87cbd9bf231b196524f206d96bf638ba16f6b9020f20803f4ae85685fbf44a
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
#
|
|
3
|
+
# Check if minimum supported dependency versions are at or approaching EOL.
|
|
4
|
+
# Uses the endoflife.date API. Outputs GitHub Actions annotations and opens
|
|
5
|
+
# an issue when action is needed.
|
|
6
|
+
#
|
|
7
|
+
# Environment:
|
|
8
|
+
# WARN_DAYS - days before EOL to start warning (default: 90)
|
|
9
|
+
# GH_TOKEN - GitHub token for creating issues (optional)
|
|
10
|
+
|
|
11
|
+
set -e
|
|
12
|
+
|
|
13
|
+
WARN_DAYS="${WARN_DAYS:-90}"
|
|
14
|
+
|
|
15
|
+
ruby_min=$(ruby -e "
|
|
16
|
+
spec = Gem::Specification.load('fx.gemspec')
|
|
17
|
+
puts spec.required_ruby_version.requirements
|
|
18
|
+
.select { |op, _| op == '>=' }
|
|
19
|
+
.map { |_, v| v.segments[0..1].join('.') }
|
|
20
|
+
.first
|
|
21
|
+
")
|
|
22
|
+
|
|
23
|
+
rails_min=$(ruby -e "
|
|
24
|
+
spec = Gem::Specification.load('fx.gemspec')
|
|
25
|
+
dep = spec.dependencies.find { |d| d.name == 'activerecord' }
|
|
26
|
+
puts dep.requirement.requirements
|
|
27
|
+
.select { |op, _| op == '>=' }
|
|
28
|
+
.map { |_, v| v.segments[0..1].join('.') }
|
|
29
|
+
.first
|
|
30
|
+
")
|
|
31
|
+
|
|
32
|
+
postgres_min=$(ruby -ryaml -e "
|
|
33
|
+
ci = YAML.load_file('.github/workflows/ci.yml')
|
|
34
|
+
versions = ci.dig('jobs', 'tests', 'strategy', 'matrix', 'postgres')
|
|
35
|
+
puts versions.map(&:to_i).min
|
|
36
|
+
")
|
|
37
|
+
|
|
38
|
+
echo "Ruby minimum: $ruby_min"
|
|
39
|
+
echo "Rails minimum: $rails_min"
|
|
40
|
+
echo "PostgreSQL minimum: $postgres_min"
|
|
41
|
+
|
|
42
|
+
alerts=""
|
|
43
|
+
|
|
44
|
+
check_eol() {
|
|
45
|
+
product="$1"
|
|
46
|
+
version="$2"
|
|
47
|
+
|
|
48
|
+
eol_date=$(curl -sf "https://endoflife.date/api/${product}/${version}.json" \
|
|
49
|
+
| ruby -rjson -e '
|
|
50
|
+
data = JSON.parse($stdin.read)
|
|
51
|
+
eol = data["eol"]
|
|
52
|
+
if eol == true
|
|
53
|
+
puts "1970-01-01"
|
|
54
|
+
elsif eol == false
|
|
55
|
+
exit
|
|
56
|
+
else
|
|
57
|
+
puts eol
|
|
58
|
+
end
|
|
59
|
+
') || return 0
|
|
60
|
+
|
|
61
|
+
if [ -z "$eol_date" ]; then
|
|
62
|
+
echo "$product $version: no EOL date set (still supported)"
|
|
63
|
+
return
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
today=$(date -u +%Y-%m-%d)
|
|
67
|
+
warn_date=$(date -u -d "$today + ${WARN_DAYS} days" +%Y-%m-%d 2>/dev/null \
|
|
68
|
+
|| date -u -v+"${WARN_DAYS}"d +%Y-%m-%d)
|
|
69
|
+
|
|
70
|
+
if [ "$eol_date" \< "$today" ] || [ "$eol_date" = "$today" ]; then
|
|
71
|
+
echo "::error::$product $version reached EOL on $eol_date"
|
|
72
|
+
alerts="$alerts- **$product $version** reached EOL on $eol_date\n"
|
|
73
|
+
elif [ "$eol_date" \< "$warn_date" ]; then
|
|
74
|
+
echo "::warning::$product $version reaches EOL on $eol_date (within ${WARN_DAYS} days)"
|
|
75
|
+
alerts="$alerts- **$product $version** reaches EOL on $eol_date (within ${WARN_DAYS} days)\n"
|
|
76
|
+
else
|
|
77
|
+
echo "$product $version: EOL on $eol_date (no action needed)"
|
|
78
|
+
fi
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
check_eol "ruby" "$ruby_min"
|
|
82
|
+
check_eol "rails" "$rails_min"
|
|
83
|
+
check_eol "postgresql" "$postgres_min"
|
|
84
|
+
|
|
85
|
+
if [ -n "$alerts" ]; then
|
|
86
|
+
title="Dependency EOL alert"
|
|
87
|
+
body=$(printf "The following minimum supported versions are at or approaching end-of-life:\n\n%b\n\nConsider bumping the minimum version in \`fx.gemspec\` and updating the CI matrix." "$alerts")
|
|
88
|
+
|
|
89
|
+
existing=$(gh issue list --state open --search "$title" --json number --jq '.[0].number' 2>/dev/null || true)
|
|
90
|
+
if [ -n "$existing" ]; then
|
|
91
|
+
echo "Issue #$existing already open, skipping creation"
|
|
92
|
+
else
|
|
93
|
+
gh issue create --title "$title" --body "$body"
|
|
94
|
+
fi
|
|
95
|
+
fi
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -8,18 +8,26 @@ on:
|
|
|
8
8
|
|
|
9
9
|
jobs:
|
|
10
10
|
tests:
|
|
11
|
-
name:
|
|
11
|
+
name: "ruby ${{ matrix.ruby }} / rails ${{ matrix.rails }} / postgres ${{ matrix.postgres }}"
|
|
12
12
|
runs-on: ubuntu-latest
|
|
13
13
|
|
|
14
14
|
strategy:
|
|
15
15
|
fail-fast: false
|
|
16
16
|
matrix:
|
|
17
|
-
ruby: ["3.
|
|
17
|
+
ruby: ["3.3", "4.0"]
|
|
18
18
|
rails: ["7.2", "8.1"]
|
|
19
|
+
postgres: ["14", "18"]
|
|
20
|
+
include:
|
|
21
|
+
- ruby: "4.0"
|
|
22
|
+
rails: "main"
|
|
23
|
+
postgres: "18"
|
|
24
|
+
allow-failure: true
|
|
25
|
+
|
|
26
|
+
continue-on-error: ${{ matrix.allow-failure || false }}
|
|
19
27
|
|
|
20
28
|
services:
|
|
21
29
|
postgres:
|
|
22
|
-
image: postgres
|
|
30
|
+
image: postgres:${{ matrix.postgres }}
|
|
23
31
|
env:
|
|
24
32
|
POSTGRES_USER: postgres
|
|
25
33
|
POSTGRES_HOST_AUTH_METHOD: trust
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: EOL Check
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: "0 9 1 * *" # 1st of each month at 09:00 UTC
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
inputs:
|
|
8
|
+
warn_days:
|
|
9
|
+
description: "Days before EOL to start warning"
|
|
10
|
+
required: false
|
|
11
|
+
default: "90"
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
issues: write
|
|
15
|
+
|
|
16
|
+
env:
|
|
17
|
+
WARN_DAYS: ${{ inputs.warn_days || '90' }}
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
check:
|
|
21
|
+
name: Check dependency EOL status
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v6
|
|
26
|
+
|
|
27
|
+
- name: Check EOL status
|
|
28
|
+
env:
|
|
29
|
+
GH_TOKEN: ${{ github.token }}
|
|
30
|
+
run: .github/scripts/check-eol
|
data/CHANGELOG.md
CHANGED
|
@@ -5,9 +5,29 @@ changelog, see the [commits] for each version via the version links.
|
|
|
5
5
|
|
|
6
6
|
[commits]: https://github.com/teoljungberg/fx/commits/master
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [0.11.0]
|
|
9
9
|
|
|
10
|
-
[
|
|
10
|
+
[0.11.0]: https://github.com/teoljungberg/fx/compare/v0.10.2...v0.11.0
|
|
11
|
+
|
|
12
|
+
- Drop EOL Ruby 3.2 (#212)
|
|
13
|
+
- Add `Function#signature` for PostgreSQL function identity (#207)
|
|
14
|
+
- Add PostgreSQL versioning policy, officially supporting PostgreSQL 14-18 (#194)
|
|
15
|
+
- Refactor Statements module to use explicit keyword arguments instead of `**options` hash (#186)
|
|
16
|
+
- Internal refactorings / improvements
|
|
17
|
+
- Add scheduled EOL check for Ruby, Rails, and PostgreSQL (#205)
|
|
18
|
+
- Add GitHub release creation to release task (#209)
|
|
19
|
+
- Add PostgreSQL 18 to CI test matrix (#194)
|
|
20
|
+
- Default `support_drop_function_without_args` to `true` (#194)
|
|
21
|
+
- Document adapter subclass pattern for custom schema dump ordering (#211)
|
|
22
|
+
- Remove duplicate command recorder specs (#202)
|
|
23
|
+
|
|
24
|
+
## [0.10.2]
|
|
25
|
+
|
|
26
|
+
[0.10.2]: https://github.com/teoljungberg/fx/compare/v0.10.1...v0.10.2
|
|
27
|
+
|
|
28
|
+
- Remove upper bound on Rails dependency (#198)
|
|
29
|
+
- Internal refactorings / improvements
|
|
30
|
+
- Add Rails main to CI matrix
|
|
11
31
|
|
|
12
32
|
## [0.10.1]
|
|
13
33
|
|
data/Gemfile
CHANGED
|
@@ -4,8 +4,13 @@ rails_version = ENV.fetch("RAILS_VERSION", "8.1")
|
|
|
4
4
|
|
|
5
5
|
gemspec
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
gem "
|
|
7
|
+
if rails_version == "main"
|
|
8
|
+
gem "activerecord", github: "rails/rails", branch: "main"
|
|
9
|
+
gem "railties", github: "rails/rails", branch: "main"
|
|
10
|
+
else
|
|
11
|
+
gem "activerecord", "~> #{rails_version}.0"
|
|
12
|
+
gem "railties", "~> #{rails_version}.0"
|
|
13
|
+
end
|
|
9
14
|
|
|
10
15
|
gem "bundler", ">= 1.5"
|
|
11
16
|
gem "pg"
|
data/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# F(x)
|
|
2
2
|
|
|
3
3
|
[](https://github.com/teoljungberg/fx/actions/workflows/ci.yml)
|
|
4
|
-
[](http://inch-ci.org/github/teoljungberg/fx)
|
|
5
4
|
|
|
6
5
|
F(x) adds methods to `ActiveRecord::Migration` to create and manage database
|
|
7
6
|
functions and triggers in Rails.
|
|
@@ -108,6 +107,35 @@ end
|
|
|
108
107
|
That's how you tell Rails to use the default as a literal SQL for the default
|
|
109
108
|
column value instead of a plain string.
|
|
110
109
|
|
|
110
|
+
## Customizing Schema Dump Order
|
|
111
|
+
|
|
112
|
+
By default, functions and triggers are dumped to `schema.rb` in the order
|
|
113
|
+
returned by the database. If you need a specific ordering (e.g., alphabetical
|
|
114
|
+
for deterministic diffs), subclass the adapter and override `#functions` or
|
|
115
|
+
`#triggers`. These methods are part of the adapter's public API and will remain
|
|
116
|
+
stable across releases:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
# config/initializers/fx.rb
|
|
120
|
+
class SortedPostgresAdapter < Fx::Adapters::Postgres
|
|
121
|
+
def functions
|
|
122
|
+
super.sort_by(&:name)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def triggers
|
|
126
|
+
super.sort_by(&:name)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
Fx.configure do |config|
|
|
131
|
+
config.database = SortedPostgresAdapter.new
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The same approach works for more advanced ordering. For example, if your
|
|
136
|
+
functions depend on each other and need to be dumped in dependency order, you
|
|
137
|
+
could use Ruby's built-in `TSort` to topologically sort them.
|
|
138
|
+
|
|
111
139
|
## Plugins/Adapters
|
|
112
140
|
|
|
113
141
|
- [MySQL](https://github.com/f-mer/fx-adapters-mysql/)
|
|
@@ -116,19 +144,22 @@ column value instead of a plain string.
|
|
|
116
144
|
|
|
117
145
|
## Version Support
|
|
118
146
|
|
|
119
|
-
F(x) follows the maintenance policies of Ruby and
|
|
120
|
-
within their official maintenance windows.
|
|
147
|
+
F(x) follows the maintenance policies of Ruby, Rails, and PostgreSQL, supporting
|
|
148
|
+
versions within their official maintenance windows.
|
|
121
149
|
|
|
122
|
-
**Ruby:** 3.
|
|
150
|
+
**Ruby:** 3.3+ ([maintenance branches])
|
|
123
151
|
|
|
124
152
|
**Rails:** 7.2, 8.0, 8.1 ([maintenance policy])
|
|
125
153
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
154
|
+
**PostgreSQL:** 14, 15, 16, 17, 18 ([versioning policy])
|
|
155
|
+
|
|
156
|
+
When a version reaches end-of-life, support will be dropped in the next minor
|
|
157
|
+
release of F(x). Older versions may continue to work but are not tested or
|
|
158
|
+
guaranteed.
|
|
129
159
|
|
|
130
160
|
[maintenance branches]: https://www.ruby-lang.org/en/downloads/branches/
|
|
131
161
|
[maintenance policy]: https://rubyonrails.org/maintenance
|
|
162
|
+
[versioning policy]: https://www.postgresql.org/support/versioning/
|
|
132
163
|
|
|
133
164
|
## Contributing
|
|
134
165
|
|
data/Rakefile
CHANGED
|
@@ -2,6 +2,15 @@ require "bundler/gem_tasks"
|
|
|
2
2
|
require "rspec/core/rake_task"
|
|
3
3
|
require "standard/rake"
|
|
4
4
|
|
|
5
|
+
Rake::Task["release"].enhance do
|
|
6
|
+
unless system("which gh > /dev/null 2>&1")
|
|
7
|
+
abort "gh CLI is not installed. Install it to create GitHub releases."
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
tag = "v#{Fx::VERSION}"
|
|
11
|
+
sh "gh release create #{tag} --verify-tag -t #{tag} --generate-notes"
|
|
12
|
+
end
|
|
13
|
+
|
|
5
14
|
namespace :dummy do
|
|
6
15
|
require_relative "spec/dummy/config/application"
|
|
7
16
|
Dummy::Application.load_tasks
|
data/fx.gemspec
CHANGED
|
@@ -24,8 +24,8 @@ Gem::Specification.new do |spec|
|
|
|
24
24
|
spec.files = `git ls-files -z`.split("\x0")
|
|
25
25
|
spec.require_paths = ["lib"]
|
|
26
26
|
|
|
27
|
-
spec.add_dependency "activerecord", ">= 7.2"
|
|
28
|
-
spec.add_dependency "railties", ">= 7.2"
|
|
27
|
+
spec.add_dependency "activerecord", ">= 7.2"
|
|
28
|
+
spec.add_dependency "railties", ">= 7.2"
|
|
29
29
|
|
|
30
|
-
spec.required_ruby_version = ">= 3.
|
|
30
|
+
spec.required_ruby_version = ">= 3.3"
|
|
31
31
|
end
|
|
@@ -10,25 +10,11 @@ module Fx
|
|
|
10
10
|
#
|
|
11
11
|
# @api private
|
|
12
12
|
class Connection < SimpleDelegator
|
|
13
|
-
# PostgreSQL
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
# https://www.postgresql.org/docs/10/sql-dropfunction.html
|
|
17
|
-
v10: 10_00_00
|
|
18
|
-
}.freeze
|
|
19
|
-
|
|
13
|
+
# All supported PostgreSQL versions (14+) support DROP FUNCTION
|
|
14
|
+
# without argument lists.
|
|
15
|
+
# https://www.postgresql.org/docs/10/sql-dropfunction.html
|
|
20
16
|
def support_drop_function_without_args
|
|
21
|
-
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
|
|
26
|
-
def server_version
|
|
27
|
-
undecorated_connection.raw_connection.server_version
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def undecorated_connection
|
|
31
|
-
__getobj__
|
|
17
|
+
true
|
|
32
18
|
end
|
|
33
19
|
end
|
|
34
20
|
end
|
|
@@ -12,7 +12,8 @@ module Fx
|
|
|
12
12
|
FUNCTIONS_WITH_DEFINITIONS_QUERY = <<~SQL.freeze
|
|
13
13
|
SELECT
|
|
14
14
|
pp.proname AS name,
|
|
15
|
-
pg_get_functiondef(pp.oid) AS definition
|
|
15
|
+
pg_get_functiondef(pp.oid) AS definition,
|
|
16
|
+
pg_get_function_identity_arguments(pp.oid) AS arguments
|
|
16
17
|
FROM pg_proc pp
|
|
17
18
|
JOIN pg_namespace pn
|
|
18
19
|
ON pn.oid = pp.pronamespace
|
data/lib/fx/adapters/postgres.rb
CHANGED
|
@@ -125,11 +125,7 @@ module Fx
|
|
|
125
125
|
#
|
|
126
126
|
# @return [void]
|
|
127
127
|
def drop_function(name)
|
|
128
|
-
|
|
129
|
-
execute("DROP FUNCTION #{name};")
|
|
130
|
-
else
|
|
131
|
-
execute("DROP FUNCTION #{name}();")
|
|
132
|
-
end
|
|
128
|
+
execute("DROP FUNCTION #{name};")
|
|
133
129
|
end
|
|
134
130
|
|
|
135
131
|
# Drops the trigger from the database
|
data/lib/fx/command_recorder.rb
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
module Fx
|
|
2
2
|
# @api private
|
|
3
3
|
module CommandRecorder
|
|
4
|
-
def create_function(*args)
|
|
5
|
-
record(:create_function, args)
|
|
4
|
+
def create_function(*args, &block)
|
|
5
|
+
record(:create_function, args, &block)
|
|
6
6
|
end
|
|
7
|
+
ruby2_keywords :create_function
|
|
7
8
|
|
|
8
|
-
def drop_function(*args)
|
|
9
|
-
record(:drop_function, args)
|
|
9
|
+
def drop_function(*args, &block)
|
|
10
|
+
record(:drop_function, args, &block)
|
|
10
11
|
end
|
|
12
|
+
ruby2_keywords :drop_function
|
|
11
13
|
|
|
12
|
-
def update_function(*args)
|
|
13
|
-
record(:update_function, args)
|
|
14
|
+
def update_function(*args, &block)
|
|
15
|
+
record(:update_function, args, &block)
|
|
14
16
|
end
|
|
17
|
+
ruby2_keywords :update_function
|
|
15
18
|
|
|
16
19
|
def invert_create_function(args)
|
|
17
20
|
[:drop_function, args]
|
|
@@ -25,17 +28,20 @@ module Fx
|
|
|
25
28
|
perform_inversion(:update_function, args)
|
|
26
29
|
end
|
|
27
30
|
|
|
28
|
-
def create_trigger(*args)
|
|
29
|
-
record(:create_trigger, args)
|
|
31
|
+
def create_trigger(*args, &block)
|
|
32
|
+
record(:create_trigger, args, &block)
|
|
30
33
|
end
|
|
34
|
+
ruby2_keywords :create_trigger
|
|
31
35
|
|
|
32
|
-
def drop_trigger(*args)
|
|
33
|
-
record(:drop_trigger, args)
|
|
36
|
+
def drop_trigger(*args, &block)
|
|
37
|
+
record(:drop_trigger, args, &block)
|
|
34
38
|
end
|
|
39
|
+
ruby2_keywords :drop_trigger
|
|
35
40
|
|
|
36
|
-
def update_trigger(*args)
|
|
37
|
-
record(:update_trigger, args)
|
|
41
|
+
def update_trigger(*args, &block)
|
|
42
|
+
record(:update_trigger, args, &block)
|
|
38
43
|
end
|
|
44
|
+
ruby2_keywords :update_trigger
|
|
39
45
|
|
|
40
46
|
def invert_create_trigger(args)
|
|
41
47
|
[:drop_trigger, args]
|
|
@@ -51,12 +57,13 @@ module Fx
|
|
|
51
57
|
|
|
52
58
|
private
|
|
53
59
|
|
|
60
|
+
MESSAGE_IRREVERSIBLE = "`%s` is reversible only if given a `revert_to_version`".freeze
|
|
61
|
+
|
|
54
62
|
def perform_inversion(method, args)
|
|
55
63
|
arguments = Arguments.new(args)
|
|
56
64
|
|
|
57
65
|
if arguments.revert_to_version.nil?
|
|
58
|
-
|
|
59
|
-
raise ActiveRecord::IrreversibleMigration, message
|
|
66
|
+
raise ActiveRecord::IrreversibleMigration, format(MESSAGE_IRREVERSIBLE, method)
|
|
60
67
|
end
|
|
61
68
|
|
|
62
69
|
[method, arguments.invert_version.to_a]
|
|
@@ -68,19 +75,19 @@ module Fx
|
|
|
68
75
|
end
|
|
69
76
|
|
|
70
77
|
def function
|
|
71
|
-
args
|
|
78
|
+
args.fetch(0)
|
|
72
79
|
end
|
|
73
80
|
|
|
74
81
|
def version
|
|
75
|
-
options
|
|
82
|
+
options.fetch(:version)
|
|
76
83
|
end
|
|
77
84
|
|
|
78
85
|
def revert_to_version
|
|
79
|
-
options
|
|
86
|
+
options.fetch(:revert_to_version, nil)
|
|
80
87
|
end
|
|
81
88
|
|
|
82
89
|
def invert_version
|
|
83
|
-
|
|
90
|
+
self.class.new([function, options_for_revert])
|
|
84
91
|
end
|
|
85
92
|
|
|
86
93
|
def to_a
|
|
@@ -92,14 +99,20 @@ module Fx
|
|
|
92
99
|
attr_reader :args
|
|
93
100
|
|
|
94
101
|
def options
|
|
95
|
-
@options ||= args
|
|
102
|
+
@options ||= args.fetch(1, {}).dup
|
|
96
103
|
end
|
|
97
104
|
|
|
98
105
|
def options_for_revert
|
|
99
|
-
options.clone.tap do |revert_options|
|
|
106
|
+
opts = options.clone.tap do |revert_options|
|
|
100
107
|
revert_options[:version] = revert_to_version
|
|
101
108
|
revert_options.delete(:revert_to_version)
|
|
102
109
|
end
|
|
110
|
+
|
|
111
|
+
keyword_hash(opts)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def keyword_hash(hash)
|
|
115
|
+
Hash.ruby2_keywords_hash(hash)
|
|
103
116
|
end
|
|
104
117
|
end
|
|
105
118
|
private_constant :Arguments
|
data/lib/fx/function.rb
CHANGED
|
@@ -4,15 +4,27 @@ module Fx
|
|
|
4
4
|
include Comparable
|
|
5
5
|
|
|
6
6
|
attr_reader :name, :definition
|
|
7
|
-
delegate :<=>, to: :name
|
|
8
7
|
|
|
9
8
|
def initialize(row)
|
|
10
9
|
@name = row.fetch("name")
|
|
11
10
|
@definition = row.fetch("definition")
|
|
11
|
+
@arguments = row.fetch("arguments", nil)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def <=>(other)
|
|
15
|
+
signature <=> other.signature
|
|
12
16
|
end
|
|
13
17
|
|
|
14
18
|
def ==(other)
|
|
15
|
-
|
|
19
|
+
signature == other.signature && definition == other.definition
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def signature
|
|
23
|
+
if @arguments.nil?
|
|
24
|
+
name
|
|
25
|
+
else
|
|
26
|
+
"#{name}(#{@arguments})"
|
|
27
|
+
end
|
|
16
28
|
end
|
|
17
29
|
|
|
18
30
|
def to_schema
|
data/lib/fx/statements.rb
CHANGED
|
@@ -26,10 +26,7 @@ module Fx
|
|
|
26
26
|
# $$ LANGUAGE plpgsql;
|
|
27
27
|
# SQL
|
|
28
28
|
#
|
|
29
|
-
def create_function(name,
|
|
30
|
-
version = options.fetch(:version, 1)
|
|
31
|
-
sql_definition = options[:sql_definition]
|
|
32
|
-
|
|
29
|
+
def create_function(name, version: 1, sql_definition: nil, revert_to_version: nil)
|
|
33
30
|
validate_version_or_sql_definition_present!(version, sql_definition)
|
|
34
31
|
sql_definition = resolve_sql_definition(sql_definition, name, version, :function)
|
|
35
32
|
|
|
@@ -47,7 +44,7 @@ module Fx
|
|
|
47
44
|
# @example Drop a function, rolling back to version 2 on rollback
|
|
48
45
|
# drop_function(:uppercase_users_name, revert_to_version: 2)
|
|
49
46
|
#
|
|
50
|
-
def drop_function(name,
|
|
47
|
+
def drop_function(name, revert_to_version: nil)
|
|
51
48
|
Fx.database.drop_function(name)
|
|
52
49
|
end
|
|
53
50
|
|
|
@@ -80,10 +77,7 @@ module Fx
|
|
|
80
77
|
# $$ LANGUAGE plpgsql;
|
|
81
78
|
# SQL
|
|
82
79
|
#
|
|
83
|
-
def update_function(name,
|
|
84
|
-
version = options[:version]
|
|
85
|
-
sql_definition = options[:sql_definition]
|
|
86
|
-
|
|
80
|
+
def update_function(name, version: nil, sql_definition: nil, revert_to_version: nil)
|
|
87
81
|
validate_version_or_sql_definition_present!(version, sql_definition)
|
|
88
82
|
|
|
89
83
|
sql_definition = resolve_sql_definition(sql_definition, name, version, :function)
|
|
@@ -113,10 +107,7 @@ module Fx
|
|
|
113
107
|
# EXECUTE FUNCTION uppercase_users_name();
|
|
114
108
|
# SQL
|
|
115
109
|
#
|
|
116
|
-
def create_trigger(name,
|
|
117
|
-
version = options[:version]
|
|
118
|
-
sql_definition = options[:sql_definition]
|
|
119
|
-
|
|
110
|
+
def create_trigger(name, version: nil, sql_definition: nil, on: nil, revert_to_version: nil)
|
|
120
111
|
validate_version_and_sql_definition_exclusive!(version, sql_definition)
|
|
121
112
|
|
|
122
113
|
version ||= 1
|
|
@@ -139,8 +130,7 @@ module Fx
|
|
|
139
130
|
# @example Drop a trigger, rolling back to version 3 on rollback
|
|
140
131
|
# drop_trigger(:log_inserts, on: :users, revert_to_version: 3)
|
|
141
132
|
#
|
|
142
|
-
def drop_trigger(name,
|
|
143
|
-
on = options.fetch(:on)
|
|
133
|
+
def drop_trigger(name, on:, revert_to_version: nil)
|
|
144
134
|
Fx.database.drop_trigger(name, on: on)
|
|
145
135
|
end
|
|
146
136
|
|
|
@@ -176,18 +166,10 @@ module Fx
|
|
|
176
166
|
# EXECUTE FUNCTION uppercase_users_name();
|
|
177
167
|
# SQL
|
|
178
168
|
#
|
|
179
|
-
def update_trigger(name,
|
|
180
|
-
version = options[:version]
|
|
181
|
-
on = options[:on]
|
|
182
|
-
sql_definition = options[:sql_definition]
|
|
183
|
-
|
|
169
|
+
def update_trigger(name, on:, version: nil, sql_definition: nil, revert_to_version: nil)
|
|
184
170
|
validate_version_or_sql_definition_present!(version, sql_definition)
|
|
185
171
|
validate_version_and_sql_definition_exclusive!(version, sql_definition)
|
|
186
172
|
|
|
187
|
-
if on.nil?
|
|
188
|
-
raise ArgumentError, "on is required"
|
|
189
|
-
end
|
|
190
|
-
|
|
191
173
|
sql_definition = resolve_sql_definition(sql_definition, name, version, :trigger)
|
|
192
174
|
|
|
193
175
|
Fx.database.update_trigger(
|
data/lib/fx/version.rb
CHANGED
|
@@ -143,44 +143,11 @@ RSpec.describe Fx::Adapters::Postgres, :db do
|
|
|
143
143
|
end
|
|
144
144
|
|
|
145
145
|
describe "#support_drop_function_without_args" do
|
|
146
|
-
it "returns true for PostgreSQL
|
|
146
|
+
it "returns true for all supported PostgreSQL versions" do
|
|
147
147
|
adapter = Fx::Adapters::Postgres.new
|
|
148
148
|
connection = adapter.send(:connection)
|
|
149
|
-
allow(connection).to receive(:server_version).and_return(10_00_00)
|
|
150
149
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
expect(result).to be(true)
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
it "returns true for PostgreSQL version 11.0" do
|
|
157
|
-
adapter = Fx::Adapters::Postgres.new
|
|
158
|
-
connection = adapter.send(:connection)
|
|
159
|
-
allow(connection).to receive(:server_version).and_return(11_00_00)
|
|
160
|
-
|
|
161
|
-
result = connection.support_drop_function_without_args
|
|
162
|
-
|
|
163
|
-
expect(result).to be(true)
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
it "returns false for PostgreSQL version 9.6" do
|
|
167
|
-
adapter = Fx::Adapters::Postgres.new
|
|
168
|
-
connection = adapter.send(:connection)
|
|
169
|
-
allow(connection).to receive(:server_version).and_return(9_06_00)
|
|
170
|
-
|
|
171
|
-
result = connection.support_drop_function_without_args
|
|
172
|
-
|
|
173
|
-
expect(result).to be(false)
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
it "returns false for PostgreSQL version 9.5" do
|
|
177
|
-
adapter = Fx::Adapters::Postgres.new
|
|
178
|
-
connection = adapter.send(:connection)
|
|
179
|
-
allow(connection).to receive(:server_version).and_return(9_05_00)
|
|
180
|
-
|
|
181
|
-
result = connection.support_drop_function_without_args
|
|
182
|
-
|
|
183
|
-
expect(result).to be(false)
|
|
150
|
+
expect(connection.support_drop_function_without_args).to be(true)
|
|
184
151
|
end
|
|
185
152
|
end
|
|
186
153
|
end
|
|
@@ -10,14 +10,6 @@ RSpec.describe Fx::CommandRecorder, :db do
|
|
|
10
10
|
expect(recorder.commands).to eq([[:create_function, [:test], nil]])
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
it "reverts to drop_function" do
|
|
14
|
-
recorder = ActiveRecord::Migration::CommandRecorder.new
|
|
15
|
-
|
|
16
|
-
recorder.create_function :test
|
|
17
|
-
|
|
18
|
-
expect(recorder.commands).to eq([[:create_function, [:test], nil]])
|
|
19
|
-
end
|
|
20
|
-
|
|
21
13
|
it "reverts to drop_function" do
|
|
22
14
|
recorder = ActiveRecord::Migration::CommandRecorder.new
|
|
23
15
|
|
|
@@ -95,14 +87,6 @@ RSpec.describe Fx::CommandRecorder, :db do
|
|
|
95
87
|
expect(recorder.commands).to eq([[:create_trigger, [:greetings], nil]])
|
|
96
88
|
end
|
|
97
89
|
|
|
98
|
-
it "reverts to drop_trigger" do
|
|
99
|
-
recorder = ActiveRecord::Migration::CommandRecorder.new
|
|
100
|
-
|
|
101
|
-
recorder.create_trigger :greetings
|
|
102
|
-
|
|
103
|
-
expect(recorder.commands).to eq([[:create_trigger, [:greetings], nil]])
|
|
104
|
-
end
|
|
105
|
-
|
|
106
90
|
it "reverts to drop_trigger" do
|
|
107
91
|
recorder = ActiveRecord::Migration::CommandRecorder.new
|
|
108
92
|
|
data/spec/fx/function_spec.rb
CHANGED
|
@@ -2,44 +2,121 @@ require "spec_helper"
|
|
|
2
2
|
|
|
3
3
|
RSpec.describe Fx::Function do
|
|
4
4
|
describe "#<=>" do
|
|
5
|
-
it "delegates to `
|
|
5
|
+
it "delegates to `signature`" do
|
|
6
6
|
function_a = Fx::Function.new(
|
|
7
7
|
"name" => "name_a",
|
|
8
|
-
"definition" => "some definition"
|
|
8
|
+
"definition" => "some definition",
|
|
9
|
+
"arguments" => ""
|
|
9
10
|
)
|
|
10
11
|
function_b = Fx::Function.new(
|
|
11
12
|
"name" => "name_b",
|
|
12
|
-
"definition" => "some definition"
|
|
13
|
+
"definition" => "some definition",
|
|
14
|
+
"arguments" => ""
|
|
13
15
|
)
|
|
14
16
|
function_c = Fx::Function.new(
|
|
15
17
|
"name" => "name_c",
|
|
16
|
-
"definition" => "some definition"
|
|
18
|
+
"definition" => "some definition",
|
|
19
|
+
"arguments" => ""
|
|
17
20
|
)
|
|
18
21
|
|
|
19
22
|
expect(function_b).to be_between(function_a, function_c)
|
|
20
23
|
end
|
|
24
|
+
|
|
25
|
+
it "orders overloads by signature" do
|
|
26
|
+
add_int = Fx::Function.new(
|
|
27
|
+
"name" => "add",
|
|
28
|
+
"definition" => "some definition",
|
|
29
|
+
"arguments" => "integer, integer"
|
|
30
|
+
)
|
|
31
|
+
add_text = Fx::Function.new(
|
|
32
|
+
"name" => "add",
|
|
33
|
+
"definition" => "some definition",
|
|
34
|
+
"arguments" => "text, text"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
expect(add_int <=> add_text).to be < 0
|
|
38
|
+
end
|
|
21
39
|
end
|
|
22
40
|
|
|
23
41
|
describe "#==" do
|
|
24
|
-
it "compares `
|
|
42
|
+
it "compares `signature` and `definition`" do
|
|
25
43
|
function_a = Fx::Function.new(
|
|
26
44
|
"name" => "name_a",
|
|
27
|
-
"definition" => "some definition"
|
|
45
|
+
"definition" => "some definition",
|
|
46
|
+
"arguments" => ""
|
|
28
47
|
)
|
|
29
48
|
function_b = Fx::Function.new(
|
|
30
49
|
"name" => "name_b",
|
|
31
|
-
"definition" => "some other definition"
|
|
50
|
+
"definition" => "some other definition",
|
|
51
|
+
"arguments" => ""
|
|
32
52
|
)
|
|
33
53
|
|
|
34
54
|
expect(function_a).not_to eq(function_b)
|
|
35
55
|
end
|
|
56
|
+
|
|
57
|
+
it "distinguishes overloads with the same name" do
|
|
58
|
+
add_int = Fx::Function.new(
|
|
59
|
+
"name" => "add",
|
|
60
|
+
"definition" => "CREATE FUNCTION add(integer)",
|
|
61
|
+
"arguments" => "integer"
|
|
62
|
+
)
|
|
63
|
+
add_text = Fx::Function.new(
|
|
64
|
+
"name" => "add",
|
|
65
|
+
"definition" => "CREATE FUNCTION add(text)",
|
|
66
|
+
"arguments" => "text"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
expect(add_int).not_to eq(add_text)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe "#signature" do
|
|
74
|
+
it "returns name with arguments when arguments are present" do
|
|
75
|
+
function = Fx::Function.new(
|
|
76
|
+
"name" => "inc",
|
|
77
|
+
"definition" => "some definition",
|
|
78
|
+
"arguments" => "integer"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
expect(function.signature).to eq("inc(integer)")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "returns name with multiple arguments" do
|
|
85
|
+
function = Fx::Function.new(
|
|
86
|
+
"name" => "add",
|
|
87
|
+
"definition" => "some definition",
|
|
88
|
+
"arguments" => "integer, integer"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
expect(function.signature).to eq("add(integer, integer)")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "returns name with empty parens for no-arg functions" do
|
|
95
|
+
function = Fx::Function.new(
|
|
96
|
+
"name" => "now_utc",
|
|
97
|
+
"definition" => "some definition",
|
|
98
|
+
"arguments" => ""
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
expect(function.signature).to eq("now_utc()")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "returns just the name when arguments key is missing" do
|
|
105
|
+
function = Fx::Function.new(
|
|
106
|
+
"name" => "now_utc",
|
|
107
|
+
"definition" => "some definition"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
expect(function.signature).to eq("now_utc")
|
|
111
|
+
end
|
|
36
112
|
end
|
|
37
113
|
|
|
38
114
|
describe "#to_schema" do
|
|
39
115
|
it "returns a schema compatible version of the function" do
|
|
40
116
|
function = Fx::Function.new(
|
|
41
117
|
"name" => "uppercase_users_name",
|
|
42
|
-
"definition" => "CREATE OR REPLACE TRIGGER uppercase_users_name ..."
|
|
118
|
+
"definition" => "CREATE OR REPLACE TRIGGER uppercase_users_name ...",
|
|
119
|
+
"arguments" => ""
|
|
43
120
|
)
|
|
44
121
|
|
|
45
122
|
expect(function.to_schema).to eq(<<-EOS)
|
|
@@ -52,7 +129,8 @@ RSpec.describe Fx::Function do
|
|
|
52
129
|
it "maintains backslashes" do
|
|
53
130
|
function = Fx::Function.new(
|
|
54
131
|
"name" => "regex",
|
|
55
|
-
"definition" => "CREATE OR REPLACE FUNCTION regex \\1"
|
|
132
|
+
"definition" => "CREATE OR REPLACE FUNCTION regex \\1",
|
|
133
|
+
"arguments" => ""
|
|
56
134
|
)
|
|
57
135
|
|
|
58
136
|
expect(function.to_schema).to eq(<<-'EOS')
|
data/spec/fx/statements_spec.rb
CHANGED
|
@@ -176,6 +176,7 @@ RSpec.describe Fx::Statements, :db do
|
|
|
176
176
|
expect do
|
|
177
177
|
connection.update_trigger(
|
|
178
178
|
:whatever,
|
|
179
|
+
on: :users,
|
|
179
180
|
version: nil,
|
|
180
181
|
sql_definition: nil
|
|
181
182
|
)
|
|
@@ -191,6 +192,7 @@ RSpec.describe Fx::Statements, :db do
|
|
|
191
192
|
expect do
|
|
192
193
|
connection.update_trigger(
|
|
193
194
|
:whatever,
|
|
195
|
+
on: :users,
|
|
194
196
|
version: 1,
|
|
195
197
|
sql_definition: "a definition"
|
|
196
198
|
)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fx
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Teo Ljungberg
|
|
@@ -16,9 +16,6 @@ dependencies:
|
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
18
|
version: '7.2'
|
|
19
|
-
- - "<"
|
|
20
|
-
- !ruby/object:Gem::Version
|
|
21
|
-
version: '8.2'
|
|
22
19
|
type: :runtime
|
|
23
20
|
prerelease: false
|
|
24
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -26,9 +23,6 @@ dependencies:
|
|
|
26
23
|
- - ">="
|
|
27
24
|
- !ruby/object:Gem::Version
|
|
28
25
|
version: '7.2'
|
|
29
|
-
- - "<"
|
|
30
|
-
- !ruby/object:Gem::Version
|
|
31
|
-
version: '8.2'
|
|
32
26
|
- !ruby/object:Gem::Dependency
|
|
33
27
|
name: railties
|
|
34
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -36,9 +30,6 @@ dependencies:
|
|
|
36
30
|
- - ">="
|
|
37
31
|
- !ruby/object:Gem::Version
|
|
38
32
|
version: '7.2'
|
|
39
|
-
- - "<"
|
|
40
|
-
- !ruby/object:Gem::Version
|
|
41
|
-
version: '8.2'
|
|
42
33
|
type: :runtime
|
|
43
34
|
prerelease: false
|
|
44
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -46,9 +37,6 @@ dependencies:
|
|
|
46
37
|
- - ">="
|
|
47
38
|
- !ruby/object:Gem::Version
|
|
48
39
|
version: '7.2'
|
|
49
|
-
- - "<"
|
|
50
|
-
- !ruby/object:Gem::Version
|
|
51
|
-
version: '8.2'
|
|
52
40
|
description: |
|
|
53
41
|
Adds methods to ActiveRecord::Migration to create and manage database functions
|
|
54
42
|
and triggers in Rails
|
|
@@ -58,7 +46,9 @@ executables: []
|
|
|
58
46
|
extensions: []
|
|
59
47
|
extra_rdoc_files: []
|
|
60
48
|
files:
|
|
49
|
+
- ".github/scripts/check-eol"
|
|
61
50
|
- ".github/workflows/ci.yml"
|
|
51
|
+
- ".github/workflows/eol-check.yml"
|
|
62
52
|
- ".gitignore"
|
|
63
53
|
- ".rspec"
|
|
64
54
|
- ".standard.yml"
|
|
@@ -149,7 +139,7 @@ licenses:
|
|
|
149
139
|
- MIT
|
|
150
140
|
metadata:
|
|
151
141
|
bug_tracker_uri: https://github.com/teoljungberg/fx/issues
|
|
152
|
-
changelog_uri: https://github.com/teoljungberg/fx/blob/v0.
|
|
142
|
+
changelog_uri: https://github.com/teoljungberg/fx/blob/v0.11.0/CHANGELOG.md
|
|
153
143
|
homepage_uri: https://github.com/teoljungberg/fx
|
|
154
144
|
source_code_uri: https://github.com/teoljungberg/fx
|
|
155
145
|
rdoc_options: []
|
|
@@ -159,14 +149,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
159
149
|
requirements:
|
|
160
150
|
- - ">="
|
|
161
151
|
- !ruby/object:Gem::Version
|
|
162
|
-
version: '3.
|
|
152
|
+
version: '3.3'
|
|
163
153
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
164
154
|
requirements:
|
|
165
155
|
- - ">="
|
|
166
156
|
- !ruby/object:Gem::Version
|
|
167
157
|
version: '0'
|
|
168
158
|
requirements: []
|
|
169
|
-
rubygems_version: 4.0.
|
|
159
|
+
rubygems_version: 4.0.3
|
|
170
160
|
specification_version: 4
|
|
171
161
|
summary: Support for database functions and triggers in Rails migrations
|
|
172
162
|
test_files: []
|