diff --git a/Gemfile.lock b/Gemfile.lock
index 803739ee9b7993e23157e5e194e73899e2619dcb..d5b8c648d4d216102655f2a889ee0c628ba0a3cf 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -27,6 +27,7 @@ PATH
   remote: gems/gitlab-active-context
   specs:
     gitlab-active-context (0.0.1)
+      activerecord
       activesupport
       connection_pool
       elasticsearch
diff --git a/Gemfile.next.lock b/Gemfile.next.lock
index f3578090d7d432156ad4a0450e043676f598af9e..fb51de33921dfd4e6413617e8512cd4436e19c39 100644
--- a/Gemfile.next.lock
+++ b/Gemfile.next.lock
@@ -27,6 +27,7 @@ PATH
   remote: gems/gitlab-active-context
   specs:
     gitlab-active-context (0.0.1)
+      activerecord
       activesupport
       connection_pool
       elasticsearch
diff --git a/ee/db/active_context/migrate/.gitkeep b/ee/db/active_context/migrate/.gitkeep
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/gems/gitlab-active-context/.rubocop.yml b/gems/gitlab-active-context/.rubocop.yml
index 29f1c5a30209a2d49e1cf89e3107fb7eb6fd887e..65518b19e7d722de0827443777c6209c2da11b4a 100644
--- a/gems/gitlab-active-context/.rubocop.yml
+++ b/gems/gitlab-active-context/.rubocop.yml
@@ -7,6 +7,9 @@ Gemfile/MissingFeatureCategory:
 Search/NamespacedClass:
   Enabled: false
 
+RSpec/AnyInstanceOf:
+  Enabled: false
+
 RSpec/MultipleMemoizedHelpers:
   Max: 25
 
@@ -14,6 +17,10 @@ RSpec/VerifiedDoubles:
   Exclude:
   - 'spec/lib/active_context/tracker_spec.rb'
 
+Naming/ClassAndModuleCamelCase:
+  AllowedNames:
+    - V1_0
+
 RSpec/VerifiedDoubleReference:
   Exclude:
   - 'spec/lib/active_context/bulk_process_queue_spec.rb'
diff --git a/gems/gitlab-active-context/Gemfile.lock b/gems/gitlab-active-context/Gemfile.lock
index 6f5a395e2bfb623d607e3ae3428f954ffb08b98e..3b01b42833730847ca8fcc625cc4b9d4aebeff6b 100644
--- a/gems/gitlab-active-context/Gemfile.lock
+++ b/gems/gitlab-active-context/Gemfile.lock
@@ -2,6 +2,7 @@ PATH
   remote: .
   specs:
     gitlab-active-context (0.0.1)
+      activerecord
       activesupport
       connection_pool
       elasticsearch
@@ -28,6 +29,12 @@ GEM
       erubi (~> 1.11)
       rails-dom-testing (~> 2.2)
       rails-html-sanitizer (~> 1.6)
+    activemodel (8.0.1)
+      activesupport (= 8.0.1)
+    activerecord (8.0.1)
+      activemodel (= 8.0.1)
+      activesupport (= 8.0.1)
+      timeout (>= 0.4.0)
     activesupport (8.0.1)
       base64
       benchmark (>= 0.3)
@@ -222,6 +229,7 @@ GEM
     securerandom (0.4.0)
     stringio (3.1.2)
     thor (1.3.2)
+    timeout (0.4.3)
     tzinfo (2.0.6)
       concurrent-ruby (~> 1.0)
     unicode-display_width (2.6.0)
diff --git a/gems/gitlab-active-context/gitlab-active-context.gemspec b/gems/gitlab-active-context/gitlab-active-context.gemspec
index 9ff6c3b07ee703535d329695a070d3c076246c50..f19f1452f5580673684db74ab83379839debbddf 100644
--- a/gems/gitlab-active-context/gitlab-active-context.gemspec
+++ b/gems/gitlab-active-context/gitlab-active-context.gemspec
@@ -19,6 +19,7 @@ Gem::Specification.new do |spec|
   spec.files = Dir['lib/**/*.rb']
   spec.require_paths = ["lib"]
 
+  spec.add_dependency 'activerecord'
   spec.add_dependency 'activesupport'
   spec.add_dependency 'connection_pool'
   spec.add_dependency 'elasticsearch'
diff --git a/gems/gitlab-active-context/lib/active_context.rb b/gems/gitlab-active-context/lib/active_context.rb
index a7c8cbe860144dc1a536483c10396a0c2a034f2c..48d0bf24a9c1a3c942f35111b62c00f753870ff3 100644
--- a/gems/gitlab-active-context/lib/active_context.rb
+++ b/gems/gitlab-active-context/lib/active_context.rb
@@ -5,6 +5,7 @@
 require 'connection_pool'
 require 'pg'
 require 'zeitwerk'
+require 'active_record'
 loader = Zeitwerk::Loader.for_gem
 loader.setup
 
diff --git a/gems/gitlab-active-context/lib/active_context/config.rb b/gems/gitlab-active-context/lib/active_context/config.rb
index 604007563944760a94ce804d69608cf1d765d420..b68843f65e11b04f937c4f62977642b4b3e4d9b1 100644
--- a/gems/gitlab-active-context/lib/active_context/config.rb
+++ b/gems/gitlab-active-context/lib/active_context/config.rb
@@ -2,7 +2,7 @@
 
 module ActiveContext
   class Config
-    Cfg = Struct.new(:enabled, :databases, :logger, :indexing_enabled, :re_enqueue_indexing_workers)
+    Cfg = Struct.new(:enabled, :databases, :logger, :indexing_enabled, :re_enqueue_indexing_workers, :migrations_path)
 
     class << self
       def configure(&block)
@@ -21,8 +21,12 @@ def databases
         current.databases || {}
       end
 
+      def migrations_path
+        current.migrations_path || Rails.root.join('ee/db/active_context/migrate')
+      end
+
       def logger
-        current.logger || Logger.new($stdout)
+        current.logger || ::Logger.new($stdout)
       end
 
       def indexing_enabled?
diff --git a/gems/gitlab-active-context/lib/active_context/databases/collection_builder.rb b/gems/gitlab-active-context/lib/active_context/databases/collection_builder.rb
new file mode 100644
index 0000000000000000000000000000000000000000..00949de9124696bfc474a37214c129f07a4399cc
--- /dev/null
+++ b/gems/gitlab-active-context/lib/active_context/databases/collection_builder.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module ActiveContext
+  module Databases
+    class CollectionBuilder
+      attr_reader :fields
+
+      def initialize
+        @fields = []
+      end
+
+      def bigint(name, index: false)
+        fields << Field::Bigint.new(name, index: index)
+      end
+
+      def prefix(name)
+        fields << Field::Prefix.new(name, index: true)
+      end
+
+      def vector(name, dimensions:, index: true)
+        fields << Field::Vector.new(name, dimensions: dimensions, index: index)
+      end
+    end
+
+    class Field
+      attr_reader :name, :options
+
+      def initialize(name, **options)
+        @name = name.to_s
+        @options = options
+      end
+
+      class Bigint < Field; end
+      class Prefix < Field; end
+      class Vector < Field; end
+    end
+  end
+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
index 51ef809e8c138b64cc7ea082115c46f4fbabda62..65aa29deafb2bdee1901bbe7efe0c6938b00bf69 100644
--- a/gems/gitlab-active-context/lib/active_context/databases/concerns/adapter.rb
+++ b/gems/gitlab-active-context/lib/active_context/databases/concerns/adapter.rb
@@ -4,15 +4,20 @@ module ActiveContext
   module Databases
     module Concerns
       module Adapter
-        attr_reader :options, :client, :indexer
+        attr_reader :options, :prefix, :client, :indexer, :executor
+
+        DEFAULT_PREFIX = 'gitlab_active_context'
+        DEFAULT_SEPARATOR = '_'
 
         delegate :search, to: :client
         delegate :all_refs, :add_ref, :empty?, :bulk, :process_bulk_errors, :reset, to: :indexer
 
         def initialize(options)
           @options = options
+          @prefix = options[:prefix] || DEFAULT_PREFIX
           @client = client_klass.new(options)
           @indexer = indexer_klass.new(options, client)
+          @executor = executor_klass.new(self)
         end
 
         def client_klass
@@ -22,6 +27,18 @@ def client_klass
         def indexer_klass
           raise NotImplementedError
         end
+
+        def executor_klass
+          raise NotImplementedError
+        end
+
+        def full_collection_name(name)
+          [prefix, name].compact.join(separator)
+        end
+
+        def separator
+          DEFAULT_SEPARATOR
+        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
index a27ecb45907beb59d6d7935cbb5a765b2a5cdaec..899e13082b919dbbb2463ec04420d167a9f93cef 100644
--- a/gems/gitlab-active-context/lib/active_context/databases/concerns/client.rb
+++ b/gems/gitlab-active-context/lib/active_context/databases/concerns/client.rb
@@ -8,10 +8,6 @@ module Client
 
         attr_reader :options
 
-        def prefix
-          options[:prefix] || DEFAULT_PREFIX
-        end
-
         def search(_)
           raise NotImplementedError
         end
diff --git a/gems/gitlab-active-context/lib/active_context/databases/concerns/executor.rb b/gems/gitlab-active-context/lib/active_context/databases/concerns/executor.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3ca6974f320734e2ff8a61ce911b04848893fd82
--- /dev/null
+++ b/gems/gitlab-active-context/lib/active_context/databases/concerns/executor.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module ActiveContext
+  module Databases
+    module Concerns
+      module Executor
+        attr_reader :adapter
+
+        def initialize(adapter)
+          @adapter = adapter
+        end
+
+        def create_collection(name, number_of_partitions:, &block)
+          builder = ActiveContext::Databases::CollectionBuilder.new
+          yield(builder) if block
+
+          full_name = adapter.full_collection_name(name)
+          do_create_collection(
+            name: full_name,
+            number_of_partitions: number_of_partitions,
+            fields: builder.fields
+          )
+        end
+
+        private
+
+        def do_create_collection(...)
+          raise NotImplementedError
+        end
+      end
+    end
+  end
+end
diff --git a/gems/gitlab-active-context/lib/active_context/databases/elasticsearch/adapter.rb b/gems/gitlab-active-context/lib/active_context/databases/elasticsearch/adapter.rb
index a75894a347976dac9e4e7b0c0b6e2c1990acb7ee..bb46833f218bcf6522363f95ee88cdb178c81744 100644
--- a/gems/gitlab-active-context/lib/active_context/databases/elasticsearch/adapter.rb
+++ b/gems/gitlab-active-context/lib/active_context/databases/elasticsearch/adapter.rb
@@ -13,6 +13,10 @@ def client_klass
         def indexer_klass
           ActiveContext::Databases::Elasticsearch::Indexer
         end
+
+        def executor_klass
+          ActiveContext::Databases::Elasticsearch::Executor
+        end
       end
     end
   end
diff --git a/gems/gitlab-active-context/lib/active_context/databases/elasticsearch/executor.rb b/gems/gitlab-active-context/lib/active_context/databases/elasticsearch/executor.rb
new file mode 100644
index 0000000000000000000000000000000000000000..49d3d169b64a43d0b6ad81b334ed54b226e94fab
--- /dev/null
+++ b/gems/gitlab-active-context/lib/active_context/databases/elasticsearch/executor.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+module ActiveContext
+  module Databases
+    module Elasticsearch
+      class Executor
+        include ActiveContext::Databases::Concerns::Executor
+
+        private
+
+        def raw_client
+          @raw_client ||= adapter.client.client
+        end
+
+        def do_create_collection(name:, number_of_partitions:, fields:)
+          strategy = PartitionStrategy.new(
+            name: name,
+            number_of_partitions: number_of_partitions
+          )
+
+          # Early return if everything exists
+          return if collection_exists?(strategy)
+
+          # Create missing partitions
+          strategy.each_partition do |partition_name|
+            create_partition(partition_name, fields) unless index_exists?(partition_name)
+          end
+
+          # Create alias if needed
+          create_alias(strategy) unless alias_exists?(strategy.collection_name)
+        end
+
+        def create_partition(name, fields)
+          mappings = {
+            mappings: {
+              properties: build_field_mappings(fields)
+            }
+          }
+          raw_client.indices.create(index: name, body: mappings)
+        end
+
+        def create_alias(strategy)
+          actions = [{
+            add: {
+              indices: strategy.partition_names,
+              alias: strategy.collection_name
+            }
+          }]
+          raw_client.indices.update_aliases(body: { actions: actions })
+        end
+
+        def build_field_mappings(fields)
+          fields.each_with_object({}) do |field, mappings|
+            mappings[field.name] = case field
+                                   when Field::Bigint
+                                     { type: 'long' }
+                                   when Field::Prefix
+                                     { type: 'keyword' }
+                                   when Field::Vector
+                                     {
+                                       type: 'dense_vector',
+                                       dims: field.options[:dimensions],
+                                       index: true,
+                                       similarity: 'cosine'
+                                     }
+                                   else
+                                     raise ArgumentError, "Unknown field type: #{field.class}"
+                                   end
+          end
+        end
+
+        def collection_exists?(strategy)
+          return false unless alias_exists?(strategy.collection_name)
+
+          strategy.fully_exists? do |partition_name|
+            index_exists?(partition_name)
+          end
+        end
+
+        def index_exists?(name)
+          raw_client.indices.exists?(index: name)
+        end
+
+        def alias_exists?(name)
+          raw_client.indices.exists_alias?(name: name)
+        end
+      end
+    end
+  end
+end
diff --git a/gems/gitlab-active-context/lib/active_context/databases/opensearch/adapter.rb b/gems/gitlab-active-context/lib/active_context/databases/opensearch/adapter.rb
index a72fac16d38a825a5df640cd5163cdb701ffb106..9a6ed90ef4eaca7042dec1dd2e4f4a44b4726f81 100644
--- a/gems/gitlab-active-context/lib/active_context/databases/opensearch/adapter.rb
+++ b/gems/gitlab-active-context/lib/active_context/databases/opensearch/adapter.rb
@@ -13,6 +13,10 @@ def client_klass
         def indexer_klass
           ActiveContext::Databases::Opensearch::Indexer
         end
+
+        def executor_klass
+          ActiveContext::Databases::Opensearch::Executor
+        end
       end
     end
   end
diff --git a/gems/gitlab-active-context/lib/active_context/databases/opensearch/executor.rb b/gems/gitlab-active-context/lib/active_context/databases/opensearch/executor.rb
new file mode 100644
index 0000000000000000000000000000000000000000..de6fef86241f137082743c6e0a9c3d2d023fd0e7
--- /dev/null
+++ b/gems/gitlab-active-context/lib/active_context/databases/opensearch/executor.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module ActiveContext
+  module Databases
+    module Opensearch
+      class Executor
+        include ActiveContext::Databases::Concerns::Executor
+      end
+    end
+  end
+end
diff --git a/gems/gitlab-active-context/lib/active_context/databases/partition_strategy.rb b/gems/gitlab-active-context/lib/active_context/databases/partition_strategy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..24fba7c29a770994431a5b85b91c46fe1f56e412
--- /dev/null
+++ b/gems/gitlab-active-context/lib/active_context/databases/partition_strategy.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module ActiveContext
+  module Databases
+    class PartitionStrategy
+      def initialize(name:, number_of_partitions:)
+        @name = name
+        @number_of_partitions = number_of_partitions
+      end
+
+      # Returns list of all partition names for this collection
+      def partition_names
+        Array.new(@number_of_partitions) do |i|
+          generate_partition_name(i)
+        end
+      end
+
+      # Returns the collection alias/view name
+      def collection_name
+        @name
+      end
+
+      # Generates a specific partition name given an index
+      def generate_partition_name(index)
+        "#{@name}#{ActiveContext.adapter.separator}#{index}"
+      end
+
+      # Helps executors check if collection is fully set up
+      def fully_exists?(&partition_exists_check)
+        partition_names.all?(&partition_exists_check)
+      end
+
+      # Iterator for operating on partitions
+      def each_partition
+        partition_names.each do |name|
+          yield name
+        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
index 3d71ee59d50ed45bc14b8d3138fee74142b52e85..dcea44fb7cc511639e4d150fbefc9e17d44c6ef2 100644
--- a/gems/gitlab-active-context/lib/active_context/databases/postgresql/adapter.rb
+++ b/gems/gitlab-active-context/lib/active_context/databases/postgresql/adapter.rb
@@ -13,6 +13,10 @@ def client_klass
         def indexer_klass
           ActiveContext::Databases::Postgresql::Indexer
         end
+
+        def executor_klass
+          ActiveContext::Databases::Postgresql::Executor
+        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
index 1ab361b20b38acc6da217b544f95837ae2d99b3d..ccc1c7afdfe5645c19629fa22d4b13ff88d937db 100644
--- a/gems/gitlab-active-context/lib/active_context/databases/postgresql/client.rb
+++ b/gems/gitlab-active-context/lib/active_context/databases/postgresql/client.rb
@@ -6,48 +6,126 @@ module Postgresql
       class Client
         include ActiveContext::Databases::Concerns::Client
 
+        class << self
+          attr_accessor :default_connection_pool
+        end
+
         DEFAULT_POOL_SIZE = 5
-        DEFAULT_POOL_TIMEOUT = 5
+        DEFAULT_CONNECT_TIMEOUT = 5
+
+        attr_reader :connection_pool, :options
 
         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
+          setup_connection_pool
         end
 
         def search(_query)
           with_connection do |conn|
-            res = conn.exec('SELECT * FROM pg_stat_activity')
+            res = conn.execute('SELECT * FROM pg_stat_activity')
             QueryResult.new(res)
           end
         end
 
+        # Provides raw PostgreSQL connection
+        def with_raw_connection(&block)
+          handle_connection(raw_connection: true, &block)
+        end
+
+        # Provides Rails-wrapped connection for using ActiveRecord methods
+        def with_connection(&block)
+          handle_connection(raw_connection: false, &block)
+        end
+
+        # Creates an ActiveRecord model for a specific table and yields it within the connection context
+        # @param table_name [String] The name of the table to create a model for
+        # @yield [Class] A dynamically created ActiveRecord model class with the correct connection
+        def with_model_for(table_name)
+          model_class = Class.new(::ActiveRecord::Base) do
+            self.table_name = table_name
+
+            def self.name
+              "ActiveContext::Model::#{table_name.classify}"
+            end
+
+            def self.to_s
+              name
+            end
+          end
+
+          with_connection do |conn|
+            model_class.define_singleton_method(:connection) { conn }
+            yield model_class
+          end
+        end
+
+        # For backward compatibility and simpler queries
+        def ar_model_for(table_name)
+          klass = nil
+          with_model_for(table_name) do |model_class|
+            klass = model_class
+          end
+          klass
+        end
+
         private
 
-        def with_connection
-          @pool.with do |conn|
-            yield(conn)
+        def handle_connection(raw_connection: false)
+          connection_pool.with_connection do |conn|
+            yield(raw_connection ? conn.raw_connection : conn)
+          rescue PG::Error, ::ActiveRecord::StatementInvalid => e
+            handle_error(e)
           end
         end
 
-        def close
-          @pool&.shutdown(&:close)
+        def handle_error(error)
+          ActiveContext::Logger.exception(error, message: 'Database error occurred')
+          raise error
+        end
+
+        def setup_connection_pool
+          model_class = create_connection_model
+          model_class.establish_connection(build_database_config.stringify_keys)
+          @connection_pool = model_class.connection_pool
+        end
+
+        def create_connection_model
+          Class.new(::ActiveRecord::Base) do
+            self.abstract_class = true
+
+            def self.name
+              "ActiveContext::ConnectionPool::#{object_id}"
+            end
+
+            def self.to_s
+              name
+            end
+          end
         end
 
-        def connection_params
+        def build_database_config
           {
+            adapter: 'postgresql',
             host: options[:host],
             port: options[:port],
-            dbname: options[:database],
-            user: options[:username],
+            database: options[:database],
+            username: options[:username],
             password: options[:password],
-            connect_timeout: options.fetch(:connect_timeout, 5)
+            connect_timeout: options.fetch(:connect_timeout, DEFAULT_CONNECT_TIMEOUT),
+            pool: calculate_pool_size,
+            prepared_statements: false,
+            advisory_locks: false,
+            database_tasks: false # This signals Rails that this is an auxiliary database
           }.compact
         end
+
+        def calculate_pool_size
+          options[:pool_size] || DEFAULT_POOL_SIZE
+        end
+
+        def close
+          connection_pool&.disconnect!
+        end
       end
     end
   end
diff --git a/gems/gitlab-active-context/lib/active_context/databases/postgresql/executor.rb b/gems/gitlab-active-context/lib/active_context/databases/postgresql/executor.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8ccb40e7a50bc492a4443a897922b51df00b1add
--- /dev/null
+++ b/gems/gitlab-active-context/lib/active_context/databases/postgresql/executor.rb
@@ -0,0 +1,198 @@
+# frozen_string_literal: true
+
+module ActiveContext
+  module Databases
+    module Postgresql
+      class Executor
+        include ActiveContext::Databases::Concerns::Executor
+
+        private
+
+        def do_create_collection(name:, number_of_partitions:, fields:)
+          strategy = PartitionStrategy.new(
+            name: name,
+            number_of_partitions: number_of_partitions
+          )
+
+          return if collection_exists?(strategy)
+
+          # Create parent table if it doesn't exist
+          create_parent_table(strategy.collection_name, fields) unless table_exists?(strategy.collection_name)
+
+          # Create child partition tables
+          strategy.each_partition do |partition_name|
+            next if table_exists?(partition_name)
+
+            create_partition_table(
+              partition_name,
+              strategy.collection_name,
+              get_partition_remainder(partition_name)
+            )
+          end
+
+          # Create indices on fields that need them
+          create_indices(strategy, fields)
+        end
+
+        def create_parent_table(name, fields)
+          fixed_columns, variable_columns = sort_fields_by_size(fields)
+
+          adapter.client.with_connection do |connection|
+            connection.create_table(name, id: false, primary_key: [:id, :partition_id],
+              options: 'PARTITION BY LIST (partition_id)') do |table|
+              # Add partition_id first as it's required for partitioning
+              table.integer :partition_id, null: false
+
+              # Add fixed columns first for better memory alignment
+              fixed_columns.each do |field|
+                add_column_from_field(table, field)
+              end
+
+              # Add id column
+              table.string :id, null: false
+
+              # Add variable width columns last
+              variable_columns.each do |field|
+                add_column_from_field(table, field)
+              end
+            end
+          end
+        end
+
+        def sort_fields_by_size(fields)
+          fixed_columns = []
+          variable_columns = []
+
+          fields.each do |field|
+            case field
+            when Field::Vector
+              # Vector fields have fixed size based on dimensions
+              fixed_columns << [field, field.options[:dimensions] * 4]
+            when Field::Bigint
+              # Bigint is 8 bytes
+              fixed_columns << [field, 8]
+            when Field::Prefix
+              # Text fields are variable width
+              variable_columns << field
+            else
+              raise ArgumentError, "Unknown field type: #{field.class}"
+            end
+          end
+
+          # Sort fixed-size columns by size in descending order for best alignment
+          [fixed_columns.sort_by { |_, size| -size }.map(&:first), variable_columns]
+        end
+
+        def add_column_from_field(table, field)
+          case field
+          when Field::Vector
+            table.column(field.name, "vector(#{field.options[:dimensions]})")
+          when Field::Bigint
+            table.bigint(field.name, **field.options.except(:index))
+          when Field::Prefix
+            table.text(field.name, **field.options.except(:index))
+          else
+            raise ArgumentError, "Unknown field type: #{field.class}"
+          end
+        end
+
+        def add_column_from_definition(table, column_def, _connection)
+          name, type_info = parse_column_definition(column_def)
+
+          if type_info[:type] == :virtual
+            # For vector columns, use raw SQL type
+            table.column(name, type_info[:options][:as])
+          else
+            table.column(name, type_info[:type], **type_info[:options])
+          end
+        end
+
+        def create_partition_table(partition_name, parent_name, partition_id)
+          adapter.client.with_connection do |connection|
+            sql = <<~SQL.squish
+              CREATE TABLE #{connection.quote_table_name(partition_name)}
+              PARTITION OF #{connection.quote_table_name(parent_name)}
+              FOR VALUES IN (#{partition_id});
+            SQL
+
+            connection.execute(sql)
+          end
+        end
+
+        def create_indices(strategy, fields)
+          fields.each do |field|
+            next unless field.options[:index]
+
+            if field.is_a?(Field::Vector)
+              strategy.each_partition do |partition_name|
+                next if index_exists?(partition_name, field)
+
+                create_vector_index(partition_name, field)
+              end
+            else
+              create_standard_index(strategy.collection_name, field)
+            end
+          end
+        end
+
+        def create_standard_index(table_name, field)
+          adapter.client.with_connection do |connection|
+            next if index_exists?(table_name, field)
+
+            connection.add_index(
+              table_name,
+              field.name,
+              name: index_name_for(table_name, field)
+            )
+          end
+        end
+
+        def create_vector_index(table_name, field)
+          adapter.client.with_connection do |connection|
+            next if index_exists?(table_name, field)
+
+            index_name = index_name_for(table_name, field)
+
+            connection.execute(<<~SQL.squish)
+              CREATE INDEX #{connection.quote_column_name(index_name)}
+              ON #{connection.quote_table_name(table_name)}
+              USING hnsw (#{connection.quote_column_name(field.name)} vector_l2_ops)
+            SQL
+          end
+        end
+
+        def index_exists?(table_name, field)
+          adapter.client.with_connection do |connection|
+            index_name = index_name_for(table_name, field)
+            connection.index_exists?(table_name, field.name, name: index_name)
+          end
+        end
+
+        def index_name_for(table_name, field)
+          "#{table_name}_#{field.name}_idx"
+        end
+
+        def collection_exists?(strategy)
+          adapter.client.with_connection do |connection|
+            exists = connection.table_exists?(strategy.collection_name)
+            next false unless exists
+
+            strategy.partition_names.all? do |partition_name|
+              connection.table_exists?(partition_name)
+            end
+          end
+        end
+
+        def table_exists?(name)
+          adapter.client.with_connection do |connection|
+            connection.table_exists?(name)
+          end
+        end
+
+        def get_partition_remainder(partition_name)
+          partition_name.split('_').last.to_i
+        end
+      end
+    end
+  end
+end
diff --git a/gems/gitlab-active-context/lib/active_context/logger.rb b/gems/gitlab-active-context/lib/active_context/logger.rb
new file mode 100644
index 0000000000000000000000000000000000000000..72094562e14575cab0ae1920c8027d1175aaf5d0
--- /dev/null
+++ b/gems/gitlab-active-context/lib/active_context/logger.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module ActiveContext
+  class Logger
+    ANONYMOUS = '<Anonymous>'
+
+    class << self
+      def debug(**kwargs)
+        log(:debug, **kwargs)
+      end
+
+      def info(**kwargs)
+        log(:info, **kwargs)
+      end
+
+      def warn(**kwargs)
+        log(:warn, **kwargs)
+      end
+
+      def error(**kwargs)
+        log(:error, **kwargs)
+      end
+
+      def fatal(**kwargs)
+        log(:fatal, **kwargs)
+      end
+
+      def exception(exception, **kwargs)
+        payload = {
+          exception_class: exception.class.name,
+          exception_message: exception.message,
+          exception_backtrace: exception.backtrace
+        }.merge(kwargs)
+
+        error(**payload)
+      end
+
+      private
+
+      def log(severity, **kwargs)
+        logger = ActiveContext::Config.logger
+
+        return unless logger
+
+        payload = build_structured_payload(**kwargs)
+        case severity
+        when :debug then logger.debug(payload)
+        when :info  then logger.info(payload)
+        when :warn  then logger.warn(payload)
+        when :error then logger.error(payload)
+        when :fatal then logger.fatal(payload)
+        end
+      end
+
+      def build_structured_payload(**params)
+        { class: self.class.name || ANONYMOUS }.merge(params).stringify_keys
+      end
+    end
+  end
+end
diff --git a/gems/gitlab-active-context/lib/active_context/migration.rb b/gems/gitlab-active-context/lib/active_context/migration.rb
new file mode 100644
index 0000000000000000000000000000000000000000..69f24c5e769a6dae4c406c7b307a098e65293fb2
--- /dev/null
+++ b/gems/gitlab-active-context/lib/active_context/migration.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module ActiveContext
+  class Migration
+    class V1_0
+      class << self
+        def milestone(version)
+          @milestone = version
+        end
+
+        def milestone_version
+          @milestone
+        end
+      end
+
+      def initialize
+        @operations = {}
+      end
+
+      def migrate!
+        raise NotImplementedError, "#{self.class.name} must implement #migrate!"
+      end
+
+      def create_collection(name, **options, &block)
+        operation = @operations[:"create_#{name}"] ||= OperationResult.new("create_#{name}")
+
+        # Only execute if not already completed
+        unless operation.completed?
+          ActiveContext.adapter.executor.create_collection(name, **options, &block)
+          operation.complete!
+        end
+
+        operation.completed?
+      end
+    end
+
+    def self.[](version)
+      version = version.to_s
+      name = "V#{version.tr('.', '_')}"
+
+      raise ArgumentError, "Unknown migration version: #{version}" unless const_defined?(name, false)
+
+      const_get(name, false)
+    end
+
+    def self.current_version
+      1.0
+    end
+  end
+end
diff --git a/gems/gitlab-active-context/lib/active_context/migration/dictionary.rb b/gems/gitlab-active-context/lib/active_context/migration/dictionary.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9b8ddd05248232ff0326b2d94760214beb64dbf7
--- /dev/null
+++ b/gems/gitlab-active-context/lib/active_context/migration/dictionary.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+module ActiveContext
+  class Migration
+    class Dictionary
+      Error = Class.new(StandardError)
+      DuplicateVersionError = Class.new(Error)
+      InvalidMigrationNameError = Class.new(Error)
+
+      class << self
+        def migrations_path
+          Array(ActiveContext::Config.migrations_path)
+        end
+
+        def instance
+          @instance ||= new
+        end
+
+        delegate :migrations, to: :instance
+      end
+
+      def initialize
+        @migrations = {}
+        load_migrations
+      end
+
+      # Returns all migrations sorted by version
+      def migrations
+        @migrations.sort_by { |version, _| version }.map(&:last)
+      end
+
+      # Find a specific migration by version
+      def find_by_version(version)
+        @migrations[version.to_s]
+      end
+
+      private
+
+      def load_migrations
+        migration_files.each do |file|
+          version, name = parse_migration_file(file)
+          version_constant = :"V#{version}"
+
+          # Only define the module if it doesn't exist
+          if ActiveContext::Migration.const_defined?(version_constant)
+            migration_module = ActiveContext::Migration.const_get(version_constant, false)
+          else
+            migration_module = Module.new
+            ActiveContext::Migration.const_set(version_constant, migration_module)
+
+            # Evaluate the migration file content within the namespace
+            migration_content = File.read(file)
+            migration_module.module_eval(migration_content)
+          end
+
+          klass_name = name.camelize
+          begin
+            # Look up the class within our namespace
+            klass = migration_module.const_get(klass_name, false)
+          rescue NameError
+            raise InvalidMigrationNameError, "Could not find migration class '#{klass_name}' in #{file}"
+          end
+
+          if @migrations.key?(version)
+            raise DuplicateVersionError, "Multiple migrations have the version number #{version}"
+          end
+
+          @migrations[version] = klass
+        end
+      end
+
+      def migration_files
+        self.class.migrations_path.flat_map do |path|
+          Dir[File.join(path, '*.rb')]
+        end
+      end
+
+      def parse_migration_file(filename)
+        basename = File.basename(filename, '.rb')
+
+        if basename =~ /\A([0-9]{14})_(.+)\z/
+          version = ::Regexp.last_match(1)
+          name = ::Regexp.last_match(2)
+          [version, name]
+        else
+          raise InvalidMigrationNameError,
+            "Invalid migration file name format: #{basename}. Expected format: YYYYMMDDHHMMSS_name.rb"
+        end
+      end
+    end
+  end
+end
diff --git a/gems/gitlab-active-context/lib/active_context/migration/operation_result.rb b/gems/gitlab-active-context/lib/active_context/migration/operation_result.rb
new file mode 100644
index 0000000000000000000000000000000000000000..823086ce62d1a0ad925304d3ae3aef763f04c1a0
--- /dev/null
+++ b/gems/gitlab-active-context/lib/active_context/migration/operation_result.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module ActiveContext
+  class Migration
+    class OperationResult
+      attr_reader :operation_name, :completed
+      alias_method :completed?, :completed
+
+      def initialize(operation_name)
+        @operation_name = operation_name
+        @completed = false
+      end
+
+      def complete!
+        @completed = true
+      end
+    end
+  end
+end
diff --git a/gems/gitlab-active-context/spec/lib/active_context/databases/elasticsearch/adapter_spec.rb b/gems/gitlab-active-context/spec/lib/active_context/databases/elasticsearch/adapter_spec.rb
index 7dd9660b36f3160a6ad08b46ebfa0246bb1d11a6..7d183a69d58fb5a7c748689e6171db43964b1170 100644
--- a/gems/gitlab-active-context/spec/lib/active_context/databases/elasticsearch/adapter_spec.rb
+++ b/gems/gitlab-active-context/spec/lib/active_context/databases/elasticsearch/adapter_spec.rb
@@ -11,4 +11,15 @@
 
     adapter.search(query)
   end
+
+  describe '#prefix' do
+    it 'returns default prefix when not specified' do
+      expect(adapter.prefix).to eq('gitlab_active_context')
+    end
+
+    it 'returns configured prefix' do
+      adapter = described_class.new(options.merge(prefix: 'custom'))
+      expect(adapter.prefix).to eq('custom')
+    end
+  end
 end
diff --git a/gems/gitlab-active-context/spec/lib/active_context/databases/elasticsearch/client_spec.rb b/gems/gitlab-active-context/spec/lib/active_context/databases/elasticsearch/client_spec.rb
index bf313814ab95c8441ead97062045c776b5d1aafd..52d31479046c03d784e0e24fb5de2b31e06da000 100644
--- a/gems/gitlab-active-context/spec/lib/active_context/databases/elasticsearch/client_spec.rb
+++ b/gems/gitlab-active-context/spec/lib/active_context/databases/elasticsearch/client_spec.rb
@@ -52,15 +52,4 @@
       )
     end
   end
-
-  describe '#prefix' do
-    it 'returns default prefix when not specified' do
-      expect(client.prefix).to eq('gitlab_active_context')
-    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/opensearch/adapter_spec.rb b/gems/gitlab-active-context/spec/lib/active_context/databases/opensearch/adapter_spec.rb
index 1afba9a992b1e5954085d9c732c8b08c27f0dbbe..859cb28f34913de34ffeb96aade0f01367683e93 100644
--- a/gems/gitlab-active-context/spec/lib/active_context/databases/opensearch/adapter_spec.rb
+++ b/gems/gitlab-active-context/spec/lib/active_context/databases/opensearch/adapter_spec.rb
@@ -11,4 +11,15 @@
 
     adapter.search(query)
   end
+
+  describe '#prefix' do
+    it 'returns default prefix when not specified' do
+      expect(adapter.prefix).to eq('gitlab_active_context')
+    end
+
+    it 'returns configured prefix' do
+      adapter = described_class.new(options.merge(prefix: 'custom'))
+      expect(adapter.prefix).to eq('custom')
+    end
+  end
 end
diff --git a/gems/gitlab-active-context/spec/lib/active_context/databases/opensearch/client_spec.rb b/gems/gitlab-active-context/spec/lib/active_context/databases/opensearch/client_spec.rb
index 40cba13c9333ef039646d6343cecc6b203223e5d..ee81655a295c832a4021e0ed8f70abc1af3f4027 100644
--- a/gems/gitlab-active-context/spec/lib/active_context/databases/opensearch/client_spec.rb
+++ b/gems/gitlab-active-context/spec/lib/active_context/databases/opensearch/client_spec.rb
@@ -78,15 +78,4 @@
       end
     end
   end
-
-  describe '#prefix' do
-    it 'returns default prefix when not specified' do
-      expect(client.prefix).to eq('gitlab_active_context')
-    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/adapter_spec.rb b/gems/gitlab-active-context/spec/lib/active_context/databases/postgresql/adapter_spec.rb
index 059efe66f485596a9753a878ac18a3e7540d6b22..0c0628cf3b7a32851914326a09aa71dcc3cb17c7 100644
--- 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
@@ -19,4 +19,15 @@
 
     adapter.search(query)
   end
+
+  describe '#prefix' do
+    it 'returns default prefix when not specified' do
+      expect(adapter.prefix).to eq('gitlab_active_context')
+    end
+
+    it 'returns configured prefix' do
+      adapter = described_class.new(options.merge(prefix: 'custom'))
+      expect(adapter.prefix).to eq('custom')
+    end
+  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
index 6e1ee75c79135b3e9dc548cd01f4eaaf4eb06384..738ba011f30b90cb7c0fd3afde3ff5dc2393db9f 100644
--- 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
@@ -16,40 +16,131 @@
   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))
+    let(:connection_pool) { instance_double(ActiveRecord::ConnectionAdapters::ConnectionPool) }
+    let(:connection_model) { class_double(ActiveRecord::Base) }
+
+    before do
+      allow_any_instance_of(described_class).to receive(:create_connection_model)
+        .and_return(connection_model)
+
+      allow(connection_model).to receive(:establish_connection)
+      allow(connection_model).to receive(:connection_pool).and_return(connection_pool)
+    end
+
+    it 'creates a connection pool through ActiveRecord' do
+      expected_config = {
+        'adapter' => 'postgresql',
+        'host' => 'localhost',
+        'port' => 5432,
+        'database' => 'test_db',
+        'username' => 'user',
+        'password' => 'pass',
+        'connect_timeout' => 5,
+        'pool' => 2,
+        'prepared_statements' => false,
+        'advisory_locks' => false,
+        'database_tasks' => false
+      }
+
+      expect(connection_model).to receive(:establish_connection)
+        .with(hash_including(expected_config))
 
       client
     end
   end
 
+  describe '#with_raw_connection' do
+    let(:raw_connection) { instance_double(PG::Connection) }
+    let(:connection_pool) { instance_double(ActiveRecord::ConnectionAdapters::ConnectionPool) }
+    let(:ar_connection) { instance_double(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) }
+    let(:connection_model) { class_double(ActiveRecord::Base) }
+    let(:yielded_values) { [] }
+
+    before do
+      allow_any_instance_of(described_class).to receive(:create_connection_model)
+        .and_return(connection_model)
+
+      allow(connection_model).to receive(:establish_connection)
+      allow(connection_model).to receive(:connection_pool).and_return(connection_pool)
+
+      allow(connection_pool).to receive(:with_connection).and_yield(ar_connection)
+      allow(ar_connection).to receive_messages(
+        raw_connection: raw_connection
+      )
+
+      allow(raw_connection).to receive(:server_version).and_return(120000)
+    end
+
+    it 'yields raw PostgreSQL connection' do
+      client.with_raw_connection do |conn|
+        yielded_values << conn
+      end
+
+      expect(yielded_values).to eq([raw_connection])
+    end
+  end
+
+  describe '#with_connection' do
+    let(:raw_connection) { instance_double(PG::Connection) }
+    let(:connection_pool) { instance_double(ActiveRecord::ConnectionAdapters::ConnectionPool) }
+    let(:ar_connection) { instance_double(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) }
+    let(:connection_model) { class_double(ActiveRecord::Base) }
+    let(:yielded_values) { [] }
+
+    before do
+      allow_any_instance_of(described_class).to receive(:create_connection_model)
+        .and_return(connection_model)
+
+      allow(connection_model).to receive(:establish_connection)
+      allow(connection_model).to receive(:connection_pool).and_return(connection_pool)
+
+      allow(connection_pool).to receive(:with_connection).and_yield(ar_connection)
+      allow(ar_connection).to receive_messages(
+        raw_connection: raw_connection
+      )
+
+      allow(raw_connection).to receive(:server_version).and_return(120000)
+    end
+
+    it 'yields ActiveRecord connection' do
+      client.with_connection do |conn|
+        yielded_values << conn
+      end
+
+      expect(yielded_values).to eq([ar_connection])
+    end
+  end
+
   describe '#search' do
-    let(:connection) { instance_double(PG::Connection) }
+    let(:raw_connection) { instance_double(PG::Connection) }
     let(:query_result) { instance_double(PG::Result) }
+    let(:connection_pool) { instance_double(ActiveRecord::ConnectionAdapters::ConnectionPool) }
+    let(:ar_connection) { instance_double(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) }
+    let(:connection_model) { class_double(ActiveRecord::Base) }
 
     before do
-      allow(PG).to receive(:connect).and_return(connection)
-      allow(connection).to receive(:exec).and_return(query_result)
+      allow_any_instance_of(described_class).to receive(:create_connection_model)
+        .and_return(connection_model)
+
+      allow(connection_model).to receive(:establish_connection)
+      allow(connection_model).to receive(:connection_pool).and_return(connection_pool)
+
+      allow(connection_pool).to receive(:with_connection).and_yield(ar_connection)
+      allow(ar_connection).to receive_messages(
+        execute: query_result,
+        raw_connection: raw_connection
+      )
+
+      allow(raw_connection).to receive(:server_version).and_return(120000)
+      allow(ActiveContext::Databases::Postgresql::QueryResult).to receive(:new)
     end
 
     it 'executes query and returns QueryResult' do
-      expect(connection).to receive(:exec).with('SELECT * FROM pg_stat_activity')
+      expect(ar_connection).to receive(:execute).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_active_context')
-    end
-
-    it 'returns configured prefix' do
-      client = described_class.new(options.merge(prefix: 'custom'))
-      expect(client.prefix).to eq('custom')
-    end
-  end
 end