Skip to content

Commit

Permalink
Optimize .mark_as_read! and extend to work on relations
Browse files Browse the repository at this point in the history
  • Loading branch information
fatkodima committed Jan 24, 2018
1 parent e3e9626 commit a75caf8
Show file tree
Hide file tree
Showing 14 changed files with 142 additions and 48 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
*.gem
*.log
.bundle
Gemfile.lock
gemfiles/*.lock
Expand Down
13 changes: 13 additions & 0 deletions lib/generators/unread/generator_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module Unread
module Generators
module GeneratorHelper
def next_migration_number(dirname)
if ActiveRecord::Base.timestamped_migrations
Time.now.utc.strftime("%Y%m%d%H%M%S")
else
"%.3d" % (current_migration_number(dirname) + 1)
end
end
end
end
end
10 changes: 2 additions & 8 deletions lib/generators/unread/migration/migration_generator.rb
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
require 'rails/generators'
require 'rails/generators/migration'
require 'generators/unread/generator_helper'

module Unread
class MigrationGenerator < Rails::Generators::Base
include Rails::Generators::Migration
extend Unread::Generators::GeneratorHelper

desc "Generates migration for read_markers"
source_root File.expand_path('../templates', __FILE__)

def create_migration_file
migration_template 'migration.rb', 'db/migrate/unread_migration.rb'
end

def self.next_migration_number(dirname)
if ActiveRecord::Base.timestamped_migrations
Time.now.utc.strftime("%Y%m%d%H%M%S")
else
"%.3d" % (current_migration_number(dirname) + 1)
end
end
end
end
21 changes: 14 additions & 7 deletions lib/generators/unread/migration/templates/migration.rb
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
require 'generators/unread/migration_helper'

class UnreadMigration < Unread::MIGRATION_BASE_CLASS
extend Unread::Generators::MigrationHelper

def self.up
create_table ReadMark, force: true, options: create_options do |t|
t.references :readable, polymorphic: { null: false }
t.references :reader, polymorphic: { null: false }
t.datetime :timestamp
end

add_index ReadMark, [:reader_id, :reader_type, :readable_type, :readable_id], name: 'read_marks_reader_readable_index', unique: true
index_name = 'read_marks_reader_readable_index'
add_index ReadMark, [:reader_id, :reader_type, :readable_type, :readable_id], name: index_name, unique: true

if postgresql_9_5?
execute <<-SQL
ALTER TABLE #{ReadMark.table_name}
ADD CONSTRAINT read_marks_reader_readable_constraint UNIQUE USING INDEX #{index_name}
SQL
end
end

def self.down
drop_table ReadMark
end

def self.create_options
options = ''
if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter) \
&& ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
options = 'DEFAULT CHARSET=latin1'
end
options
mysql? ? 'DEFAULT CHARSET=latin1' : ''
end
end
16 changes: 16 additions & 0 deletions lib/generators/unread/migration_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module Unread
module Generators
module MigrationHelper
def mysql?
defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter) \
&& ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
end

def postgresql_9_5?
defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) \
&& ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) \
&& ActiveRecord::Base.connection.send(:postgresql_version) >= 90500
end
end
end
end
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
require 'rails/generators'
require 'rails/generators/migration'
require 'generators/unread/generator_helper'

module Unread
class PolymorphicReaderMigrationGenerator < Rails::Generators::Base
include Rails::Generators::Migration
extend Unread::Generators::GeneratorHelper

desc "Generates update migration to make reader of read_markers polymorphic"
source_root File.expand_path('../templates', __FILE__)

def create_migration_file
migration_template 'unread_polymorphic_reader_migration.rb', 'db/migrate/unread_polymorphic_reader_migration.rb'
end

def self.next_migration_number(dirname)
if ActiveRecord::Base.timestamped_migrations
Time.now.utc.strftime("%Y%m%d%H%M%S")
else
"%.3d" % (current_migration_number(dirname) + 1)
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
require 'rails/generators'
require 'rails/generators/migration'
require 'generators/unread/generator_helper'

module Unread
class PostgresqlUniqueConstraintMigrationGenerator < Rails::Generators::Base
include Rails::Generators::Migration
extend Unread::Generators::GeneratorHelper

desc "Generates update migration to add unique constraint for PostgresSQL database"
source_root File.expand_path('../templates', __FILE__)

def create_migration_file
migration_template 'unread_postgresql_unique_constraint_migration.rb', 'db/migrate/unread_postgresql_unique_constraint_migration.rb'
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
require 'generators/unread/migration_helper'

class UnreadPostgresqlUniqueConstraintMigration < Unread::MIGRATION_BASE_CLASS
extend Unread::Generators::MigrationHelper

INDEX_NAME = 'read_marks_reader_readable_index'
INDEX_COLUMNS = [:reader_id, :reader_type, :readable_type, :readable_id]

def self.up
if postgresql_9_5?
unless index_exists?(ReadMark, INDEX_COLUMNS, unique: true)
add_index ReadMark, INDEX_COLUMNS, name: INDEX_NAME, unique: true
end

execute <<-SQL
ALTER TABLE #{ReadMark.table_name}
ADD CONSTRAINT read_marks_reader_readable_constraint UNIQUE USING INDEX #{INDEX_NAME}
SQL
end
end

def self.down
if postgresql_9_5?
execute <<-SQL
ALTER TABLE #{ReadMark.table_name}
DROP CONSTRAINT IF EXISTS read_marks_reader_readable_constraint
SQL
add_index ReadMark, INDEX_COLUMNS, name: INDEX_NAME, unique: true
end
end
end
3 changes: 3 additions & 0 deletions lib/unread.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'active_record'
require 'upsert'

require 'unread/base'
require 'unread/read_mark'
Expand All @@ -7,9 +8,11 @@
require 'unread/readable_scopes'
require 'unread/reader_scopes'
require 'unread/garbage_collector'
require 'unread/active_record_relation'
require 'unread/version'

ActiveRecord::Base.send :include, Unread
ActiveRecord::Relation.send :include, ActiveRecordRelation::Unread

Unread::MIGRATION_BASE_CLASS = if ActiveRecord::VERSION::MAJOR >= 5
ActiveRecord::Migration[5.0]
Expand Down
7 changes: 7 additions & 0 deletions lib/unread/active_record_relation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module ActiveRecordRelation
module Unread
def mark_as_read!(options)
klass.mark_as_read!(self, options)
end
end
end
53 changes: 32 additions & 21 deletions lib/unread/readable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,46 @@ def mark_as_read!(target, options)

if target == :all
reset_read_marks_for_user(reader)
elsif target.respond_to?(:each)
mark_collection_as_read(target, reader)
elsif target.is_a?(ActiveRecord::Relation)
mark_relation_as_read(target, reader)
else
raise ArgumentError
mark_collection_as_read(target, reader)
end
end

def mark_relation_as_read(relation, reader)
raise ArgumentError unless relation.klass == self

ReadMark.transaction do
global_timestamp = reader.read_mark_global(self).try(:timestamp)
on = readable_options[:on]

if global_timestamp
relation = relation.where("#{on} > ?", global_timestamp)
end

Upsert.batch(connection, ReadMark.table_name) do |upsert|
relation.pluck(relation.klass.primary_key, on).each do |readable_id, timestamp|
upsert.row({ readable_id: readable_id, readable_type: readable_parent.name, reader_id: reader.id, reader_type: reader.class.name }, timestamp: timestamp)
end
end
true
end
end

def mark_collection_as_read(collection, reader)
ReadMark.transaction do
global_timestamp = reader.read_mark_global(self).try(:timestamp)

collection.each do |obj|
raise ArgumentError unless obj.is_a?(self)
timestamp = obj.send(readable_options[:on])

if global_timestamp && global_timestamp >= timestamp
# The object is implicitly marked as read, so there is nothing to do
else
# This transaction is needed, so that parent transaction won't rollback even there's an error.
ReadMark.transaction(requires_new: true) do
begin
rm = obj.read_marks.where(reader_id: reader.id, reader_type: reader.class.base_class.name).first || obj.read_marks.build
rm.reader_id = reader.id
rm.reader_type = reader.class.base_class.name
rm.timestamp = timestamp
rm.save!
rescue ActiveRecord::RecordNotUnique
raise ActiveRecord::Rollback
end
Upsert.batch(connection, ReadMark.table_name) do |upsert|
Array(collection).each do |obj|
raise ArgumentError unless obj.is_a?(self)
timestamp = obj.send(readable_options[:on])

if global_timestamp && global_timestamp >= timestamp
# The object is implicitly marked as read, so there is nothing to do
else
upsert.row({ readable_id: obj.id, readable_type: readable_parent.name, reader_id: reader.id, reader_type: reader.class.name }, timestamp: timestamp)
end
end
end
Expand Down
2 changes: 2 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ def setup_db

ActiveRecord::Base.establish_connection(db_name.to_sym)
ActiveRecord::Base.default_timezone = :utc
ActiveRecord::Base.logger = Logger.new(File.join(__dir__, "debug.log"))
ActiveRecord::Base.logger.level = ENV["CI"] ? ::Logger::ERROR : ::Logger::DEBUG
ActiveRecord::Migration.verbose = false

UnreadMigration.up
Expand Down
3 changes: 0 additions & 3 deletions spec/unread/readable_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,6 @@
it "should mark the rest as read when the first record is not unique" do
Email.mark_as_read! [ @email1 ], for: @reader

allow(@email1).to receive_message_chain("read_marks.build").and_return(@email1.read_marks.build)
allow(@email1).to receive_message_chain("read_marks.where").and_return([])

expect do
Email.mark_as_read! [ @email1, @email2 ], for: @reader
end.to change(ReadMark, :count).by(1)
Expand Down
3 changes: 2 additions & 1 deletion unread.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ Gem::Specification.new do |s|
s.require_paths = ["lib"]

s.add_dependency 'activerecord', '>= 3'
s.add_dependency 'upsert', '~> 2.2.1'

s.add_development_dependency 'rake'
s.add_development_dependency 'timecop'
s.add_development_dependency 'sqlite3'
s.add_development_dependency 'mysql2'
s.add_development_dependency 'pg'
s.add_development_dependency 'pg', '< 1'
s.add_development_dependency 'rspec'
s.add_development_dependency 'simplecov'
s.add_development_dependency 'term-ansicolor'
Expand Down

0 comments on commit a75caf8

Please sign in to comment.