pg_aggregates 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d75685b8a6abb03dda216865bad3cbffc419d6d472b06695b98a5c92a5e05df0
4
+ data.tar.gz: 8445256690fb9a3bba119b749e1bb8d356f5024472527413402a05312a9c935e
5
+ SHA512:
6
+ metadata.gz: 67babad257045327620a6b6d99c3bba1abc0821c9490ee00fb744aae15b843c63b27294f71be65c3b00efb52e231ecf63e4ca88db8c18d3125e5eaf1d70c243c
7
+ data.tar.gz: cf6465126ee2fbeeea0e5504463b4d0520ac9690917948b2bead65a10accffbc292a4e44fa3369535bb2f01bbd51023085c6115f4ba8e50c9b390aaa6ad9e231
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-11-04
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 mhenrixon
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,122 @@
1
+ # PgAggregates
2
+
3
+ PgAggregates provides Rails integration for managing PostgreSQL aggregate functions. It allows you to version your aggregate functions and handle them through migrations, similar to how you manage database schema changes.
4
+
5
+ ## Features
6
+
7
+ - Versioned aggregate functions
8
+ - Rails generator for creating new aggregates
9
+ - Migration support for adding/removing aggregates
10
+ - Proper schema.rb dumping
11
+ - Support for multiple PostgreSQL versions
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem 'pg_aggregates'
19
+ ```
20
+
21
+ And then execute:
22
+
23
+ ```bash
24
+ $ bundle install
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### Creating a New Aggregate
30
+
31
+ Generate a new aggregate function:
32
+
33
+ ```bash
34
+ $ rails generate pg:aggregate sum_squares
35
+ ```
36
+
37
+ This will create:
38
+ - A SQL file in `db/aggregates/sum_squares_v1.sql`
39
+ - A migration file to create the aggregate
40
+
41
+ You can specify a version:
42
+
43
+ ```bash
44
+ $ rails generate pg:aggregate array_sum --version 2
45
+ ```
46
+
47
+ ### SQL Definition
48
+
49
+ Edit the generated SQL file (`db/aggregates/sum_squares_v1.sql`):
50
+
51
+ ```sql
52
+ CREATE AGGREGATE sum_squares(numeric) (
53
+ sfunc = numeric_add,
54
+ stype = numeric,
55
+ initcond = '0'
56
+ );
57
+ ```
58
+
59
+ ### Migrations
60
+
61
+ The generated migration will look like:
62
+
63
+ ```ruby
64
+ class CreateAggregateSumSquares < ActiveRecord::Migration[7.0]
65
+ def change
66
+ create_aggregate "sum_squares", version: 1
67
+ end
68
+ end
69
+ ```
70
+
71
+ You can also create aggregates inline:
72
+
73
+ ```ruby
74
+ class CreateAggregateArraySum < ActiveRecord::Migration[7.0]
75
+ def change
76
+ create_aggregate "array_sum", sql_definition: <<-SQL
77
+ CREATE AGGREGATE array_sum(numeric[]) (
78
+ sfunc = array_append,
79
+ stype = numeric[],
80
+ initcond = '{}'
81
+ );
82
+ SQL
83
+ end
84
+ end
85
+ ```
86
+
87
+ ### Managing Versions
88
+
89
+ When you need to update an aggregate, create a new version:
90
+
91
+ 1. Generate a new version:
92
+ ```bash
93
+ $ rails generate pg:aggregate sum_squares --version 2
94
+ ```
95
+
96
+ 2. Update the SQL in `db/aggregates/sum_squares_v2.sql`
97
+
98
+ 3. Create a migration to update to the new version:
99
+ ```ruby
100
+ class UpdateAggregateSumSquares < ActiveRecord::Migration[7.0]
101
+ def change
102
+ drop_aggregate "sum_squares", "numeric"
103
+ create_aggregate "sum_squares", version: 2
104
+ end
105
+ end
106
+ ```
107
+
108
+ ## Development
109
+
110
+ 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.
111
+
112
+ ## Contributing
113
+
114
+ Bug reports and pull requests are welcome on GitHub at https://github.com/mhenrixon/pg_aggregates. 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/mhenrixon/pg_aggregates/blob/main/CODE_OF_CONDUCT.md).
115
+
116
+ ## License
117
+
118
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
119
+
120
+ ## Code of Conduct
121
+
122
+ Everyone interacting in the PgAggregates project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/mhenrixon/pg_aggregates/blob/main/CODE_OF_CONDUCT.md).
@@ -0,0 +1,48 @@
1
+ require "rails/generators"
2
+ require "rails/generators/active_record"
3
+
4
+ module Pg
5
+ module Generators
6
+ class AggregateGenerator < Rails::Generators::NamedBase
7
+ include Rails::Generators::Migration
8
+
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ class_option :version,
12
+ type: :string,
13
+ default: "1",
14
+ desc: "Specify a version for the aggregate"
15
+
16
+ def create_aggregate_file
17
+ @version = options[:version]
18
+ @aggregate_name = file_name
19
+
20
+ template(
21
+ "aggregate.sql.erb",
22
+ "db/aggregates/#{file_name}_v#{@version}.sql"
23
+ )
24
+ end
25
+
26
+ def create_migration_file
27
+ @version = options[:version]
28
+ @aggregate_name = file_name
29
+ @migration_version = migration_version
30
+
31
+ migration_template(
32
+ "migration.rb.erb",
33
+ "db/migrate/create_aggregate_#{file_name}.rb"
34
+ )
35
+ end
36
+
37
+ private
38
+
39
+ def self.next_migration_number(dirname)
40
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
41
+ end
42
+
43
+ def migration_version
44
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,13 @@
1
+ CREATE AGGREGATE <%= @aggregate_name %>(
2
+ -- Specify your input type(s)
3
+ anyelement
4
+ ) (
5
+ -- State function - called for each input value
6
+ sfunc = array_append,
7
+
8
+ -- State data type
9
+ stype = anyarray,
10
+
11
+ -- Initial state value
12
+ initcond = '{}'
13
+ );
@@ -0,0 +1,5 @@
1
+ class CreateAggregate<%= @aggregate_name.camelize %> < ActiveRecord::Migration<%= @migration_version %>
2
+ def change
3
+ create_aggregate "<%= @aggregate_name %>", version: <%= @version %>
4
+ end
5
+ end
@@ -0,0 +1,22 @@
1
+ module PgAggregates
2
+ class AggregateDefinition
3
+ attr_reader :name, :version
4
+
5
+ def initialize(name, version:)
6
+ @name = name
7
+ @version = version
8
+ end
9
+
10
+ def to_sql
11
+ File.read(path)
12
+ end
13
+
14
+ def path
15
+ Rails.root.join("db", "aggregates", "#{name}_v#{version}.sql").to_s
16
+ end
17
+
18
+ def full_name
19
+ name.to_s.include?(".") ? name.to_s : "public.#{name}"
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ module PgAggregates
2
+ module CommandRecorder
3
+ def create_aggregate(*args, &block)
4
+ record(:create_aggregate, args, &block)
5
+ end
6
+
7
+ def drop_aggregate(*args)
8
+ record(:drop_aggregate, args)
9
+ end
10
+
11
+ def invert_create_aggregate(args)
12
+ [:drop_aggregate, args]
13
+ end
14
+
15
+ def invert_drop_aggregate(args)
16
+ [:create_aggregate, args]
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,25 @@
1
+ module PgAggregates
2
+ class FileVersion
3
+ attr_reader :path, :name, :version
4
+
5
+ def initialize(path)
6
+ @path = Pathname.new(path)
7
+ @name = @path.basename.to_s.sub(/_v\d+\.sql$/, "")
8
+ @version = extract_version
9
+ end
10
+
11
+ def sql_definition
12
+ File.read(path).strip
13
+ end
14
+
15
+ private
16
+
17
+ def extract_version
18
+ if (match = @path.basename.to_s.match(/_v(\d+)\.sql$/))
19
+ match[1].to_i
20
+ else
21
+ 0
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ module PgAggregates
2
+ class Railtie < Rails::Railtie
3
+ initializer "postgres_aggregates.load" do
4
+ ActiveSupport.on_load(:active_record) do
5
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.include PgAggregates::SchemaStatements
6
+ ActiveRecord::Migration::CommandRecorder.include PgAggregates::CommandRecorder
7
+ ActiveRecord::SchemaDumper.prepend PgAggregates::SchemaDumper
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,45 @@
1
+ module PgAggregates
2
+ module SchemaDumper
3
+ def tables(stream)
4
+ # First dump aggregates
5
+ dump_custom_aggregates(stream)
6
+ stream.puts
7
+
8
+ super
9
+ end
10
+
11
+ private
12
+
13
+ def dump_custom_aggregates(stream)
14
+ # Group all versions of each aggregate
15
+ aggregate_versions = {}
16
+
17
+ Dir.glob(Rails.root.join("db/aggregates/*_v*.sql").to_s).each do |file|
18
+ file_version = FileVersion.new(file)
19
+ aggregate_versions[file_version.name] ||= []
20
+ aggregate_versions[file_version.name] << file_version
21
+ end
22
+
23
+ # For each aggregate, use the latest version
24
+ latest_versions = aggregate_versions.transform_values do |versions|
25
+ versions.max_by(&:version)
26
+ end
27
+
28
+ # Sort by name to ensure consistent ordering
29
+ latest_versions.keys.sort.each do |aggregate_name|
30
+ file_version = latest_versions[aggregate_name]
31
+
32
+ # Add a comment showing the version history
33
+ all_versions = aggregate_versions[aggregate_name].map(&:version).sort
34
+ version_comment = all_versions.size > 1 ? " -- versions: #{all_versions.join(', ')}" : ""
35
+
36
+ stream.puts <<-AGG
37
+ create_aggregate "#{aggregate_name}", sql_definition: <<-SQL#{version_comment}
38
+ #{file_version.sql_definition}
39
+ SQL
40
+
41
+ AGG
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,27 @@
1
+ module PgAggregates
2
+ module SchemaStatements
3
+ def create_aggregate(name, version: nil, sql_definition: nil)
4
+ raise ArgumentError, "Must provide either sql_definition or version" if sql_definition.nil? && version.nil?
5
+
6
+ if sql_definition
7
+ execute sql_definition
8
+ else
9
+ # Fallback to file-based definition if needed
10
+ aggregate_definition = PgAggregates::AggregateDefinition.new(name, version: version)
11
+
12
+ # Check if file exists before trying to read it
13
+ unless File.exist?(aggregate_definition.path)
14
+ raise ArgumentError, "Could not find aggregate definition file: #{aggregate_definition.path}"
15
+ end
16
+
17
+ execute aggregate_definition.to_sql
18
+ end
19
+ end
20
+
21
+ def drop_aggregate(name, *arg_types, force: false)
22
+ arg_types_sql = arg_types.any? ? "(#{arg_types.join(', ')})" : ""
23
+ force_clause = force ? " CASCADE" : ""
24
+ execute "DROP AGGREGATE IF EXISTS #{name}#{arg_types_sql}#{force_clause}"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgAggregates
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/railtie"
4
+
5
+ require_relative "pg_aggregates/file_version"
6
+ require_relative "pg_aggregates/aggregate_definition"
7
+ require_relative "pg_aggregates/schema_statements"
8
+ require_relative "pg_aggregates/command_recorder"
9
+ require_relative "pg_aggregates/schema_dumper"
10
+ require_relative "pg_aggregates/railtie"
11
+
12
+ module PgAggregates
13
+ class Error < StandardError; end
14
+
15
+ class << self
16
+ def database
17
+ ActiveRecord::Base.connection
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,4 @@
1
+ module PgAggregates
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,176 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pg_aggregates
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - mhenrixon
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-11-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: railties
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '6.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '6.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pg
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '1.1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '1.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '13.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '13.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.12'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.12'
83
+ - !ruby/object:Gem::Dependency
84
+ name: ammeter
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.1'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.1'
97
+ - !ruby/object:Gem::Dependency
98
+ name: database_cleaner-active_record
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: appraisal
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: Manage PostgreSQL aggregate functions in your Rails application with
126
+ versioned migrations and schema handling. This cuts the need for keeping a structure.sql
127
+ email:
128
+ - mikael@mhenrixon.com
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - ".rspec"
134
+ - CHANGELOG.md
135
+ - LICENSE.txt
136
+ - README.md
137
+ - lib/generators/pg/aggregate/aggregate_generator.rb
138
+ - lib/generators/pg/aggregate/templates/aggregate.sql.erb
139
+ - lib/generators/pg/aggregate/templates/migration.rb.erb
140
+ - lib/pg_aggregates.rb
141
+ - lib/pg_aggregates/aggregate_definition.rb
142
+ - lib/pg_aggregates/command_recorder.rb
143
+ - lib/pg_aggregates/file_version.rb
144
+ - lib/pg_aggregates/railtie.rb
145
+ - lib/pg_aggregates/schema_dumper.rb
146
+ - lib/pg_aggregates/schema_statements.rb
147
+ - lib/pg_aggregates/version.rb
148
+ - sig/pg_aggregates.rbs
149
+ homepage: https://github.com/mhenrixon/pg_aggregates
150
+ licenses:
151
+ - MIT
152
+ metadata:
153
+ homepage_uri: https://github.com/mhenrixon/pg_aggregates
154
+ source_code_uri: https://github.com/mhenrixon/pg_aggregates
155
+ changelog_uri: https://github.com/mhenrixon/pg_aggregates/blob/main/CHANGELOG.md
156
+ rubygems_mfa_required: 'true'
157
+ post_install_message:
158
+ rdoc_options: []
159
+ require_paths:
160
+ - lib
161
+ required_ruby_version: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: 3.0.0
166
+ required_rubygems_version: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - ">="
169
+ - !ruby/object:Gem::Version
170
+ version: '0'
171
+ requirements: []
172
+ rubygems_version: 3.5.22
173
+ signing_key:
174
+ specification_version: 4
175
+ summary: Rails integration for PostgreSQL aggregate functions
176
+ test_files: []