replicator 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/.rbenv-gemsets +1 -0
- data/.rbenv-version +1 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.rdoc +47 -0
- data/Rakefile +10 -0
- data/VERSION +1 -0
- data/lib/replicator.rb +299 -0
- data/replicate.gemspec +24 -0
- data/test/replicator_test.rb +24 -0
- data/test/test_helper.rb +6 -0
- metadata +153 -0
data/.gitignore
ADDED
data/.rbenv-gemsets
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
replicator
|
data/.rbenv-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ree-1.8.7-2012.02
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Justin Balthrop
|
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.rdoc
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
= Replicator
|
2
|
+
|
3
|
+
Easy Postgres replication for Rails. Replicates specific columns from one table to
|
4
|
+
another using auto-generated triggers. Just add simple directives in your migrations.
|
5
|
+
|
6
|
+
== Usage:
|
7
|
+
|
8
|
+
ActiveRecord::Migration.extend(Replicator)
|
9
|
+
|
10
|
+
class AddReplication < ActiveRecord::Migration
|
11
|
+
def self.up
|
12
|
+
replicate :names,
|
13
|
+
:to => :users,
|
14
|
+
:fields => [:first_name, :last_name]
|
15
|
+
|
16
|
+
replicate :data,
|
17
|
+
:to => :users,
|
18
|
+
:fields => [:eye_color, :height]
|
19
|
+
:key => 'user_id',
|
20
|
+
:prefix => 'data'
|
21
|
+
|
22
|
+
replicate :events,
|
23
|
+
:to => :users,
|
24
|
+
:fields => {:start_year => :year},
|
25
|
+
:key => 'user_id',
|
26
|
+
:prefix => 'type',
|
27
|
+
:prefix_map => {'BirthEvent' => 'birth', 'GraduationEvent' => 'grad'}
|
28
|
+
|
29
|
+
replicate :locations,
|
30
|
+
:to => :users,
|
31
|
+
:fields => [:latitude, :longitude, {[:city, :state, :country] => :location}],
|
32
|
+
:through => 'events.address_id',
|
33
|
+
:key => 'user_id',
|
34
|
+
:prefix => 'events.type',
|
35
|
+
:prefix_map => {'BirthEvent' => 'birth', 'GraduationEvent' => 'grad'}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
There are a lot of options. The code is mostly self explanatory.
|
40
|
+
|
41
|
+
== Install:
|
42
|
+
|
43
|
+
gem install replicator
|
44
|
+
|
45
|
+
== License:
|
46
|
+
|
47
|
+
Copyright (c) 2009 Justin Balthrop, Geni.com; Published under The MIT License, see License.txt
|
data/Rakefile
ADDED
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/lib/replicator.rb
ADDED
@@ -0,0 +1,299 @@
|
|
1
|
+
module Replicator
|
2
|
+
def replicate(table, opts)
|
3
|
+
action = opts.delete(:action) || :create
|
4
|
+
trigger = Trigger.new(table, opts)
|
5
|
+
|
6
|
+
case action
|
7
|
+
when :create
|
8
|
+
execute(trigger.create_sql)
|
9
|
+
when :drop
|
10
|
+
execute(trigger.drop_sql)
|
11
|
+
when :initialize
|
12
|
+
sql_by_slice = trigger.initialize_sql
|
13
|
+
sql_by_slice.each do |slice, sql|
|
14
|
+
execute("DROP TABLE IF EXISTS #{slice[:name]}")
|
15
|
+
execute(sql)
|
16
|
+
end
|
17
|
+
replicated_table_slices(trigger.to).concat(sql_by_slice.keys)
|
18
|
+
else
|
19
|
+
raise "invalid action: #{action}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def create_replicated_table(table_name)
|
24
|
+
table = nil
|
25
|
+
fields = []
|
26
|
+
joins = []
|
27
|
+
|
28
|
+
replicated_table_slices(table_name).each do |slice|
|
29
|
+
if table
|
30
|
+
joins << "LEFT OUTER JOIN #{slice[:name]} ON #{table}.id = #{slice[:name]}.id"
|
31
|
+
else
|
32
|
+
table = slice[:name]
|
33
|
+
fields << "#{table}.id"
|
34
|
+
end
|
35
|
+
fields << slice[:fields].collect {|f| "#{slice[:name]}.#{f}"}
|
36
|
+
end
|
37
|
+
|
38
|
+
execute %{
|
39
|
+
CREATE TABLE #{table_name} AS
|
40
|
+
SELECT #{fields.flatten.join(', ')}
|
41
|
+
FROM #{table}
|
42
|
+
#{joins.join(' ')}
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
def replicated_table_slices(table_name = nil)
|
47
|
+
if table_name
|
48
|
+
replicated_table_slices[table_name.to_sym] ||= []
|
49
|
+
else
|
50
|
+
@replicated_table_slices ||= {}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class Trigger
|
55
|
+
attr_reader :from, :to, :key, :through_table, :through_key, :condition, :prefix, :prefix_map, :dependent
|
56
|
+
|
57
|
+
def initialize(table, opts)
|
58
|
+
@from = table
|
59
|
+
@to = opts[:to]
|
60
|
+
@name = opts[:name]
|
61
|
+
@key = opts[:key] || 'id'
|
62
|
+
@condition = opts[:condition] || opts[:if]
|
63
|
+
@prefix = opts[:prefix]
|
64
|
+
@prefix_map = opts[:prefix_map]
|
65
|
+
@timestamps = opts[:timestamps]
|
66
|
+
@dependent = opts[:dependent]
|
67
|
+
|
68
|
+
if opts[:through]
|
69
|
+
@through_table, @through_key = opts[:through].split('.')
|
70
|
+
raise "through must be of the form 'table.field'" unless @through_table and @through_key
|
71
|
+
end
|
72
|
+
|
73
|
+
# Use opts[:prefixes] to specify valid prefixes and use the identity mapping.
|
74
|
+
if @prefix_map.nil? and opts[:prefixes]
|
75
|
+
@prefix_map = {}
|
76
|
+
opts[:prefixes].each do |prefix|
|
77
|
+
@prefix_map[prefix] = prefix
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
if @prefix_map
|
82
|
+
@prefix, prefix_table = @prefix.split('.').reverse
|
83
|
+
|
84
|
+
if prefix_table.nil? or prefix_table == from
|
85
|
+
@prefix_through = false
|
86
|
+
elsif prefix_table == through_table
|
87
|
+
@prefix_through = true
|
88
|
+
else
|
89
|
+
"unknown prefix table: #{prefix_table}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
@fields = {}
|
94
|
+
opts[:fields] = [opts[:fields]] unless opts[:fields].kind_of?(Array)
|
95
|
+
opts[:fields].each do |field|
|
96
|
+
if field.kind_of?(Hash)
|
97
|
+
@fields.merge!(field)
|
98
|
+
else
|
99
|
+
@fields[field] = field
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def fields(opts = nil)
|
105
|
+
if opts
|
106
|
+
opts[:row] ||= 'ROW'
|
107
|
+
# Add the prefixes and return an array of arrays.
|
108
|
+
@fields.collect do |from_field, to_field|
|
109
|
+
if from_field.kind_of?(Array)
|
110
|
+
from_field = from_field.collect {|f| "COALESCE(#{opts[:row]}.#{f}, '')"}.join(" || ' ' || ")
|
111
|
+
else
|
112
|
+
from_field = "#{opts[:row]}.#{from_field}"
|
113
|
+
end
|
114
|
+
to_field = [opts[:prefix], to_field].compact.join('_')
|
115
|
+
[from_field, to_field]
|
116
|
+
end
|
117
|
+
else
|
118
|
+
# Just return the hash.
|
119
|
+
@fields
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def create_sql
|
124
|
+
%{
|
125
|
+
CREATE OR REPLACE FUNCTION #{name}() RETURNS TRIGGER AS $$
|
126
|
+
DECLARE
|
127
|
+
ROW RECORD;
|
128
|
+
THROUGH RECORD;
|
129
|
+
BEGIN
|
130
|
+
IF (TG_OP = 'DELETE') THEN
|
131
|
+
ROW := OLD;
|
132
|
+
ELSE
|
133
|
+
ROW := NEW;
|
134
|
+
END IF;
|
135
|
+
#{loop_sql}
|
136
|
+
IF #{conditions_sql} THEN
|
137
|
+
IF (TG_OP = 'DELETE') THEN
|
138
|
+
#{delete_sql(:indent => 18)}
|
139
|
+
ELSE
|
140
|
+
IF COUNT(*) = 0 FROM #{to} WHERE id = #{primary_key} THEN
|
141
|
+
#{insert_sql}
|
142
|
+
END IF;
|
143
|
+
#{update_all_sql(:indent => 18)}
|
144
|
+
END IF;
|
145
|
+
END IF;
|
146
|
+
#{end_loop_sql}
|
147
|
+
RETURN NULL;
|
148
|
+
END;
|
149
|
+
$$ LANGUAGE plpgsql;
|
150
|
+
CREATE TRIGGER #{name} AFTER INSERT OR UPDATE OR DELETE ON #{from}
|
151
|
+
FOR EACH ROW EXECUTE PROCEDURE #{name}();
|
152
|
+
}
|
153
|
+
end
|
154
|
+
|
155
|
+
def initialize_sql
|
156
|
+
sql_by_slice = {}
|
157
|
+
|
158
|
+
if through?
|
159
|
+
tables = "#{from}, #{through_table}"
|
160
|
+
join = "#{from}.id = #{through_table}.#{through_key}"
|
161
|
+
else
|
162
|
+
tables = from
|
163
|
+
end
|
164
|
+
|
165
|
+
if prefix_map
|
166
|
+
prefix_map.each do |prefix_value, mapping|
|
167
|
+
slice_fields = []
|
168
|
+
field_sql = fields(:prefix => mapping, :row => from).collect do |from_field, to_field|
|
169
|
+
slice_fields << to_field
|
170
|
+
"#{from_field} AS #{to_field}"
|
171
|
+
end.join(', ')
|
172
|
+
where_sql = "WHERE " << [join, "#{prefix_field(true)} = '#{prefix_value}'"].compact.join(' AND ')
|
173
|
+
table_name = "#{name}_#{mapping}"
|
174
|
+
|
175
|
+
slice = {:name => table_name, :fields => slice_fields}
|
176
|
+
sql_by_slice[slice] = %{
|
177
|
+
CREATE TABLE #{table_name} AS
|
178
|
+
SELECT #{primary_key(true)} AS id, #{field_sql} FROM #{tables} #{where_sql}
|
179
|
+
}
|
180
|
+
end
|
181
|
+
else
|
182
|
+
slice_fields = []
|
183
|
+
field_sql = fields(:prefix => prefix, :row => from).collect do |from_field, to_field|
|
184
|
+
slice_fields << to_field
|
185
|
+
"#{from_field} AS #{to_field}"
|
186
|
+
end.join(', ')
|
187
|
+
where_sql = "WHERE #{join}" if join
|
188
|
+
|
189
|
+
slice = {:name => name, :fields => slice_fields}
|
190
|
+
sql_by_slice[slice] = %{
|
191
|
+
CREATE TABLE #{name} AS
|
192
|
+
SELECT #{primary_key(true)} AS id, #{field_sql} FROM #{tables} #{where_sql}
|
193
|
+
}
|
194
|
+
end
|
195
|
+
sql_by_slice
|
196
|
+
end
|
197
|
+
|
198
|
+
def drop_sql
|
199
|
+
"DROP FUNCTION IF EXISTS #{name}() CASCADE"
|
200
|
+
end
|
201
|
+
|
202
|
+
def timestamps?
|
203
|
+
@timestamps
|
204
|
+
end
|
205
|
+
|
206
|
+
def through?
|
207
|
+
not through_table.nil?
|
208
|
+
end
|
209
|
+
|
210
|
+
def prefix_through?
|
211
|
+
@prefix_through
|
212
|
+
end
|
213
|
+
|
214
|
+
private
|
215
|
+
|
216
|
+
def prefix_field(initialize = false)
|
217
|
+
if initialize
|
218
|
+
"#{prefix_through? ? through_table : from}.#{prefix}"
|
219
|
+
else
|
220
|
+
"#{prefix_through? ? 'THROUGH' : 'ROW'}.#{prefix}"
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def primary_key(initialize = false)
|
225
|
+
if initialize
|
226
|
+
"#{through? ? through_table : from}.#{key}"
|
227
|
+
else
|
228
|
+
@primary_key ||= "#{through? ? 'THROUGH' : 'ROW'}.#{key}"
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def name
|
233
|
+
@name ||= "replicate_#{from}_to_#{to}"
|
234
|
+
end
|
235
|
+
|
236
|
+
def loop_sql
|
237
|
+
"FOR THROUGH IN SELECT * FROM #{through_table} WHERE #{through_key} = ROW.id LOOP" if through?
|
238
|
+
end
|
239
|
+
|
240
|
+
def end_loop_sql
|
241
|
+
"END LOOP;" if through?
|
242
|
+
end
|
243
|
+
|
244
|
+
def conditions_sql
|
245
|
+
conditions = []
|
246
|
+
conditions << "#{primary_key} IS NOT NULL"
|
247
|
+
conditions << "#{prefix_field} IN (#{prefixes_sql})" if prefix_map
|
248
|
+
conditions << condition if condition
|
249
|
+
conditions.join(' AND ')
|
250
|
+
end
|
251
|
+
|
252
|
+
def prefixes_sql
|
253
|
+
prefix_map.keys.collect {|p| "'#{p}'"}.join(',')
|
254
|
+
end
|
255
|
+
|
256
|
+
def insert_sql
|
257
|
+
if timestamps?
|
258
|
+
"INSERT INTO #{to} (id, created_at) VALUES (#{primary_key}, NOW());"
|
259
|
+
else
|
260
|
+
"INSERT INTO #{to} (id) VALUES (#{primary_key});"
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def update_sql(opts = {})
|
265
|
+
updates = fields(:prefix => opts[:prefix]).collect do |from_field, to_field|
|
266
|
+
from_field = 'NULL' if opts[:clear]
|
267
|
+
"#{to_field} = #{from_field}"
|
268
|
+
end
|
269
|
+
updates << "updated_at = NOW()" if timestamps?
|
270
|
+
sql = "SET #{updates.join(', ')}"
|
271
|
+
sql = "UPDATE #{to} #{sql} WHERE #{to}.id = #{primary_key};"
|
272
|
+
sql
|
273
|
+
end
|
274
|
+
|
275
|
+
def update_all_sql(opts = {})
|
276
|
+
return update_sql(opts.merge(:prefix => prefix)) unless prefix_map
|
277
|
+
|
278
|
+
sql = ''
|
279
|
+
opts[:indent] ||= 0
|
280
|
+
newline = "\n#{' ' * opts[:indent]}"
|
281
|
+
cond = 'IF'
|
282
|
+
prefix_map.each do |prefix_value, mapping|
|
283
|
+
sql << "#{cond} #{prefix_field} = '#{prefix_value}' THEN" + newline
|
284
|
+
sql << " #{update_sql(opts.merge(:prefix => mapping))}" + newline
|
285
|
+
cond = 'ELSIF'
|
286
|
+
end
|
287
|
+
sql << "END IF;"
|
288
|
+
sql
|
289
|
+
end
|
290
|
+
|
291
|
+
def delete_sql(opts = {})
|
292
|
+
if dependent == :destroy
|
293
|
+
"DELETE FROM #{to} WHERE id = #{primary_key};"
|
294
|
+
elsif dependent == :nullify
|
295
|
+
update_all_sql(:indent => opts[:indent], :clear => true)
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
data/replicate.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.name = "replicator"
|
6
|
+
gem.version = IO.read('VERSION')
|
7
|
+
gem.authors = ["Justin Balthrop"]
|
8
|
+
gem.email = ["git@justinbalthrop.com"]
|
9
|
+
gem.description = %q{Easy Postgres replication for Rails}
|
10
|
+
gem.summary = gem.description
|
11
|
+
gem.homepage = "https://github.com/ninjudd/replicator"
|
12
|
+
|
13
|
+
gem.add_development_dependency 'shoulda', '3.0.1'
|
14
|
+
gem.add_development_dependency 'mocha'
|
15
|
+
gem.add_development_dependency 'rake'
|
16
|
+
gem.add_development_dependency 'activerecord-postgresql-adapter'
|
17
|
+
|
18
|
+
gem.add_dependency 'activerecord', '~> 2.3.9'
|
19
|
+
|
20
|
+
gem.files = `git ls-files`.split($/)
|
21
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
22
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
23
|
+
gem.require_paths = ["lib"]
|
24
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class ReplicatorTest < Test::Unit::TestCase
|
4
|
+
def test_sql_update_all
|
5
|
+
t = Replicator::Trigger.new :locations,
|
6
|
+
:to => :users,
|
7
|
+
:fields => [:latitude, :longitude, {[:city, :state, :country] => :location}],
|
8
|
+
:through => 'events.address_id',
|
9
|
+
:key => 'user_id',
|
10
|
+
:prefix => 'events.type',
|
11
|
+
:prefix_map => {'BirthEvent' => 'birth', 'GraduationEvent' => 'grad'}
|
12
|
+
|
13
|
+
sql = t.create_sql
|
14
|
+
# This is not a good to test this. I should actually add the triggers and check that they work.
|
15
|
+
assert_match 'FOR THROUGH IN SELECT * FROM events WHERE address_id = ROW.id LOOP', sql
|
16
|
+
assert_match 'INSERT INTO users (id) VALUES (THROUGH.user_id)', sql
|
17
|
+
['birth', 'grad'].each do |prefix|
|
18
|
+
city_state_country = ['city', 'state', 'country'].map {|f| "COALESCE(ROW.#{f}, '')"}.join(" || ' ' || ")
|
19
|
+
assert_match "#{prefix}_location = " + city_state_country, sql
|
20
|
+
assert_match "#{prefix}_latitude = ROW.latitude", sql
|
21
|
+
assert_match "#{prefix}_longitude = ROW.longitude", sql
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: replicator
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Justin Balthrop
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2013-06-03 00:00:00 -07:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: shoulda
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - "="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 5
|
30
|
+
segments:
|
31
|
+
- 3
|
32
|
+
- 0
|
33
|
+
- 1
|
34
|
+
version: 3.0.1
|
35
|
+
type: :development
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: mocha
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 3
|
46
|
+
segments:
|
47
|
+
- 0
|
48
|
+
version: "0"
|
49
|
+
type: :development
|
50
|
+
version_requirements: *id002
|
51
|
+
- !ruby/object:Gem::Dependency
|
52
|
+
name: rake
|
53
|
+
prerelease: false
|
54
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
hash: 3
|
60
|
+
segments:
|
61
|
+
- 0
|
62
|
+
version: "0"
|
63
|
+
type: :development
|
64
|
+
version_requirements: *id003
|
65
|
+
- !ruby/object:Gem::Dependency
|
66
|
+
name: activerecord-postgresql-adapter
|
67
|
+
prerelease: false
|
68
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
hash: 3
|
74
|
+
segments:
|
75
|
+
- 0
|
76
|
+
version: "0"
|
77
|
+
type: :development
|
78
|
+
version_requirements: *id004
|
79
|
+
- !ruby/object:Gem::Dependency
|
80
|
+
name: activerecord
|
81
|
+
prerelease: false
|
82
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ~>
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
hash: 17
|
88
|
+
segments:
|
89
|
+
- 2
|
90
|
+
- 3
|
91
|
+
- 9
|
92
|
+
version: 2.3.9
|
93
|
+
type: :runtime
|
94
|
+
version_requirements: *id005
|
95
|
+
description: Easy Postgres replication for Rails
|
96
|
+
email:
|
97
|
+
- git@justinbalthrop.com
|
98
|
+
executables: []
|
99
|
+
|
100
|
+
extensions: []
|
101
|
+
|
102
|
+
extra_rdoc_files: []
|
103
|
+
|
104
|
+
files:
|
105
|
+
- .gitignore
|
106
|
+
- .rbenv-gemsets
|
107
|
+
- .rbenv-version
|
108
|
+
- Gemfile
|
109
|
+
- LICENSE
|
110
|
+
- README.rdoc
|
111
|
+
- Rakefile
|
112
|
+
- VERSION
|
113
|
+
- lib/replicator.rb
|
114
|
+
- replicate.gemspec
|
115
|
+
- test/replicator_test.rb
|
116
|
+
- test/test_helper.rb
|
117
|
+
has_rdoc: true
|
118
|
+
homepage: https://github.com/ninjudd/replicator
|
119
|
+
licenses: []
|
120
|
+
|
121
|
+
post_install_message:
|
122
|
+
rdoc_options: []
|
123
|
+
|
124
|
+
require_paths:
|
125
|
+
- lib
|
126
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
127
|
+
none: false
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
hash: 3
|
132
|
+
segments:
|
133
|
+
- 0
|
134
|
+
version: "0"
|
135
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
136
|
+
none: false
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
hash: 3
|
141
|
+
segments:
|
142
|
+
- 0
|
143
|
+
version: "0"
|
144
|
+
requirements: []
|
145
|
+
|
146
|
+
rubyforge_project:
|
147
|
+
rubygems_version: 1.5.2
|
148
|
+
signing_key:
|
149
|
+
specification_version: 3
|
150
|
+
summary: Easy Postgres replication for Rails
|
151
|
+
test_files:
|
152
|
+
- test/replicator_test.rb
|
153
|
+
- test/test_helper.rb
|