PostgreSQL’s temporary tables

Let’s say you have a new ActiveRecord concern to add a new feature to your models. This concern extracts a database behavior. For simplicity, we are going to use a database query for tagging:

module TaggableConcern
  extend ActiveSupport::Concern

  included do
    scope :with_tag, ->(tag) {
      where('? = ANY(tags)', tag)
    }

    scope :with_tags, ->(tags) {
      where('tags @> ARRAY[?]::varchar[]', Array(tags))
    }
  end
end

How do you test concerns in isolation without including them in an application model?

PostgreSQL is packed full of wonderful features. One of those features is hidden in the CREATE TABLE synopsis:

CREATE TEMPORARY TABLE table_name (...)
ON COMMIT { PRESERVE ROWS | DELETE ROWS | DROP }

Using this in Rails is pretty straightforward because create_table accepts :temporary as boolean argument to mark the table as ephemeral and options parameter to add extra options for the table definition, like when to remove it:

create_table :my_table, temporary: true, options: 'ON COMMIT DROP' do |t|
  ...
end

Here is an example using this:

RSpec.describe TaggableConcern, type: :model do
  before do
    class TempBook < ApplicationRecord
      include TaggableConcern

      connection.create_table :temp_books, temporary: true, options: 'ON COMMIT DROP' do |t|
        t.string :name
        t.string :tags, array: true

        t.timestamps
      end
    end
  end

  let!(:ruby) do
    TempBook.create!(
      name: 'Programming Rails',
      tags: %w[ruby rails web]
    )
  end

  let!(:elixir) do
    TempBook.create!(
      name: 'Programming Phoenix',
      tags: %w[elixir phoenix web]
    )
  end

  let!(:go) do
    TempBook.create!(
      name: 'Programming Go',
      tags: %w[go programming]
    )
  end

  describe '.with_tag' do
    subject { TempBook.with_tag(query) }

    context 'with nil value' do
      let(:query) { nil }

      it { is_expected.to be_empty }
    end

    context 'with one match' do
      let(:query) { 'ruby' }

      it { is_expected.to match_array([ruby]) }
    end

    context 'with multiple matches' do
      let(:query) { 'web' }

      it { is_expected.to match_array([ruby, elixir]) }
    end
  end

  describe '.with_tags' do
    subject { TempBook.with_tags(query) }

    context 'with nil value' do
      let(:query) { nil }

      it { is_expected.to be_empty }
    end

    context 'with empty array' do
      let(:query) { [] }

      it { is_expected.to be_empty }
    end

    context 'with a common tag' do
      let(:query) { 'web' }

      it { is_expected.to match_array([ruby, elixir]) }
    end

    context 'with a common tag as array' do
      let(:query) { ['web'] }

      it { is_expected.to match_array([ruby, elixir]) }
    end

    context 'with mixed tags' do
      let(:query) { ['ruby', 'elixir'] }

      it { is_expected.to be_empty }
    end

    context 'with mixed tags' do
      let(:query) { ['ruby', 'web'] }

      it { is_expected.to match_array([ruby]) }
    end
  end
end
Finished in 0.1783 seconds (files took 0.59146 seconds to load)
9 examples, 0 failures