pg_aggregates 0.2.1 → 0.2.2
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/.ruby-version +1 -1
- data/gemfiles/rails_6.1.gemfile +4 -2
- data/gemfiles/rails_7.0.gemfile +4 -2
- data/gemfiles/rails_7.1.gemfile +4 -2
- data/lib/pg_aggregates/railtie.rb +2 -4
- data/lib/pg_aggregates/schema_dumper.rb +68 -28
- data/lib/pg_aggregates/schema_statements.rb +71 -0
- data/lib/pg_aggregates/version.rb +1 -1
- data/lib/pg_aggregates.rb +20 -4
- metadata +3 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 404507e81fbfe0ea608413efa6535d34c15beae11392bd86e8c4a0f98525e815
|
4
|
+
data.tar.gz: 953d598b8e88883db7d60153e7bcd048efe97475cd3f72949411a4cc7d7872b7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e6091519ab8526a079af9c4adce52c596e7c7f02ddf07d41d6a506a57200120a21eb58863b212d1071aa72732d30c17aa76a8622569715001a43b6ed4b84f687
|
7
|
+
data.tar.gz: 1df224a0da63cb50de1c435eb1a3a4c3d62e48a0efbe9deb92f8d1edbc4adeecb44745f29766368a8fe522319966224e2f01c3d5b260cdde04a93ec12868f8f5
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
3.3
|
1
|
+
3.4.3
|
data/gemfiles/rails_6.1.gemfile
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# This file was generated by Appraisal
|
2
4
|
|
3
5
|
source "https://rubygems.org"
|
4
6
|
|
7
|
+
gem "activerecord", "~> 6.1.0"
|
5
8
|
gem "ammeter"
|
6
9
|
gem "appraisal"
|
7
10
|
gem "base64"
|
@@ -11,12 +14,11 @@ gem "drb"
|
|
11
14
|
gem "gem-release"
|
12
15
|
gem "logger"
|
13
16
|
gem "mutex_m"
|
17
|
+
gem "railties", "~> 6.1.0"
|
14
18
|
gem "rake"
|
15
19
|
gem "rspec"
|
16
20
|
gem "rubocop-mhenrixon"
|
17
21
|
gem "rubocop-rake"
|
18
22
|
gem "rubocop-rspec"
|
19
|
-
gem "activerecord", "~> 6.1.0"
|
20
|
-
gem "railties", "~> 6.1.0"
|
21
23
|
|
22
24
|
gemspec path: "../"
|
data/gemfiles/rails_7.0.gemfile
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# This file was generated by Appraisal
|
2
4
|
|
3
5
|
source "https://rubygems.org"
|
4
6
|
|
7
|
+
gem "activerecord", "~> 7.0.0"
|
5
8
|
gem "ammeter"
|
6
9
|
gem "appraisal"
|
7
10
|
gem "base64"
|
@@ -11,12 +14,11 @@ gem "drb"
|
|
11
14
|
gem "gem-release"
|
12
15
|
gem "logger"
|
13
16
|
gem "mutex_m"
|
17
|
+
gem "railties", "~> 7.0.0"
|
14
18
|
gem "rake"
|
15
19
|
gem "rspec"
|
16
20
|
gem "rubocop-mhenrixon"
|
17
21
|
gem "rubocop-rake"
|
18
22
|
gem "rubocop-rspec"
|
19
|
-
gem "activerecord", "~> 7.0.0"
|
20
|
-
gem "railties", "~> 7.0.0"
|
21
23
|
|
22
24
|
gemspec path: "../"
|
data/gemfiles/rails_7.1.gemfile
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# This file was generated by Appraisal
|
2
4
|
|
3
5
|
source "https://rubygems.org"
|
4
6
|
|
7
|
+
gem "activerecord", "~> 7.1.0"
|
5
8
|
gem "ammeter"
|
6
9
|
gem "appraisal"
|
7
10
|
gem "base64"
|
@@ -11,12 +14,11 @@ gem "drb"
|
|
11
14
|
gem "gem-release"
|
12
15
|
gem "logger"
|
13
16
|
gem "mutex_m"
|
17
|
+
gem "railties", "~> 7.1.0"
|
14
18
|
gem "rake"
|
15
19
|
gem "rspec"
|
16
20
|
gem "rubocop-mhenrixon"
|
17
21
|
gem "rubocop-rake"
|
18
22
|
gem "rubocop-rspec"
|
19
|
-
gem "activerecord", "~> 7.1.0"
|
20
|
-
gem "railties", "~> 7.1.0"
|
21
23
|
|
22
24
|
gemspec path: "../"
|
@@ -2,11 +2,9 @@
|
|
2
2
|
|
3
3
|
module PgAggregates
|
4
4
|
class Railtie < Rails::Railtie
|
5
|
-
initializer "
|
5
|
+
initializer "pg_aggregates.load", before: "fx.load" do
|
6
6
|
ActiveSupport.on_load(:active_record) do
|
7
|
-
|
8
|
-
ActiveRecord::Migration::CommandRecorder.include PgAggregates::CommandRecorder
|
9
|
-
ActiveRecord::SchemaDumper.prepend PgAggregates::SchemaDumper
|
7
|
+
PgAggregates.load
|
10
8
|
end
|
11
9
|
end
|
12
10
|
end
|
@@ -1,50 +1,90 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "ostruct"
|
4
|
+
|
3
5
|
module PgAggregates
|
4
6
|
module SchemaDumper
|
5
|
-
|
6
|
-
|
7
|
+
# Define a Struct for aggregate information
|
8
|
+
AggregateDefinition = Struct.new(:name, :definition)
|
7
9
|
|
8
|
-
|
10
|
+
# Override the tables method to inject aggregate dumping *after* functions (handled by fx)
|
11
|
+
# but *before* tables.
|
12
|
+
def tables(stream)
|
13
|
+
# Dump custom aggregates first
|
9
14
|
dump_custom_aggregates(stream)
|
10
|
-
|
15
|
+
|
16
|
+
stream.puts # Add a newline for separation before tables
|
17
|
+
|
18
|
+
# Call the next implementation (likely fx's tables or Rails' tables)
|
19
|
+
super
|
11
20
|
end
|
12
21
|
|
13
22
|
private
|
14
23
|
|
15
24
|
def dump_custom_aggregates(stream)
|
16
|
-
|
17
|
-
aggregate_versions = {}
|
18
|
-
|
19
|
-
Dir.glob(Rails.root.join("db/aggregates/*_v*.sql").to_s).each do |file|
|
20
|
-
file_version = FileVersion.new(file)
|
21
|
-
aggregate_versions[file_version.name] ||= []
|
22
|
-
aggregate_versions[file_version.name] << file_version
|
23
|
-
end
|
25
|
+
aggregates = dumpable_aggregates_in_database
|
24
26
|
|
25
|
-
return if
|
27
|
+
return if aggregates.empty?
|
26
28
|
|
27
29
|
stream.puts " # These are custom PostgreSQL aggregates that were defined"
|
28
30
|
|
29
|
-
# For each aggregate, use the latest version
|
30
|
-
latest_versions = aggregate_versions.transform_values do |versions|
|
31
|
-
versions.max_by(&:version)
|
32
|
-
end
|
33
|
-
|
34
31
|
# Sort by name to ensure consistent ordering
|
35
|
-
|
36
|
-
|
32
|
+
aggregates.sort_by(&:name).each do |aggregate|
|
33
|
+
stream.puts <<~AGG
|
34
|
+
create_aggregate "#{aggregate.name}", sql_definition: <<-SQL
|
35
|
+
#{aggregate.definition}
|
36
|
+
SQL
|
37
|
+
|
38
|
+
AGG
|
39
|
+
end
|
40
|
+
end
|
37
41
|
|
38
|
-
|
39
|
-
|
40
|
-
|
42
|
+
# Fetches all aggregate functions from the database
|
43
|
+
def dumpable_aggregates_in_database
|
44
|
+
@dumpable_aggregates_in_database ||= begin
|
45
|
+
# SQL query to fetch aggregate functions from PostgreSQL
|
46
|
+
# This query gets the aggregate name and constructs the CREATE AGGREGATE statement
|
47
|
+
sql = <<~SQL
|
48
|
+
SELECT
|
49
|
+
p.proname AS name,
|
50
|
+
format(
|
51
|
+
'CREATE AGGREGATE %s(%s) (%s);',
|
52
|
+
p.proname,
|
53
|
+
pg_get_function_arguments(p.oid),
|
54
|
+
array_to_string(array_agg(format('%s = %s', option_name, option_value)), E',\n ')
|
55
|
+
) AS definition
|
56
|
+
FROM pg_proc p
|
57
|
+
JOIN pg_namespace n ON p.pronamespace = n.oid
|
58
|
+
JOIN pg_aggregate a ON a.aggfnoid = p.oid
|
59
|
+
JOIN LATERAL (
|
60
|
+
SELECT 'sfunc' AS option_name, p2.proname::text AS option_value
|
61
|
+
FROM pg_proc p2
|
62
|
+
WHERE p2.oid = a.aggtransfn
|
63
|
+
UNION ALL
|
64
|
+
SELECT 'stype', format_type(a.aggtranstype, NULL)
|
65
|
+
UNION ALL
|
66
|
+
SELECT 'finalfunc', p3.proname::text
|
67
|
+
FROM pg_proc p3
|
68
|
+
WHERE p3.oid = a.aggfinalfn AND a.aggfinalfn != 0
|
69
|
+
UNION ALL
|
70
|
+
SELECT 'initcond', quote_literal(a.agginitval)
|
71
|
+
WHERE a.agginitval IS NOT NULL
|
72
|
+
) options ON true
|
73
|
+
WHERE n.nspname = 'public'
|
74
|
+
GROUP BY p.proname, p.oid
|
75
|
+
ORDER BY p.proname;
|
76
|
+
SQL
|
41
77
|
|
42
|
-
|
43
|
-
|
44
|
-
#{file_version.sql_definition}
|
45
|
-
SQL
|
78
|
+
# Get the appropriate connection
|
79
|
+
connection = ActiveRecord::Base.connection
|
46
80
|
|
47
|
-
|
81
|
+
# Execute the query and transform results into aggregate objects
|
82
|
+
connection.execute(sql).map do |result|
|
83
|
+
AggregateDefinition.new(
|
84
|
+
result["name"],
|
85
|
+
result["definition"].strip
|
86
|
+
)
|
87
|
+
end
|
48
88
|
end
|
49
89
|
end
|
50
90
|
end
|
@@ -2,9 +2,18 @@
|
|
2
2
|
|
3
3
|
module PgAggregates
|
4
4
|
module SchemaStatements
|
5
|
+
# Reserved words to skip when checking function dependencies
|
6
|
+
RESERVED_WORDS = ["public"].freeze
|
7
|
+
|
5
8
|
def create_aggregate(name, version: nil, sql_definition: nil)
|
6
9
|
raise ArgumentError, "Must provide either sql_definition or version" if sql_definition.nil? && version.nil?
|
7
10
|
|
11
|
+
# First, check if the function already exists to avoid duplicate creation attempts
|
12
|
+
return if aggregate_exists?(name)
|
13
|
+
|
14
|
+
# Check if dependent functions exist before attempting to create
|
15
|
+
check_dependent_functions(sql_definition || read_aggregate_definition(name, version))
|
16
|
+
|
8
17
|
if sql_definition
|
9
18
|
execute sql_definition
|
10
19
|
else
|
@@ -18,6 +27,14 @@ module PgAggregates
|
|
18
27
|
|
19
28
|
execute aggregate_definition.to_sql
|
20
29
|
end
|
30
|
+
rescue ActiveRecord::StatementInvalid => e
|
31
|
+
raise unless /function .* does not exist/.match?(e.message)
|
32
|
+
|
33
|
+
puts "WARNING: Failed to create aggregate #{name} because a required function does not exist."
|
34
|
+
puts " This could indicate a dependency ordering issue."
|
35
|
+
puts " Error: #{e.message}"
|
36
|
+
|
37
|
+
raise
|
21
38
|
end
|
22
39
|
|
23
40
|
def drop_aggregate(name, *arg_types, force: false)
|
@@ -25,5 +42,59 @@ module PgAggregates
|
|
25
42
|
force_clause = force ? " CASCADE" : ""
|
26
43
|
execute "DROP AGGREGATE IF EXISTS #{name}#{arg_types_sql}#{force_clause}"
|
27
44
|
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def aggregate_exists?(name)
|
49
|
+
sql = <<-SQL
|
50
|
+
SELECT 1
|
51
|
+
FROM pg_catalog.pg_proc p
|
52
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
|
53
|
+
WHERE p.proname = '#{name}'
|
54
|
+
AND p.prokind = 'a'
|
55
|
+
SQL
|
56
|
+
|
57
|
+
result = execute(sql)
|
58
|
+
result.any?
|
59
|
+
rescue StandardError
|
60
|
+
false
|
61
|
+
end
|
62
|
+
|
63
|
+
def read_aggregate_definition(name, version)
|
64
|
+
aggregate_definition = PgAggregates::AggregateDefinition.new(name, version: version)
|
65
|
+
File.read(aggregate_definition.path) if File.exist?(aggregate_definition.path)
|
66
|
+
end
|
67
|
+
|
68
|
+
def check_dependent_functions(sql_definition)
|
69
|
+
return unless sql_definition
|
70
|
+
|
71
|
+
# Extract function names mentioned in the aggregate definition
|
72
|
+
# Be careful to extract only the function name, not schema qualifiers
|
73
|
+
function_matches = sql_definition.scan(/sfunc\s*=\s*['"]?(?:(?:[a-zA-Z0-9_]+\.)?([a-zA-Z0-9_]+))['"]?/i)
|
74
|
+
function_names = function_matches.flatten.compact.uniq
|
75
|
+
|
76
|
+
# For each referenced function, check if it exists
|
77
|
+
function_names.each do |function_name|
|
78
|
+
# Skip if function_name is empty or a reserved word
|
79
|
+
next if function_name.nil? || function_name.empty? || RESERVED_WORDS.include?(function_name.downcase)
|
80
|
+
|
81
|
+
check_sql = <<-SQL
|
82
|
+
SELECT 1#{" "}
|
83
|
+
FROM pg_catalog.pg_proc p
|
84
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
|
85
|
+
WHERE p.proname = '#{function_name}'
|
86
|
+
AND n.nspname = 'public'
|
87
|
+
SQL
|
88
|
+
|
89
|
+
result = execute(check_sql)
|
90
|
+
unless result.any?
|
91
|
+
puts "WARNING: Aggregate depends on function '#{function_name}' which does not exist"
|
92
|
+
puts " This will likely fail. Create the function first."
|
93
|
+
end
|
94
|
+
end
|
95
|
+
rescue StandardError => e
|
96
|
+
# Log the error but continue
|
97
|
+
puts "WARNING: Error checking function dependencies: #{e.message}"
|
98
|
+
end
|
28
99
|
end
|
29
100
|
end
|
data/lib/pg_aggregates.rb
CHANGED
@@ -10,11 +10,27 @@ require_relative "pg_aggregates/schema_dumper"
|
|
10
10
|
require_relative "pg_aggregates/railtie"
|
11
11
|
|
12
12
|
module PgAggregates
|
13
|
+
module_function
|
14
|
+
|
13
15
|
class Error < StandardError; end
|
14
16
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
17
|
+
def load
|
18
|
+
# This is crucial - we must ensure proper load order:
|
19
|
+
# 1. extensions
|
20
|
+
# 2. types
|
21
|
+
# 3. functions
|
22
|
+
# 4. aggregates
|
23
|
+
# 5. tables
|
24
|
+
|
25
|
+
# Add schema statements and command recorder
|
26
|
+
ActiveRecord::ConnectionAdapters::AbstractAdapter.include PgAggregates::SchemaStatements
|
27
|
+
ActiveRecord::Migration::CommandRecorder.include PgAggregates::CommandRecorder
|
28
|
+
|
29
|
+
# Hook into the schema dumper, with dependency awareness
|
30
|
+
ActiveRecord::SchemaDumper.prepend PgAggregates::SchemaDumper
|
31
|
+
end
|
32
|
+
|
33
|
+
def database
|
34
|
+
ActiveRecord::Base.connection
|
19
35
|
end
|
20
36
|
end
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pg_aggregates
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- mhenrixon
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: activerecord
|
@@ -92,7 +91,6 @@ metadata:
|
|
92
91
|
source_code_uri: https://github.com/mhenrixon/pg_aggregates
|
93
92
|
changelog_uri: https://github.com/mhenrixon/pg_aggregates/blob/main/CHANGELOG.md
|
94
93
|
rubygems_mfa_required: 'true'
|
95
|
-
post_install_message:
|
96
94
|
rdoc_options: []
|
97
95
|
require_paths:
|
98
96
|
- lib
|
@@ -107,8 +105,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
107
105
|
- !ruby/object:Gem::Version
|
108
106
|
version: '0'
|
109
107
|
requirements: []
|
110
|
-
rubygems_version: 3.
|
111
|
-
signing_key:
|
108
|
+
rubygems_version: 3.6.7
|
112
109
|
specification_version: 4
|
113
110
|
summary: Rails integration for PostgreSQL aggregate functions
|
114
111
|
test_files: []
|