ocelot 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/ocelot.rb +90 -0
- data/lib/ocelot/cli.rb +9 -0
- data/lib/ocelot/database.rb +41 -0
- data/lib/ocelot/dsl.rb +146 -0
- data/lib/ocelot/graph.rb +14 -0
- data/lib/ocelot/processor.rb +182 -0
- data/lib/ocelot/rules.rb +53 -0
- metadata +63 -0
data/lib/ocelot.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
module Ocelot
|
2
|
+
class Base
|
3
|
+
|
4
|
+
def connection
|
5
|
+
Ocelot.connection
|
6
|
+
end
|
7
|
+
|
8
|
+
def in?(*args)
|
9
|
+
Ocelot.in?(*args)
|
10
|
+
end
|
11
|
+
|
12
|
+
def remove_callbacks(*args)
|
13
|
+
Ocelot.remove_callbacks(*args)
|
14
|
+
end
|
15
|
+
|
16
|
+
def logger(*args)
|
17
|
+
Ocelot.logger(*args)
|
18
|
+
end
|
19
|
+
|
20
|
+
def obj_to_s(*args)
|
21
|
+
Ocelot.obj_to_s(*args)
|
22
|
+
end
|
23
|
+
|
24
|
+
def safely(*args, &b)
|
25
|
+
Ocelot.safely(*args) do |*bargs|
|
26
|
+
b.call(*bargs)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
require 'ocelot/graph'
|
32
|
+
require 'ocelot/dsl'
|
33
|
+
require 'ocelot/rules'
|
34
|
+
require 'ocelot/database'
|
35
|
+
require 'ocelot/processor'
|
36
|
+
|
37
|
+
class << self
|
38
|
+
attr_accessor :connection
|
39
|
+
attr_accessor :logger
|
40
|
+
|
41
|
+
def obj_to_s(obj, assoc=nil)
|
42
|
+
assoc = assoc.nil? ? "" : ".#{assoc.name}"
|
43
|
+
"#{obj.class}(#{obj.id})#{assoc}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def safely(obj, assoc=nil, val=nil)
|
47
|
+
begin
|
48
|
+
yield
|
49
|
+
rescue => e
|
50
|
+
target = obj.is_a?(Class) ? "#{obj}" : "#{obj_to_s(obj)}"
|
51
|
+
|
52
|
+
if assoc.nil?
|
53
|
+
logger.error "Could not read or write #{target}: #{e.message}"
|
54
|
+
elsif val.nil?
|
55
|
+
logger.error "Could not read data for #{target}.#{assoc}: #{e.message}"
|
56
|
+
else
|
57
|
+
logger.error "Could not write #{target}.#{assoc} adding #{val.class}: #{e.message}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# this should really be a class-id pair store instead of the entire object,
|
63
|
+
# /maybe/
|
64
|
+
def in?(set, node)
|
65
|
+
result = set.include? node
|
66
|
+
set << node
|
67
|
+
result
|
68
|
+
end
|
69
|
+
|
70
|
+
def remove_callbacks(obj)
|
71
|
+
if obj.class.respond_to? :delete_observers
|
72
|
+
obj.class.delete_observers
|
73
|
+
end
|
74
|
+
|
75
|
+
class << obj
|
76
|
+
def valid?
|
77
|
+
true
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
ActiveRecord::Callbacks::CALLBACKS.each do |cb|
|
82
|
+
class << obj
|
83
|
+
def run_callbacks(*goaway)
|
84
|
+
true
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
data/lib/ocelot/cli.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'active_record'
|
3
|
+
require 'active_record/connection_adapters/mysql2_adapter'
|
4
|
+
require 'active_record/connection_adapters/abstract/connection_pool'
|
5
|
+
|
6
|
+
module Ocelot
|
7
|
+
class Connection
|
8
|
+
attr_accessor :source
|
9
|
+
attr_accessor :target
|
10
|
+
attr_accessor :use_source
|
11
|
+
|
12
|
+
def initialize(source, target)
|
13
|
+
self.source = ActiveRecord::Base.mysql2_connection(source)
|
14
|
+
self.target = ActiveRecord::Base.mysql2_connection(target)
|
15
|
+
self.use_source = true
|
16
|
+
|
17
|
+
# ar doesn't like oop much, monkeypatch lots o stuff
|
18
|
+
ActiveRecord::ConnectionAdapters::ConnectionHandler.class_variable_set(:@@conn, self)
|
19
|
+
ActiveRecord::ConnectionAdapters::ConnectionHandler.class_eval do
|
20
|
+
def retrieve_connection(klass)
|
21
|
+
@@conn.current_connection
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
ActiveRecord::ConnectionAdapters::AbstractAdapter.class_eval do
|
26
|
+
attr_reader :last_query
|
27
|
+
|
28
|
+
def log_with_last_query(sql, name, &block)
|
29
|
+
@last_query = [sql, name]
|
30
|
+
log_without_last_query(sql, name, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
alias_method_chain :log, :last_query
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def current_connection
|
38
|
+
self.use_source ? source : target
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/ocelot/dsl.rb
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'ocelot'
|
2
|
+
require 'ocelot/rules'
|
3
|
+
|
4
|
+
module Ocelot
|
5
|
+
class Dsl
|
6
|
+
attr_accessor :rules
|
7
|
+
attr_accessor :filters
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@rules = {}
|
11
|
+
@filters = {}
|
12
|
+
@seeds = lambda { [] }
|
13
|
+
@logger = Logger.new(STDERR)
|
14
|
+
end
|
15
|
+
|
16
|
+
def source(source=nil)
|
17
|
+
@source = source unless source.nil?
|
18
|
+
@source
|
19
|
+
end
|
20
|
+
|
21
|
+
def seeds(&seeds)
|
22
|
+
@seeds = seeds unless seeds.nil?
|
23
|
+
@seeds
|
24
|
+
end
|
25
|
+
|
26
|
+
def target(target=nil)
|
27
|
+
@target = target unless target.nil?
|
28
|
+
@target
|
29
|
+
end
|
30
|
+
|
31
|
+
def classes(classes=nil)
|
32
|
+
@classes = classes.split(/\s+/) unless classes.nil?
|
33
|
+
@classes
|
34
|
+
end
|
35
|
+
|
36
|
+
def logger
|
37
|
+
@logger
|
38
|
+
end
|
39
|
+
|
40
|
+
def log_level(level=nil)
|
41
|
+
@logger.level = eval "Logger::#{level}" unless level.nil?
|
42
|
+
level
|
43
|
+
end
|
44
|
+
|
45
|
+
def init(&p)
|
46
|
+
@init = p unless p.nil?
|
47
|
+
@init
|
48
|
+
end
|
49
|
+
|
50
|
+
def rule(clazz, &p)
|
51
|
+
@rules[clazz] = ScriptRule.new(p)
|
52
|
+
end
|
53
|
+
|
54
|
+
def filter(clazz, &p)
|
55
|
+
@filters[clazz] = ScriptFilter.new(p)
|
56
|
+
end
|
57
|
+
|
58
|
+
def go
|
59
|
+
Ocelot::logger = logger
|
60
|
+
Ocelot::connection = Ocelot::Connection.new(source, target)
|
61
|
+
|
62
|
+
unless init.nil?
|
63
|
+
Ocelot.connection.use_source = false
|
64
|
+
init.call
|
65
|
+
Ocelot.connection.use_source = true
|
66
|
+
end
|
67
|
+
|
68
|
+
copier = Ocelot::Processor.new
|
69
|
+
copier.classes = classes.collect { |c| eval c }
|
70
|
+
copier.seeds = seeds.call.flatten
|
71
|
+
copier.extra_rules = rules
|
72
|
+
copier.extra_filters = filters
|
73
|
+
|
74
|
+
if true
|
75
|
+
copier.go
|
76
|
+
else
|
77
|
+
copier_thread = Thread.new(copier) do
|
78
|
+
copier.go
|
79
|
+
end
|
80
|
+
copier_thread.run
|
81
|
+
copier_thread.set_trace_func proc { |*args| copier.watcher.trace(*args) }
|
82
|
+
last_warning = nil
|
83
|
+
|
84
|
+
# pole status
|
85
|
+
while !copier_thread.status.nil? and copier_thread.status != false
|
86
|
+
copier_thread.join(30)
|
87
|
+
warning = copier.watcher.ping
|
88
|
+
unless warning.nil? or last_warning == warning
|
89
|
+
logger.warn warning
|
90
|
+
last_warning = warning
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class ScriptFilter < Ocelot::Rules::Filter
|
98
|
+
alias :old_load :load
|
99
|
+
def initialize(p)
|
100
|
+
@p = p
|
101
|
+
end
|
102
|
+
|
103
|
+
def load(obj, collection, name)
|
104
|
+
@obj, @collection, @name = obj, collection, name
|
105
|
+
|
106
|
+
@result = nil
|
107
|
+
instance_exec(obj, collection, name, &@p)
|
108
|
+
@result || default
|
109
|
+
end
|
110
|
+
|
111
|
+
def nothing
|
112
|
+
@result = []
|
113
|
+
end
|
114
|
+
|
115
|
+
def result(val)
|
116
|
+
@result = val
|
117
|
+
end
|
118
|
+
|
119
|
+
def default
|
120
|
+
@result = old_load @obj, @collection, @name
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
class ScriptRule < Ocelot::Rules::DefaultRule
|
125
|
+
alias :old_save :save
|
126
|
+
|
127
|
+
def initialize(p)
|
128
|
+
@p = p
|
129
|
+
end
|
130
|
+
|
131
|
+
def inspect
|
132
|
+
@inspect = true
|
133
|
+
end
|
134
|
+
|
135
|
+
def persist(obj)
|
136
|
+
old_save obj
|
137
|
+
end
|
138
|
+
|
139
|
+
def save(obj, *args)
|
140
|
+
@inspect = false
|
141
|
+
args = [obj, *args]
|
142
|
+
instance_exec(*args, &@p)
|
143
|
+
return @inspect
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
data/lib/ocelot/graph.rb
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
module Ocelot
|
2
|
+
class Watcher
|
3
|
+
attr_accessor :op
|
4
|
+
attr_accessor :association
|
5
|
+
attr_accessor :sql
|
6
|
+
attr_accessor :line
|
7
|
+
attr_accessor :rule
|
8
|
+
attr_reader :last
|
9
|
+
attr_reader :obj
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@last = Time.now
|
13
|
+
@mutex = Mutex.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def obj=(obj)
|
17
|
+
@obj = Ocelot::obj_to_s(obj)
|
18
|
+
end
|
19
|
+
|
20
|
+
def ping
|
21
|
+
sync do |w|
|
22
|
+
"Might be stuck processing #{w.obj}#{w.association.nil? ? "" : "."}#{w.association} at #{w.line} using #{w.rule}, may be exec'ing #{ActiveRecord::Base.connection.last_query}" if Time.now - w.last > 20
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def sync
|
27
|
+
@mutex.synchronize do
|
28
|
+
yield self
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def trace(event, file, line, id, binding, clazz)
|
33
|
+
if clazz == Ocelot::Processor
|
34
|
+
@last = Time.now
|
35
|
+
@line = line
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Processor < Base
|
41
|
+
attr_accessor :seeds
|
42
|
+
attr_accessor :classes
|
43
|
+
attr_accessor :extra_rules
|
44
|
+
attr_accessor :extra_filters
|
45
|
+
attr_reader :watcher
|
46
|
+
attr_reader :saved
|
47
|
+
|
48
|
+
def initialize
|
49
|
+
@extra_rules = {}
|
50
|
+
@extra_filters = {}
|
51
|
+
@seeds = []
|
52
|
+
@unrecognised = Set.new
|
53
|
+
@watcher = Watcher.new
|
54
|
+
end
|
55
|
+
|
56
|
+
def process_filter(params)
|
57
|
+
obj = params.delete :obj
|
58
|
+
collection = params.delete :collection
|
59
|
+
name = params.delete :name
|
60
|
+
|
61
|
+
result = nil
|
62
|
+
filter = @filters[obj.class]
|
63
|
+
|
64
|
+
if !filter.nil?
|
65
|
+
watcher.sync do |w|
|
66
|
+
w.obj = obj
|
67
|
+
w.association = name
|
68
|
+
w.rule = filter
|
69
|
+
end
|
70
|
+
|
71
|
+
logger.info "Filtering #{obj_to_s(obj)}.#{name} via #{filter.class}"
|
72
|
+
result = filter.load(obj, collection, name)
|
73
|
+
elsif !@unrecognised.include? obj.class
|
74
|
+
logger.error "Unrecognised type #{obj.class}"
|
75
|
+
@unrecognised << obj.class
|
76
|
+
end
|
77
|
+
result || []
|
78
|
+
end
|
79
|
+
|
80
|
+
def process_rule(params)
|
81
|
+
obj = params.delete :obj
|
82
|
+
src = params.delete :src
|
83
|
+
target = params.delete :target
|
84
|
+
calller = params.delete :caller
|
85
|
+
|
86
|
+
rule = @rules[obj.class]
|
87
|
+
|
88
|
+
watcher.sync do |w|
|
89
|
+
w.obj = obj
|
90
|
+
w.association = nil
|
91
|
+
w.rule = rule
|
92
|
+
end
|
93
|
+
|
94
|
+
if !rule.nil?
|
95
|
+
logger.info "Saving #{obj_to_s(obj)} thru #{rule.class} via #{calller}"
|
96
|
+
return rule.save(obj, src, target)
|
97
|
+
elsif !@unrecognised.include? obj.class
|
98
|
+
logger.warn "Unrecognised type #{obj.class}"
|
99
|
+
@unrecognised << obj.class
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def go
|
104
|
+
default_rule = Rules::DefaultRule.new
|
105
|
+
default_filter = Rules::Filter.new
|
106
|
+
|
107
|
+
@rules = Hash.new
|
108
|
+
@filters = Hash.new
|
109
|
+
@saved = Set.new
|
110
|
+
@inspected = Set.new
|
111
|
+
|
112
|
+
array_rule = ArrayRule.new
|
113
|
+
|
114
|
+
@classes.each { |c| @rules[c] = default_rule; @filters[c] = default_filter }
|
115
|
+
@rules.merge! @extra_rules
|
116
|
+
@filters.merge! @extra_filters
|
117
|
+
seeds = @seeds.collect { |o| [o, "script seed"] }
|
118
|
+
|
119
|
+
Graph::Traverser.run(seeds) do |objs, queue|
|
120
|
+
obj, calller = *objs
|
121
|
+
|
122
|
+
unless in? @saved, obj
|
123
|
+
@inspected << obj unless process_rule(:obj => obj, :caller => calller)
|
124
|
+
end
|
125
|
+
|
126
|
+
unless in? @inspected, obj
|
127
|
+
obj.class.reflect_on_all_associations.each do |assoc|
|
128
|
+
safely obj do
|
129
|
+
connection.use_source = true
|
130
|
+
value = obj.send(assoc.name)
|
131
|
+
|
132
|
+
unless value.nil?
|
133
|
+
|
134
|
+
# simple has_one (A->B handling), save B and conditionally inspect
|
135
|
+
if !assoc.collection?
|
136
|
+
unless in? @saved, value
|
137
|
+
process_rule(:obj => value, :caller => calller) and (queue << [value, obj_to_s(obj, assoc)])
|
138
|
+
end
|
139
|
+
# more complicated has many (A-B* and A-mapobj-B*), saving and inspecting each child
|
140
|
+
# monkeypatching the array_rule must yield B and whether to inspect it
|
141
|
+
# be very very very careful, as it handles saving objects whie yielding results
|
142
|
+
else
|
143
|
+
array_rule.save(obj, value, assoc, self) do |child, inspect|
|
144
|
+
@saved << child
|
145
|
+
if inspect
|
146
|
+
queue << [child, obj_to_s(obj, assoc)]
|
147
|
+
else
|
148
|
+
@inspected << child
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
class ArrayRule < Rules::Rule
|
160
|
+
include Ocelot
|
161
|
+
|
162
|
+
def save(obj, array, assoc, c)
|
163
|
+
safely obj, assoc.name do
|
164
|
+
c.process_filter(:obj => obj, :collection => array, :name => assoc.name).each do |val|
|
165
|
+
unless in? c.saved, val
|
166
|
+
use_source_db!
|
167
|
+
inspect = c.process_rule(:obj => val, :caller => obj_to_s(obj))
|
168
|
+
yield val, inspect
|
169
|
+
end
|
170
|
+
|
171
|
+
use_target_db!
|
172
|
+
if assoc.is_a? ActiveRecord::Reflection::ThroughReflection
|
173
|
+
safely assoc.through_reflection.klass, assoc.name, val do
|
174
|
+
c.process_rule :obj => assoc.through_reflection.klass.new, :src => obj, :target => val, :caller => obj_to_s(obj,assoc)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
data/lib/ocelot/rules.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
module Ocelot
|
2
|
+
module Rules
|
3
|
+
class Filter < Ocelot::Base
|
4
|
+
def filter(value)
|
5
|
+
true
|
6
|
+
end
|
7
|
+
|
8
|
+
def load(obj, collection, name)
|
9
|
+
result = []
|
10
|
+
collection.each do |val|
|
11
|
+
safely(obj, name) do
|
12
|
+
result << val if filter val
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
class Rule < Base
|
20
|
+
def relationships(obj)
|
21
|
+
obj.class.reflect_on_all_associations.collect { |assoc| assoc.name }
|
22
|
+
end
|
23
|
+
|
24
|
+
def use_target_db!
|
25
|
+
Ocelot::connection.use_source = false
|
26
|
+
end
|
27
|
+
|
28
|
+
def use_source_db!
|
29
|
+
Ocelot::connection.use_source = true
|
30
|
+
end
|
31
|
+
|
32
|
+
def save_to_db(obj, *extra)
|
33
|
+
begin
|
34
|
+
use_target_db!
|
35
|
+
obj.save(false)
|
36
|
+
ensure
|
37
|
+
use_source_db!
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class DefaultRule < Rule
|
43
|
+
def save(obj, *extra)
|
44
|
+
id = obj.id
|
45
|
+
obj = obj.clone
|
46
|
+
obj.id = id
|
47
|
+
remove_callbacks(obj)
|
48
|
+
save_to_db(obj)
|
49
|
+
true
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ocelot
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.0
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- spencer p
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-04-17 00:00:00 -04:00
|
14
|
+
default_executable:
|
15
|
+
dependencies: []
|
16
|
+
|
17
|
+
description: Extreme alpha
|
18
|
+
email:
|
19
|
+
- exussum@gmail.com
|
20
|
+
executables: []
|
21
|
+
|
22
|
+
extensions: []
|
23
|
+
|
24
|
+
extra_rdoc_files: []
|
25
|
+
|
26
|
+
files:
|
27
|
+
- lib/ocelot/cli.rb
|
28
|
+
- lib/ocelot/database.rb
|
29
|
+
- lib/ocelot/dsl.rb
|
30
|
+
- lib/ocelot/graph.rb
|
31
|
+
- lib/ocelot/processor.rb
|
32
|
+
- lib/ocelot/rules.rb
|
33
|
+
- lib/ocelot.rb
|
34
|
+
has_rdoc: true
|
35
|
+
homepage: ""
|
36
|
+
licenses: []
|
37
|
+
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
none: false
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: "0"
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: "0"
|
55
|
+
requirements: []
|
56
|
+
|
57
|
+
rubyforge_project:
|
58
|
+
rubygems_version: 1.6.2
|
59
|
+
signing_key:
|
60
|
+
specification_version: 3
|
61
|
+
summary: DBA like tool to copy a slice of data from one db to another
|
62
|
+
test_files: []
|
63
|
+
|