Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [Unreleased]

### Fixed

- Activity chart no longer crashes the dashboard on multi-database setups where Solid Queue runs on a different engine than the host app's primary database (e.g. a MySQL primary with a dedicated PostgreSQL queue database via `config.solid_queue.connects_to`). The SQL dialect for the time-bucketing query is now detected from the Solid Queue model's own connection instead of `ActiveRecord::Base`, so a `PG::UndefinedFunction: function unix_timestamp(...) does not exist` error is no longer raised. Single-database and same-engine setups are unaffected. (#42)

## [2.2.0] - 2026-06-24

### Added
Expand Down
17 changes: 11 additions & 6 deletions app/services/solid_queue_monitor/chart_data_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def build_buckets(start_time, bucket_seconds)
# This works identically on PostgreSQL and SQLite.
def bucket_counts(model, column, start_time, end_time, interval, exclude_nil: false)
start_epoch = start_time.to_i
expr = bucket_index_expr(column, start_epoch, interval)
expr = bucket_index_expr(model, column, start_epoch, interval)

scope = model.where(column => start_time..end_time)
scope = scope.where.not(column => nil) if exclude_nil
Expand All @@ -82,18 +82,23 @@ def fill_buckets(buckets, index_counts)
# PostgreSQL: CAST((EXTRACT(EPOCH FROM col) - start) / interval AS INTEGER)
# SQLite: CAST((CAST(strftime('%s', col) AS INTEGER) - start) / interval AS INTEGER)
# MySQL: CAST((UNIX_TIMESTAMP(col) - start) / interval AS SIGNED)
def bucket_index_expr(column, start_epoch, interval_seconds)
if adapter?('sqlite')
#
# The adapter is detected from the model's own connection, not
# ActiveRecord::Base's. Solid Queue is commonly hosted on a dedicated
# database (config.solid_queue.connects_to), which may use a different
# engine than the host app's primary connection.
def bucket_index_expr(model, column, start_epoch, interval_seconds)
if adapter?(model, 'sqlite')
"CAST((CAST(strftime('%s', #{column}) AS INTEGER) - #{start_epoch}) / #{interval_seconds} AS INTEGER)"
elsif adapter?('mysql') || adapter?('trilogy')
elsif adapter?(model, 'mysql') || adapter?(model, 'trilogy')
"FLOOR((UNIX_TIMESTAMP(#{column}) - #{start_epoch}) / #{interval_seconds})"
else
"FLOOR((EXTRACT(EPOCH FROM #{column}) - #{start_epoch}) / #{interval_seconds})::integer"
end
end

def adapter?(name)
ActiveRecord::Base.connection.adapter_name.downcase.include?(name)
def adapter?(model, name)
model.connection.adapter_name.downcase.include?(name)
end
end
end
47 changes: 47 additions & 0 deletions spec/services/solid_queue_monitor/chart_data_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,53 @@
end
end

describe 'database adapter detection (multi-database safety)' do
let(:service) { described_class.new }

# Builds the bucket-index SQL for a model whose connection reports the given
# adapter, independent of the test database engine.
def expr_for(adapter_name)
connection = instance_double(ActiveRecord::ConnectionAdapters::AbstractAdapter,
adapter_name: adapter_name)
model = class_double(SolidQueue::Job, connection: connection)
service.send(:bucket_index_expr, model, 'created_at', 1000, 3600)
end

it 'uses PostgreSQL EXTRACT(EPOCH ...) syntax for a Postgres connection' do
expect(expr_for('PostgreSQL'))
.to eq('FLOOR((EXTRACT(EPOCH FROM created_at) - 1000) / 3600)::integer')
end

it 'uses MySQL UNIX_TIMESTAMP syntax for a MySQL connection' do
expect(expr_for('Mysql2')).to include('UNIX_TIMESTAMP(created_at)')
end

it 'uses MySQL syntax for a Trilogy connection' do
expect(expr_for('Trilogy')).to include('UNIX_TIMESTAMP(created_at)')
end

it 'uses SQLite strftime syntax for a SQLite connection' do
expect(expr_for('SQLite')).to include("strftime('%s', created_at)")
end

context 'when Solid Queue runs on a different engine than the primary database' do
# Regression for issue #42: MySQL primary + dedicated PostgreSQL queue
# database (config.solid_queue.connects_to). The dialect must follow the
# Solid Queue model's connection, not ActiveRecord::Base's.
before do
primary = instance_double(ActiveRecord::ConnectionAdapters::AbstractAdapter,
adapter_name: 'Mysql2')
allow(ActiveRecord::Base).to receive(:connection).and_return(primary)
end

it 'builds Postgres SQL when the queue model is on Postgres' do
expr = expr_for('PostgreSQL')
expect(expr).to include('EXTRACT(EPOCH FROM created_at)')
expect(expr).not_to include('UNIX_TIMESTAMP')
end
end
end

describe 'constants' do
it 'defines all time ranges' do
expect(described_class::TIME_RANGES.keys).to eq(%w[15m 30m 1h 3h 6h 12h 1d 3d 1w])
Expand Down
Loading