From 0ffc1f838e1e4716dcac93e07d3d1ac0359615bc Mon Sep 17 00:00:00 2001
From: Dmitry Gruzd <dgruzd@gitlab.com>
Date: Thu, 2 Jan 2025 19:46:34 +0000
Subject: [PATCH] Apply 3 suggestion(s) to 2 file(s)

Co-authored-by: Ravi Kumar <rkumar@gitlab.com>
---
 Gemfile.lock                                  |  3 +
 Gemfile.next.lock                             |  3 +
 gems/gitlab-active-context/Gemfile.lock       |  4 ++
 .../gitlab-active-context.gemspec             |  3 +
 .../lib/active_context.rb                     | 14 ++++-
 .../lib/active_context/adapter.rb             | 31 +++++++++++
 .../lib/active_context/config.rb              | 14 ++---
 .../databases/concerns/adapter.rb             | 21 +++++++
 .../databases/concerns/client.rb              | 21 +++++++
 .../databases/concerns/query_result.rb        | 15 +++++
 .../databases/postgresql/adapter.rb           | 15 +++++
 .../databases/postgresql/client.rb            | 54 ++++++++++++++++++
 .../databases/postgresql/query_result.rb      | 35 ++++++++++++
 .../spec/active_context_spec.rb               | 31 +++++++++++
 .../databases/postgresql/adapter_spec.rb      | 22 ++++++++
 .../databases/postgresql/client_spec.rb       | 55 +++++++++++++++++++
 .../databases/postgresql/query_result_spec.rb | 55 +++++++++++++++++++
 17 files changed, 388 insertions(+), 8 deletions(-)
 create mode 100644 gems/gitlab-active-context/lib/active_context/adapter.rb
 create mode 100644 gems/gitlab-active-context/lib/active_context/databases/concerns/adapter.rb
 create mode 100644 gems/gitlab-active-context/lib/active_context/databases/concerns/client.rb
 create mode 100644 gems/gitlab-active-context/lib/active_context/databases/concerns/query_result.rb
 create mode 100644 gems/gitlab-active-context/lib/active_context/databases/postgresql/adapter.rb
 create mode 100644 gems/gitlab-active-context/lib/active_context/databases/postgresql/client.rb
 create mode 100644 gems/gitlab-active-context/lib/active_context/databases/postgresql/query_result.rb
 create mode 100644 gems/gitlab-active-context/spec/lib/active_context/databases/postgresql/adapter_spec.rb
 create mode 100644 gems/gitlab-active-context/spec/lib/active_context/databases/postgresql/client_spec.rb
 create mode 100644 gems/gitlab-active-context/spec/lib/active_context/databases/postgresql/query_result_spec.rb

diff --git a/Gemfile.lock b/Gemfile.lock
index 0a711333a8c11..92eeb2b933329 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -27,6 +27,9 @@ PATH
   remote: gems/gitlab-active-context
   specs:
     gitlab-active-context (0.0.1)
+      activesupport
+      connection_pool
+      pg
       zeitwerk
 
 PATH
diff --git a/Gemfile.next.lock b/Gemfile.next.lock
index cc360eb448a08..c1b711fecdb38 100644
--- a/Gemfile.next.lock
+++ b/Gemfile.next.lock
@@ -27,6 +27,9 @@ PATH
   remote: gems/gitlab-active-context
   specs:
     gitlab-active-context (0.0.1)
+      activesupport
+      connection_pool
+      pg
       zeitwerk
 
 PATH
diff --git a/gems/gitlab-active-context/Gemfile.lock b/gems/gitlab-active-context/Gemfile.lock
index f20e0bf5e27c8..8bb6eb61ac590 100644
--- a/gems/gitlab-active-context/Gemfile.lock
+++ b/gems/gitlab-active-context/Gemfile.lock
@@ -2,6 +2,9 @@ PATH
   remote: .
   specs:
     gitlab-active-context (0.0.1)
+      activesupport
+      connection_pool
+      pg
       zeitwerk
 
 GEM
@@ -87,6 +90,7 @@ GEM
     parser (3.3.6.0)
       ast (~> 2.4.1)
       racc
+    pg (1.5.9)
     psych (5.2.1)
       date
       stringio
diff --git a/gems/gitlab-active-context/gitlab-active-context.gemspec b/gems/gitlab-active-context/gitlab-active-context.gemspec
index fe9c5bb7f7c3b..e96cded82b8f7 100644
--- a/gems/gitlab-active-context/gitlab-active-context.gemspec
+++ b/gems/gitlab-active-context/gitlab-active-context.gemspec
@@ -19,6 +19,9 @@ Gem::Specification.new do |spec|
   spec.files = Dir['lib/**/*.rb']
   spec.require_paths = ["lib"]
 
+  spec.add_dependency 'activesupport'
+  spec.add_dependency 'connection_pool'
+  spec.add_dependency 'pg'
   spec.add_dependency 'zeitwerk'
 
   spec.add_development_dependency 'gitlab-styles'
diff --git a/gems/gitlab-active-context/lib/active_context.rb b/gems/gitlab-active-context/lib/active_context.rb
index f27e283de1903..b729911ca99ce 100644
--- a/gems/gitlab-active-context/lib/active_context.rb
+++ b/gems/gitlab-active-context/lib/active_context.rb
@@ -1,6 +1,10 @@
 # frozen_string_literal: true
 
-require "zeitwerk"
+require 'active_support/core_ext/string/inflections'
+require 'active_support/core_ext/module/delegation'
+require 'connection_pool'
+require 'pg'
+require 'zeitwerk'
 loader = Zeitwerk::Loader.for_gem
 loader.setup
 
@@ -8,4 +12,12 @@ module ActiveContext
   def self.configure(...)
     ActiveContext::Config.configure(...)
   end
+
+  def self.config
+    ActiveContext::Config.current
+  end
+
+  def self.adapter
+    ActiveContext::Adapter.current
+  end
 end
diff --git a/gems/gitlab-active-context/lib/active_context/adapter.rb b/gems/gitlab-active-context/lib/active_context/adapter.rb
new file mode 100644
index 0000000000000..48f0462a75d92
--- /dev/null
+++ b/gems/gitlab-active-context/lib/active_context/adapter.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module ActiveContext
+  module Adapter
+    class << self
+      def current
+        @current ||= load_adapter
+      end
+
+      private
+
+      def load_adapter
+        config = ActiveContext::Config.current
+        return nil unless config.enabled
+
+        name, hash = config.databases.first
+        return nil unless name
+
+        adapter = hash.fetch(:adapter)
+        return nil unless adapter
+
+        adapter_klass = adapter.safe_constantize
+        return nil unless adapter_klass
+
+        options = hash.fetch(:options)
+
+        adapter_klass.new(options)
+      end
+    end
+  end
+end
diff --git a/gems/gitlab-active-context/lib/active_context/config.rb b/gems/gitlab-active-context/lib/active_context/config.rb
index 60f8e1399e89e..06ca412babbbe 100644
--- a/gems/gitlab-active-context/lib/active_context/config.rb
+++ b/gems/gitlab-active-context/lib/active_context/config.rb
@@ -2,27 +2,27 @@
 
 module ActiveContext
   class Config
-    CONFIG = Struct.new(:enabled, :databases, :logger)
+    Cfg = Struct.new(:enabled, :databases, :logger)
 
     class << self
       def configure(&block)
         @instance = new(block)
       end
 
-      def config
-        @instance&.config || {}
+      def current
+        @instance&.config || Cfg.new
       end
 
       def enabled?
-        config.enabled || false
+        current.enabled || false
       end
 
       def databases
-        config.databases || {}
+        current.databases || {}
       end
 
       def logger
-        config.logger || Logger.new($stdout)
+        current.logger || Logger.new($stdout)
       end
     end
 
@@ -31,7 +31,7 @@ def initialize(config_block)
     end
 
     def config
-      struct = CONFIG.new
+      struct = Cfg.new
       @config_block.call(struct)
       struct
     end
diff --git a/gems/gitlab-active-context/lib/active_context/databases/concerns/adapter.rb b/gems/gitlab-active-context/lib/active_context/databases/concerns/adapter.rb
new file mode 100644
index 0000000000000..bf8854efbfa9b
--- /dev/null
+++ b/gems/gitlab-active-context/lib/active_context/databases/concerns/adapter.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module ActiveContext
+  module Databases
+    module Concerns
+      module Adapter
+        attr_reader :client
+
+        delegate :search, to: :client
+
+        def initialize(options)
+          @client = client_klass.new(options)
+        end
+
+        def client_klass
+          raise NotImplementedError
+        end
+      end
+    end
+  end
+end
diff --git a/gems/gitlab-active-context/lib/active_context/databases/concerns/client.rb b/gems/gitlab-active-context/lib/active_context/databases/concerns/client.rb
new file mode 100644
index 0000000000000..08cb6b5da672a
--- /dev/null
+++ b/gems/gitlab-active-context/lib/active_context/databases/concerns/client.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module ActiveContext
+  module Databases
+    module Concerns
+      module Client
+        DEFAULT_PREFIX = 'gitlab'
+
+        attr_reader :options
+
+        def prefix
+          options[:prefix] || DEFAULT_PREFIX
+        end
+
+        def search(_)
+          raise NotImplementedError
+        end
+      end
+    end
+  end
+end
diff --git a/gems/gitlab-active-context/lib/active_context/databases/concerns/query_result.rb b/gems/gitlab-active-context/lib/active_context/databases/concerns/query_result.rb
new file mode 100644
index 0000000000000..9ee353a6acfd0
--- /dev/null
+++ b/gems/gitlab-active-context/lib/active_context/databases/concerns/query_result.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module ActiveContext
+  module Databases
+    module Concerns
+      module QueryResult
+        include Enumerable
+
+        def each
+          raise NotImplementedError
+        end
+      end
+    end
+  end
+end
diff --git a/gems/gitlab-active-context/lib/active_context/databases/postgresql/adapter.rb b/gems/gitlab-active-context/lib/active_context/databases/postgresql/adapter.rb
new file mode 100644
index 0000000000000..33f2b2b199b83
--- /dev/null
+++ b/gems/gitlab-active-context/lib/active_context/databases/postgresql/adapter.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module ActiveContext
+  module Databases
+    module Postgresql
+      class Adapter
+        include ActiveContext::Databases::Concerns::Adapter
+
+        def client_klass
+          ActiveContext::Databases::Postgresql::Client
+        end
+      end
+    end
+  end
+end
diff --git a/gems/gitlab-active-context/lib/active_context/databases/postgresql/client.rb b/gems/gitlab-active-context/lib/active_context/databases/postgresql/client.rb
new file mode 100644
index 0000000000000..1ab361b20b38a
--- /dev/null
+++ b/gems/gitlab-active-context/lib/active_context/databases/postgresql/client.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module ActiveContext
+  module Databases
+    module Postgresql
+      class Client
+        include ActiveContext::Databases::Concerns::Client
+
+        DEFAULT_POOL_SIZE = 5
+        DEFAULT_POOL_TIMEOUT = 5
+
+        def initialize(options)
+          @options = options
+          @pool = ConnectionPool.new(
+            size: options.fetch(:pool_size, DEFAULT_POOL_SIZE),
+            timeout: options.fetch(:pool_timeout, DEFAULT_POOL_TIMEOUT)
+          ) do
+            PG.connect(connection_params)
+          end
+        end
+
+        def search(_query)
+          with_connection do |conn|
+            res = conn.exec('SELECT * FROM pg_stat_activity')
+            QueryResult.new(res)
+          end
+        end
+
+        private
+
+        def with_connection
+          @pool.with do |conn|
+            yield(conn)
+          end
+        end
+
+        def close
+          @pool&.shutdown(&:close)
+        end
+
+        def connection_params
+          {
+            host: options[:host],
+            port: options[:port],
+            dbname: options[:database],
+            user: options[:username],
+            password: options[:password],
+            connect_timeout: options.fetch(:connect_timeout, 5)
+          }.compact
+        end
+      end
+    end
+  end
+end
diff --git a/gems/gitlab-active-context/lib/active_context/databases/postgresql/query_result.rb b/gems/gitlab-active-context/lib/active_context/databases/postgresql/query_result.rb
new file mode 100644
index 0000000000000..dfdf4920b3717
--- /dev/null
+++ b/gems/gitlab-active-context/lib/active_context/databases/postgresql/query_result.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module ActiveContext
+  module Databases
+    module Postgresql
+      class QueryResult
+        include ActiveContext::Databases::Concerns::QueryResult
+
+        def initialize(pg_result)
+          @pg_result = pg_result
+        end
+
+        def each
+          return enum_for(:each) unless block_given?
+
+          pg_result.each do |row|
+            yield row
+          end
+        end
+
+        def count
+          pg_result.ntuples
+        end
+
+        def clear
+          pg_result.clear if pg_result.respond_to?(:clear)
+        end
+
+        private
+
+        attr_reader :pg_result
+      end
+    end
+  end
+end
diff --git a/gems/gitlab-active-context/spec/active_context_spec.rb b/gems/gitlab-active-context/spec/active_context_spec.rb
index 6b9bc738671b2..81708b4f02c93 100644
--- a/gems/gitlab-active-context/spec/active_context_spec.rb
+++ b/gems/gitlab-active-context/spec/active_context_spec.rb
@@ -28,4 +28,35 @@
       expect(ActiveContext::Config.logger).to be_a(::Logger)
     end
   end
+
+  describe '.config' do
+    it 'returns the current configuration' do
+      config = described_class.config
+      expect(config).to be_a(ActiveContext::Config::Cfg)
+    end
+  end
+
+  describe '.adapter' do
+    it 'returns nil when not configured' do
+      expect(described_class.adapter).to be_nil
+    end
+
+    it 'returns configured adapter' do
+      described_class.configure do |config|
+        config.enabled = true
+        config.databases = {
+          main: {
+            adapter: 'ActiveContext::Databases::Postgresql::Adapter',
+            options: {
+              host: 'localhost',
+              port: 5432,
+              database: 'test_db'
+            }
+          }
+        }
+      end
+
+      expect(described_class.adapter).to be_a(ActiveContext::Databases::Postgresql::Adapter)
+    end
+  end
 end
diff --git a/gems/gitlab-active-context/spec/lib/active_context/databases/postgresql/adapter_spec.rb b/gems/gitlab-active-context/spec/lib/active_context/databases/postgresql/adapter_spec.rb
new file mode 100644
index 0000000000000..059efe66f4855
--- /dev/null
+++ b/gems/gitlab-active-context/spec/lib/active_context/databases/postgresql/adapter_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+RSpec.describe ActiveContext::Databases::Postgresql::Adapter do
+  let(:options) do
+    {
+      host: 'localhost',
+      port: 5432,
+      database: 'test_db',
+      username: 'user',
+      password: 'pass'
+    }
+  end
+
+  subject(:adapter) { described_class.new(options) }
+
+  it 'delegates search to client' do
+    query = ActiveContext::Query.filter(foo: :bar)
+    expect(adapter.client).to receive(:search).with(query)
+
+    adapter.search(query)
+  end
+end
diff --git a/gems/gitlab-active-context/spec/lib/active_context/databases/postgresql/client_spec.rb b/gems/gitlab-active-context/spec/lib/active_context/databases/postgresql/client_spec.rb
new file mode 100644
index 0000000000000..7b5e2e7191789
--- /dev/null
+++ b/gems/gitlab-active-context/spec/lib/active_context/databases/postgresql/client_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+RSpec.describe ActiveContext::Databases::Postgresql::Client do
+  let(:options) do
+    {
+      host: 'localhost',
+      port: 5432,
+      database: 'test_db',
+      username: 'user',
+      password: 'pass',
+      pool_size: 2,
+      pool_timeout: 1
+    }
+  end
+
+  subject(:client) { described_class.new(options) }
+
+  describe '#initialize' do
+    it 'creates a connection pool' do
+      expect(ConnectionPool).to receive(:new)
+        .with(hash_including(size: 2, timeout: 1))
+
+      client
+    end
+  end
+
+  describe '#search' do
+    let(:connection) { instance_double(PG::Connection) }
+    let(:query_result) { instance_double(PG::Result) }
+
+    before do
+      allow(PG).to receive(:connect).and_return(connection)
+      allow(connection).to receive(:exec).and_return(query_result)
+    end
+
+    it 'executes query and returns QueryResult' do
+      expect(connection).to receive(:exec).with('SELECT * FROM pg_stat_activity')
+      expect(ActiveContext::Databases::Postgresql::QueryResult)
+        .to receive(:new).with(query_result)
+
+      client.search('test query')
+    end
+  end
+
+  describe '#prefix' do
+    it 'returns default prefix when not specified' do
+      expect(client.prefix).to eq('gitlab')
+    end
+
+    it 'returns configured prefix' do
+      client = described_class.new(options.merge(prefix: 'custom'))
+      expect(client.prefix).to eq('custom')
+    end
+  end
+end
diff --git a/gems/gitlab-active-context/spec/lib/active_context/databases/postgresql/query_result_spec.rb b/gems/gitlab-active-context/spec/lib/active_context/databases/postgresql/query_result_spec.rb
new file mode 100644
index 0000000000000..20f52b6efc145
--- /dev/null
+++ b/gems/gitlab-active-context/spec/lib/active_context/databases/postgresql/query_result_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+RSpec.describe ActiveContext::Databases::Postgresql::QueryResult do
+  let(:pg_result) { instance_double(PG::Result) }
+
+  subject(:query_result) { described_class.new(pg_result) }
+
+  describe '#each' do
+    it 'yields each row' do
+      rows = [
+        { 'id' => 1, 'name' => 'test1' },
+        { 'id' => 2, 'name' => 'test2' }
+      ]
+
+      allow(pg_result).to receive(:each).and_yield(rows[0]).and_yield(rows[1])
+
+      expect { |b| query_result.each(&b) }.to yield_successive_args(*rows)
+    end
+
+    it 'returns enumerator when no block given' do
+      expect(query_result.each).to be_a(Enumerator)
+    end
+  end
+
+  describe '#count' do
+    it 'returns number of tuples' do
+      allow(pg_result).to receive(:ntuples).and_return(5)
+      expect(query_result.count).to eq(5)
+    end
+  end
+
+  describe '#clear' do
+    context 'when pg_result responds to clear' do
+      before do
+        allow(pg_result).to receive(:respond_to?).with(:clear).and_return(true)
+      end
+
+      it 'clears the result' do
+        expect(pg_result).to receive(:clear)
+        query_result.clear
+      end
+    end
+
+    context 'when pg_result does not respond to clear' do
+      before do
+        allow(pg_result).to receive(:respond_to?).with(:clear).and_return(false)
+      end
+
+      it 'does nothing' do
+        expect(pg_result).not_to receive(:clear)
+        query_result.clear
+      end
+    end
+  end
+end
-- 
GitLab