deleted_at 0.4.0rc1 → 0.5.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/{spec/support/rails/public/favicon.ico → CHANGELOG.md} +0 -0
  3. data/{LICENSE.txt → LICENSE} +0 -0
  4. data/README.md +29 -15
  5. data/lib/deleted_at.rb +29 -29
  6. data/lib/deleted_at/active_record.rb +37 -0
  7. data/lib/deleted_at/core.rb +51 -0
  8. data/lib/deleted_at/{views.rb → legacy.rb} +23 -14
  9. data/lib/deleted_at/railtie.rb +5 -4
  10. data/lib/deleted_at/relation.rb +72 -0
  11. data/lib/deleted_at/table_definition.rb +10 -0
  12. data/lib/deleted_at/version.rb +1 -1
  13. metadata +30 -67
  14. data/.gitignore +0 -78
  15. data/.rspec +0 -2
  16. data/.travis.yml +0 -43
  17. data/CODE_OF_CONDUCT.md +0 -74
  18. data/Gemfile +0 -19
  19. data/Rakefile +0 -6
  20. data/deleted_at.gemspec +0 -42
  21. data/gemfiles/activerecord-4.0.Gemfile +0 -3
  22. data/gemfiles/activerecord-4.1.Gemfile +0 -3
  23. data/gemfiles/activerecord-4.2.Gemfile +0 -3
  24. data/gemfiles/activerecord-5.0.Gemfile +0 -3
  25. data/gemfiles/activerecord-5.1.Gemfile +0 -3
  26. data/lib/deleted_at/active_record/base.rb +0 -102
  27. data/lib/deleted_at/active_record/connection_adapters/abstract/schema_definition.rb +0 -17
  28. data/lib/deleted_at/active_record/relation.rb +0 -43
  29. data/spec/deleted_at/active_record/base_spec.rb +0 -21
  30. data/spec/deleted_at/active_record/relation_spec.rb +0 -166
  31. data/spec/deleted_at/views_spec.rb +0 -76
  32. data/spec/spec_helper.rb +0 -28
  33. data/spec/support/rails/app/models/animals/dog.rb +0 -5
  34. data/spec/support/rails/app/models/comment.rb +0 -6
  35. data/spec/support/rails/app/models/post.rb +0 -7
  36. data/spec/support/rails/app/models/user.rb +0 -7
  37. data/spec/support/rails/config/database.yml +0 -4
  38. data/spec/support/rails/config/routes.rb +0 -3
  39. data/spec/support/rails/db/schema.rb +0 -27
  40. data/spec/support/rails/log/.gitignore +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c164168eb496804835ab5f48676d72a12fb549dccb649b0249355403db9b5b7e
4
- data.tar.gz: 6e684b4ef6a4146f40b6a0684b5aa18df975894df8da62ec62907d34bece3ec9
3
+ metadata.gz: df0de7db0fb5c4de89e941d080ac07fc08265ae29c2ae550e9e11b1057699763
4
+ data.tar.gz: e79f565b01952c39277e5c49f100441c3b19cfd6245056192b8261c4bd33541c
5
5
  SHA512:
6
- metadata.gz: f16daa013ef9ba2c06a42dea4ed2cd1d3e4008d419daa987aa01224408c1a11dd2d38ad93c5685119acfc28b46db4de240ff66ab93a629a7feec2dc632ddf905
7
- data.tar.gz: 916c6f5c3e8cf4ebabcb628b23e4015fb16f3c83eb57ef5c393bdfafdb16973149dbedafcf3cfba593bd68f7da6f6dd39200c527ce3c9555fec88d303081bcd2
6
+ metadata.gz: 0a18356dbbecaa30e77709349b85797ca3a50edadfde5474f62a3db841b52c28a14285ca4498318d17f4a3bf5b5b1e246936d69087b218e68592e6d99a0affe0
7
+ data.tar.gz: e16f8f3c9b010aa85a0922fe2673d14866eb0451a14988edc1bd6be3efe6d83e75993562046c1ea93ec2b188923d0b4e5596031fadd4439ac989369440a838d9
File without changes
data/README.md CHANGED
@@ -1,17 +1,17 @@
1
1
  [![Version ](https://img.shields.io/gem/v/deleted_at.svg?maxAge=2592000)](https://rubygems.org/gems/deleted_at)
2
2
  [![Build Status ](https://travis-ci.org/TwilightCoders/deleted_at.svg)](https://travis-ci.org/TwilightCoders/deleted_at)
3
- [![Code Climate ](https://api.codeclimate.com/v1/badges/762cdcd63990efa768b0/maintainability)](https://codeclimate.com/github/TwilightCoders/deleted_at)
3
+ [![Code Climate ](https://api.codeclimate.com/v1/badges/762cdcd63990efa768b0/maintainability)](https://codeclimate.com/github/TwilightCoders/deleted_at/maintainability)
4
4
  [![Test Coverage](https://codeclimate.com/github/TwilightCoders/deleted_at/badges/coverage.svg)](https://codeclimate.com/github/TwilightCoders/deleted_at/coverage)
5
+ [![Dependencies ](https://gemnasium.com/badges/github.com/TwilightCoders/deleted_at.svg)](https://gemnasium.com/github.com/TwilightCoders/deleted_at)
5
6
 
6
7
  # DeletedAt
7
8
 
8
- Deleting data is never good. A common solution is to use `default_scope`, but conventional wisdom (and for good reason) deams this a bad practice. So how do we achieve the same effect with minimal intervention. What we're looking for is the cliche "clean" solution.
9
-
10
- DeletedAt leverages the power of SQL views to achieve the same effect. It also takes advantage of Ruby's flexibility.
9
+ Hide your "deleted" data (unless specifically asked for) without resorting to `default_scope` by leveraging in-line sub-selects.
11
10
 
12
11
  ## Requirements
13
12
 
14
- `DeletedAt` requires PostgreSQL 9.1+ and Ruby 2.0.0+ (as the `pg` gem requires Ruby 2.0.0).
13
+ - Ruby 2.3+
14
+ - ActiveRecord 4.2+
15
15
 
16
16
  ## Installation
17
17
 
@@ -31,45 +31,59 @@ Or install it yourself as:
31
31
 
32
32
  ## Usage
33
33
 
34
- Using `DeletedAt` is very simple. It follows a familiar pattern seen throughout the rest of the Ruby/Rails community.
34
+ Invoking `with_deleted_at` sets the class up to use the `deleted_at` functionality.
35
35
 
36
36
  ```ruby
37
37
  class User < ActiveRecord::Base
38
- # Feel free to include/extend other modules before or after, as you see fit...
39
-
40
38
  with_deleted_at
41
39
 
42
40
  # the rest of your model code...
43
41
  end
44
42
  ```
45
43
 
46
- You'll (probably) need to migrate your database for `deleted_at` to work properly.
44
+ To work properly, the tables that back these models must have a `deleted_at` timestamp column.
47
45
 
48
46
  ```ruby
49
47
  class AddDeletedAtColumnToUsers < ActiveRecord::Migration
50
48
 
51
49
  def up
52
50
  add_column :users, :deleted_at, 'timestamp with time zone'
53
-
54
- DeletedAt.install(User)
55
51
  end
56
52
 
57
53
  def down
58
- DeletedAt.uninstall(User)
59
-
60
54
  remove_column :users, :deleted_at, 'timestamp with time zone'
61
55
  end
62
56
 
63
57
  end
64
58
  ```
65
59
 
60
+ If you're starting with a brand-new table, the existing `timestamps` DSL has been extended to accept `deleted_at: true` as an option, for convenience. Or you can do it seperately as shown above.
61
+
62
+ ```ruby
63
+ class CreatCommentsTable < ActiveRecord::Migration
64
+
65
+ def up
66
+ create_table :comments do |t|
67
+ # ...
68
+ # to the `timestamps` DSL
69
+ t.timestamps null: false, deleted_at: true
70
+ end
71
+ end
72
+
73
+ def down
74
+ drop_table :comments
75
+ end
76
+
77
+ end
78
+ ```
79
+
66
80
  ## Development
67
81
 
68
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
82
+ After checking out the repo, run `bundle` to install dependencies. Then, run `bundle exec rspec` to run the tests.
69
83
 
70
84
  ## Contributing
71
85
 
72
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/deleted_at. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
86
+ Bug reports and pull requests are welcome on GitHub at https://github.com/TwilightCoders/deleted_at. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
73
87
 
74
88
  ## License
75
89
 
@@ -1,15 +1,18 @@
1
- require "deleted_at/version"
2
- require 'deleted_at/views'
3
- require 'deleted_at/active_record/base'
4
- require 'deleted_at/active_record/relation'
5
- require 'deleted_at/active_record/connection_adapters/abstract/schema_definition'
6
-
1
+ require 'deleted_at/version'
7
2
  require 'deleted_at/railtie' if defined?(Rails::Railtie)
8
3
 
9
4
  module DeletedAt
10
5
 
6
+ MissingColumn = Class.new(StandardError)
7
+
8
+ DEFAULT_OPTIONS = {
9
+ column: :deleted_at,
10
+ proc: -> { Time.now.utc }
11
+ }
12
+
11
13
  class << self
12
14
  attr_writer :logger
15
+ attr_reader :disabled
13
16
 
14
17
  def logger
15
18
  @logger ||= Logger.new($stdout).tap do |log|
@@ -19,37 +22,34 @@ module DeletedAt
19
22
  end
20
23
  end
21
24
 
22
- def self.load
23
- ::ActiveRecord::Relation.send :prepend, DeletedAt::ActiveRecord::Relation
24
- ::ActiveRecord::Base.send :include, DeletedAt::ActiveRecord::Base
25
- ::ActiveRecord::ConnectionAdapters::TableDefinition.send :prepend, DeletedAt::ActiveRecord::ConnectionAdapters::TableDefinition
26
- end
27
-
28
- def self.install(model)
29
- return false unless model.has_deleted_at_column?
30
-
31
- DeletedAt::Views.install_present_view(model)
32
- DeletedAt::Views.install_deleted_view(model)
25
+ @disabled = false
33
26
 
34
- # Now that the views have been installed, initialize the new class extensions
35
- # e.g. User -> User::All and User::Deleted
36
- model.with_deleted_at
27
+ def self.disabled?
28
+ @disabled == true
37
29
  end
38
30
 
39
- def self.uninstall(model)
40
- return false unless model.has_deleted_at_column?
31
+ def self.disable
32
+ @disabled = true
33
+ end
41
34
 
42
- DeletedAt::Views.uninstall_deleted_view(model)
43
- DeletedAt::Views.uninstall_present_view(model)
35
+ def self.enable
36
+ @disabled = false
37
+ end
44
38
 
45
- # We've removed the database views, now remove the class extensions
46
- DeletedAt::ActiveRecord::Base.remove_class_views(model)
39
+ def self.gemspec
40
+ @gemspec ||= eval(`gem spec deleted_at --ruby`).freeze
47
41
  end
48
42
 
49
- def self.testify(value)
50
- value == true || value == 't' || value == 1 || value == '1'
43
+ def self.install(model)
44
+ logger.warn <<-STR
45
+ Great news! You're using the new and improved version of DeletedAt. No more table renaming.
46
+ You'll want to migrate your old models to use the new (non-view based) functionality.
47
+ Follow the instructions at #{gemspec.homepage}.
48
+ STR
51
49
  end
52
50
 
53
- private
51
+ def self.uninstall(model)
52
+
53
+ end
54
54
 
55
55
  end
@@ -0,0 +1,37 @@
1
+ require 'active_record'
2
+ require 'deleted_at/relation'
3
+
4
+ module DeletedAt
5
+ module ActiveRecord
6
+
7
+ def self.prepended(subclass)
8
+ subclass.const_get(:ActiveRecord_Relation).prepend(DeletedAt::Relation)
9
+ subclass.const_get(:ActiveRecord_AssociationRelation).prepend(DeletedAt::Relation)
10
+ subclass.extend(ClassMethods)
11
+ end
12
+
13
+ module ClassMethods
14
+
15
+ def inherited(subclass)
16
+ super
17
+ subclass.with_deleted_at self.deleted_at
18
+ end
19
+
20
+ def all
21
+ const_get(:Present)
22
+ end
23
+
24
+ def const_missing(const)
25
+ case const
26
+ when :All, :Deleted, :Present
27
+ all_without_deleted_at.tap do |rel|
28
+ rel.deleted_at_scope = const
29
+ end
30
+ else super
31
+ end
32
+ end
33
+ end
34
+
35
+ end
36
+
37
+ end
@@ -0,0 +1,51 @@
1
+ require 'deleted_at/active_record'
2
+
3
+ module DeletedAt
4
+
5
+ module Core
6
+
7
+ def self.prepended(subclass)
8
+ class << subclass
9
+ cattr_accessor :deleted_at do
10
+ DeletedAt::DEFAULT_OPTIONS
11
+ end
12
+ alias all_without_deleted_at all
13
+ end
14
+
15
+ subclass.extend(ClassMethods)
16
+ end
17
+
18
+ def self.raise_missing(klass)
19
+ message = "Missing `#{klass.deleted_at[:column]}` in `#{klass.name}` when trying to employ `deleted_at`"
20
+ raise(DeletedAt::MissingColumn, message)
21
+ end
22
+
23
+ def self.has_deleted_at_column?(klass)
24
+ klass.columns.map(&:name).include?(klass.deleted_at[:column].to_s)
25
+ end
26
+
27
+ module ClassMethods
28
+
29
+ def with_deleted_at(options={}, &block)
30
+ return if ::DeletedAt.disabled?
31
+
32
+ self.deleted_at.merge(options)
33
+ self.deleted_at[:proc] = block if block_given?
34
+
35
+ DeletedAt::Core.raise_missing(self) unless Core.has_deleted_at_column?(self)
36
+
37
+ self.prepend(DeletedAt::ActiveRecord)
38
+
39
+ end
40
+
41
+ def deleted_at_attributes
42
+ attributes = {
43
+ deleted_at[:column] => deleted_at[:proc].call
44
+ }
45
+ end
46
+
47
+ end # End ClassMethods
48
+
49
+ end
50
+
51
+ end
@@ -1,15 +1,30 @@
1
1
  module DeletedAt
2
- module Views
2
+ module Legacy
3
+ def self.uninstall(model)
4
+ return false unless model.has_deleted_at_column?
3
5
 
4
- def self.install_present_view(model)
6
+ uninstall_deleted_view(model)
5
7
  uninstall_present_view(model)
8
+ end
9
+
10
+ def self.install(model)
11
+ return false unless model.has_deleted_at_column?
12
+
13
+ install_present_view(model)
14
+ install_deleted_view(model)
15
+ end
16
+
17
+ private
18
+
19
+ def self.install_present_view(model)
20
+ # uninstall_present_view(model)
6
21
  present_table_name = present_view(model)
7
22
 
8
23
  while_spoofing_table_name(model, all_table(model)) do
9
24
  model.connection.execute("ALTER TABLE \"#{present_table_name}\" RENAME TO \"#{model.table_name}\"")
10
25
  model.connection.execute <<-SQL
11
26
  CREATE OR REPLACE VIEW "#{present_table_name}"
12
- AS #{ model.where(model.deleted_at_column => nil).to_sql }
27
+ AS #{ model.select('*').where(model.deleted_at_column => nil).to_sql }
13
28
  SQL
14
29
  end
15
30
  end
@@ -21,7 +36,7 @@ module DeletedAt
21
36
  while_spoofing_table_name(model, all_table(model)) do
22
37
  model.connection.execute <<-SQL
23
38
  CREATE OR REPLACE VIEW "#{table_name}"
24
- AS #{ model.where.not(model.deleted_at_column => nil).to_sql }
39
+ AS #{ model.select('*').where.not(model.deleted_at_column => nil).to_sql }
25
40
  SQL
26
41
  end
27
42
  end
@@ -29,23 +44,23 @@ module DeletedAt
29
44
  def self.all_table_exists?(model)
30
45
  query = model.connection.execute <<-SQL
31
46
  SELECT EXISTS (
32
- SELECT 1
47
+ SELECT true
33
48
  FROM information_schema.tables
34
49
  WHERE table_name = '#{all_table(model)}'
35
50
  ) AS exists;
36
51
  SQL
37
- DeletedAt.testify(query.first['exists'])
52
+ query.first['exists']
38
53
  end
39
54
 
40
55
  def self.deleted_view_exists?(model)
41
56
  query = model.connection.execute <<-SQL
42
57
  SELECT EXISTS (
43
- SELECT 1
58
+ SELECT true
44
59
  FROM information_schema.tables
45
60
  WHERE table_name = '#{deleted_view(model)}'
46
61
  ) AS exists;
47
62
  SQL
48
- DeletedAt.testify(query.first['exists'])
63
+ query.first['exists']
49
64
  end
50
65
 
51
66
  def self.present_view(model)
@@ -61,9 +76,6 @@ module DeletedAt
61
76
  end
62
77
 
63
78
  def self.uninstall_present_view(model)
64
- # Legacy
65
- model.connection.execute("DROP VIEW IF EXISTS \"#{model.table_name}/present\"")
66
- # New
67
79
  return unless all_table_exists?(model)
68
80
  model.connection.execute("DROP VIEW IF EXISTS \"#{present_view(model)}\"")
69
81
  model.connection.execute("ALTER TABLE \"#{all_table(model)}\" RENAME TO \"#{present_view(model)}\"")
@@ -73,14 +85,11 @@ module DeletedAt
73
85
  model.connection.execute("DROP VIEW IF EXISTS \"#{deleted_view(model)}\"")
74
86
  end
75
87
 
76
- private
77
-
78
88
  def self.while_spoofing_table_name(model, new_name, &block)
79
89
  old_name = model.table_name
80
90
  model.table_name = new_name
81
91
  yield
82
92
  model.table_name = old_name
83
93
  end
84
-
85
94
  end
86
95
  end
@@ -1,13 +1,14 @@
1
1
  require 'rails/railtie'
2
+ require 'deleted_at/core'
3
+ require 'deleted_at/table_definition'
2
4
 
3
5
  module DeletedAt
4
6
  class Railtie < Rails::Railtie
5
-
6
- initializer 'deleted_at.load' do
7
+ initializer 'deleted_at.load' do |_app|
7
8
  ActiveSupport.on_load(:active_record) do
8
- DeletedAt.load
9
+ ::ActiveRecord::Base.prepend(DeletedAt::Core)
10
+ ::ActiveRecord::ConnectionAdapters::TableDefinition.prepend(DeletedAt::TableDefinition)
9
11
  end
10
12
  end
11
-
12
13
  end
13
14
  end
@@ -0,0 +1,72 @@
1
+ module DeletedAt
2
+
3
+ module Relation
4
+
5
+ def self.prepended(subclass)
6
+ subclass.class_eval do
7
+ attr_writer :deleted_at_scope
8
+ end
9
+ end
10
+
11
+ def deleted_at_scope
12
+ @deleted_at_scope ||= :Present
13
+ end
14
+
15
+ def deleted_at_select
16
+ scoped_arel = case deleted_at_scope
17
+ when :Deleted
18
+ table.where(table[deleted_at[:column]].not_eq(nil))
19
+ when :Present
20
+ table.where(table[deleted_at[:column]].eq(nil))
21
+ end
22
+ end
23
+
24
+
25
+ def deleted_at_subselect(arel)
26
+ if (subselect = deleted_at_select)
27
+ subselect.project(arel_columns(columns.map(&:name)))
28
+ Arel::Nodes::TableAlias.new(Arel::Nodes::Grouping.new(subselect.ast), table_name)
29
+ end
30
+ end
31
+
32
+ def build_arel
33
+ super.tap do |arel|
34
+ if (subselect = deleted_at_subselect(arel)) && !arel.froms.include?(subselect)
35
+ DeletedAt.logger.debug("DeletedAt sub-selecting from #{subselect.to_sql}")
36
+ arel.from(subselect)
37
+ end
38
+ end
39
+ end
40
+
41
+ # Deletes the records matching +conditions+ without instantiating the records
42
+ # first, and hence not calling the +destroy+ method nor invoking callbacks. This
43
+ # is a single SQL DELETE statement that goes straight to the database, much more
44
+ # efficient than +destroy_all+. Be careful with relations though, in particular
45
+ # <tt>:dependent</tt> rules defined on associations are not honored. Returns the
46
+ # number of rows affected.
47
+ #
48
+ # Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')")
49
+ # Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else'])
50
+ # Post.where(person_id: 5).where(category: ['Something', 'Else']).delete_all
51
+ #
52
+ # Both calls delete the affected posts all at once with a single DELETE statement.
53
+ # If you need to destroy dependent associations or call your <tt>before_*</tt> or
54
+ # +after_destroy+ callbacks, use the +destroy_all+ method instead.
55
+ #
56
+ # If an invalid method is supplied, +delete_all+ raises an ActiveRecord error:
57
+ #
58
+ # Post.limit(100).delete_all
59
+ # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit
60
+ def delete_all(*args)
61
+ conditions = args.pop
62
+ if conditions
63
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
64
+ Passing conditions to delete_all is not supported in DeletedAt
65
+ To achieve the same use where(conditions).delete_all.
66
+ MESSAGE
67
+ end
68
+ update_all(klass.deleted_at_attributes)
69
+ end
70
+ end
71
+
72
+ end