diff --git a/.rspec b/.rspec deleted file mode 100644 index 8c18f1a..0000000 --- a/.rspec +++ /dev/null @@ -1,2 +0,0 @@ ---format documentation ---color diff --git a/.travis.yml b/.travis.yml index af3de38..38d0671 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,15 +20,41 @@ after_script: - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT addons: - postgresql: 9.3 + postgresql: 9.5 rvm: + - 2.6 - 2.5 - 2.4 - 2.3 gemfile: + - gemfiles/activerecord-6.0.Gemfile - gemfiles/activerecord-5.2.Gemfile - gemfiles/activerecord-5.1.Gemfile - gemfiles/activerecord-5.0.Gemfile - gemfiles/activerecord-4.2.Gemfile + +jobs: + include: + - gemfile: gemfiles/activerecord-edge.Gemfile + rvm: 2.5 + env: EDGE_TESTING=true + - gemfile: gemfiles/activerecord-edge.Gemfile + rvm: 2.6 + env: EDGE_TESTING=true + + allow_failures: + - gemfile: gemfiles/activerecord-edge.Gemfile + + exclude: + - rvm: 2.6 + gemfile: gemfiles/activerecord-4.2.Gemfile + - rvm: 2.3 + gemfile: gemfiles/activerecord-6.0.Gemfile + - rvm: 2.4 + gemfile: gemfiles/activerecord-6.0.Gemfile + - rvm: 2.3 + gemfile: gemfiles/activerecord-edge.Gemfile + - rvm: 2.4 + gemfile: gemfiles/activerecord-edge.Gemfile diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f94a63..d740e01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,22 @@ # DeletedAt +## 0.6.0 _(May 25, 2019)_ +- Major overhaul of functionality, extracted to [`ActiveRecord::Framing`](https://github.com/TwilightCoders/active_record-framing) and refined into gem + - Consumes `ActiveRecord::Framing` +- Simplified gem and distilled to only domain specific functionality +- Adds official support for ActiveSupport up to 5.2 +- Adds official support for Ruby 2.6 +- Unbound upper constraint for ActiveSupport +- 100% test coverage + ## 0.5.0 _(June 25, 2018)_ - Removed use of invasive views in preference of sub-selects - Dropped support for Ruby 2.0, 2.1, 2.2 -- Dropped support for Rails 4.1 +- Dropped support for ActiveSupport 4.1 - Default `deleted_at` options using `Proc` ## 0.4.0 _(Never Released)_ -- Specs for Rails 4.0-5.1 +- Specs for ActiveSupport 4.0-5.1 - Uses `combustion` gem for cleaner and more comprehensive testing - Added badges to ReadMe - Using `:prepend` to leverage ancestry chain diff --git a/Gemfile b/Gemfile index 61bfac7..fa75df1 100644 --- a/Gemfile +++ b/Gemfile @@ -1,16 +1,3 @@ source 'https://rubygems.org' gemspec - -group :test do - - # Generates coverage stats of specs - gem 'simplecov' - - gem 'rspec' - - gem 'database_cleaner' - - gem 'combustion' - -end diff --git a/LICENSE b/LICENSE index ad8b568..9e5927b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2017 Dale Stevens +Copyright (c) 2019 Dale Stevens Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index dd889f8..2a610f1 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,9 @@ Hide your "deleted" data (unless specifically asked for) [without resorting to]( ## Requirements - Ruby 2.3+ -- ActiveRecord 4.1+ +- ActiveRecord 4.2+ + +_Note: Be sure to check the builds to be sure your version is in-fact supported. The requirements are left unbounded on the upper constraint for posterity, but may not be gaurenteed to work._ ## Installation @@ -102,10 +104,9 @@ class IndexDeletedAtColumns < ActiveRecord::Migration end ``` - ## [Upgrading](#upgrading) -If you've used `deleted_at` prior to v0.5.0, you'll need to migrate your schema. The new version of `deleted_at` no longer uses views, instead constructing a subselect on the relations. This significantly reduces code polution and monkey patching, as well as reducing the runtime memory usage for rails. Your Database will look (and be) a lot cleaner with no `deleted_at` views and your ERDs will be much cleaner as well. +If you've used `deleted_at` prior to v0.5.0, you'll need to migrate your schema. The new version of `deleted_at` no longer uses views, instead constructing a common table expression (CTE) on the relations. This significantly reduces code polution and monkey patching, as well as reducing the runtime memory usage for rails. Your Database will look (and be) a lot cleaner with no `deleted_at` views (and your ERDs will be much cleaner as well). Here is an example of a migration for upgrading ```ruby diff --git a/deleted_at.gemspec b/deleted_at.gemspec index fb629dc..838bbf2 100644 --- a/deleted_at.gemspec +++ b/deleted_at.gemspec @@ -24,15 +24,19 @@ Gem::Specification.new do |spec| spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ['lib'] - rails_versions = ['>= 4.2', '< 6'] + rails_versions = ['>= 4.2', '< 6.1'] spec.required_ruby_version = '>= 2.3' spec.add_runtime_dependency 'activerecord', rails_versions - - spec.add_development_dependency 'pg', '~> 0' - spec.add_development_dependency 'pry-byebug', '~> 3' - spec.add_development_dependency 'bundler', '~> 1.3' - spec.add_development_dependency 'rake', '~> 12.0' - spec.add_development_dependency 'combustion', '~> 0.7' + spec.add_runtime_dependency 'active_record-framing', '~> 0.1.0-10' + + spec.add_development_dependency 'pg' + spec.add_development_dependency 'bundler' + spec.add_development_dependency 'rspec' + spec.add_development_dependency 'combustion' + spec.add_development_dependency 'pry-byebug' + spec.add_development_dependency 'rake' + spec.add_development_dependency 'database_cleaner' + spec.add_development_dependency 'simplecov' end diff --git a/gemfiles/activerecord-4.2.Gemfile b/gemfiles/activerecord-4.2.Gemfile index 0c94061..88c864d 100644 --- a/gemfiles/activerecord-4.2.Gemfile +++ b/gemfiles/activerecord-4.2.Gemfile @@ -1,3 +1,4 @@ eval_gemfile File.join(File.dirname(__FILE__), "../Gemfile") +gem 'pg', '~> 0.20' gem 'activerecord', '~> 4.2.0' diff --git a/gemfiles/activerecord-6.0.Gemfile b/gemfiles/activerecord-6.0.Gemfile new file mode 100644 index 0000000..f32ff01 --- /dev/null +++ b/gemfiles/activerecord-6.0.Gemfile @@ -0,0 +1,3 @@ +eval_gemfile File.join(File.dirname(__FILE__), "../Gemfile") + +gem 'activerecord', '~> 6.0.0' diff --git a/gemfiles/activerecord-edge.Gemfile b/gemfiles/activerecord-edge.Gemfile new file mode 100644 index 0000000..6930d58 --- /dev/null +++ b/gemfiles/activerecord-edge.Gemfile @@ -0,0 +1,3 @@ +eval_gemfile File.join(File.dirname(__FILE__), "../Gemfile") + +gem 'activerecord', github: 'rails/rails', require: 'active_record' diff --git a/lib/deleted_at.rb b/lib/deleted_at.rb index 10a8818..73f720b 100644 --- a/lib/deleted_at.rb +++ b/lib/deleted_at.rb @@ -1,9 +1,14 @@ +require 'active_record-framing' + require 'deleted_at/version' +require 'deleted_at/relation' +require 'deleted_at/core' +require 'deleted_at/table_definition' require 'deleted_at/railtie' if defined?(Rails::Railtie) module DeletedAt - MissingColumn = Class.new(StandardError) + MissingColumnError = Class.new(StandardError) DEFAULT_OPTIONS = { column: :deleted_at, diff --git a/lib/deleted_at/active_record.rb b/lib/deleted_at/active_record.rb index b9367ca..cd077e4 100644 --- a/lib/deleted_at/active_record.rb +++ b/lib/deleted_at/active_record.rb @@ -1,50 +1,73 @@ -require 'active_record' -require 'deleted_at/relation' - module DeletedAt module ActiveRecord def self.prepended(subclass) - subclass.init_deleted_at_relations subclass.extend(ClassMethods) + + subclass.class_eval do + init_deleted_at_relations + default_frame { where(deleted_at[:column] => nil) } + frame :all, -> {} + frame :deleted, -> { where.not(deleted_at[:column] => nil) } + end end def initialize(*args) - super - @destroyed = !deleted_at.nil? + super.tap do + @destroyed = deleted_at_nil? + end end def destroy - soft_delete - super + run_callbacks(:destroy) do + soft_delete + end end def delete soft_delete - super + end + + def destroy! + run_callbacks(:destroy) do + soft_delete + end + end + + def delete! + soft_delete end private def soft_delete + return if destroyed? update_columns(self.class.deleted_at_attributes) @destroyed = true + freeze + self + end + + def deleted_at_nil? + !read_attribute(self.class.deleted_at[:column]).nil? end module ClassMethods def inherited(subclass) super - subclass.init_deleted_at_relations + # subclass.init_deleted_at_relations if deleted_at[:inherit] + end + + def deleted_at_attributes + attributes = { + deleted_at[:column] => deleted_at[:proc].call + } end - def const_missing(const) - case const - when :All, :Deleted - all.tap do |_query| - _query.deleted_at_scope = const - end - else super + def init_deleted_at_relations + instance_variable_get(:@relation_delegate_cache).each do |base, klass| + klass.send(:prepend, DeletedAt::Relation) end end diff --git a/lib/deleted_at/core.rb b/lib/deleted_at/core.rb index 6ec5bf2..1bd3aab 100644 --- a/lib/deleted_at/core.rb +++ b/lib/deleted_at/core.rb @@ -1,5 +1,4 @@ require 'deleted_at/active_record' - module DeletedAt module Core @@ -15,39 +14,33 @@ class << subclass def self.raise_missing(klass) message = "Missing `#{klass.deleted_at[:column]}` in `#{klass.name}` when trying to employ `deleted_at`" - raise(DeletedAt::MissingColumn, message) + raise(DeletedAt::MissingColumnError, message) end def self.has_deleted_at_column?(klass) klass.columns.map(&:name).include?(klass.deleted_at.dig(:column).to_s) end + def self.deleted_at_ready?(klass) + !::DeletedAt.disabled? && + klass != ::ActiveRecord::Base && + !klass.abstract_class? && + klass.connected? && + klass.table_exists? && + !(klass < DeletedAt::ActiveRecord) + end + module ClassMethods def with_deleted_at(options={}, &block) self.deleted_at = DeletedAt::DEFAULT_OPTIONS.merge(options) self.deleted_at[:proc] = block if block_given? - return if ::DeletedAt.disabled? - + return unless Core.deleted_at_ready?(self) DeletedAt::Core.raise_missing(self) unless Core.has_deleted_at_column?(self) self.prepend(DeletedAt::ActiveRecord) - end - - def deleted_at_attributes - attributes = { - deleted_at[:column] => deleted_at[:proc].call - } - end - - def init_deleted_at_relations - instance_variable_get(:@relation_delegate_cache).each do |base, klass| - klass.send(:prepend, DeletedAt::Relation) - end - end - end # End ClassMethods end diff --git a/lib/deleted_at/legacy.rb b/lib/deleted_at/legacy.rb index 626f6fe..8c5045c 100644 --- a/lib/deleted_at/legacy.rb +++ b/lib/deleted_at/legacy.rb @@ -10,8 +10,10 @@ def self.uninstall(model) def self.install(model) return false unless Core.has_deleted_at_column?(model) - install_present_view(model) - install_deleted_view(model) + model.unframed do + install_present_view(model) + install_deleted_view(model) + end end private @@ -24,7 +26,7 @@ def self.install_present_view(model) model.connection.execute("ALTER TABLE \"#{present_table_name}\" RENAME TO \"#{model.table_name}\"") model.connection.execute <<-SQL CREATE OR REPLACE VIEW "#{present_table_name}" - AS #{ model.select('*').where(model.deleted_at_column => nil).to_sql } + AS #{ model.select('*').where(model.deleted_at[:column] => nil).to_sql } SQL end end @@ -36,7 +38,7 @@ def self.install_deleted_view(model) while_spoofing_table_name(model, all_table(model)) do model.connection.execute <<-SQL CREATE OR REPLACE VIEW "#{table_name}" - AS #{ model.select('*').where.not(model.deleted_at_column => nil).to_sql } + AS #{ model.select('*').where.not(model.deleted_at[:column] => nil).to_sql } SQL end end diff --git a/lib/deleted_at/railtie.rb b/lib/deleted_at/railtie.rb index acddc96..dbfb9aa 100644 --- a/lib/deleted_at/railtie.rb +++ b/lib/deleted_at/railtie.rb @@ -1,6 +1,4 @@ require 'rails/railtie' -require 'deleted_at/core' -require 'deleted_at/table_definition' module DeletedAt class Railtie < Rails::Railtie diff --git a/lib/deleted_at/relation.rb b/lib/deleted_at/relation.rb index a2f81ed..a20f8f2 100644 --- a/lib/deleted_at/relation.rb +++ b/lib/deleted_at/relation.rb @@ -1,52 +1,7 @@ module DeletedAt module Relation - - def self.prepended(subclass) - subclass.class_eval do - attr_writer :deleted_at_scope - end - end - - def deleted_at_scope - @deleted_at_scope ||= :Present - end - - def deleted_at_select - scoped_arel = case deleted_at_scope - when :Deleted - vanilla.dup.where(table[klass.deleted_at[:column]].not_eq(nil)) - when :Present - vanilla.dup.where(table[klass.deleted_at[:column]].eq(nil)) - end - end - - def vanilla - # @vanilla ||= klass.const_get(:All).unscope(:where).freeze - @vanilla ||= klass.unscoped.tap do |rel| - rel.deleted_at_scope = :All - end.freeze - end - - # Rails 4.x - def from_value - if (subselect = deleted_at_select) - [subselect, ::ActiveRecord::Base.connection.quote_table_name(table_name)] - else - super - end - end - - # Rails 5.x - def from_clause - if (subselect = deleted_at_select) - ::ActiveRecord::Relation::FromClause.new(subselect, ::ActiveRecord::Base.connection.quote_table_name(table_name)) - else - super - end - end - def delete_all(*args) - if args.pop + if args.any? ActiveSupport::Deprecation.warn(<<~STR) Passing conditions to delete_all is not supported in DeletedAt To achieve the same use where(conditions).delete_all. diff --git a/lib/deleted_at/version.rb b/lib/deleted_at/version.rb index c9fc23e..d2e1baa 100644 --- a/lib/deleted_at/version.rb +++ b/lib/deleted_at/version.rb @@ -1,3 +1,3 @@ module DeletedAt - VERSION = "0.5.0-2" + VERSION = "0.6.0-7" end diff --git a/spec/deleted_at/active_record_spec.rb b/spec/deleted_at/active_record_spec.rb deleted file mode 100644 index 9bb4bcf..0000000 --- a/spec/deleted_at/active_record_spec.rb +++ /dev/null @@ -1,9 +0,0 @@ -require "spec_helper" - -describe DeletedAt::ActiveRecord do - - it 'should let other missing consts through' do - expect{ Admin::Blarg }.to raise_error(NameError) - end - -end diff --git a/spec/deleted_at/core_spec.rb b/spec/deleted_at/core_spec.rb index 4ae3e6b..c909d7d 100644 --- a/spec/deleted_at/core_spec.rb +++ b/spec/deleted_at/core_spec.rb @@ -7,7 +7,7 @@ it "raises exception when using with_deleted_at" do expected_stderr = "Missing `deleted_at` in `Comment` when trying to employ `deleted_at`" allow(Comment).to receive(:has_deleted_at_views?).and_return(true) - expect{ Comment.with_deleted_at }.to raise_exception(DeletedAt::MissingColumn) + expect{ Comment.with_deleted_at }.to raise_exception(DeletedAt::MissingColumnError) end end @@ -25,11 +25,21 @@ User.create(name: 'john') User.create(name: 'sally') - User.first.destroy + u = User.first + u.destroy expect(User::Deleted.first.class).to eq(User) end + it "doesn't obstruct destroy callbacks" do + User.create(name: 'sally') + + u = User.first + expect_any_instance_of(User).to receive(:say_something) + + u.destroy + end + it 'works with complex eager loading' do bob = User.create(name: 'bob') (1..5).each do |i| @@ -42,19 +52,4 @@ expect(User.eager_load(posts: :comments).find_by(name: 'bob')).to eq(bob) end - context 'with default_scope' do - it 'should have the default scope in the subquery' do - Admin.create(name: 'bob', kind: 1) - Admin.create(name: 'john', kind: 1) - Admin.create(name: 'sally', kind: 0) - - Admin.first.destroy - - User.first - - # SELECT "users".* FROM (SELECT "users".* FROM "users" WHERE "users"."kind" = $1 AND "users"."deleted_at" IS NULL) "users" WHERE "users"."kind" = 1 - expect(Admin::Deleted.first.class).to eq(Admin) - end - end - end diff --git a/spec/deleted_at/relation_spec.rb b/spec/deleted_at/relation_spec.rb index 1f8022d..9e1093e 100644 --- a/spec/deleted_at/relation_spec.rb +++ b/spec/deleted_at/relation_spec.rb @@ -2,6 +2,18 @@ describe DeletedAt::Core do + context 'models with dependent: :destroy' do + it 'should also destroy dependents' do + user = User.create(name: 'bob') + + 4.times do + Post.create(user: user) + end + + expect{user.destroy}.to_not raise_exception + end + end + context "models using deleted_at" do it "#destroy should set deleted_at" do @@ -16,33 +28,54 @@ expect(User::Deleted.count).to eq(1) end - it "#delete should set deleted_at" do + it "#delete! should set deleted_at" do User.create(name: 'bob') User.create(name: 'john') User.create(name: 'sally') - User.first.delete + User.first.delete! expect(User.count).to eq(2) expect(User::All.count).to eq(3) expect(User::Deleted.count).to eq(1) end - context 'associations' do + it "#destroy! should set deleted_at" do + User.create(name: 'bob') + User.create(name: 'john') + User.create(name: 'sally') - it 'should scope properly' do + User.first.destroy! - user = User.create(name: 'bob') - (1..4).each do - Post.create(user: user) - end + expect(User.count).to eq(2) + expect(User::All.count).to eq(3) + expect(User::Deleted.count).to eq(1) + end - post = user.posts.first.delete + it "#delete should set deleted_at" do + User.create(name: 'bob') + User.create(name: 'john') + User.create(name: 'sally') - expect(user.posts.count).to eq(3) + User.first.delete - end + expect(User.count).to eq(2) + expect(User::All.count).to eq(3) + expect(User::Deleted.count).to eq(1) + end + + it "#destroy twice should set deleted_at and not fail" do + User.create(name: 'bob') + User.create(name: 'john') + User.create(name: 'sally') + u = User.first + u.destroy! + u.destroy! + + expect(User.count).to eq(2) + expect(User::All.count).to eq(3) + expect(User::Deleted.count).to eq(1) end context '#destroy_all' do @@ -65,6 +98,7 @@ User.where(name: 'bob').destroy_all + expect(User.count).to eq(2) expect(User::All.count).to eq(3) expect(User::Deleted.count).to eq(1) @@ -73,28 +107,28 @@ context '#delete_all' do it "should set deleted_at" do - Animals::Dog.create(name: 'bob') - Animals::Dog.create(name: 'john') - Animals::Dog.create(name: 'sally') + User.create(name: 'bob') + User.create(name: 'john') + User.create(name: 'sally') # conditions should not matter - Animals::Dog.all.delete_all(name: 'bob') + User.all.delete_all(name: 'bob') - expect(Animals::Dog.count).to eq(0) - expect(Animals::Dog::All.count).to eq(3) - expect(Animals::Dog::Deleted.count).to eq(3) + expect(User.count).to eq(0) + expect(User::All.count).to eq(3) + expect(User::Deleted.count).to eq(3) end it "with conditions should set deleted_at" do - Animals::Dog.create(name: 'bob') - Animals::Dog.create(name: 'john') - Animals::Dog.create(name: 'sally') + User.create(name: 'bob') + User.create(name: 'john') + User.create(name: 'sally') - Animals::Dog.where(name: 'bob').delete_all + User.where(name: 'bob').delete_all - expect(Animals::Dog.count).to eq(2) - expect(Animals::Dog::All.count).to eq(3) - expect(Animals::Dog::Deleted.count).to eq(1) + expect(User.count).to eq(2) + expect(User::All.count).to eq(3) + expect(User::Deleted.count).to eq(1) end end diff --git a/spec/support/rails/app/models/admin.rb b/spec/support/rails/app/models/admin.rb index 220699c..d005da8 100644 --- a/spec/support/rails/app/models/admin.rb +++ b/spec/support/rails/app/models/admin.rb @@ -1,5 +1,7 @@ class Admin < User + with_deleted_at + default_scope { # select(arel_table[Arel.star], arel_table[:tableoid])# select(arel_table[:id], arel_table[:kind]).where(kind: 1) diff --git a/spec/support/rails/app/models/user.rb b/spec/support/rails/app/models/user.rb index a2be2e7..0738380 100644 --- a/spec/support/rails/app/models/user.rb +++ b/spec/support/rails/app/models/user.rb @@ -1,7 +1,7 @@ class User < ::ActiveRecord::Base with_deleted_at - has_many :posts + has_many :posts, dependent: :destroy has_many :comments scope :admins, -> { @@ -9,4 +9,10 @@ class User < ::ActiveRecord::Base where(kind: 1) } + after_destroy :say_something + + def say_something + # Doesn't need to do anything + end + end diff --git a/spec/support/rails/config/database.yml b/spec/support/rails/config/database.yml index 9e828b4..1442568 100644 --- a/spec/support/rails/config/database.yml +++ b/spec/support/rails/config/database.yml @@ -1,4 +1,6 @@ test: adapter: postgresql + host: localhost encoding: unicode database: deleted_at-test + prepared_statements: false diff --git a/spec/support/rails/db/schema.rb b/spec/support/rails/db/schema.rb index f9f168d..c90ad5f 100644 --- a/spec/support/rails/db/schema.rb +++ b/spec/support/rails/db/schema.rb @@ -11,6 +11,7 @@ create_table :documents, force: true do |t| t.integer :user_id t.string :title + t.integer :scope t.timestamps null: false, deleted_at: true end @@ -21,15 +22,4 @@ t.timestamps null: false end - create_table 'animal/dogs', force: true do |t| - t.string :name - t.timestamps null: false, deleted_at: true - end - - create_table :cats, force: true do |t| - t.string :name - # t.string :type - t.timestamps null: false, deleted_at: true - end - end diff --git a/spec/support/rails/log/.gitignore b/spec/support/rails/log/.gitignore deleted file mode 100644 index bf0824e..0000000 --- a/spec/support/rails/log/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.log \ No newline at end of file diff --git a/spec/support/rails/public/favicon.ico b/spec/support/rails/public/favicon.ico deleted file mode 100644 index e69de29..0000000