pg_aggregates 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7dfdc0600d1533080703b0fd15ac61385df0e081cf30aa85a9813fc09680bfd7
4
- data.tar.gz: 3c9999602a5635ecfd3d0c141a86b7fd67b6a47b15f2a6da80843940d4fece31
3
+ metadata.gz: 404507e81fbfe0ea608413efa6535d34c15beae11392bd86e8c4a0f98525e815
4
+ data.tar.gz: 953d598b8e88883db7d60153e7bcd048efe97475cd3f72949411a4cc7d7872b7
5
5
  SHA512:
6
- metadata.gz: e872d45e8ab297e6a47d6d2a8282b932786208e8cb3f36cc684b2094df195b3d4d4091c46c38a00193722c597ec8f0270466b95002ece1d5b496cb88e528ab72
7
- data.tar.gz: c2fcccd281d02a9d422bbeb87e852370e558b45adf97c02ac5fb7dab2203db0dd358739ffe50913e3a824288d916c52fe4f593ef7042721a19d536ef67f8054a
6
+ metadata.gz: e6091519ab8526a079af9c4adce52c596e7c7f02ddf07d41d6a506a57200120a21eb58863b212d1071aa72732d30c17aa76a8622569715001a43b6ed4b84f687
7
+ data.tar.gz: 1df224a0da63cb50de1c435eb1a3a4c3d62e48a0efbe9deb92f8d1edbc4adeecb44745f29766368a8fe522319966224e2f01c3d5b260cdde04a93ec12868f8f5
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.3.5
1
+ 3.4.3
@@ -1,21 +1,24 @@
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"
8
11
  gem "bigdecimal"
9
12
  gem "database_cleaner"
10
13
  gem "drb"
14
+ gem "gem-release"
11
15
  gem "logger"
12
16
  gem "mutex_m"
17
+ gem "railties", "~> 6.1.0"
13
18
  gem "rake"
14
19
  gem "rspec"
15
20
  gem "rubocop-mhenrixon"
16
21
  gem "rubocop-rake"
17
22
  gem "rubocop-rspec"
18
- gem "activerecord", "~> 6.1.0"
19
- gem "railties", "~> 6.1.0"
20
23
 
21
24
  gemspec path: "../"
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- pg_aggregates (0.1.0)
4
+ pg_aggregates (0.1.1)
5
5
  activerecord (>= 6.1)
6
6
  pg (>= 1.1)
7
7
  railties (>= 6.1)
@@ -56,6 +56,7 @@ GEM
56
56
  diff-lcs (1.5.1)
57
57
  drb (2.2.1)
58
58
  erubi (1.13.0)
59
+ gem-release (2.2.2)
59
60
  i18n (1.14.6)
60
61
  concurrent-ruby (~> 1.0)
61
62
  json (2.7.6)
@@ -160,6 +161,7 @@ DEPENDENCIES
160
161
  bigdecimal
161
162
  database_cleaner
162
163
  drb
164
+ gem-release
163
165
  logger
164
166
  mutex_m
165
167
  pg_aggregates!
@@ -1,21 +1,24 @@
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"
8
11
  gem "bigdecimal"
9
12
  gem "database_cleaner"
10
13
  gem "drb"
14
+ gem "gem-release"
11
15
  gem "logger"
12
16
  gem "mutex_m"
17
+ gem "railties", "~> 7.0.0"
13
18
  gem "rake"
14
19
  gem "rspec"
15
20
  gem "rubocop-mhenrixon"
16
21
  gem "rubocop-rake"
17
22
  gem "rubocop-rspec"
18
- gem "activerecord", "~> 7.0.0"
19
- gem "railties", "~> 7.0.0"
20
23
 
21
24
  gemspec path: "../"
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- pg_aggregates (0.1.0)
4
+ pg_aggregates (0.1.1)
5
5
  activerecord (>= 6.1)
6
6
  pg (>= 1.1)
7
7
  railties (>= 6.1)
@@ -55,6 +55,7 @@ GEM
55
55
  diff-lcs (1.5.1)
56
56
  drb (2.2.1)
57
57
  erubi (1.13.0)
58
+ gem-release (2.2.2)
58
59
  i18n (1.14.6)
59
60
  concurrent-ruby (~> 1.0)
60
61
  json (2.7.6)
@@ -175,6 +176,7 @@ DEPENDENCIES
175
176
  bigdecimal
176
177
  database_cleaner
177
178
  drb
179
+ gem-release
178
180
  logger
179
181
  mutex_m
180
182
  pg_aggregates!
@@ -1,21 +1,24 @@
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"
8
11
  gem "bigdecimal"
9
12
  gem "database_cleaner"
10
13
  gem "drb"
14
+ gem "gem-release"
11
15
  gem "logger"
12
16
  gem "mutex_m"
17
+ gem "railties", "~> 7.1.0"
13
18
  gem "rake"
14
19
  gem "rspec"
15
20
  gem "rubocop-mhenrixon"
16
21
  gem "rubocop-rake"
17
22
  gem "rubocop-rspec"
18
- gem "activerecord", "~> 7.1.0"
19
- gem "railties", "~> 7.1.0"
20
23
 
21
24
  gemspec path: "../"
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- pg_aggregates (0.1.0)
4
+ pg_aggregates (0.1.1)
5
5
  activerecord (>= 6.1)
6
6
  pg (>= 1.1)
7
7
  railties (>= 6.1)
@@ -69,6 +69,7 @@ GEM
69
69
  diff-lcs (1.5.1)
70
70
  drb (2.2.1)
71
71
  erubi (1.13.0)
72
+ gem-release (2.2.2)
72
73
  i18n (1.14.6)
73
74
  concurrent-ruby (~> 1.0)
74
75
  io-console (0.7.2)
@@ -206,6 +207,7 @@ DEPENDENCIES
206
207
  bigdecimal
207
208
  database_cleaner
208
209
  drb
210
+ gem-release
209
211
  logger
210
212
  mutex_m
211
213
  pg_aggregates!
@@ -2,11 +2,9 @@
2
2
 
3
3
  module PgAggregates
4
4
  class Railtie < Rails::Railtie
5
- initializer "postgres_aggregates.load" do
5
+ initializer "pg_aggregates.load", before: "fx.load" do
6
6
  ActiveSupport.on_load(:active_record) do
7
- ActiveRecord::ConnectionAdapters::AbstractAdapter.include PgAggregates::SchemaStatements
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
7
+ # Define a Struct for aggregate information
8
+ AggregateDefinition = Struct.new(:name, :definition)
9
+
10
+ # Override the tables method to inject aggregate dumping *after* functions (handled by fx)
11
+ # but *before* tables.
5
12
  def tables(stream)
6
- # First dump aggregates
13
+ # Dump custom aggregates first
7
14
  dump_custom_aggregates(stream)
8
- stream.puts
9
15
 
16
+ stream.puts # Add a newline for separation before tables
17
+
18
+ # Call the next implementation (likely fx's tables or Rails' tables)
10
19
  super
11
20
  end
12
21
 
13
22
  private
14
23
 
15
24
  def dump_custom_aggregates(stream)
16
- # Group all versions of each aggregate
17
- aggregate_versions = {}
25
+ aggregates = dumpable_aggregates_in_database
18
26
 
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
24
-
25
- return if aggregate_versions.empty?
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
- latest_versions.keys.sort.each do |aggregate_name|
36
- file_version = latest_versions[aggregate_name]
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
- # Add a comment showing the version history
39
- all_versions = aggregate_versions[aggregate_name].map(&:version).sort
40
- version_comment = all_versions.size > 1 ? " -- versions: #{all_versions.join(", ")}" : ""
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
- stream.puts <<-AGG
43
- create_aggregate "#{aggregate_name}", sql_definition: <<-SQL#{version_comment}
44
- #{file_version.sql_definition}
45
- SQL
78
+ # Get the appropriate connection
79
+ connection = ActiveRecord::Base.connection
46
80
 
47
- AGG
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgAggregates
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.2"
5
5
  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
- class << self
16
- def database
17
- ActiveRecord::Base.connection
18
- end
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.1.1
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: 2024-11-04 00:00:00.000000000 Z
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.2.33
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: []