From ad0d3d536f5ff928844bb47d9faaca774be3d34d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B4natas=20Davi=20Paganini?= Date: Wed, 5 Jun 2024 08:53:38 -0300 Subject: [PATCH] Skip schema dumper when extension is not available (#66) --- bin/console | 4 ++- lib/timescaledb.rb | 9 ++++++ lib/timescaledb/connection.rb | 13 ++++++++- lib/timescaledb/connection_handling.rb | 13 ++++++--- lib/timescaledb/extension.rb | 23 +++++++++++++++ lib/timescaledb/schema_dumper.rb | 23 ++++++++++++--- spec/timescaledb/connection_spec.rb | 39 ++++++++++++++++++++++++++ spec/timescaledb_spec.rb | 10 +++++++ 8 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 lib/timescaledb/extension.rb create mode 100644 spec/timescaledb/connection_spec.rb diff --git a/bin/console b/bin/console index 7ee8095..68c83db 100755 --- a/bin/console +++ b/bin/console @@ -9,7 +9,9 @@ def uri_from_test ENV['PG_URI_TEST'] end -ActiveRecord::Base.establish_connection(ARGV[0] || uri_from_test) +uri = ARGV[0] || uri_from_test +ActiveRecord::Base.establish_connection(uri) +Timescaledb.establish_connection(uri) Timescaledb::Hypertable.find_each do |hypertable| class_name = hypertable.hypertable_name.singularize.camelize diff --git a/lib/timescaledb.rb b/lib/timescaledb.rb index 348c9b4..4dc31db 100644 --- a/lib/timescaledb.rb +++ b/lib/timescaledb.rb @@ -17,11 +17,20 @@ require_relative 'timescaledb/stats' require_relative 'timescaledb/stats_report' require_relative 'timescaledb/migration_helpers' +require_relative 'timescaledb/extension' require_relative 'timescaledb/version' module Timescaledb module_function + def connection + Connection.instance + end + + def extension + Extension + end + def chunks Chunk.all end diff --git a/lib/timescaledb/connection.rb b/lib/timescaledb/connection.rb index 43d6f0a..6a23cef 100644 --- a/lib/timescaledb/connection.rb +++ b/lib/timescaledb/connection.rb @@ -1,6 +1,11 @@ require 'singleton' module Timescaledb + # Minimal connection setup for Timescaledb directly with the PG. + # The concept is use a singleton component that can query + # independently of the ActiveRecord::Base connections. + # This is useful for the extension and hypertable metadata. + # It can also #use_connection from active record if needed. class Connection include Singleton @@ -34,10 +39,16 @@ def connected? !@config.nil? end + # Override the connection with a raw PG connection. + # @param [PG::Connection] connection The raw PG connection. + def use_connection connection + @connection = connection + end + private def connection @connection ||= PG.connect(@config) end end -end \ No newline at end of file +end diff --git a/lib/timescaledb/connection_handling.rb b/lib/timescaledb/connection_handling.rb index 0b9a08a..3e31a5e 100644 --- a/lib/timescaledb/connection_handling.rb +++ b/lib/timescaledb/connection_handling.rb @@ -1,16 +1,21 @@ module Timescaledb class ConnectionNotEstablishedError < StandardError; end - # @param [String] config The postgres connection string. + module_function + + # @param [String] config with the postgres connection string. def establish_connection(config) Connection.instance.config = config end - module_function :establish_connection + + # @param [PG::Connection] to use it directly from a raw connection + def use_connection conn + Connection.instance.use_connection conn + end def connection raise ConnectionNotEstablishedError.new unless Connection.instance.connected? Connection.instance end - module_function :connection -end \ No newline at end of file +end diff --git a/lib/timescaledb/extension.rb b/lib/timescaledb/extension.rb new file mode 100644 index 0000000..32d9f84 --- /dev/null +++ b/lib/timescaledb/extension.rb @@ -0,0 +1,23 @@ +module Timescaledb + + # Provides metadata around the extension in the database + module Extension + module_function + # @return String version of the timescaledb extension + def version + @version ||= Timescaledb.connection.query_first(<<~SQL)&.version + SELECT extversion as version + FROM pg_extension + WHERE extname = 'timescaledb' + SQL + end + + def installed? + version.present? + end + + def update! + Timescaledb.connection.execute('ALTER EXTENSION timescaledb UPDATE') + end + end +end diff --git a/lib/timescaledb/schema_dumper.rb b/lib/timescaledb/schema_dumper.rb index 4ab6489..b0ab811 100644 --- a/lib/timescaledb/schema_dumper.rb +++ b/lib/timescaledb/schema_dumper.rb @@ -7,14 +7,29 @@ module Timescaledb # * retention policies # * continuous aggregates # * compression settings + # It also ignores Timescale related schemas when dumping the schema. + # It also ignores dumping options as extension is not installed or no hypertables are available. module SchemaDumper def tables(stream) super # This will call #table for each table in the database - return unless Timescaledb::Hypertable.table_exists? - timescale_hypertables(stream) - timescale_retention_policies(stream) - timescale_continuous_aggregates(stream) # Define these before any Scenic views that might use them + if exports_timescaledb_metadata? + timescale_hypertables(stream) + timescale_retention_policies(stream) + timescale_continuous_aggregates(stream) # Define these before any Scenic views that might use them + end + end + + # Ignore dumps in case DB is not eligible for TimescaleDB metadata. + # @return [Boolean] true if the extension is installed and hypertables are available, otherwise false. + private def exports_timescaledb_metadata? + # Note it's safe to use the raw connection here because we're only reading from the database + # and not modifying it. We're also on the same connection pool as ActiveRecord::Base. + # The dump process also runs standalone, so we don't need to worry about the connection being + # used elsewhere. + Timescaledb.use_connection @connection.raw_connection + + Timescaledb.extension.installed? && Timescaledb.hypertables.any? end # Ignores Timescale related schemas when dumping the schema diff --git a/spec/timescaledb/connection_spec.rb b/spec/timescaledb/connection_spec.rb new file mode 100644 index 0000000..333e32c --- /dev/null +++ b/spec/timescaledb/connection_spec.rb @@ -0,0 +1,39 @@ +RSpec.describe Timescaledb do + describe '.establish_connection' do + it 'returns a PG::Connection object' do + expect do + Timescaledb.establish_connection(ENV['PG_URI_TEST']) + end.to_not raise_error + end + end + + describe ::Timescaledb::Connection do + subject(:connection) { Timescaledb::Connection.instance } + + it 'returns a Connection object' do + is_expected.to be_a(Timescaledb::Connection) + end + + it 'has fast access to the connection' do + expect(connection.send(:connection)).to be_a(PG::Connection) + end + + describe '#connected?' do + it { expect(connection.connected?).to be_truthy } + end + + describe '#query_first' do + let(:sql) { "select 1 as one" } + subject(:result) { connection.query_first(sql) } + + it { expect(result).to be_a(OpenStruct) } + end + + describe '#query' do + let(:sql) { "select 1 as one" } + subject(:result) { connection.query(sql) } + + it { expect(result).to eq([OpenStruct.new({"one" => "1"})]) } + end + end +end diff --git a/spec/timescaledb_spec.rb b/spec/timescaledb_spec.rb index 13202c8..cb7bd17 100644 --- a/spec/timescaledb_spec.rb +++ b/spec/timescaledb_spec.rb @@ -3,6 +3,16 @@ expect(Timescaledb::VERSION).not_to be nil end + describe ".extension" do + describe ".installed?" do + it { expect(Timescaledb.extension.installed?).to be_truthy } + end + + describe ".version" do + it { expect(Timescaledb.extension.version).not_to be_empty } + end + end + describe ".chunks" do subject { Timescaledb.chunks }