fx-aggregate 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/.github/workflows/ci.yml +53 -0
- data/.gitignore +13 -0
- data/.rspec +2 -0
- data/.standard.yml +3 -0
- data/.tool-versions +1 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +44 -0
- data/Rakefile +24 -0
- data/bin/console +14 -0
- data/bin/rake +17 -0
- data/bin/rspec +17 -0
- data/bin/setup +9 -0
- data/bin/standardrb +27 -0
- data/fx-aggregate.gemspec +34 -0
- data/lib/fx-aggregate/adapters/postgres/aggregates.rb +61 -0
- data/lib/fx-aggregate/adapters/postgres.rb +60 -0
- data/lib/fx-aggregate/aggregate.rb +75 -0
- data/lib/fx-aggregate/command_recorder.rb +27 -0
- data/lib/fx-aggregate/definition.rb +15 -0
- data/lib/fx-aggregate/railtie.rb +15 -0
- data/lib/fx-aggregate/schema_dumper.rb +24 -0
- data/lib/fx-aggregate/statements.rb +104 -0
- data/lib/fx-aggregate/version.rb +3 -0
- data/lib/fx-aggregate.rb +22 -0
- data/lib/generators/fx/aggregate/USAGE +10 -0
- data/lib/generators/fx/aggregate/aggregate_generator.rb +120 -0
- data/lib/generators/fx/aggregate/templates/db/migrate/create_aggregate.erb +5 -0
- data/lib/generators/fx/aggregate/templates/db/migrate/update_aggregate.erb +5 -0
- metadata +93 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e151a7544a00f2a530deac87732962dd53a23de1cede96e75ef638356bda8179
|
4
|
+
data.tar.gz: 3997927b6b491bbada4786486a96f83832be7e5dc6bd7f7a3d0ec69166507466
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 041fa0fff61293ec4ed61020c3c5e6fe36b1300c3ce0ff885ae6679acb791b83b4c3ebb77139c7b06041bd5fea42db0236e786c32adb090007db65c6d11cb5ee
|
7
|
+
data.tar.gz: b53284c7853da505f78621622bd43e24043b273b07fca485244079e8675e1eb2cd36dc0658f1d8da3c90895b38b6a53bcdbdbd2291498a2dc9ee4e0f2ec3fc9e
|
@@ -0,0 +1,53 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: main
|
6
|
+
pull_request:
|
7
|
+
branches: "*"
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
tests:
|
11
|
+
name: Ruby ${{ matrix.ruby }}, Rails ${{ matrix.rails }}
|
12
|
+
runs-on: ubuntu-latest
|
13
|
+
|
14
|
+
strategy:
|
15
|
+
fail-fast: false
|
16
|
+
matrix:
|
17
|
+
ruby: ["3.0", "3.1", "3.2", "3.3", "3.4"]
|
18
|
+
rails: ["7.0", "7.1", "7.2", "8.0.0"]
|
19
|
+
continue-on-error: [false]
|
20
|
+
|
21
|
+
services:
|
22
|
+
postgres:
|
23
|
+
image: postgres:13
|
24
|
+
env:
|
25
|
+
POSTGRES_USER: postgres
|
26
|
+
POSTGRES_HOST_AUTH_METHOD: trust
|
27
|
+
ports:
|
28
|
+
- 5432:5432
|
29
|
+
# Set health checks to wait until postgres has started
|
30
|
+
options: >-
|
31
|
+
--health-cmd pg_isready
|
32
|
+
--health-interval 10s
|
33
|
+
--health-timeout 5s
|
34
|
+
--health-retries 5
|
35
|
+
|
36
|
+
env:
|
37
|
+
POSTGRES_USER: postgres
|
38
|
+
|
39
|
+
steps:
|
40
|
+
- uses: actions/checkout@v3
|
41
|
+
|
42
|
+
- name: Set up Ruby
|
43
|
+
uses: ruby/setup-ruby@v1
|
44
|
+
with:
|
45
|
+
ruby-version: ${{ matrix.ruby }}
|
46
|
+
bundler-cache: true
|
47
|
+
rubygems: latest
|
48
|
+
|
49
|
+
- name: Setup environment
|
50
|
+
run: bin/setup
|
51
|
+
|
52
|
+
- name: Run tests
|
53
|
+
run: bundle exec rake
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.standard.yml
ADDED
data/.tool-versions
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby 3.3.1
|
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in fx-aggregate.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
gem "ammeter", ">= 1.1.3"
|
7
|
+
gem "bundler", ">= 1.5"
|
8
|
+
gem "database_cleaner"
|
9
|
+
gem "pg"
|
10
|
+
gem "pry"
|
11
|
+
gem "rake"
|
12
|
+
gem "redcarpet"
|
13
|
+
gem "rspec", ">= 3.3"
|
14
|
+
gem "standardrb"
|
15
|
+
gem "yard"
|
16
|
+
gem "warning"
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 Agustin
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# Fx::Aggregate
|
2
|
+
|
3
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/fx/aggregate`. To experiment with that code, run `bin/console` for an interactive prompt.
|
4
|
+
|
5
|
+
TODO: Delete this and the text above, and describe your gem
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'fx-aggregate'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle install
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install fx-aggregate
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
TODO: Write usage instructions here
|
26
|
+
|
27
|
+
## Development
|
28
|
+
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
+
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
|
+
|
33
|
+
## Contributing
|
34
|
+
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/fx-aggregate. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/fx-aggregate/blob/master/CODE_OF_CONDUCT.md).
|
36
|
+
|
37
|
+
|
38
|
+
## License
|
39
|
+
|
40
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
41
|
+
|
42
|
+
## Code of Conduct
|
43
|
+
|
44
|
+
Everyone interacting in the Fx::Aggregate project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/fx-aggregate/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rspec/core/rake_task"
|
3
|
+
require "standard/rake"
|
4
|
+
|
5
|
+
namespace :dummy do
|
6
|
+
require_relative "spec/dummy/config/application"
|
7
|
+
Dummy::Application.load_tasks
|
8
|
+
end
|
9
|
+
|
10
|
+
task(:spec).clear
|
11
|
+
desc "Run specs other than spec/acceptance"
|
12
|
+
RSpec::Core::RakeTask.new("spec") do |task|
|
13
|
+
task.exclude_pattern = "spec/acceptance/**/*_spec.rb"
|
14
|
+
task.verbose = false
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Run acceptance specs in spec/acceptance"
|
18
|
+
RSpec::Core::RakeTask.new("spec:acceptance") do |task|
|
19
|
+
task.pattern = "spec/acceptance/**/*_spec.rb"
|
20
|
+
task.verbose = false
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "Run the specs and acceptance tests"
|
24
|
+
task default: %w[spec spec:acceptance standard]
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "fx-aggregate"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
require "pry"
|
11
|
+
Pry.start
|
12
|
+
|
13
|
+
# require "irb"
|
14
|
+
# IRB.start(__FILE__)
|
data/bin/rake
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
#
|
4
|
+
# This file was generated by Bundler.
|
5
|
+
#
|
6
|
+
# The application 'rake' is installed as part of a gem, and
|
7
|
+
# this file is here to facilitate running it.
|
8
|
+
#
|
9
|
+
|
10
|
+
require "pathname"
|
11
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
12
|
+
Pathname.new(__FILE__).realpath)
|
13
|
+
|
14
|
+
require "rubygems"
|
15
|
+
require "bundler/setup"
|
16
|
+
|
17
|
+
load Gem.bin_path("rake", "rake")
|
data/bin/rspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
#
|
4
|
+
# This file was generated by Bundler.
|
5
|
+
#
|
6
|
+
# The application 'rspec' is installed as part of a gem, and
|
7
|
+
# this file is here to facilitate running it.
|
8
|
+
#
|
9
|
+
|
10
|
+
require "pathname"
|
11
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
12
|
+
Pathname.new(__FILE__).realpath)
|
13
|
+
|
14
|
+
require "rubygems"
|
15
|
+
require "bundler/setup"
|
16
|
+
|
17
|
+
load Gem.bin_path("rspec-core", "rspec")
|
data/bin/setup
ADDED
data/bin/standardrb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'standardrb' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
|
12
|
+
|
13
|
+
bundle_binstub = File.expand_path("bundle", __dir__)
|
14
|
+
|
15
|
+
if File.file?(bundle_binstub)
|
16
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
17
|
+
load(bundle_binstub)
|
18
|
+
else
|
19
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
20
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
require "rubygems"
|
25
|
+
require "bundler/setup"
|
26
|
+
|
27
|
+
load Gem.bin_path("standard", "standardrb")
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require_relative "lib/fx-aggregate/version"
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "fx-aggregate"
|
5
|
+
spec.version = FxAggregate::VERSION
|
6
|
+
spec.authors = ["Agustin"]
|
7
|
+
spec.email = ["agustin@mailbutler.io"]
|
8
|
+
|
9
|
+
spec.summary = "An extension for the fx gem to manage PostgreSQL aggregate functions in Rails migrations."
|
10
|
+
spec.description = <<~DESCRIPTION
|
11
|
+
Adds methods to ActiveRecord::Migration to create and manage database aggregates
|
12
|
+
functions in Rails
|
13
|
+
DESCRIPTION
|
14
|
+
spec.homepage = "https://github.com/agustin/fx-aggregate"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
|
18
|
+
|
19
|
+
spec.metadata = {
|
20
|
+
"bug_tracker_uri" => "#{spec.homepage}/issues",
|
21
|
+
"changelog_uri" => "#{spec.homepage}/blob/v#{spec.version}/CHANGELOG.md",
|
22
|
+
"homepage_uri" => spec.homepage,
|
23
|
+
"source_code_uri" => spec.homepage
|
24
|
+
}
|
25
|
+
|
26
|
+
# Specify which files should be added to the gem when it is released.
|
27
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
28
|
+
spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
|
29
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
30
|
+
end
|
31
|
+
spec.require_paths = ["lib"]
|
32
|
+
|
33
|
+
spec.add_dependency "fx", ">= 0.9"
|
34
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require "fx-aggregate/aggregate"
|
2
|
+
|
3
|
+
module Fx
|
4
|
+
module Adapters
|
5
|
+
class Postgres
|
6
|
+
# Fetches defined aggregates from the postgres connection.
|
7
|
+
# @api private
|
8
|
+
class Aggregates
|
9
|
+
# The SQL query used by F(x) to retrieve the aggregates considered
|
10
|
+
# dumpable into `db/schema.rb`.
|
11
|
+
AGGREGATES_WITH_DEFINITIONS_QUERY = <<-EOS.freeze
|
12
|
+
SELECT
|
13
|
+
pp.proname AS name,
|
14
|
+
pg_get_function_identity_arguments(pp.oid) AS arguments,
|
15
|
+
pa.*,
|
16
|
+
format_type(pa.aggtranstype, null) AS aggtranstype,
|
17
|
+
format_type(pa.aggmtranstype, null) AS aggmtranstype
|
18
|
+
FROM pg_proc pp
|
19
|
+
JOIN pg_aggregate pa
|
20
|
+
ON pa.aggfnoid = pp.oid
|
21
|
+
JOIN pg_namespace pn
|
22
|
+
ON pn.oid = pp.pronamespace
|
23
|
+
LEFT JOIN pg_depend pd
|
24
|
+
ON pd.objid = pp.oid AND pd.deptype = 'e'
|
25
|
+
WHERE pn.nspname = 'public' AND pd.objid IS NULL
|
26
|
+
ORDER BY pp.oid;
|
27
|
+
EOS
|
28
|
+
|
29
|
+
# Wraps #all as a static facade.
|
30
|
+
#
|
31
|
+
# @return [Array<Fx::Aggregate>]
|
32
|
+
def self.all(*args)
|
33
|
+
new(*args).all
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize(connection)
|
37
|
+
@connection = connection
|
38
|
+
end
|
39
|
+
|
40
|
+
# All of the aggregates that this connection has defined.
|
41
|
+
#
|
42
|
+
# @return [Array<Fx::Aggregate>]
|
43
|
+
def all
|
44
|
+
aggregates_from_postgres.map { |aggregate| to_fx_aggregate(aggregate) }
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
attr_reader :connection
|
50
|
+
|
51
|
+
def aggregates_from_postgres
|
52
|
+
connection.execute(AGGREGATES_WITH_DEFINITIONS_QUERY)
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_fx_aggregate(result)
|
56
|
+
Fx::Aggregate.new(result)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "fx-aggregate/adapters/postgres/aggregates"
|
2
|
+
|
3
|
+
module FxAggregate
|
4
|
+
module Adapters
|
5
|
+
module Postgres
|
6
|
+
# Returns an array of aggregates in the database.
|
7
|
+
#
|
8
|
+
# This collection of aggregates is used by the [Fx::SchemaDumper] to
|
9
|
+
# populate the `schema.rb` file.
|
10
|
+
#
|
11
|
+
# @return [Array<Fx::Aggregate>]
|
12
|
+
def aggregates
|
13
|
+
::Fx::Adapters::Postgres::Aggregates.all(connection)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Creates an aggregate in the database.
|
17
|
+
#
|
18
|
+
# This is typically called in a migration via
|
19
|
+
# {Fx::Statements::Aggregate#create_aggregate}.
|
20
|
+
#
|
21
|
+
# @param sql_definition The SQL schema for the aggregate.
|
22
|
+
#
|
23
|
+
# @return [void]
|
24
|
+
def create_aggregate(sql_definition)
|
25
|
+
execute(sql_definition)
|
26
|
+
end
|
27
|
+
|
28
|
+
# # Updates an aggregate in the database.
|
29
|
+
#
|
30
|
+
# This is typically called in a migration via
|
31
|
+
# {Fx::Statements::Aggregate#update_aggregate}.
|
32
|
+
#
|
33
|
+
# @param name The name of the aggregate.
|
34
|
+
# @param sql_definition The SQL schema for the aggregate.
|
35
|
+
#
|
36
|
+
# @return [void]
|
37
|
+
def update_aggregate(name, sql_definition)
|
38
|
+
drop_aggregate(name)
|
39
|
+
create_aggregate(sql_definition)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Drops the aggregate from the database
|
43
|
+
#
|
44
|
+
# This is typically called in a migration via
|
45
|
+
# {Fx::Statements::Aggregate#drop_aggregate}.
|
46
|
+
#
|
47
|
+
# @param name The name of the aggregate to drop
|
48
|
+
#
|
49
|
+
# @return [void]
|
50
|
+
def drop_aggregate(name)
|
51
|
+
defs = aggregates.select { |aggregate| aggregate.name == name.to_s }
|
52
|
+
|
53
|
+
defs.each do |aggregate|
|
54
|
+
execute "DROP AGGREGATE #{name}(#{aggregate.arguments});"
|
55
|
+
end
|
56
|
+
# execute "DROP AGGREGATE #{name}();"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Fx
|
2
|
+
class Aggregate
|
3
|
+
include Comparable
|
4
|
+
|
5
|
+
attr_reader :name, :arguments, :definition
|
6
|
+
delegate :<=>, to: :name
|
7
|
+
|
8
|
+
def initialize(aggregate_row)
|
9
|
+
@name = aggregate_row.fetch("name")
|
10
|
+
@arguments = aggregate_row.fetch("arguments", "")
|
11
|
+
@definition = aggregate_row.except("name", "arguments")
|
12
|
+
end
|
13
|
+
|
14
|
+
def ==(other)
|
15
|
+
other.is_a?(self.class) &&
|
16
|
+
name == other.name &&
|
17
|
+
arguments == other.arguments &&
|
18
|
+
definition == other.definition
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_schema
|
22
|
+
<<-SCHEMA
|
23
|
+
create_aggregate :#{name}, sql_definition: <<-\SQL
|
24
|
+
CREATE AGGREGATE #{name}(#{arguments})(
|
25
|
+
#{options_for_create_statement.join(",\n").indent(6).lstrip}
|
26
|
+
);
|
27
|
+
SQL
|
28
|
+
SCHEMA
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
Field = Struct.new(:option, :type)
|
34
|
+
private_constant :Field
|
35
|
+
|
36
|
+
# Maps pg_aggregate columns to their definition field and type.
|
37
|
+
FIELDS = {
|
38
|
+
"aggtransfn" => Field.new("SFUNC", "raw"),
|
39
|
+
"aggtranstype" => Field.new("STYPE", "raw"),
|
40
|
+
"aggtransspace" => Field.new("SSPACE", "int"),
|
41
|
+
"aggfinalfn" => Field.new("FINALFUNC", "raw"),
|
42
|
+
"aggfinalextra" => Field.new("FINALFUNC_EXTRA", "bool"),
|
43
|
+
"agginitval" => Field.new("INITCOND", "string")
|
44
|
+
}.freeze
|
45
|
+
|
46
|
+
def options_for_create_statement
|
47
|
+
FIELDS.map do |key, field|
|
48
|
+
value = format_value(field.type, @definition[key])
|
49
|
+
next if !value
|
50
|
+
|
51
|
+
if value == true
|
52
|
+
field.option
|
53
|
+
else
|
54
|
+
"#{field.option} = #{value}"
|
55
|
+
end
|
56
|
+
end.compact
|
57
|
+
end
|
58
|
+
|
59
|
+
def format_value(type, value)
|
60
|
+
return if value.nil?
|
61
|
+
return if value == "-"
|
62
|
+
|
63
|
+
case type
|
64
|
+
when "bool"
|
65
|
+
return value == "t"
|
66
|
+
when "int"
|
67
|
+
return if value.to_i == 0
|
68
|
+
when "string"
|
69
|
+
return "'#{value}'"
|
70
|
+
end
|
71
|
+
|
72
|
+
value
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module FxAggregate
|
2
|
+
module CommandRecorder
|
3
|
+
def create_aggregate(*args)
|
4
|
+
record(:create_aggregate, args)
|
5
|
+
end
|
6
|
+
|
7
|
+
def drop_aggregate(*args)
|
8
|
+
record(:drop_aggregate, args)
|
9
|
+
end
|
10
|
+
|
11
|
+
def update_aggregate(*args)
|
12
|
+
record(:update_aggregate, args)
|
13
|
+
end
|
14
|
+
|
15
|
+
def invert_create_aggregate(args)
|
16
|
+
[:drop_aggregate, args]
|
17
|
+
end
|
18
|
+
|
19
|
+
def invert_drop_aggregate(args)
|
20
|
+
perform_inversion(:create_aggregate, args)
|
21
|
+
end
|
22
|
+
|
23
|
+
def invert_update_aggregate(args)
|
24
|
+
perform_inversion(:update_aggregate, args)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module FxAggregate
|
2
|
+
module Definition
|
3
|
+
AGGREGATE = "aggregate".freeze
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def aggregate(name:, version:)
|
11
|
+
new(name: name, version: version, type: AGGREGATE)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "rails/railtie"
|
2
|
+
|
3
|
+
module FxAggregate
|
4
|
+
# Automatically initializes Fx in the context of a Rails application when
|
5
|
+
# ActiveRecord is loaded.
|
6
|
+
#
|
7
|
+
# @see Fx.load
|
8
|
+
class Railtie < Rails::Railtie
|
9
|
+
initializer "fx-aggregate.load", after: "fx.load" do
|
10
|
+
ActiveSupport.on_load :active_record do
|
11
|
+
FxAggregate.load
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module FxAggregate
|
2
|
+
module SchemaDumper
|
3
|
+
def tables(stream)
|
4
|
+
super
|
5
|
+
aggregates(stream)
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def aggregates(stream)
|
11
|
+
if dumpable_aggregates_in_database.any?
|
12
|
+
stream.puts
|
13
|
+
end
|
14
|
+
|
15
|
+
dumpable_aggregates_in_database.each do |aggregate|
|
16
|
+
stream.puts(aggregate.to_schema)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def dumpable_aggregates_in_database
|
21
|
+
@_dumpable_aggregates_in_database ||= Fx.database.aggregates
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module FxAggregate
|
2
|
+
module Statements
|
3
|
+
# Create a new database aggregate.
|
4
|
+
#
|
5
|
+
# @param name [String, Symbol] The name of the database aggregate.
|
6
|
+
# @param version [Fixnum] The version number of the aggregate, used to
|
7
|
+
# find the definition file in `db/aggregates`. This defaults to `1` if
|
8
|
+
# not provided.
|
9
|
+
# @param sql_definition [String] The SQL query for the aggregate schema.
|
10
|
+
# If both `sql_definition` and `version` are provided,
|
11
|
+
# `sql_definition` takes prescedence.
|
12
|
+
# @return The database response from executing the create statement.
|
13
|
+
#
|
14
|
+
# @example Create from `db/aggregates/median_v2.sql`
|
15
|
+
# create_aggregate(:median, version: 2)
|
16
|
+
#
|
17
|
+
# @example Create from provided SQL string
|
18
|
+
# create_aggregate(:median, sql_definition: <<-SQL)
|
19
|
+
# CREATE AGGREGATE median(NUMERIC)(
|
20
|
+
# SFUNC = array_append,
|
21
|
+
# STYPE = NUMERIC[],
|
22
|
+
# FINALFUNC = array_median,
|
23
|
+
# INITCOND = '{}'
|
24
|
+
# )
|
25
|
+
# SQL
|
26
|
+
#
|
27
|
+
def create_aggregate(name, options = {})
|
28
|
+
version = options.fetch(:version, 1)
|
29
|
+
sql_definition = options[:sql_definition]
|
30
|
+
|
31
|
+
if version.nil? && sql_definition.nil?
|
32
|
+
raise(
|
33
|
+
ArgumentError,
|
34
|
+
"version or sql_definition must be specified"
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
sql_definition = sql_definition.strip_heredoc if sql_definition
|
39
|
+
sql_definition ||= Fx::Definition.aggregate(name: name, version: version).to_sql
|
40
|
+
|
41
|
+
Fx.database.create_aggregate(sql_definition)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Drop a database aggregate by name.
|
45
|
+
#
|
46
|
+
# @param name [String, Symbol] The name of the database aggregate.
|
47
|
+
# @param revert_to_version [Fixnum] Used to reverse the `drop_aggregate`
|
48
|
+
# command on `rake db:rollback`. The provided version will be passed as
|
49
|
+
# the `version` argument to {#create_aggregate}.
|
50
|
+
# @return The database response from executing the drop statement.
|
51
|
+
#
|
52
|
+
# @example Drop a aggregate, rolling back to version 2 on rollback
|
53
|
+
# drop_aggregate(:median, revert_to_version: 2)
|
54
|
+
#
|
55
|
+
def drop_aggregate(name, options = {})
|
56
|
+
Fx.database.drop_aggregate(name)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Update a database aggregate.
|
60
|
+
#
|
61
|
+
# @param name [String, Symbol] The name of the database aggregate.
|
62
|
+
# @param version [Fixnum] The version number of the aggregate, used to
|
63
|
+
# find the definition file in `db/aggregates`. This defaults to `1` if
|
64
|
+
# not provided.
|
65
|
+
# @param sql_definition [String] The SQL query for the aggregate schema.
|
66
|
+
# If both `sql_definition` and `version` are provided,
|
67
|
+
# `sql_definition` takes prescedence.
|
68
|
+
# @return The database response from executing the create statement.
|
69
|
+
#
|
70
|
+
# @example Update aggregate to a given version
|
71
|
+
# update_aggregate(
|
72
|
+
# :median,
|
73
|
+
# version: 3,
|
74
|
+
# revert_to_version: 2,
|
75
|
+
# )
|
76
|
+
#
|
77
|
+
# @example Update aggregate from provided SQL string
|
78
|
+
# update_aggregate(:median, sql_definition: <<-SQL)
|
79
|
+
# CREATE AGGREGATE median(NUMERIC)(
|
80
|
+
# SFUNC = array_append,
|
81
|
+
# STYPE = NUMERIC[],
|
82
|
+
# FINALFUNC = array_median,
|
83
|
+
# INITCOND = '{}'
|
84
|
+
# )
|
85
|
+
# SQL
|
86
|
+
#
|
87
|
+
def update_aggregate(name, options = {})
|
88
|
+
version = options[:version]
|
89
|
+
sql_definition = options[:sql_definition]
|
90
|
+
|
91
|
+
if version.nil? && sql_definition.nil?
|
92
|
+
raise(
|
93
|
+
ArgumentError,
|
94
|
+
"version or sql_definition must be specified"
|
95
|
+
)
|
96
|
+
end
|
97
|
+
|
98
|
+
sql_definition = sql_definition.strip_heredoc if sql_definition
|
99
|
+
sql_definition ||= Fx::Definition.aggregate(name: name, version: version).to_sql
|
100
|
+
|
101
|
+
Fx.database.update_aggregate(name, sql_definition)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
data/lib/fx-aggregate.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require "fx"
|
2
|
+
|
3
|
+
require "fx-aggregate/version"
|
4
|
+
require "fx-aggregate/adapters/postgres"
|
5
|
+
require "fx-aggregate/aggregate"
|
6
|
+
require "fx-aggregate/command_recorder"
|
7
|
+
require "fx-aggregate/definition"
|
8
|
+
require "fx-aggregate/statements"
|
9
|
+
require "fx-aggregate/schema_dumper"
|
10
|
+
require "fx-aggregate/railtie"
|
11
|
+
|
12
|
+
module FxAggregate
|
13
|
+
def self.load
|
14
|
+
Fx::Adapters::Postgres.include(FxAggregate::Adapters::Postgres)
|
15
|
+
Fx::CommandRecorder.include(FxAggregate::CommandRecorder)
|
16
|
+
Fx::Definition.include(FxAggregate::Definition)
|
17
|
+
Fx::SchemaDumper.include(FxAggregate::SchemaDumper)
|
18
|
+
Fx::Statements.include(FxAggregate::Statements)
|
19
|
+
|
20
|
+
true
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
Description:
|
2
|
+
Create a new database aggregate for your application. This will create a new aggregate definition file and the accompanying migration.
|
3
|
+
|
4
|
+
When --no-migration is passed, skips generating a migration.
|
5
|
+
|
6
|
+
Examples:
|
7
|
+
rails generate fx:aggregate test
|
8
|
+
|
9
|
+
create: db/aggregates/test_v01.sql
|
10
|
+
create: db/migrate/[TIMESTAMP]_create_test_aggregate.rb
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
require "rails/generators/active_record"
|
3
|
+
|
4
|
+
module Fx
|
5
|
+
module Generators
|
6
|
+
# @api private
|
7
|
+
class AggregateGenerator < Rails::Generators::NamedBase
|
8
|
+
include Rails::Generators::Migration
|
9
|
+
source_root File.expand_path("../templates", __FILE__)
|
10
|
+
|
11
|
+
class_option :migration, type: :boolean
|
12
|
+
|
13
|
+
def create_aggregates_directory
|
14
|
+
unless aggregate_definition_path.exist?
|
15
|
+
empty_directory(aggregate_definition_path)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_aggregate_definition
|
20
|
+
if creating_new_aggregate?
|
21
|
+
create_file definition.path
|
22
|
+
else
|
23
|
+
copy_file previous_definition.full_path, definition.full_path
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_migration_file
|
28
|
+
return if skip_migration_creation?
|
29
|
+
if updating_existing_aggregate?
|
30
|
+
migration_template(
|
31
|
+
"db/migrate/update_aggregate.erb",
|
32
|
+
"db/migrate/update_aggregate_#{file_name}_to_version_#{version}.rb"
|
33
|
+
)
|
34
|
+
else
|
35
|
+
migration_template(
|
36
|
+
"db/migrate/create_aggregate.erb",
|
37
|
+
"db/migrate/create_aggregate_#{file_name}.rb"
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.next_migration_number(dir)
|
43
|
+
::ActiveRecord::Generators::Base.next_migration_number(dir)
|
44
|
+
end
|
45
|
+
|
46
|
+
no_tasks do
|
47
|
+
def previous_version
|
48
|
+
@_previous_version ||= Dir.entries(aggregate_definition_path)
|
49
|
+
.map { |name| version_regex.match(name).try(:[], "version").to_i }
|
50
|
+
.max
|
51
|
+
end
|
52
|
+
|
53
|
+
def version
|
54
|
+
@_version ||= previous_version.next
|
55
|
+
end
|
56
|
+
|
57
|
+
def migration_class_name
|
58
|
+
if updating_existing_aggregate?
|
59
|
+
"UpdateAggregate#{class_name}ToVersion#{version}"
|
60
|
+
else
|
61
|
+
super
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def activerecord_migration_class
|
66
|
+
if ActiveRecord::Migration.respond_to?(:current_version)
|
67
|
+
"ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]"
|
68
|
+
else
|
69
|
+
"ActiveRecord::Migration"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def formatted_name
|
74
|
+
if singular_name.include?(".")
|
75
|
+
"\"#{singular_name}\""
|
76
|
+
else
|
77
|
+
":#{singular_name}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def aggregate_definition_path
|
85
|
+
@_aggregate_definition_path ||= Rails.root.join(*%w[db aggregates])
|
86
|
+
end
|
87
|
+
|
88
|
+
def version_regex
|
89
|
+
/\A#{file_name}_v(?<version>\d+)\.sql\z/
|
90
|
+
end
|
91
|
+
|
92
|
+
def updating_existing_aggregate?
|
93
|
+
previous_version > 0
|
94
|
+
end
|
95
|
+
|
96
|
+
def creating_new_aggregate?
|
97
|
+
previous_version == 0
|
98
|
+
end
|
99
|
+
|
100
|
+
def definition
|
101
|
+
Fx::Definition.new(name: file_name, version: version, type: "aggregate")
|
102
|
+
end
|
103
|
+
|
104
|
+
def previous_definition
|
105
|
+
Fx::Definition.new(name: file_name, version: previous_version, type: "aggregate")
|
106
|
+
end
|
107
|
+
|
108
|
+
# Skip creating migration file if:
|
109
|
+
# - migrations option is nil or false
|
110
|
+
def skip_migration_creation?
|
111
|
+
!migration
|
112
|
+
end
|
113
|
+
|
114
|
+
# True unless explicitly false
|
115
|
+
def migration
|
116
|
+
options[:migration] != false
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fx-aggregate
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Agustin
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-01-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: fx
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.9'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.9'
|
27
|
+
description: |
|
28
|
+
Adds methods to ActiveRecord::Migration to create and manage database aggregates
|
29
|
+
functions in Rails
|
30
|
+
email:
|
31
|
+
- agustin@mailbutler.io
|
32
|
+
executables: []
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files: []
|
35
|
+
files:
|
36
|
+
- ".github/workflows/ci.yml"
|
37
|
+
- ".gitignore"
|
38
|
+
- ".rspec"
|
39
|
+
- ".standard.yml"
|
40
|
+
- ".tool-versions"
|
41
|
+
- Gemfile
|
42
|
+
- LICENSE.txt
|
43
|
+
- README.md
|
44
|
+
- Rakefile
|
45
|
+
- bin/console
|
46
|
+
- bin/rake
|
47
|
+
- bin/rspec
|
48
|
+
- bin/setup
|
49
|
+
- bin/standardrb
|
50
|
+
- fx-aggregate.gemspec
|
51
|
+
- lib/fx-aggregate.rb
|
52
|
+
- lib/fx-aggregate/adapters/postgres.rb
|
53
|
+
- lib/fx-aggregate/adapters/postgres/aggregates.rb
|
54
|
+
- lib/fx-aggregate/aggregate.rb
|
55
|
+
- lib/fx-aggregate/command_recorder.rb
|
56
|
+
- lib/fx-aggregate/definition.rb
|
57
|
+
- lib/fx-aggregate/railtie.rb
|
58
|
+
- lib/fx-aggregate/schema_dumper.rb
|
59
|
+
- lib/fx-aggregate/statements.rb
|
60
|
+
- lib/fx-aggregate/version.rb
|
61
|
+
- lib/generators/fx/aggregate/USAGE
|
62
|
+
- lib/generators/fx/aggregate/aggregate_generator.rb
|
63
|
+
- lib/generators/fx/aggregate/templates/db/migrate/create_aggregate.erb
|
64
|
+
- lib/generators/fx/aggregate/templates/db/migrate/update_aggregate.erb
|
65
|
+
homepage: https://github.com/agustin/fx-aggregate
|
66
|
+
licenses:
|
67
|
+
- MIT
|
68
|
+
metadata:
|
69
|
+
bug_tracker_uri: https://github.com/agustin/fx-aggregate/issues
|
70
|
+
changelog_uri: https://github.com/agustin/fx-aggregate/blob/v0.1.0/CHANGELOG.md
|
71
|
+
homepage_uri: https://github.com/agustin/fx-aggregate
|
72
|
+
source_code_uri: https://github.com/agustin/fx-aggregate
|
73
|
+
post_install_message:
|
74
|
+
rdoc_options: []
|
75
|
+
require_paths:
|
76
|
+
- lib
|
77
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: 2.7.0
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
requirements: []
|
88
|
+
rubygems_version: 3.5.9
|
89
|
+
signing_key:
|
90
|
+
specification_version: 4
|
91
|
+
summary: An extension for the fx gem to manage PostgreSQL aggregate functions in Rails
|
92
|
+
migrations.
|
93
|
+
test_files: []
|