ankane / blazer

Business intelligence made simple
MIT License
4.5k stars 471 forks source link

[Idea] Allow ResultCache to be injected and extended #479

Closed gillisd closed 1 month ago

gillisd commented 1 month ago

Feature Request

Current Behavior

Currently, the ResultCache is instantiated within the DataSource class:

def result_cache
  @result_cache ||= Blazer::ResultCache.new(self)
end

This makes it difficult to develop extensions for Blazer such as acting on query results.

Proposed Changes

  1. Allow ResultCache to be injected when initializing DataSource.
  2. Implement a mechanism (similar to ActiveSupport callbacks) to extend ResultCache functionality.
  3. Add a configuration option to specify a custom ResultCache class in the Blazer settings.

Use Case

My end goal is to create an extension for Blazer that records both query and result history. ResultCache seems like the most straightforward way to capture queries and results in transit.

Current Workaround

Currently using a non-invasive approach by setting Blazer.cache to a thin wrapper around Rails.cache. This wrapper sends ActiveSupport notifications (e.g., blazer.cache_write) that can be subscribed to. However, this method loses context, such as the original query, as it only provides access to the cache key and result.

Benefits

  1. Improved flexibility and extensibility of the caching mechanism.
  2. Ability to capture more context (e.g., original query) when caching results.
  3. Easier implementation of query and result history tracking.

Proposed Implementation

  1. Modify the DataSource class to accept a custom ResultCache instance, (showing both a DI example and a settings -> constantization example:
module Blazer
  class DataSource
    # ...

    def initialize(id, settings, result_cache = nil)
      @id = id
      @settings = settings
      @result_cache = result_cache
    end

    # ...

    def result_cache
      @result_cache ||= begin
        cache_class = settings["result_cache_class"]
        cache_class ? cache_class.constantize.new(self) : Blazer::ResultCache.new(self)
      end
    end

    # ...
  end
end
  1. Implement an extension mechanism for ResultCache, possibly using ActiveSupport::Callbacks:
module Blazer
  class ResultCache
    include ActiveSupport::Callbacks

    define_callbacks :cache_write, :cache_read, :cache_delete

    def initialize(data_source)
      @data_source = data_source
    end

    def read_statement(statement)
      run_callbacks :cache_read do
        # Existing read logic
      end
    end

    def write_statement(statement, result, options = {})
      run_callbacks :cache_write do
        # Existing write logic
      end
    end

    def delete_statement(statement)
      run_callbacks :cache_delete do
        # Existing delete logic
      end
    end

    # Implement similar changes for read_run, write_run, and delete_run methods
  end
end
  1. Implement a way for the user to specify a custom ResultCache

Setting a Custom ResultCache

This is the more complicated part from a dev experience perspective. If doing the settings based approach I outlined above, an implementation could be something like this, but curious to know what you think:

# config/blazer.yml
data_sources:
  main:
    url: <%= ENV["DATABASE_URL"] %>
    result_cache_class: "MyApp::CustomResultCache"

Thank you for all that you do! 🙏

ankane commented 1 month ago

Hi @gillisd, thanks for the suggestion and proposed implementation. However, I don't think it's common enough to add / support.