gravis-acts_as_archive 0.2.6
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +18 -0
- data/README.markdown +119 -0
- data/Rakefile +9 -0
- data/bin/acts_as_archive +2 -0
- data/init.rb +1 -0
- data/lib/acts_as_archive/base/adapters/mysql.rb +26 -0
- data/lib/acts_as_archive/base/adapters/postgresql.rb +43 -0
- data/lib/acts_as_archive/base/destroy.rb +71 -0
- data/lib/acts_as_archive/base/restore.rb +36 -0
- data/lib/acts_as_archive/base/table.rb +99 -0
- data/lib/acts_as_archive/base.rb +31 -0
- data/lib/acts_as_archive/migration.rb +49 -0
- data/lib/acts_as_archive.rb +19 -0
- data/rails/init.rb +5 -0
- data/require.rb +49 -0
- data/spec/acts_as_archive/base/destroy_spec.rb +117 -0
- data/spec/acts_as_archive/base/restore_spec.rb +58 -0
- data/spec/acts_as_archive/base/table_spec.rb +74 -0
- data/spec/acts_as_archive/base_spec.rb +24 -0
- data/spec/acts_as_archive/migration_spec.rb +37 -0
- data/spec/db/config/database.mysql.yml +6 -0
- data/spec/db/config/database.postgresql.yml +6 -0
- data/spec/db/migrate/001_add_to_articles.rb +9 -0
- data/spec/db/migrate_2/001_add_to_articles.rb +9 -0
- data/spec/db/models/article.rb +4 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +92 -0
- metadata +108 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Copyright (c) 2009 Winton Welsh
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
5
|
+
the Software without restriction, including without limitation the rights to
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
7
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
8
|
+
subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
15
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
16
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
ActsAsArchive
|
2
|
+
=============
|
3
|
+
|
4
|
+
Don't delete your records, move them to a different table.
|
5
|
+
|
6
|
+
Like <code>acts\_as\_paranoid</code>, but doesn't mess with your SQL queries.
|
7
|
+
|
8
|
+
Install
|
9
|
+
-------
|
10
|
+
|
11
|
+
<pre>
|
12
|
+
sudo gem install acts_as_archive
|
13
|
+
</pre>
|
14
|
+
|
15
|
+
**environment.rb**:
|
16
|
+
|
17
|
+
<pre>
|
18
|
+
config.gem 'acts_as_archive'
|
19
|
+
</pre>
|
20
|
+
|
21
|
+
Update models
|
22
|
+
-------------
|
23
|
+
|
24
|
+
Add <code>acts\_as\_archive</code> to your models:
|
25
|
+
|
26
|
+
<pre>
|
27
|
+
class Article < ActiveRecord::Base
|
28
|
+
acts_as_archive
|
29
|
+
end
|
30
|
+
</pre>
|
31
|
+
|
32
|
+
<a name="create_archive_tables"></a>
|
33
|
+
|
34
|
+
Create archive tables
|
35
|
+
---------------------
|
36
|
+
|
37
|
+
Add this line to a migration:
|
38
|
+
|
39
|
+
<pre>
|
40
|
+
ActsAsArchive.update Article, Comment
|
41
|
+
</pre>
|
42
|
+
|
43
|
+
Replace <code>Article, Comment</code> with your own models that use <code>acts_as_archive</code>.
|
44
|
+
|
45
|
+
Archive tables mirror your table's structure, but with an additional <code>deleted_at</code> column.
|
46
|
+
|
47
|
+
There is an [alternate way to create archive tables](http://wiki.github.com/winton/acts_as_archive/alternatives-to-migrations) if you don't like migrations.
|
48
|
+
|
49
|
+
That's it!
|
50
|
+
----------
|
51
|
+
|
52
|
+
Use <code>destroy</code>, <code>delete</code>, and <code>delete_all</code> like you normally would.
|
53
|
+
|
54
|
+
Records move into the archive table instead of being destroyed.
|
55
|
+
|
56
|
+
What if my schema changes?
|
57
|
+
--------------------------
|
58
|
+
|
59
|
+
New migrations are automatically applied to the archive table.
|
60
|
+
|
61
|
+
No action is necessary on your part.
|
62
|
+
|
63
|
+
Query the archive
|
64
|
+
-----------------
|
65
|
+
|
66
|
+
Add <code>::Archive</code> to your ActiveRecord class:
|
67
|
+
|
68
|
+
<pre>
|
69
|
+
Article::Archive.find(:first)
|
70
|
+
</pre>
|
71
|
+
|
72
|
+
Restore from the archive
|
73
|
+
------------------------
|
74
|
+
|
75
|
+
Use <code>restore\_all</code> to copy archived records back to your table:
|
76
|
+
|
77
|
+
<pre>
|
78
|
+
Article.restore_all([ 'id = ?', 1 ])
|
79
|
+
</pre>
|
80
|
+
|
81
|
+
Auto-migrate from acts\_as\_paranoid
|
82
|
+
------------------------------------
|
83
|
+
|
84
|
+
If you previously used <code>acts\_as\_paranoid</code>, the <code>ActsAsArchive.update</code>
|
85
|
+
call will automatically move your deleted records to the archive table
|
86
|
+
(see <a href="#create_archive_tables">_Create archive tables_</a>).
|
87
|
+
|
88
|
+
Original <code>deleted_at</code> values are preserved.
|
89
|
+
|
90
|
+
Add indexes to the archive table
|
91
|
+
--------------------------------
|
92
|
+
|
93
|
+
To keep insertions fast, there are no indexes on your archive table by default.
|
94
|
+
|
95
|
+
If you are querying your archive a lot, you will want to add indexes:
|
96
|
+
|
97
|
+
<pre>
|
98
|
+
class Article < ActiveRecord::Base
|
99
|
+
acts_as_archive :indexes => [ :id, [:subject_id, :subject_type], :created_at, :deleted_at ]
|
100
|
+
end
|
101
|
+
</pre>
|
102
|
+
|
103
|
+
Call <code>ActsAsArchive.update</code> upon adding new indexes
|
104
|
+
(see <a href="#create_archive_tables">_Create archive tables_</a>).
|
105
|
+
|
106
|
+
Delete records without archiving
|
107
|
+
--------------------------------
|
108
|
+
|
109
|
+
To destroy a record without archiving:
|
110
|
+
|
111
|
+
<pre>
|
112
|
+
article.destroy!
|
113
|
+
</pre>
|
114
|
+
|
115
|
+
To delete multiple records without archiving:
|
116
|
+
|
117
|
+
<pre>
|
118
|
+
Article.delete_all!(["id in (?)", [1,2,3]])
|
119
|
+
</pre>
|
data/Rakefile
ADDED
data/bin/acts_as_archive
ADDED
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/rails/init"
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ActsAsArchive
|
2
|
+
module Base
|
3
|
+
module Adapters
|
4
|
+
module MySQL
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
def archive_table_indexed_columns
|
9
|
+
index_query = "SHOW INDEX FROM archived_#{table_name}"
|
10
|
+
indexes = connection.select_all(index_query)
|
11
|
+
final_indexes = []
|
12
|
+
current_index = 0
|
13
|
+
indexes.each do |index|
|
14
|
+
if index['Seq_in_index'] != '1'
|
15
|
+
final_indexes[current_index-1] = Array(final_indexes[current_index-1]).flatten.concat(Array(index['Column_name']))
|
16
|
+
else
|
17
|
+
final_indexes[current_index] = index['Column_name']
|
18
|
+
current_index += 1
|
19
|
+
end
|
20
|
+
end
|
21
|
+
return final_indexes
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module ActsAsArchive
|
2
|
+
module Base
|
3
|
+
module Adapters
|
4
|
+
module PostgreSQL
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
def archive_table_indexed_columns
|
9
|
+
# This query comes courtesy of cope360:
|
10
|
+
# http://stackoverflow.com/questions/2204058/show-which-columns-an-index-is-on-in-postgresql/2213199#2213199
|
11
|
+
index_query = <<-SQL
|
12
|
+
select
|
13
|
+
t.relname as table_name,
|
14
|
+
i.relname as index_name,
|
15
|
+
array_to_string(array_agg(a.attname), ', ') as column_names
|
16
|
+
from
|
17
|
+
pg_class t,
|
18
|
+
pg_class i,
|
19
|
+
pg_index ix,
|
20
|
+
pg_attribute a
|
21
|
+
where
|
22
|
+
t.oid = ix.indrelid
|
23
|
+
and i.oid = ix.indexrelid
|
24
|
+
and a.attrelid = t.oid
|
25
|
+
and a.attnum = ANY(ix.indkey)
|
26
|
+
and t.relkind = 'r'
|
27
|
+
and t.relname = 'archived_#{table_name}'
|
28
|
+
group by
|
29
|
+
t.relname,
|
30
|
+
i.relname
|
31
|
+
order by
|
32
|
+
t.relname,
|
33
|
+
i.relname
|
34
|
+
SQL
|
35
|
+
|
36
|
+
indexes = connection.select_all(index_query).collect do |r|
|
37
|
+
r["column_names"].split(", ").size > 1 ? r["column_names"].split(", ") : r["column_names"]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module ActsAsArchive
|
2
|
+
module Base
|
3
|
+
module Destroy
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
unless base.included_modules.include?(InstanceMethods)
|
7
|
+
base.class_eval do
|
8
|
+
alias_method :destroy_without_callbacks!, :destroy_without_callbacks
|
9
|
+
class <<self
|
10
|
+
alias_method :delete_all!, :delete_all
|
11
|
+
end
|
12
|
+
end
|
13
|
+
base.send :extend, ClassMethods
|
14
|
+
base.send :include, InstanceMethods
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
def copy_to_archive(conditions, import=false)
|
20
|
+
add_conditions!(where = '', conditions)
|
21
|
+
insert_cols = column_names.clone
|
22
|
+
select_cols = column_names.clone
|
23
|
+
if insert_cols.include?('deleted_at')
|
24
|
+
unless import
|
25
|
+
select_cols[select_cols.index('deleted_at')] = "'#{Time.now.utc.to_s(:db)}'"
|
26
|
+
end
|
27
|
+
else
|
28
|
+
insert_cols << 'deleted_at'
|
29
|
+
select_cols << "'#{Time.now.utc.to_s(:db)}'"
|
30
|
+
end
|
31
|
+
|
32
|
+
insert_cols.map! { |col| connection.quote_column_name(col) }
|
33
|
+
select_cols.map! { |col| col =~ /^\'/ ? col : connection.quote_column_name(col) }
|
34
|
+
|
35
|
+
connection.execute(%{
|
36
|
+
INSERT INTO archived_#{table_name} (#{insert_cols.join(', ')})
|
37
|
+
SELECT #{select_cols.join(', ')}
|
38
|
+
FROM #{table_name}
|
39
|
+
#{where}
|
40
|
+
})
|
41
|
+
connection.execute("DELETE FROM #{table_name} #{where}")
|
42
|
+
end
|
43
|
+
|
44
|
+
def delete_all(conditions=nil)
|
45
|
+
copy_to_archive(conditions)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
module InstanceMethods
|
50
|
+
def destroy_without_callbacks
|
51
|
+
unless new_record?
|
52
|
+
self.class.copy_to_archive("#{self.class.primary_key} = #{id}")
|
53
|
+
end
|
54
|
+
@destroyed = true
|
55
|
+
freeze
|
56
|
+
end
|
57
|
+
|
58
|
+
def destroy!
|
59
|
+
transaction { destroy_with_callbacks! }
|
60
|
+
end
|
61
|
+
|
62
|
+
def destroy_with_callbacks!
|
63
|
+
return false if callback(:before_destroy) == false
|
64
|
+
result = destroy_without_callbacks!
|
65
|
+
callback(:after_destroy)
|
66
|
+
result
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module ActsAsArchive
|
2
|
+
module Base
|
3
|
+
module Restore
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
unless base.included_modules.include?(InstanceMethods)
|
7
|
+
base.send :extend, ClassMethods
|
8
|
+
base.send :include, InstanceMethods
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
|
14
|
+
def copy_from_archive(conditions)
|
15
|
+
add_conditions!(where = '', conditions)
|
16
|
+
col_names = column_names - [ 'deleted_at' ]
|
17
|
+
col_names.map! { |col| connection.quote_column_name(col) }
|
18
|
+
connection.execute(%{
|
19
|
+
INSERT INTO #{table_name} (#{col_names.join(', ')})
|
20
|
+
SELECT #{col_names.join(', ')}
|
21
|
+
FROM archived_#{table_name}
|
22
|
+
#{where}
|
23
|
+
})
|
24
|
+
connection.execute("DELETE FROM archived_#{table_name} #{where}")
|
25
|
+
end
|
26
|
+
|
27
|
+
def restore_all(conditions=nil)
|
28
|
+
copy_from_archive(conditions)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module InstanceMethods
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module ActsAsArchive
|
2
|
+
module Base
|
3
|
+
module Table
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
unless base.included_modules.include?(InstanceMethods)
|
7
|
+
base.send :extend, ClassMethods
|
8
|
+
base.send :include, InstanceMethods
|
9
|
+
|
10
|
+
if base.connection.class.to_s.include?('Mysql')
|
11
|
+
base.send :extend, ActsAsArchive::Base::Adapters::MySQL
|
12
|
+
elsif base.connection.class.to_s.include?('PostgreSQL')
|
13
|
+
base.send :extend, ActsAsArchive::Base::Adapters::PostgreSQL
|
14
|
+
else
|
15
|
+
raise 'acts_as_archive does not support this database adapter'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
|
22
|
+
def archive_table_exists?
|
23
|
+
connection.table_exists?("archived_#{table_name}")
|
24
|
+
end
|
25
|
+
|
26
|
+
def create_archive_table
|
27
|
+
if table_exists? && !archive_table_exists?
|
28
|
+
connection.execute(%{
|
29
|
+
CREATE TABLE archived_#{table_name}
|
30
|
+
#{"ENGINE=InnoDB" if connection.class.to_s.include?('Mysql')}
|
31
|
+
AS SELECT * from #{table_name}
|
32
|
+
WHERE false;
|
33
|
+
})
|
34
|
+
columns = connection.columns("archived_#{table_name}").collect(&:name)
|
35
|
+
unless columns.include?('deleted_at')
|
36
|
+
connection.add_column("archived_#{table_name}", :deleted_at, :datetime)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def create_archive_indexes
|
42
|
+
if archive_table_exists?
|
43
|
+
indexes = archive_table_indexed_columns
|
44
|
+
|
45
|
+
(archive_indexes - indexes).each do |index|
|
46
|
+
begin
|
47
|
+
connection.add_index("archived_#{table_name}", index)
|
48
|
+
rescue ActiveRecord::StatementInvalid => e
|
49
|
+
Rails.logger.warn "Can't add index : #{index.inspect} on #{table_name} (#{e.to_s})"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
(indexes - archive_indexes).each do |index|
|
53
|
+
begin
|
54
|
+
connection.remove_index("archived_#{table_name}", index)
|
55
|
+
rescue ActiveRecord::StatementInvalid => e
|
56
|
+
Rails.logger.warn "Can't remove index : #{index.inspect} on #{table_name} (#{e.to_s})"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
def migrate_from_acts_as_paranoid
|
64
|
+
if column_names.include?('deleted_at')
|
65
|
+
if table_exists? && archive_table_exists?
|
66
|
+
condition = "deleted_at IS NOT NULL"
|
67
|
+
if self.count_by_sql("SELECT COUNT(*) FROM #{table_name} WHERE #{condition}") > 0
|
68
|
+
# Base::Destroy.copy_to_archive
|
69
|
+
copy_to_archive(condition, true)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def archive_table_indexed_columns
|
78
|
+
case connection.class.to_s
|
79
|
+
when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
|
80
|
+
index_query = "SHOW INDEX FROM archived_#{table_name}"
|
81
|
+
indexes = connection.select_all(index_query).collect do |r|
|
82
|
+
r["Column_name"]
|
83
|
+
end
|
84
|
+
when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
|
85
|
+
index_query = "SELECT indexname FROM pg_indexes WHERE tablename = '#{table_name}'"
|
86
|
+
indexes = connection.select_all(index_query).collect do |r|
|
87
|
+
r["indexname"].split("_on_").last.split("_and_")
|
88
|
+
end
|
89
|
+
else
|
90
|
+
raise "Unsupported Database"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
module InstanceMethods
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module ActsAsArchive
|
2
|
+
|
3
|
+
module Base
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ActMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ActMethods
|
9
|
+
def acts_as_archive(options={})
|
10
|
+
class_eval <<-end_eval
|
11
|
+
|
12
|
+
def self.acts_as_archive?
|
13
|
+
self.to_s == #{self.to_s.inspect}
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.archive_indexes
|
17
|
+
#{Array(options[:indexes]).map{|index| index.is_a?(Array) ? index.map(&:to_s) : index.to_s}.inspect}
|
18
|
+
end
|
19
|
+
|
20
|
+
class Archive < ActiveRecord::Base
|
21
|
+
self.record_timestamps = false
|
22
|
+
self.table_name = "archived_#{self.table_name}"
|
23
|
+
end
|
24
|
+
end_eval
|
25
|
+
include Destroy
|
26
|
+
include Restore
|
27
|
+
include Table
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module ActsAsArchive
|
2
|
+
module Migration
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
unless base.included_modules.include?(InstanceMethods)
|
6
|
+
base.send :extend, ClassMethods
|
7
|
+
base.class_eval do
|
8
|
+
class <<self
|
9
|
+
unless method_defined?(:method_missing_without_archive)
|
10
|
+
alias_method :method_missing_without_archive, :method_missing
|
11
|
+
alias_method :method_missing, :method_missing_with_archive
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
|
20
|
+
def method_missing_with_archive(method, *arguments, &block)
|
21
|
+
args = Marshal.load(Marshal.dump(arguments))
|
22
|
+
method_missing_without_archive(method, *arguments, &block)
|
23
|
+
supported = [
|
24
|
+
:add_column, :add_timestamps, :change_column,
|
25
|
+
:change_column_default, :change_table,
|
26
|
+
:drop_table, :remove_column, :remove_columns,
|
27
|
+
:remove_timestamps, :rename_column, :rename_table
|
28
|
+
]
|
29
|
+
if args.include?(:deleted_at) || args.include?('deleted_at')
|
30
|
+
# Don't change the archive's deleted_at column
|
31
|
+
return
|
32
|
+
end
|
33
|
+
if !args.empty? && supported.include?(method)
|
34
|
+
connection = ActiveRecord::Base.connection
|
35
|
+
args[0] = "archived_" + ActiveRecord::Migrator.proper_table_name(args[0])
|
36
|
+
if method == :rename_table
|
37
|
+
args[1] = "archived_" + args[1].to_s
|
38
|
+
end
|
39
|
+
if connection.table_exists?(args[0])
|
40
|
+
connection.send(method, *args, &block)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
module InstanceMethods
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require File.expand_path("#{File.dirname(__FILE__)}/../require")
|
2
|
+
Require.lib!
|
3
|
+
|
4
|
+
module ActsAsArchive
|
5
|
+
|
6
|
+
def self.update(*models)
|
7
|
+
models.each do |klass|
|
8
|
+
if klass.respond_to?(:acts_as_archive?) && klass.acts_as_archive?
|
9
|
+
time = Benchmark.measure do
|
10
|
+
klass.create_archive_table
|
11
|
+
klass.migrate_from_acts_as_paranoid
|
12
|
+
klass.create_archive_indexes
|
13
|
+
end
|
14
|
+
$stdout.puts "-- ActsAsArchive.update(#{models.join(', ')})"
|
15
|
+
$stdout.puts " -> #{"%.4fs" % time.real}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/rails/init.rb
ADDED
data/require.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'require'
|
3
|
+
require 'require'
|
4
|
+
|
5
|
+
Require do
|
6
|
+
gem(:activerecord) { require 'active_record' }
|
7
|
+
gem :require, '=0.2.1'
|
8
|
+
gem(:rake, '=0.8.7') { require 'rake' }
|
9
|
+
gem :rspec, '=1.3.0'
|
10
|
+
|
11
|
+
gemspec do
|
12
|
+
author 'Winton Welsh'
|
13
|
+
dependencies do
|
14
|
+
gem :require
|
15
|
+
end
|
16
|
+
email 'mail@wintoni.us'
|
17
|
+
name 'gravis-acts_as_archive'
|
18
|
+
homepage "http://github.com/gravis/#{name}"
|
19
|
+
summary "Don't delete your records, move them to a different table"
|
20
|
+
version '0.2.6'
|
21
|
+
end
|
22
|
+
|
23
|
+
lib do
|
24
|
+
require "lib/acts_as_archive/base"
|
25
|
+
require "lib/acts_as_archive/base/adapters/mysql"
|
26
|
+
require "lib/acts_as_archive/base/adapters/postgresql"
|
27
|
+
require "lib/acts_as_archive/base/destroy"
|
28
|
+
require "lib/acts_as_archive/base/restore"
|
29
|
+
require "lib/acts_as_archive/base/table"
|
30
|
+
require "lib/acts_as_archive/migration"
|
31
|
+
end
|
32
|
+
|
33
|
+
rails_init { require 'lib/acts_as_archive' }
|
34
|
+
|
35
|
+
rakefile do
|
36
|
+
gem(:rake) { require 'rake/gempackagetask' }
|
37
|
+
gem(:rspec) { require 'spec/rake/spectask' }
|
38
|
+
require 'require/tasks'
|
39
|
+
end
|
40
|
+
|
41
|
+
spec_helper do
|
42
|
+
require 'require/spec_helper'
|
43
|
+
gem :activerecord
|
44
|
+
require 'logger'
|
45
|
+
require 'yaml'
|
46
|
+
require 'pp'
|
47
|
+
require 'rails/init'
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + "/../../spec_helper")
|
2
|
+
|
3
|
+
describe ActsAsArchive::Base::Destroy do
|
4
|
+
|
5
|
+
before(:all) do
|
6
|
+
establish_test_db
|
7
|
+
Article.create_archive_table
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'delete_all!' do
|
11
|
+
|
12
|
+
before(:all) do
|
13
|
+
create_records
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should really delete all records" do
|
17
|
+
Article.delete_all!
|
18
|
+
Article.count.should == 0
|
19
|
+
Article::Archive.count.should == 0
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
describe 'destroy!' do
|
25
|
+
|
26
|
+
before(:all) do
|
27
|
+
create_records
|
28
|
+
@article = Article.first
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should really destroy a records" do
|
32
|
+
@article.destroy!
|
33
|
+
Article::Archive.count.should == 0
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
describe 'delete_all' do
|
39
|
+
|
40
|
+
before(:all) do
|
41
|
+
@articles = create_records
|
42
|
+
end
|
43
|
+
|
44
|
+
describe 'with conditions' do
|
45
|
+
|
46
|
+
before(:all) do
|
47
|
+
# Mini delete_all parameter test
|
48
|
+
Article.delete_all [ 'id = ?', @articles[0].id ]
|
49
|
+
Article.delete_all "id = #{@articles[1].id}"
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should move some records to the archive table" do
|
53
|
+
Article.count.should == 3
|
54
|
+
Article::Archive.count.should == 2
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should preserve record attributes" do
|
58
|
+
2.times do |x|
|
59
|
+
original = @articles[x]
|
60
|
+
copy = Article::Archive.find(original.id)
|
61
|
+
article_match?(original, copy)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe 'without conditions' do
|
67
|
+
|
68
|
+
before(:all) do
|
69
|
+
Article.delete_all
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should move all records to the archive table" do
|
73
|
+
Article.count.should == 0
|
74
|
+
Article::Archive.count.should == 5
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should preserve record attributes" do
|
78
|
+
5.times do |x|
|
79
|
+
original = @articles[x]
|
80
|
+
copy = Article::Archive.find(original.id)
|
81
|
+
article_match?(original, copy)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
[ :destroy, :delete ].each do |d|
|
88
|
+
|
89
|
+
describe d do
|
90
|
+
|
91
|
+
before(:all) do
|
92
|
+
@articles = create_records
|
93
|
+
Article.find(@articles[0..1].collect(&:id)).each do |a|
|
94
|
+
a.send(d)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should move some records to the archive table" do
|
99
|
+
Article.count.should == 3
|
100
|
+
Article::Archive.count.should == 2
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should preserve record attributes" do
|
104
|
+
2.times do |x|
|
105
|
+
original = @articles[x]
|
106
|
+
copy = Article::Archive.find(original.id)
|
107
|
+
article_match?(original, copy)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
it "should mark the object as destroyed" do
|
112
|
+
@articles[3].send(d)
|
113
|
+
@articles[3].destroyed?.should == true
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + "/../../spec_helper")
|
2
|
+
|
3
|
+
describe ActsAsArchive::Base::Restore do
|
4
|
+
|
5
|
+
before(:all) do
|
6
|
+
establish_test_db
|
7
|
+
Article.create_archive_table
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'restore_all' do
|
11
|
+
|
12
|
+
before(:all) do
|
13
|
+
@articles = create_records(Article::Archive)
|
14
|
+
end
|
15
|
+
|
16
|
+
describe 'with conditions' do
|
17
|
+
|
18
|
+
before(:all) do
|
19
|
+
# Mini restore parameter test
|
20
|
+
Article.restore_all [ 'id = ?', @articles[0].id ]
|
21
|
+
Article.restore_all "id = #{@articles[1].id}"
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should move some records to the article table" do
|
25
|
+
Article::Archive.count.should == 3
|
26
|
+
Article.count.should == 2
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should preserve record attributes" do
|
30
|
+
2.times do |x|
|
31
|
+
original = @articles[x]
|
32
|
+
copy = Article.find(original.id)
|
33
|
+
article_match?(original, copy)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe 'without conditions' do
|
39
|
+
|
40
|
+
before(:all) do
|
41
|
+
Article.restore_all
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should move all records to the archive table" do
|
45
|
+
Article::Archive.count.should == 0
|
46
|
+
Article.count.should == 5
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should preserve record attributes" do
|
50
|
+
5.times do |x|
|
51
|
+
original = @articles[x]
|
52
|
+
copy = Article.find(original.id)
|
53
|
+
article_match?(original, copy)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + "/../../spec_helper")
|
2
|
+
|
3
|
+
describe ActsAsArchive::Base::Table do
|
4
|
+
|
5
|
+
before(:all) do
|
6
|
+
establish_test_db
|
7
|
+
Article.create_archive_table
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'create_archive_table' do
|
11
|
+
|
12
|
+
before(:all) do
|
13
|
+
@article_columns = connection.columns("articles").collect(&:name)
|
14
|
+
@archive_columns = connection.columns("archived_articles").collect(&:name)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should create an archive table" do
|
18
|
+
connection.table_exists?("archived_articles").should == true
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should create an archive table with the same structure as the original table" do
|
22
|
+
@article_columns.each do |col|
|
23
|
+
@archive_columns.include?(col).should == true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should add a deleted_at column to the archive table" do
|
28
|
+
(@archive_columns - @article_columns).should == [ 'deleted_at' ]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'create_archive_indexes' do
|
33
|
+
|
34
|
+
before(:all) do
|
35
|
+
Article.create_archive_indexes
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should create archive indexes" do
|
39
|
+
indexes.to_set.should == [ "id", ["subject_id", "subject_type"], "deleted_at", "column_that_does_not_exist_yet" ].to_set
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should destroy archive indexes" do
|
43
|
+
Article.class_eval { acts_as_archive }
|
44
|
+
Article.create_archive_indexes
|
45
|
+
indexes.should == ["column_that_does_not_exist_yet"]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'migrate_from_acts_as_paranoid' do
|
50
|
+
|
51
|
+
before(:all) do
|
52
|
+
connection.add_column(:articles, :deleted_at, :datetime)
|
53
|
+
Article.reset_column_information
|
54
|
+
end
|
55
|
+
|
56
|
+
before(:each) do
|
57
|
+
connection.execute("DELETE FROM #{Article::Archive.table_name}")
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should move deleted records to the archive" do
|
61
|
+
create_records(Article, :deleted_at => Time.now.utc)
|
62
|
+
Article.migrate_from_acts_as_paranoid
|
63
|
+
Article.count.should == 0
|
64
|
+
Article::Archive.count.should == 5
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should not move non-deleted records to the archive" do
|
68
|
+
create_records
|
69
|
+
Article.migrate_from_acts_as_paranoid
|
70
|
+
Article.count.should == 5
|
71
|
+
Article::Archive.count.should == 0
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
|
2
|
+
|
3
|
+
describe ActsAsArchive::Base do
|
4
|
+
|
5
|
+
before(:all) do
|
6
|
+
establish_test_db
|
7
|
+
end
|
8
|
+
|
9
|
+
describe 'acts_as_archive' do
|
10
|
+
|
11
|
+
it "should add self.acts_as_archive? to the model" do
|
12
|
+
Article.respond_to?(:acts_as_archive?).should == true
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should add self.archive_indexes to the model" do
|
16
|
+
Article.respond_to?(:archive_indexes).should == true
|
17
|
+
Article.archive_indexes.should == [ 'id', ['subject_id','subject_type'], 'deleted_at' ]
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should add Archive class to the model" do
|
21
|
+
defined?(Article::Archive).should == "constant"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe ActsAsArchive::Migration do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
establish_test_db
|
7
|
+
Article.create_archive_table
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'method_missing_with_archive' do
|
11
|
+
|
12
|
+
it 'should migrate both tables up' do
|
13
|
+
migrate_up
|
14
|
+
(@new_article_columns - @old_article_columns).should == [ 'permalink' ]
|
15
|
+
(@new_archive_columns - @old_archive_columns).should == [ 'permalink' ]
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should migrate both tables down' do
|
19
|
+
migrate_up
|
20
|
+
@old_article_columns = @new_article_columns
|
21
|
+
@old_archive_columns = @new_archive_columns
|
22
|
+
ActiveRecord::Migrator.migrate("#{SPEC}/db/migrate", 0)
|
23
|
+
@new_article_columns = columns("articles")
|
24
|
+
@new_archive_columns = columns("archived_articles")
|
25
|
+
(@old_article_columns - @new_article_columns).should == [ 'permalink' ]
|
26
|
+
(@old_archive_columns - @new_archive_columns).should == [ 'permalink' ]
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should not touch the archive's deleted_at column" do
|
30
|
+
connection.add_column(:articles, :deleted_at, :datetime)
|
31
|
+
Article.reset_column_information
|
32
|
+
migrate_up("migrate_2")
|
33
|
+
(@old_article_columns - @new_article_columns).should == [ 'deleted_at' ]
|
34
|
+
(@old_archive_columns - @new_archive_columns).should == []
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
require File.expand_path("#{File.dirname(__FILE__)}/../require")
|
2
|
+
Require.spec_helper!
|
3
|
+
|
4
|
+
Spec::Runner.configure do |config|
|
5
|
+
end
|
6
|
+
|
7
|
+
def db_type
|
8
|
+
ENV['DB_TYPE'] ? ENV['DB_TYPE'] : 'mysql'
|
9
|
+
end
|
10
|
+
|
11
|
+
def article_match?(original, copy)
|
12
|
+
copy.id.should == original.id
|
13
|
+
copy.title.should == original.title
|
14
|
+
copy.body.should == original.body
|
15
|
+
if copy.respond_to?(:deleted_at)
|
16
|
+
copy.deleted_at.strftime('%j%H%M').should == Time.now.utc.strftime('%j%H%M')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def columns(table)
|
21
|
+
connection.columns(table).collect(&:name)
|
22
|
+
end
|
23
|
+
|
24
|
+
def connection
|
25
|
+
ActiveRecord::Base.connection
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_records(klass=Article, values={})
|
29
|
+
articles = []
|
30
|
+
table = klass.table_name
|
31
|
+
cols = columns(table)
|
32
|
+
connection.execute("DELETE FROM #{table}")
|
33
|
+
(1..5).collect do |x|
|
34
|
+
vals = cols.collect do |c|
|
35
|
+
if values.keys.include?(c.intern)
|
36
|
+
values[c.intern] ? "'#{values[c.intern]}'" : "NULL"
|
37
|
+
else
|
38
|
+
case c.intern
|
39
|
+
when :id; x
|
40
|
+
when :deleted_at; 'NULL'
|
41
|
+
when :read; true
|
42
|
+
when :subject_id; x
|
43
|
+
else "'#{c.capitalize} #{x}'"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
connection.execute(%{
|
48
|
+
INSERT INTO #{table} (#{cols.collect { |c| "#{connection.quote_column_name(c)}" }.join(', ')})
|
49
|
+
VALUES (#{vals.join(', ')})
|
50
|
+
})
|
51
|
+
klass.find(x)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def establish_test_db
|
56
|
+
# Establish connection
|
57
|
+
unless ActiveRecord::Base.connected?
|
58
|
+
config = YAML::load(File.open("#{SPEC}/db/config/database.#{db_type}.yml"))
|
59
|
+
ActiveRecord::Base.configurations = config
|
60
|
+
ActiveRecord::Base.establish_connection(config['test'])
|
61
|
+
end
|
62
|
+
# Establish logger
|
63
|
+
logger_file = File.open("#{SPEC}/db/log/test.log", 'a')
|
64
|
+
logger_file.sync = true
|
65
|
+
@logger = Logger.new(logger_file)
|
66
|
+
ActiveRecord::Base.logger = @logger
|
67
|
+
# The database should have only a simple articles table
|
68
|
+
connection.execute("DROP TABLE IF EXISTS articles")
|
69
|
+
connection.execute("DROP TABLE IF EXISTS archived_articles")
|
70
|
+
connection.execute("DROP TABLE IF EXISTS schema_migrations")
|
71
|
+
connection.create_table(:articles) do |t|
|
72
|
+
t.string :title
|
73
|
+
t.string :body
|
74
|
+
t.boolean :read # break mysql w/o quotation
|
75
|
+
t.integer :subject_id
|
76
|
+
t.string :subject_type
|
77
|
+
end
|
78
|
+
# Load the model
|
79
|
+
load "#{SPEC}/db/models/article.rb"
|
80
|
+
end
|
81
|
+
|
82
|
+
def indexes
|
83
|
+
Article.send(:archive_table_indexed_columns).concat(['column_that_does_not_exist_yet'])
|
84
|
+
end
|
85
|
+
|
86
|
+
def migrate_up(directory='migrate')
|
87
|
+
@old_article_columns = columns("articles")
|
88
|
+
@old_archive_columns = columns("archived_articles")
|
89
|
+
ActiveRecord::Migrator.migrate("#{SPEC}/db/#{directory}")
|
90
|
+
@new_article_columns = columns("articles")
|
91
|
+
@new_archive_columns = columns("archived_articles")
|
92
|
+
end
|
metadata
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gravis-acts_as_archive
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 2
|
9
|
+
- 6
|
10
|
+
version: 0.2.6
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Winton Welsh
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-05-31 00:00:00 +02:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: require
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - "="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 21
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
- 2
|
33
|
+
- 1
|
34
|
+
version: 0.2.1
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
description:
|
38
|
+
email: mail@wintoni.us
|
39
|
+
executables:
|
40
|
+
- acts_as_archive
|
41
|
+
extensions: []
|
42
|
+
|
43
|
+
extra_rdoc_files:
|
44
|
+
- README.markdown
|
45
|
+
files:
|
46
|
+
- bin/acts_as_archive
|
47
|
+
- init.rb
|
48
|
+
- lib/acts_as_archive/base/adapters/mysql.rb
|
49
|
+
- lib/acts_as_archive/base/adapters/postgresql.rb
|
50
|
+
- lib/acts_as_archive/base/destroy.rb
|
51
|
+
- lib/acts_as_archive/base/restore.rb
|
52
|
+
- lib/acts_as_archive/base/table.rb
|
53
|
+
- lib/acts_as_archive/base.rb
|
54
|
+
- lib/acts_as_archive/migration.rb
|
55
|
+
- lib/acts_as_archive.rb
|
56
|
+
- MIT-LICENSE
|
57
|
+
- rails/init.rb
|
58
|
+
- Rakefile
|
59
|
+
- README.markdown
|
60
|
+
- require.rb
|
61
|
+
- spec/acts_as_archive/base/destroy_spec.rb
|
62
|
+
- spec/acts_as_archive/base/restore_spec.rb
|
63
|
+
- spec/acts_as_archive/base/table_spec.rb
|
64
|
+
- spec/acts_as_archive/base_spec.rb
|
65
|
+
- spec/acts_as_archive/migration_spec.rb
|
66
|
+
- spec/db/config/database.mysql.yml
|
67
|
+
- spec/db/config/database.postgresql.yml
|
68
|
+
- spec/db/migrate/001_add_to_articles.rb
|
69
|
+
- spec/db/migrate_2/001_add_to_articles.rb
|
70
|
+
- spec/db/models/article.rb
|
71
|
+
- spec/spec.opts
|
72
|
+
- spec/spec_helper.rb
|
73
|
+
has_rdoc: true
|
74
|
+
homepage: http://github.com/gravis/gravis-acts_as_archive
|
75
|
+
licenses: []
|
76
|
+
|
77
|
+
post_install_message:
|
78
|
+
rdoc_options: []
|
79
|
+
|
80
|
+
require_paths:
|
81
|
+
- lib
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
hash: 3
|
88
|
+
segments:
|
89
|
+
- 0
|
90
|
+
version: "0"
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
|
+
none: false
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
hash: 3
|
97
|
+
segments:
|
98
|
+
- 0
|
99
|
+
version: "0"
|
100
|
+
requirements: []
|
101
|
+
|
102
|
+
rubyforge_project:
|
103
|
+
rubygems_version: 1.3.7
|
104
|
+
signing_key:
|
105
|
+
specification_version: 3
|
106
|
+
summary: Don't delete your records, move them to a different table
|
107
|
+
test_files: []
|
108
|
+
|