Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support sqlite #17

Merged
merged 4 commits into from
Oct 27, 2023
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
14 changes: 13 additions & 1 deletion .github/workflows/rspec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,32 @@ jobs:
BUNDLE_JOBS: 4
BUNDLE_RETRY: 3
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
DATABASE_URL: postgres://postgres:postgres@localhost:5432
POSTGRES_URL: postgres://postgres:postgres@localhost:5432
DB: ${{ matrix.db }}
DB_NAME: slotted_counters_test
CI: true
strategy:
fail-fast: false
matrix:
ruby: ["3.0"]
gemfile: ["gemfiles/rails7.gemfile"]
db: ["sqlite"]
include:
- ruby: "2.7"
gemfile: "gemfiles/rails6.gemfile"
db: "postgres"
- ruby: "3.1"
gemfile: "gemfiles/railsmaster.gemfile"
db: "postgres"
- ruby: "3.0"
gemfile: "gemfiles/rails7.gemfile"
db: "postgres"
- ruby: "2.7"
gemfile: "gemfiles/rails6.gemfile"
db: "sqlite"
- ruby: "3.1"
gemfile: "gemfiles/railsmaster.gemfile"
db: "sqlite"
services:
postgres:
image: postgres:14
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## master

- Add SQLite support [#17](https://github.com/evilmartians/activerecord-slotted_counters/pull/17) ([@prog-supdex][])

## 0.1.4 (2023-04-19)

- Fix "can't modify frozen String" for the pg adapter (ruby 2.7) [#15](https://github.com/evilmartians/activerecord-slotted_counters/pull/15) ([@LukinEgor][])
Expand All @@ -25,3 +27,4 @@
[@palkan]: https://github.com/palkan
[@LukinEgor]: https://github.com/LukinEgor
[@danielwestendorf]: https://github.com/danielwestendorf
[@prog-supdex]: https://github.com/prog-supdex
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Using `counter_cache: true` on `belongs_to` associations also works as expected.

## Limitations / TODO

- Gem supports only PostgreSQL for Rails 6
- Gem supports only PostgreSQL and SQLite3 for Rails 6

## Contributing

Expand Down
7 changes: 5 additions & 2 deletions lib/activerecord_slotted_counters/adapters/pg_upsert.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ def initialize(klass)
@klass = klass
end

def apply?
ActiveRecord::VERSION::MAJOR < 7 && klass.connection.adapter_name == ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::ADAPTER_NAME
def apply?(current_adapter_name)
return false if ActiveRecord::VERSION::MAJOR >= 7
return false unless defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)

current_adapter_name == ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::ADAPTER_NAME
end

def bulk_insert(attributes, on_duplicate: nil, unique_by: nil)
Expand Down
2 changes: 1 addition & 1 deletion lib/activerecord_slotted_counters/adapters/rails_upsert.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def initialize(klass)
@klass = klass
end

def apply?
def apply?(_)
ActiveRecord::VERSION::MAJOR >= 7
end

Expand Down
103 changes: 103 additions & 0 deletions lib/activerecord_slotted_counters/adapters/sqlite_upsert.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# frozen_string_literal: true

module ActiveRecordSlottedCounters
module Adapters
class SqliteUpsert
attr_reader :klass

def initialize(klass)
@klass = klass
end

def apply?(current_adapter_name)
return false if ActiveRecord::VERSION::MAJOR >= 7
return false unless defined?(ActiveRecord::ConnectionAdapters::SQLite3Adapter)

current_adapter_name == ActiveRecord::ConnectionAdapters::SQLite3Adapter::ADAPTER_NAME
end

def bulk_insert(attributes, on_duplicate: nil, unique_by: nil)
raise ArgumentError, "Values must not be empty" if attributes.empty?

keys = attributes.first.keys + klass.all_timestamp_attributes_in_model

current_time = klass.current_time_from_proper_timezone
data = attributes.map { |attr| attr.values + [current_time, current_time] }

columns = columns_for_attributes(keys)

fields_str = quote_column_names(columns)
values_str = quote_many_records(columns, data)

sql = <<~SQL
INSERT INTO #{klass.quoted_table_name}
(#{fields_str})
VALUES #{values_str}
SQL

if unique_by.present?
index = unique_indexes.find { |i| i.name.to_sym == unique_by }
columns = columns_for_attributes(index.columns)
fields = quote_column_names(columns)

sql += " ON CONFLICT (#{fields})"
end

if on_duplicate.present?
sql += " DO UPDATE SET #{on_duplicate}"
end

sql += " RETURNING \"id\""

klass.connection.exec_query(sql)
end

private

def unique_indexes
klass.connection.schema_cache.indexes(klass.table_name).select(&:unique)
end

def columns_for_attributes(attributes)
attributes.map do |attribute|
klass.column_for_attribute(attribute)
end
end

def quote_column_names(columns, table_name: false)
columns.map do |column|
column_name = klass.connection.quote_column_name(column.name)
if table_name
"#{klass.quoted_table_name}.#{column_name}"
else
column_name
end
end.join(",")
end

def quote_record(columns, record_values)
values_str = record_values.each_with_index.map do |value, i|
type = klass.connection.lookup_cast_type_from_column(columns[i])
klass.connection.quote(type.serialize(value))
end.join(",")
"(#{values_str})"
end

def quote_many_records(columns, data)
data.map { |values| quote_record(columns, values) }.join(",")
end

def postgresql_connection?(adapter_name)
return false unless defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)

adapter_name == ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::ADAPTER_NAME
end

def sqlite_connection?(adapter_name)
return false unless defined?(ActiveRecord::ConnectionAdapters::SQLite3Adapter)

adapter_name == ActiveRecord::ConnectionAdapters::SQLite3Adapter::ADAPTER_NAME
end
end
end
end
12 changes: 8 additions & 4 deletions lib/activerecord_slotted_counters/has_slotted_counter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

require "activerecord_slotted_counters/adapters/rails_upsert"
require "activerecord_slotted_counters/adapters/pg_upsert"
require "activerecord_slotted_counters/adapters/sqlite_upsert"

module ActiveRecordSlottedCounters
class SlottedCounter < ::ActiveRecord::Base
Expand Down Expand Up @@ -41,15 +42,18 @@ def slotted_counter_db_adapter

def set_slotted_counter_db_adapter
available_adapters = [
ActiveRecordSlottedCounters::Adapters::RailsUpsert,
ActiveRecordSlottedCounters::Adapters::PgUpsert
ActiveRecordSlottedCounters::Adapters::SqliteUpsert,
ActiveRecordSlottedCounters::Adapters::PgUpsert,
ActiveRecordSlottedCounters::Adapters::RailsUpsert
]

current_adapter_name = connection.adapter_name

adapter = available_adapters
.map { |adapter| adapter.new(self) }
.detect { |adapter| adapter.apply? }
.detect { |adapter| adapter.apply?(current_adapter_name) }

raise NotSupportedAdapter.new(connection.adapter_name) if adapter.nil?
raise NotSupportedAdapter.new(current_adapter_name) if adapter.nil?

adapter
end
Expand Down
11 changes: 9 additions & 2 deletions spec/slotted_counter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,18 @@
def insert_association_sql(association_class, article_id)
association_table = association_class.arel_table
foreign_key = association_class.reflections["article"].foreign_key
current_date_sql_command =
if defined?(ActiveRecord::ConnectionAdapters::SQLite3Adapter)
"date('now')"
else
"now()"
end

insert_manager = Arel::InsertManager.new
insert_manager.insert([
[association_table[foreign_key], article_id],
[association_table[:created_at], Arel.sql("now()")],
[association_table[:updated_at], Arel.sql("now()")]
[association_table[:created_at], Arel.sql(current_date_sql_command)],
[association_table[:updated_at], Arel.sql(current_date_sql_command)]
])

insert_manager.to_sql
Expand Down
35 changes: 20 additions & 15 deletions spec/support/active_record_init.rb
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
# frozen_string_literal: true

connection_params =
if ENV.key?("DATABASE_URL")
{"url" => ENV["DATABASE_URL"]}
else
{
"host" => ENV.fetch("DB_HOST", "localhost"),
"username" => ENV.fetch("DB_USER", "postgres"),
"port" => ENV.fetch("DB_PORT", "9339").to_i
}
DB_CONFIG =
if ENV["DB"] == "postgres"
require "active_record/database_configurations"
url = ENV.fetch("POSTGRES_URL")

config = ActiveRecord::DatabaseConfigurations::UrlConfig.new(
"test",
"primary",
url,
{"database" => ENV.fetch("DB_NAME", "slotted_counters_test")}
)
config.respond_to?(:configuration_hash) ? config.configuration_hash : config.config
elsif ENV["DB"] == "sqlite"
# Make sure we don't have a DATABASE_URL set (it can be used by libs, e.g., database_cleaner)
ENV.delete("DATABASE_URL") if ENV["DATABASE_URL"]

{adapter: "sqlite3", database: ":memory:"}
end

ActiveRecord::Base.establish_connection(
{
"adapter" => "postgresql",
"database" => "slotted_counters_test"
}.merge(connection_params)
)
$stdout.puts "⚙️ Using #{DB_CONFIG[:adapter]} adapter for a database"

ActiveRecord::Base.establish_connection(**DB_CONFIG)

ActiveRecord::Schema.define do
create_table "views", force: :cascade do |t|
Expand Down
11 changes: 9 additions & 2 deletions spec/support/shared_examples_for_cache_counters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,18 @@
def insert_comment_sql(comment_class, article_id)
comment_table = comment_class.arel_table
foreign_key = comment_class.reflections["article"].foreign_key
current_date_sql_command =
if defined?(ActiveRecord::ConnectionAdapters::SQLite3Adapter)
"date('now')"
else
"now()"
end

insert_manager = Arel::InsertManager.new
insert_manager.insert([
[comment_table[foreign_key], article_id],
[comment_table[:created_at], Arel.sql("now()")],
[comment_table[:updated_at], Arel.sql("now()")]
[comment_table[:created_at], Arel.sql(current_date_sql_command)],
[comment_table[:updated_at], Arel.sql(current_date_sql_command)]
])

insert_manager.to_sql
Expand Down
Loading