|
| 1 | +require 'spec_helper' |
| 2 | + |
| 3 | +class Download < ActiveRecord::Base |
| 4 | + include Timescaledb::ContinuousAggregatesHelper |
| 5 | + |
| 6 | + acts_as_hypertable time_column: 'ts' |
| 7 | + |
| 8 | + scope :total_downloads, -> { select("count(*) as total") } |
| 9 | + scope :downloads_by_gem, -> { select("gem_name, count(*) as total").group(:gem_name) } |
| 10 | + scope :downloads_by_version, -> { select("gem_name, gem_version, count(*) as total").group(:gem_name, :gem_version) } |
| 11 | + |
| 12 | + continuous_aggregates( |
| 13 | + time_column: 'ts', |
| 14 | + timeframes: [:minute, :hour, :day, :month], |
| 15 | + scopes: [:total_downloads, :downloads_by_gem, :downloads_by_version], |
| 16 | + refresh_policy: { |
| 17 | + minute: { start_offset: "10 minutes", end_offset: "1 minute", schedule_interval: "1 minute" }, |
| 18 | + hour: { start_offset: "4 hour", end_offset: "1 hour", schedule_interval: "1 hour" }, |
| 19 | + day: { start_offset: "3 day", end_offset: "1 day", schedule_interval: "1 hour" }, |
| 20 | + month: { start_offset: "3 month", end_offset: "1 hour", schedule_interval: "1 hour" } |
| 21 | + } |
| 22 | + ) |
| 23 | +end |
| 24 | + |
| 25 | +RSpec.describe Timescaledb::ContinuousAggregatesHelper do |
| 26 | + let(:test_class) do |
| 27 | + Download |
| 28 | + end |
| 29 | + |
| 30 | + before(:all) do |
| 31 | + ActiveRecord::Base.connection.instance_exec do |
| 32 | + hypertable_options = { |
| 33 | + time_column: 'ts', |
| 34 | + chunk_time_interval: '1 day', |
| 35 | + compress_segmentby: 'gem_name, gem_version', |
| 36 | + compress_orderby: 'ts DESC', |
| 37 | + } |
| 38 | + create_table(:downloads, id: false, hypertable: hypertable_options) do |t| |
| 39 | + t.timestamptz :ts, null: false |
| 40 | + t.text :gem_name, :gem_version, null: false |
| 41 | + t.jsonb :payload |
| 42 | + end |
| 43 | + end |
| 44 | + end |
| 45 | + |
| 46 | + after(:all) do |
| 47 | + ActiveRecord::Base.connection.drop_table :downloads, if_exists: true |
| 48 | + end |
| 49 | + |
| 50 | + describe '.continuous_aggregates' do |
| 51 | + it 'defines aggregate classes' do |
| 52 | + expect(test_class.const_defined?(:TotalDownloadsPerMinute)).to be true |
| 53 | + expect(test_class.const_defined?(:TotalDownloadsPerHour)).to be true |
| 54 | + expect(test_class.const_defined?(:TotalDownloadsPerDay)).to be true |
| 55 | + expect(test_class.const_defined?(:TotalDownloadsPerMonth)).to be true |
| 56 | + |
| 57 | + expect(test_class.const_defined?(:DownloadsByVersionPerMinute)).to be true |
| 58 | + expect(test_class.const_defined?(:DownloadsByVersionPerHour)).to be true |
| 59 | + expect(test_class.const_defined?(:DownloadsByVersionPerDay)).to be true |
| 60 | + expect(test_class.const_defined?(:DownloadsByVersionPerMonth)).to be true |
| 61 | + |
| 62 | + expect(test_class.const_defined?(:DownloadsByGemPerMinute)).to be true |
| 63 | + expect(test_class.const_defined?(:DownloadsByGemPerHour)).to be true |
| 64 | + expect(test_class.const_defined?(:DownloadsByGemPerDay)).to be true |
| 65 | + expect(test_class.const_defined?(:DownloadsByGemPerMonth)).to be true |
| 66 | + end |
| 67 | + |
| 68 | + it 'sets up correct table names for aggregates' do |
| 69 | + expect(test_class::TotalDownloadsPerMinute.table_name).to eq('total_downloads_per_minute') |
| 70 | + expect(test_class::TotalDownloadsPerHour.table_name).to eq('total_downloads_per_hour') |
| 71 | + expect(test_class::TotalDownloadsPerDay.table_name).to eq('total_downloads_per_day') |
| 72 | + expect(test_class::TotalDownloadsPerMonth.table_name).to eq('total_downloads_per_month') |
| 73 | + |
| 74 | + expect(test_class::DownloadsByVersionPerMinute.table_name).to eq('downloads_by_version_per_minute') |
| 75 | + expect(test_class::DownloadsByVersionPerHour.table_name).to eq('downloads_by_version_per_hour') |
| 76 | + expect(test_class::DownloadsByVersionPerDay.table_name).to eq('downloads_by_version_per_day') |
| 77 | + expect(test_class::DownloadsByVersionPerMonth.table_name).to eq('downloads_by_version_per_month') |
| 78 | + |
| 79 | + expect(test_class::DownloadsByGemPerMinute.table_name).to eq('downloads_by_gem_per_minute') |
| 80 | + expect(test_class::DownloadsByGemPerHour.table_name).to eq('downloads_by_gem_per_hour') |
| 81 | + expect(test_class::DownloadsByGemPerDay.table_name).to eq('downloads_by_gem_per_day') |
| 82 | + expect(test_class::DownloadsByGemPerMonth.table_name).to eq('downloads_by_gem_per_month') |
| 83 | + end |
| 84 | + |
| 85 | + it 'defines rollup scope for aggregates' do |
| 86 | + test_class.create_continuous_aggregates |
| 87 | + aggregate_classes = [test_class::TotalDownloadsPerMinute, test_class::TotalDownloadsPerHour, test_class::TotalDownloadsPerDay, test_class::TotalDownloadsPerMonth] |
| 88 | + aggregate_classes.each do |agg_class| |
| 89 | + expect(agg_class).to respond_to(:rollup) |
| 90 | + expect(agg_class.rollup.to_sql).to include('time_bucket') |
| 91 | + expect(agg_class.rollup.to_sql).to include('count(*) as total') |
| 92 | + end |
| 93 | + end |
| 94 | + |
| 95 | + it 'defines time-based scopes for aggregates' do |
| 96 | + aggregate_classes = [test_class::TotalDownloadsPerMinute, test_class::TotalDownloadsPerHour, test_class::TotalDownloadsPerDay, test_class::TotalDownloadsPerMonth] |
| 97 | + aggregate_scopes = [:total_downloads, :downloads_by_gem, :downloads_by_version] |
| 98 | + |
| 99 | + aggregate_scopes.each do |scope| |
| 100 | + aggregate_classes.each do |agg_class| |
| 101 | + expect(agg_class).to respond_to(scope) |
| 102 | + end |
| 103 | + end |
| 104 | + end |
| 105 | + end |
| 106 | + |
| 107 | + describe '.create_continuous_aggregates' do |
| 108 | + before do |
| 109 | + allow(ActiveRecord::Base.connection).to receive(:execute).and_call_original |
| 110 | + end |
| 111 | + |
| 112 | + it 'creates materialized views for each aggregate' do |
| 113 | + test_class.create_continuous_aggregates |
| 114 | + |
| 115 | + expect(ActiveRecord::Base.connection).to have_received(:execute).with(/CREATE MATERIALIZED VIEW.*downloads_total_downloads_per_minute/i) |
| 116 | + expect(ActiveRecord::Base.connection).to have_received(:execute).with(/CREATE MATERIALIZED VIEW.*downloads_total_downloads_per_hour/i) |
| 117 | + expect(ActiveRecord::Base.connection).to have_received(:execute).with(/CREATE MATERIALIZED VIEW.*downloads_total_downloads_per_day/i) |
| 118 | + expect(ActiveRecord::Base.connection).to have_received(:execute).with(/CREATE MATERIALIZED VIEW.*downloads_total_downloads_per_month/i) |
| 119 | + end |
| 120 | + |
| 121 | + it 'sets up refresh policies for each aggregate' do |
| 122 | + test_class.create_continuous_aggregates |
| 123 | + |
| 124 | + expect(ActiveRecord::Base.connection).to have_received(:execute).with(/add_continuous_aggregate_policy.*downloads_minutely/i) |
| 125 | + expect(ActiveRecord::Base.connection).to have_received(:execute).with(/add_continuous_aggregate_policy.*downloads_total_downloads_per_hour/i) |
| 126 | + expect(ActiveRecord::Base.connection).to have_received(:execute).with(/add_continuous_aggregate_policy.*downloads_total_downloads_per_day/i) |
| 127 | + expect(ActiveRecord::Base.connection).to have_received(:execute).with(/add_continuous_aggregate_policy.*downloads_total_downloads_per_month/i) |
| 128 | + end |
| 129 | + end |
| 130 | + |
| 131 | + describe 'refresh policies' do |
| 132 | + it 'defines appropriate refresh policies for each timeframe' do |
| 133 | + policies = { |
| 134 | + minute: { start_offset: "INTERVAL '10 minutes'", end_offset: "INTERVAL '1 minute'", schedule_interval: "INTERVAL '1 minute'" }, |
| 135 | + hour: { start_offset: "INTERVAL '4 hour'", end_offset: "INTERVAL '1 hour'", schedule_interval: "INTERVAL '1 hour'" }, |
| 136 | + day: { start_offset: "INTERVAL '3 day'", end_offset: "INTERVAL '1 day'", schedule_interval: "INTERVAL '1 day'" }, |
| 137 | + month: { start_offset: "INTERVAL '3 month'", end_offset: "INTERVAL '1 day'", schedule_interval: "INTERVAL '1 day'" } |
| 138 | + } |
| 139 | + |
| 140 | + policies.each do |timeframe, expected_policy| |
| 141 | + actual_policy = test_class.const_get(timeframe).refresh_policy |
| 142 | + expect(actual_policy).to eq(expected_policy) |
| 143 | + end |
| 144 | + end |
| 145 | + end |
| 146 | +end |
0 commit comments