mcfly 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +21 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/README.md +113 -0
- data/Rakefile +27 -0
- data/lib/mcfly/controller.rb +21 -0
- data/lib/mcfly/delete_trig.sql +18 -0
- data/lib/mcfly/has_mcfly.rb +63 -0
- data/lib/mcfly/insert_trig.sql +27 -0
- data/lib/mcfly/migration.rb +27 -0
- data/lib/mcfly/update_append_only_trig.sql +25 -0
- data/lib/mcfly/update_trig.sql +62 -0
- data/lib/mcfly/version.rb +3 -0
- data/lib/mcfly.rb +36 -0
- data/lib/tasks/mcfly_tasks.rake +4 -0
- data/mcfly.gemspec +27 -0
- data/spec/dummy/.rspec +1 -0
- data/spec/dummy/README.rdoc +261 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/javascripts/application.js +15 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.gitkeep +0 -0
- data/spec/dummy/app/models/.gitkeep +0 -0
- data/spec/dummy/app/models/market_price.rb +26 -0
- data/spec/dummy/app/models/security_instrument.rb +15 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config/application.rb +65 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +17 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +67 -0
- data/spec/dummy/config/environments/test.rb +37 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +15 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +58 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/migrate/001_create_security_instruments.rb +8 -0
- data/spec/dummy/db/migrate/002_create_market_prices.rb +14 -0
- data/spec/dummy/db/schema.rb +37 -0
- data/spec/dummy/lib/assets/.gitkeep +0 -0
- data/spec/dummy/log/.gitkeep +0 -0
- data/spec/dummy/log/development.log +3 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +25 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/model_spec.rb +206 -0
- data/spec/spec_helper.rb +51 -0
- metadata +184 -0
data/.gitignore
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*~
|
19
|
+
*.komodoproject
|
20
|
+
.rvmrc
|
21
|
+
spec/dummy/log
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2013 Arman Bostani
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
# Mcfly
|
2
|
+
|
3
|
+
Mcfly is a database table versioning system. It's useful for tracking
|
4
|
+
and auditing changes to database tables. It's also very easy to
|
5
|
+
access the current state of Mcfly tables at any point in time.
|
6
|
+
|
7
|
+
![](http://i.imgur.com/IG77ww0.jpg)
|
8
|
+
|
9
|
+
## Features
|
10
|
+
|
11
|
+
* All row versions are stored in the same table.
|
12
|
+
|
13
|
+
* Different row versions are accessed through scoping.
|
14
|
+
|
15
|
+
* Applications can use Mcfly to time-warp all tables to previous
|
16
|
+
points in time.
|
17
|
+
|
18
|
+
* Table queries for points in time are symmetric. i.e. queries to
|
19
|
+
access data in the present look just like queries available in any
|
20
|
+
particular point in time.
|
21
|
+
|
22
|
+
* Implemented as database triggers. So, the versioning system is
|
23
|
+
language/platform agnostic.
|
24
|
+
|
25
|
+
## Installation
|
26
|
+
|
27
|
+
$ gem install mcfly
|
28
|
+
|
29
|
+
Or add it to your `Gemfile`, etc.
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
To create Mcfly enabled tables, they need to be created using
|
34
|
+
`McFly::McFlyMigration` or `McFly::McFlyAppendOnlyMigration` instead
|
35
|
+
of the usual `ActiveRecord::Migration`.
|
36
|
+
|
37
|
+
class CreateSecurityInstruments < McFlyAppendOnlyMigration
|
38
|
+
def change
|
39
|
+
create_table :security_instruments do |t|
|
40
|
+
t.string :name, null: false
|
41
|
+
t.string :settlement_class, limit: 1, null: false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class CreateMarketPrices < McFlyMigration
|
47
|
+
def change
|
48
|
+
create_table :market_prices do |t|
|
49
|
+
t.references :security_instrument, null: false
|
50
|
+
t.decimal :coupon, null: false
|
51
|
+
t.integer :settlement_mm, null: false
|
52
|
+
t.integer :settlement_yy, null: false
|
53
|
+
# NULL indicates unknown price
|
54
|
+
t.decimal :price
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
These migration add the necessary versioning triggers for INSERT,
|
60
|
+
UPDATE and DELETE operations. The append-only migration disallows
|
61
|
+
updates. As such, append-only Mcfly tables rows to be INSERTed or
|
62
|
+
DELETEed, but not modified.
|
63
|
+
|
64
|
+
When you declare `has_mcfly` in your model, Mcfly adds some basic
|
65
|
+
functionality to the class.
|
66
|
+
|
67
|
+
class SecurityInstrument < ActiveRecord::Base
|
68
|
+
has_mcfly append_only: true
|
69
|
+
|
70
|
+
attr_accessible :name, :settlement_class
|
71
|
+
validates_presence_of :name, :settlement_class
|
72
|
+
mcfly_validates_uniqueness_of :name
|
73
|
+
|
74
|
+
mcfly_lookup :lookup, sig: 2 do
|
75
|
+
|pt, name|
|
76
|
+
find_by_name(name)
|
77
|
+
end
|
78
|
+
|
79
|
+
mcfly_lookup :lookup_all, sig: 1 do
|
80
|
+
|pt| all
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
The `has_mcfly` declaration provides the `mcfly_lookup` generator
|
85
|
+
which is scopes queries to the proper timeline. Also,
|
86
|
+
`mcfly_validates_uniqueness_of` is Mcfly's scoped version of
|
87
|
+
ActiveRecord's `validates_uniqueness_of`.
|
88
|
+
|
89
|
+
... TODO ... show examples of adding rows and accessing versions of
|
90
|
+
data ...
|
91
|
+
|
92
|
+
## Implementation
|
93
|
+
|
94
|
+
TODO
|
95
|
+
|
96
|
+
## Limitations
|
97
|
+
|
98
|
+
Currently, Mcfly only works with PostgreSQL databases.
|
99
|
+
|
100
|
+
## History
|
101
|
+
|
102
|
+
The database table versioning mechanism used in Mcfly was originally
|
103
|
+
developed at [TWINSUN][]. It has since been modified and enhanced at
|
104
|
+
[PENNYMAC][].
|
105
|
+
|
106
|
+
## License
|
107
|
+
|
108
|
+
Delorean has been released under the MIT license. Please check the
|
109
|
+
[LICENSE][] file for more details.
|
110
|
+
|
111
|
+
[license]: https://github.com/rubygems/rubygems.org/blob/master/MIT-LICENSE
|
112
|
+
[pennymac]: http://www.pennymacusa.com
|
113
|
+
[twinsun]: http://www.twinsun.com
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rdoc/task'
|
9
|
+
rescue LoadError
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
17
|
+
rdoc.title = 'Mcfly'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
|
26
|
+
Bundler::GemHelper.install_tasks
|
27
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Mcfly
|
2
|
+
module Controller
|
3
|
+
def self.included(base)
|
4
|
+
base.before_filter :set_mcfly_whodunnit
|
5
|
+
end
|
6
|
+
|
7
|
+
# Returns the user who is responsible for any changes that occur.
|
8
|
+
# By default this calls `current_user` and returns the result.
|
9
|
+
#
|
10
|
+
# Override this method in your controller to call a different
|
11
|
+
# method, e.g. `current_person`, or anything you like.
|
12
|
+
def user_for_mcfly
|
13
|
+
current_user rescue nil
|
14
|
+
end
|
15
|
+
|
16
|
+
# Tells Mcfly who is responsible for any changes that occur.
|
17
|
+
def set_mcfly_whodunnit
|
18
|
+
::Mcfly.whodunnit = user_for_mcfly
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
CREATE OR REPLACE FUNCTION "%{table}_delete" ()
|
2
|
+
RETURNS TRIGGER
|
3
|
+
AS $$
|
4
|
+
|
5
|
+
BEGIN
|
6
|
+
IF OLD.obsoleted_dt <> 'infinity' THEN
|
7
|
+
RAISE EXCEPTION 'can not delete old row version';
|
8
|
+
END IF;
|
9
|
+
|
10
|
+
UPDATE "%{table}" SET "obsoleted_dt" = 'now()' WHERE id = OLD.id;
|
11
|
+
|
12
|
+
RETURN NULL; -- the row is not actually deleted
|
13
|
+
END;
|
14
|
+
$$ LANGUAGE plpgsql;
|
15
|
+
|
16
|
+
DROP TRIGGER IF EXISTS %{table}_delete ON %{table};
|
17
|
+
CREATE TRIGGER "%{table}_delete" BEFORE DELETE ON "%{table}" FOR EACH ROW
|
18
|
+
EXECUTE PROCEDURE "%{table}_delete"();
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'delorean_lang'
|
2
|
+
|
3
|
+
module McFly
|
4
|
+
module Model
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.send :extend, ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def has_mcfly(options = {})
|
12
|
+
# FIXME: this methods gets a append_only option sometimes. It
|
13
|
+
# needs to add model level validations which prevent update
|
14
|
+
# when this option is present. Note that we need to allow
|
15
|
+
# delete. Deletion of McFly objects obsoletes them by setting
|
16
|
+
# obsoleted_dt.
|
17
|
+
|
18
|
+
send :include, InstanceMethods
|
19
|
+
after_initialize :record_init
|
20
|
+
# FIXME: :created_dt should also be readonly. However, we set
|
21
|
+
# it for debugging purposes. Should consider making this
|
22
|
+
# readonly once we're in production.
|
23
|
+
attr_readonly :group_id, :obsoleted_dt, :user_id
|
24
|
+
end
|
25
|
+
|
26
|
+
def mcfly_lookup(name, options = {}, &block)
|
27
|
+
delorean_fn(name, options) do |t, *args|
|
28
|
+
raise "time cannot be nil" if t.nil?
|
29
|
+
ts = (t == Float::INFINITY) ? 'infinity' : t
|
30
|
+
self.where("obsoleted_dt >= ? AND created_dt < ?", ts, ts).scoping do
|
31
|
+
block.call(t, *args)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def mcfly_validates_uniqueness_of(*attr_names)
|
37
|
+
# add :obsoleted_dt to the uniqueness scope
|
38
|
+
|
39
|
+
attr_names << {} unless attr_names.last.is_a?(Hash)
|
40
|
+
|
41
|
+
attr_names.last[:scope] ||= []
|
42
|
+
attr_names.last[:scope] << :obsoleted_dt
|
43
|
+
|
44
|
+
# Set uniqueness error message if not set. FIXME: need to
|
45
|
+
# figure out how to change the base message. It still
|
46
|
+
# prepends the pluralized main attr.
|
47
|
+
attr_names.last[:message] ||= "- record must be unique"
|
48
|
+
|
49
|
+
validates_uniqueness_of(*attr_names)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
module InstanceMethods
|
54
|
+
def record_init
|
55
|
+
# Set obsoleted_dt to non NIL to ensure constraints are properly
|
56
|
+
# constructed
|
57
|
+
self.obsoleted_dt = 'infinity' unless self.obsoleted_dt
|
58
|
+
self.user_id ||= Mcfly.whodunnit.try(:id)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
CREATE OR REPLACE FUNCTION "%{table}_insert" ()
|
2
|
+
RETURNS TRIGGER
|
3
|
+
AS $$
|
4
|
+
BEGIN
|
5
|
+
-- "obsoleted_dt" is set when a history row is created by
|
6
|
+
-- UPDATE. Leave it alone.
|
7
|
+
IF NEW.obsoleted_dt <> 'infinity' THEN
|
8
|
+
RETURN NEW;
|
9
|
+
END IF;
|
10
|
+
|
11
|
+
NEW.obsoleted_dt = 'infinity';
|
12
|
+
NEW.group_id = NEW.id;
|
13
|
+
|
14
|
+
-- FIXME: Handle cases where created_dt is sent in on creation. This
|
15
|
+
-- is only useful for debugging. Consider removing the surronding
|
16
|
+
-- IF for production version.
|
17
|
+
IF NEW.created_dt IS NULL THEN
|
18
|
+
NEW.created_dt = 'now()';
|
19
|
+
END IF;
|
20
|
+
|
21
|
+
RETURN NEW;
|
22
|
+
END;
|
23
|
+
$$ LANGUAGE plpgsql;
|
24
|
+
|
25
|
+
DROP TRIGGER IF EXISTS %{table}_insert ON %{table};
|
26
|
+
CREATE TRIGGER "%{table}_insert" BEFORE INSERT ON "%{table}" FOR EACH ROW
|
27
|
+
EXECUTE PROCEDURE "%{table}_insert"();
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class McFlyMigration < ActiveRecord::Migration
|
2
|
+
INSERT_TRIG, UPDATE_TRIG, UPDATE_APPEND_ONLY_TRIG, DELETE_TRIG =
|
3
|
+
%w{insert_trig update_trig update_append_only_trig delete_trig}.map { |f|
|
4
|
+
File.read(File.dirname(__FILE__) + "/#{f}.sql")
|
5
|
+
}
|
6
|
+
|
7
|
+
TRIGS = [INSERT_TRIG, UPDATE_TRIG, DELETE_TRIG]
|
8
|
+
|
9
|
+
def create_table(table_name, options = {}, &block)
|
10
|
+
super { |t|
|
11
|
+
t.integer :group_id, null: false
|
12
|
+
# can't use created_at/updated_at as those are automatically
|
13
|
+
# filled by ActiveRecord.
|
14
|
+
t.timestamp :created_dt, null: false
|
15
|
+
t.timestamp :obsoleted_dt, null: false
|
16
|
+
t.references :user, null: false
|
17
|
+
block.call(t)
|
18
|
+
}
|
19
|
+
|
20
|
+
self.class::TRIGS.each {|sql| execute sql % {table: table_name}}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class McFlyAppendOnlyMigration < McFlyMigration
|
25
|
+
# append-only update trigger disallows updates
|
26
|
+
TRIGS = [INSERT_TRIG, UPDATE_APPEND_ONLY_TRIG, DELETE_TRIG]
|
27
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
CREATE OR REPLACE FUNCTION "%{table}_update" ()
|
2
|
+
RETURNS TRIGGER
|
3
|
+
AS $$
|
4
|
+
DECLARE
|
5
|
+
|
6
|
+
BEGIN
|
7
|
+
IF OLD.obsoleted_dt <> 'infinity' THEN
|
8
|
+
RAISE EXCEPTION 'can not update obsoleted append-only row';
|
9
|
+
END IF;
|
10
|
+
|
11
|
+
-- If obsoleted_dt is being set, assume that the row is being
|
12
|
+
-- obsoleted. We return the OLD row so that other field updates are
|
13
|
+
-- ignored. This is used by DELETE.
|
14
|
+
IF NEW.obsoleted_dt <> 'infinity' THEN
|
15
|
+
OLD.obsoleted_dt = NEW.obsoleted_dt;
|
16
|
+
return OLD;
|
17
|
+
END IF;
|
18
|
+
|
19
|
+
RAISE EXCEPTION 'can not update append-only row';
|
20
|
+
END;
|
21
|
+
$$ LANGUAGE plpgsql;
|
22
|
+
|
23
|
+
DROP TRIGGER IF EXISTS %{table}_update ON %{table};
|
24
|
+
CREATE TRIGGER "%{table}_update" BEFORE UPDATE ON "%{table}" FOR EACH ROW
|
25
|
+
EXECUTE PROCEDURE "%{table}_update"();
|
@@ -0,0 +1,62 @@
|
|
1
|
+
CREATE OR REPLACE FUNCTION "%{table}_update" ()
|
2
|
+
RETURNS TRIGGER
|
3
|
+
AS $$
|
4
|
+
DECLARE
|
5
|
+
rec "%{table}";
|
6
|
+
new_id INT4;
|
7
|
+
now timestamp;
|
8
|
+
|
9
|
+
BEGIN
|
10
|
+
IF OLD.obsoleted_dt <> 'infinity' THEN
|
11
|
+
RAISE EXCEPTION 'can not modify old row version';
|
12
|
+
END IF;
|
13
|
+
|
14
|
+
-- If obsoleted_dt is being set, assume that the row is being
|
15
|
+
-- obsoleted. We return the OLD row so that other field updates are
|
16
|
+
-- ignored. This is used by DELETE.
|
17
|
+
IF NEW.obsoleted_dt <> 'infinity' THEN
|
18
|
+
OLD.obsoleted_dt = NEW.obsoleted_dt;
|
19
|
+
return OLD;
|
20
|
+
END IF;
|
21
|
+
|
22
|
+
-- copy old version of the row into rec
|
23
|
+
SELECT INTO rec * FROM "%{table}" WHERE "id" = NEW.id;
|
24
|
+
|
25
|
+
-- new_id is a new primary key that we'll use for the obsoleted row.
|
26
|
+
SELECT nextval('"%{table}_id_seq"') INTO new_id;
|
27
|
+
|
28
|
+
-- not sure if PGSQL will return the same value for now() in the
|
29
|
+
-- same transaction. So, use the same variable to be sure.
|
30
|
+
now = 'now()';
|
31
|
+
|
32
|
+
rec.id = new_id;
|
33
|
+
rec.group_id = NEW.id;
|
34
|
+
|
35
|
+
-- FIXME: The following IF/ELSE handles cases where created_dt is
|
36
|
+
-- sent in on update. This is only useful for debugging. Consider
|
37
|
+
-- removing the surronding IF (and ELSE part) for production
|
38
|
+
-- version.
|
39
|
+
IF NEW.created_dt = OLD.created_dt THEN
|
40
|
+
-- Set the modified row's created_dt. The obsoleted_dt field was
|
41
|
+
-- already infinity, so we don't need to set it.
|
42
|
+
NEW.created_dt = now;
|
43
|
+
rec.obsoleted_dt = now;
|
44
|
+
ELSE
|
45
|
+
IF NEW.created_dt <= OLD.created_dt THEN
|
46
|
+
RAISE EXCEPTION 'new created_dt must be greater than old';
|
47
|
+
END IF;
|
48
|
+
|
49
|
+
rec.obsoleted_dt = NEW.created_dt;
|
50
|
+
END IF;
|
51
|
+
|
52
|
+
-- insert rec, note that the insert trigger will get called. The
|
53
|
+
-- obsoleted_dt is set so INSERT should not do anything with this row.
|
54
|
+
INSERT INTO "%{table}" VALUES (rec.*);
|
55
|
+
|
56
|
+
RETURN NEW;
|
57
|
+
END;
|
58
|
+
$$ LANGUAGE plpgsql;
|
59
|
+
|
60
|
+
DROP TRIGGER IF EXISTS %{table}_update ON %{table};
|
61
|
+
CREATE TRIGGER "%{table}_update" BEFORE UPDATE ON "%{table}" FOR EACH ROW
|
62
|
+
EXECUTE PROCEDURE "%{table}_update"();
|
data/lib/mcfly.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'mcfly/migration'
|
2
|
+
require 'mcfly/has_mcfly'
|
3
|
+
require 'mcfly/controller'
|
4
|
+
require 'active_support'
|
5
|
+
|
6
|
+
module Mcfly
|
7
|
+
# ATTRIBUTION: some of the code in this project has been shamelessly
|
8
|
+
# lifted form paper_trail.
|
9
|
+
|
10
|
+
# Sets who is responsible for any changes that occur. You would
|
11
|
+
# normally use this in a migration or on the console, when working
|
12
|
+
# with models directly.
|
13
|
+
def self.whodunnit=(value)
|
14
|
+
mcfly_store[:whodunnit] = value
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.whodunnit
|
18
|
+
mcfly_store[:whodunnit]
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# Thread-safe hash to hold Mcfly's data.
|
24
|
+
def self.mcfly_store
|
25
|
+
Thread.current[:mcfly] ||= {}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
ActiveSupport.on_load(:active_record) do
|
30
|
+
include Delorean::Model
|
31
|
+
include McFly::Model
|
32
|
+
end
|
33
|
+
|
34
|
+
ActiveSupport.on_load(:action_controller) do
|
35
|
+
include Mcfly::Controller
|
36
|
+
end
|
data/mcfly.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
|
3
|
+
require "mcfly/version"
|
4
|
+
|
5
|
+
# Describe your gem and declare its dependencies:
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "mcfly"
|
8
|
+
s.version = Mcfly::VERSION
|
9
|
+
s.authors = ["Arman Bostani"]
|
10
|
+
s.email = ["arman.bostani@pnmac.com"]
|
11
|
+
s.homepage = "https://github.com/arman000/mcfly"
|
12
|
+
s.summary = %q{A database table versioning system.}
|
13
|
+
s.description = s.summary
|
14
|
+
s.files = `git ls-files`.split($\)
|
15
|
+
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
|
18
|
+
s.add_dependency "rails", "~> 3.2.11"
|
19
|
+
s.add_dependency "pg"
|
20
|
+
|
21
|
+
# FIXME: Delorean is added here for historical reasons. It should
|
22
|
+
# be removed as a dependency.
|
23
|
+
s.add_dependency "delorean_lang"
|
24
|
+
|
25
|
+
s.add_development_dependency "rspec"
|
26
|
+
s.add_development_dependency "rspec-rails"
|
27
|
+
end
|
data/spec/dummy/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|