mosql 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.travis.yml +11 -0
- data/Gemfile.lock +1 -1
- data/README.md +38 -5
- data/Rakefile +1 -1
- data/bin/mosql +0 -2
- data/lib/mosql/cli.rb +18 -12
- data/lib/mosql/schema.rb +73 -16
- data/lib/mosql/sql.rb +9 -8
- data/lib/mosql/version.rb +1 -1
- data/test/_lib.rb +0 -3
- data/test/functional/cli.rb +77 -0
- data/test/functional/schema.rb +39 -1
- data/test/functional/sql.rb +4 -4
- data/test/unit/lib/mosql/schema.rb +198 -0
- metadata +6 -4
- data/test/unit/lib/mongo-sql/schema.rb +0 -102
data/.travis.yml
ADDED
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -53,8 +53,15 @@ types. An example collection map might be:
|
|
53
53
|
mongodb:
|
54
54
|
blog_posts:
|
55
55
|
:columns:
|
56
|
-
-
|
57
|
-
|
56
|
+
- id:
|
57
|
+
:source: _id
|
58
|
+
:type: TEXT
|
59
|
+
- author_name:
|
60
|
+
:source: author.name
|
61
|
+
:type: TEXT
|
62
|
+
- author_bio:
|
63
|
+
:source: author.bio
|
64
|
+
:type: TEXT
|
58
65
|
- title: TEXT
|
59
66
|
- created: DOUBLE PRECISION
|
60
67
|
:meta:
|
@@ -67,9 +74,33 @@ mapping
|
|
67
74
|
<Mongo DB name> -> { <Mongo Collection Name> -> <Collection Definition> }
|
68
75
|
|
69
76
|
Where a `<Collection Definition>` is a hash with `:columns` and
|
70
|
-
`:meta` fields.
|
71
|
-
|
72
|
-
|
77
|
+
`:meta` fields.
|
78
|
+
|
79
|
+
`:columns` is a list of hashes mapping SQL column names to an hash
|
80
|
+
describing that column. This hash may contain the following fields:
|
81
|
+
|
82
|
+
* `:source`: The name of the attribute inside of MongoDB.
|
83
|
+
* `:type`: (Mandatory) The SQL type.
|
84
|
+
|
85
|
+
|
86
|
+
Use of the `:source` attribute allows for renaming attributes, and
|
87
|
+
extracting elements of a nested hash using MongoDB's
|
88
|
+
[dot notation][dot-notation]. In the above example, the `name` and
|
89
|
+
`bio` fields of the `author` sub-document will be expanded, and the
|
90
|
+
MongoDB `_id` field will be mapped to an SQL `id` column.
|
91
|
+
|
92
|
+
At present, MoSQL does not support using the dot notation to access
|
93
|
+
elements inside arrays.
|
94
|
+
|
95
|
+
As a shorthand, you can specify a one-elment hash of the form `name:
|
96
|
+
TYPE`, in which case `name` will be used for both the source attribute
|
97
|
+
and the name of the destination column. You can see this shorthand for
|
98
|
+
the `title` and `created` attributes, above.
|
99
|
+
|
100
|
+
Every defined collection must include a mapping for the `_id`
|
101
|
+
attribute.
|
102
|
+
|
103
|
+
`:meta` contains metadata about this collection/table. It is
|
73
104
|
required to include at least `:table`, naming the SQL table this
|
74
105
|
collection will be mapped to. `extra_props` determines the handling of
|
75
106
|
unknown fields in MongoDB objects -- more about that later.
|
@@ -78,6 +109,8 @@ By default, `mosql` looks for a collection map in a file named
|
|
78
109
|
`collections.yml` in your current working directory, but you can
|
79
110
|
specify a different one with `-c` or `--collections`.
|
80
111
|
|
112
|
+
[dot-notation]: http://docs.mongodb.org/manual/core/document/#dot-notation
|
113
|
+
|
81
114
|
## Usage
|
82
115
|
|
83
116
|
Once you have a collection map. MoSQL usage is easy. The basic form
|
data/Rakefile
CHANGED
data/bin/mosql
CHANGED
data/lib/mosql/cli.rb
CHANGED
@@ -106,8 +106,11 @@ module MoSQL
|
|
106
106
|
def connect_mongo
|
107
107
|
@mongo = Mongo::Connection.from_uri(options[:mongo])
|
108
108
|
config = @mongo['admin'].command(:ismaster => 1)
|
109
|
-
if !config['setName']
|
110
|
-
log.warn("`#{options[:mongo]}' is not a replset.
|
109
|
+
if !config['setName'] && !options[:skip_tail]
|
110
|
+
log.warn("`#{options[:mongo]}' is not a replset.")
|
111
|
+
log.warn("Will run the initial import, then stop.")
|
112
|
+
log.warn("Pass `--skip-tail' to suppress this warning.")
|
113
|
+
options[:skip_tail] = true
|
111
114
|
end
|
112
115
|
options[:service] ||= config['setName']
|
113
116
|
end
|
@@ -121,7 +124,7 @@ module MoSQL
|
|
121
124
|
end
|
122
125
|
|
123
126
|
def load_collections
|
124
|
-
collections = YAML.
|
127
|
+
collections = YAML.load_file(@options[:collections])
|
125
128
|
@schemamap = MoSQL::Schema.new(collections)
|
126
129
|
end
|
127
130
|
|
@@ -140,7 +143,9 @@ module MoSQL
|
|
140
143
|
initial_import
|
141
144
|
end
|
142
145
|
|
143
|
-
|
146
|
+
unless options[:skip_tail]
|
147
|
+
optail
|
148
|
+
end
|
144
149
|
end
|
145
150
|
|
146
151
|
# Helpers
|
@@ -159,7 +164,7 @@ module MoSQL
|
|
159
164
|
items.each do |it|
|
160
165
|
h = {}
|
161
166
|
cols.zip(it).each { |k,v| h[k] = v }
|
162
|
-
@sql.upsert(table, h)
|
167
|
+
@sql.upsert(table, @schemamap.primary_sql_key_for_ns(ns), h)
|
163
168
|
end
|
164
169
|
end
|
165
170
|
end
|
@@ -189,7 +194,9 @@ module MoSQL
|
|
189
194
|
def initial_import
|
190
195
|
@schemamap.create_schema(@sql.db, !options[:no_drop_tables])
|
191
196
|
|
192
|
-
|
197
|
+
unless options[:skip_tail]
|
198
|
+
start_ts = @mongo['local']['oplog.rs'].find_one({}, {:sort => [['$natural', -1]]})['ts']
|
199
|
+
end
|
193
200
|
|
194
201
|
want_dbs = @schemamap.all_mongo_dbs & @mongo.database_names
|
195
202
|
want_dbs.each do |dbname|
|
@@ -203,7 +210,7 @@ module MoSQL
|
|
203
210
|
end
|
204
211
|
end
|
205
212
|
|
206
|
-
tailer.write_timestamp(start_ts)
|
213
|
+
tailer.write_timestamp(start_ts) unless options[:skip_tail]
|
207
214
|
end
|
208
215
|
|
209
216
|
def import_collection(ns, collection)
|
@@ -240,8 +247,6 @@ module MoSQL
|
|
240
247
|
end
|
241
248
|
|
242
249
|
def optail
|
243
|
-
return if options[:skip_tail]
|
244
|
-
|
245
250
|
tailer.tail_from(options[:tail_from] ?
|
246
251
|
BSON::Timestamp.new(options[:tail_from].to_i, 0) :
|
247
252
|
nil)
|
@@ -253,12 +258,13 @@ module MoSQL
|
|
253
258
|
end
|
254
259
|
|
255
260
|
def sync_object(ns, _id)
|
256
|
-
|
257
|
-
|
261
|
+
primary_sql_key = @schemamap.primary_sql_key_for_ns(ns)
|
262
|
+
sqlid = @sql.transform_one_ns(ns, { '_id' => _id })[primary_sql_key]
|
263
|
+
obj = collection_for_ns(ns).find_one({:_id => _id})
|
258
264
|
if obj
|
259
265
|
@sql.upsert_ns(ns, obj)
|
260
266
|
else
|
261
|
-
@sql.table_for_ns(ns).where(
|
267
|
+
@sql.table_for_ns(ns).where(primary_sql_key.to_sym => sqlid).delete()
|
262
268
|
end
|
263
269
|
end
|
264
270
|
|
data/lib/mosql/schema.rb
CHANGED
@@ -4,19 +4,44 @@ module MoSQL
|
|
4
4
|
class Schema
|
5
5
|
include MoSQL::Logging
|
6
6
|
|
7
|
-
def
|
8
|
-
|
7
|
+
def to_array(lst)
|
8
|
+
array = []
|
9
9
|
lst.each do |ent|
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
if ent.is_a?(Hash) && ent[:source].is_a?(String) && ent[:type].is_a?(String)
|
11
|
+
# new configuration format
|
12
|
+
array << {
|
13
|
+
:source => ent.delete(:source),
|
14
|
+
:type => ent.delete(:type),
|
15
|
+
:name => ent.first.first,
|
16
|
+
}
|
17
|
+
elsif ent.is_a?(Hash) && ent.keys.length == 1 && ent.values.first.is_a?(String)
|
18
|
+
array << {
|
19
|
+
:source => ent.first.first,
|
20
|
+
:name => ent.first.first,
|
21
|
+
:type => ent.first.last
|
22
|
+
}
|
23
|
+
else
|
24
|
+
raise "Invalid ordered hash entry #{ent.inspect}"
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
array
|
29
|
+
end
|
30
|
+
|
31
|
+
def check_columns!(ns, spec)
|
32
|
+
seen = Set.new
|
33
|
+
spec[:columns].each do |col|
|
34
|
+
if seen.include?(col[:source])
|
35
|
+
raise "Duplicate source #{col[:source]} in column definition #{col[:name]} for #{ns}."
|
36
|
+
end
|
37
|
+
seen.add(col[:source])
|
13
38
|
end
|
14
|
-
hash
|
15
39
|
end
|
16
40
|
|
17
|
-
def parse_spec(spec)
|
41
|
+
def parse_spec(ns, spec)
|
18
42
|
out = spec.dup
|
19
|
-
out[:columns] =
|
43
|
+
out[:columns] = to_array(spec[:columns])
|
44
|
+
check_columns!(ns, out)
|
20
45
|
out
|
21
46
|
end
|
22
47
|
|
@@ -25,7 +50,7 @@ module MoSQL
|
|
25
50
|
map.each do |dbname, db|
|
26
51
|
@map[dbname] ||= {}
|
27
52
|
db.each do |cname, spec|
|
28
|
-
@map[dbname][cname] = parse_spec(spec)
|
53
|
+
@map[dbname][cname] = parse_spec("#{dbname}.#{cname}", spec)
|
29
54
|
end
|
30
55
|
end
|
31
56
|
end
|
@@ -35,13 +60,16 @@ module MoSQL
|
|
35
60
|
meta = collection[:meta]
|
36
61
|
log.info("Creating table '#{meta[:table]}'...")
|
37
62
|
db.send(clobber ? :create_table! : :create_table?, meta[:table]) do
|
38
|
-
collection[:columns].each do |
|
39
|
-
column
|
63
|
+
collection[:columns].each do |col|
|
64
|
+
column col[:name], col[:type]
|
65
|
+
|
66
|
+
if col[:source].to_sym == :_id
|
67
|
+
primary_key [col[:name].to_sym]
|
68
|
+
end
|
40
69
|
end
|
41
70
|
if meta[:extra_props]
|
42
71
|
column '_extra_props', 'TEXT'
|
43
72
|
end
|
44
|
-
primary_key [:_id]
|
45
73
|
end
|
46
74
|
end
|
47
75
|
end
|
@@ -62,13 +90,36 @@ module MoSQL
|
|
62
90
|
schema
|
63
91
|
end
|
64
92
|
|
93
|
+
def fetch_and_delete_dotted(obj, dotted)
|
94
|
+
pieces = dotted.split(".")
|
95
|
+
breadcrumbs = []
|
96
|
+
while pieces.length > 1
|
97
|
+
key = pieces.shift
|
98
|
+
breadcrumbs << [obj, key]
|
99
|
+
obj = obj[key]
|
100
|
+
return nil unless obj.is_a?(Hash)
|
101
|
+
end
|
102
|
+
|
103
|
+
val = obj.delete(pieces.first)
|
104
|
+
|
105
|
+
breadcrumbs.reverse.each do |obj, key|
|
106
|
+
obj.delete(key) if obj[key].empty?
|
107
|
+
end
|
108
|
+
|
109
|
+
val
|
110
|
+
end
|
111
|
+
|
65
112
|
def transform(ns, obj, schema=nil)
|
66
113
|
schema ||= find_ns!(ns)
|
67
114
|
|
68
115
|
obj = obj.dup
|
69
116
|
row = []
|
70
|
-
schema[:columns].each do |
|
71
|
-
|
117
|
+
schema[:columns].each do |col|
|
118
|
+
|
119
|
+
source = col[:source]
|
120
|
+
type = col[:type]
|
121
|
+
|
122
|
+
v = fetch_and_delete_dotted(obj, source)
|
72
123
|
case v
|
73
124
|
when BSON::Binary, BSON::ObjectId
|
74
125
|
v = v.to_s
|
@@ -91,7 +142,10 @@ module MoSQL
|
|
91
142
|
end
|
92
143
|
|
93
144
|
def all_columns(schema)
|
94
|
-
cols =
|
145
|
+
cols = []
|
146
|
+
schema[:columns].each do |col|
|
147
|
+
cols << col[:name]
|
148
|
+
end
|
95
149
|
if schema[:meta][:extra_props]
|
96
150
|
cols << "_extra_props"
|
97
151
|
end
|
@@ -100,7 +154,6 @@ module MoSQL
|
|
100
154
|
|
101
155
|
def copy_data(db, ns, objs)
|
102
156
|
schema = find_ns!(ns)
|
103
|
-
data = objs.map { |o| transform_to_copy(ns, o, schema) }.join("\n")
|
104
157
|
db.synchronize do |pg|
|
105
158
|
sql = "COPY \"#{schema[:meta][:table]}\" " +
|
106
159
|
"(#{all_columns(schema).map {|c| "\"#{c}\""}.join(",")}) FROM STDIN"
|
@@ -145,5 +198,9 @@ module MoSQL
|
|
145
198
|
def collections_for_mongo_db(db)
|
146
199
|
(@map[db]||{}).keys
|
147
200
|
end
|
201
|
+
|
202
|
+
def primary_sql_key_for_ns(ns)
|
203
|
+
find_ns!(ns)[:columns].find {|c| c[:source] == '_id'}[:name]
|
204
|
+
end
|
148
205
|
end
|
149
206
|
end
|
data/lib/mosql/sql.rb
CHANGED
@@ -35,35 +35,36 @@ module MoSQL
|
|
35
35
|
|
36
36
|
def upsert_ns(ns, obj)
|
37
37
|
h = transform_one_ns(ns, obj)
|
38
|
-
upsert(table_for_ns(ns), h)
|
38
|
+
upsert(table_for_ns(ns), @schema.primary_sql_key_for_ns(ns), h)
|
39
39
|
end
|
40
40
|
|
41
41
|
# obj must contain an _id field. All other fields will be ignored.
|
42
42
|
def delete_ns(ns, obj)
|
43
|
+
primary_sql_key = @schema.primary_sql_key_for_ns(ns)
|
43
44
|
h = transform_one_ns(ns, obj)
|
44
|
-
raise "No
|
45
|
-
table_for_ns(ns).where(
|
45
|
+
raise "No #{primary_sql_key} found in transform of #{obj.inspect}" if h[primary_sql_key].nil?
|
46
|
+
table_for_ns(ns).where(primary_sql_key.to_sym => h[primary_sql_key]).delete
|
46
47
|
end
|
47
48
|
|
48
|
-
def upsert(table, item)
|
49
|
+
def upsert(table, table_primary_key, item)
|
49
50
|
begin
|
50
|
-
upsert!(table, item)
|
51
|
+
upsert!(table, table_primary_key, item)
|
51
52
|
rescue Sequel::DatabaseError => e
|
52
53
|
wrapped = e.wrapped_exception
|
53
54
|
if wrapped.result
|
54
|
-
log.warn("Ignoring row (
|
55
|
+
log.warn("Ignoring row (#{table_primary_key}=#{item[table_primary_key]}): #{e}")
|
55
56
|
else
|
56
57
|
raise e
|
57
58
|
end
|
58
59
|
end
|
59
60
|
end
|
60
61
|
|
61
|
-
def upsert!(table, item)
|
62
|
+
def upsert!(table, table_primary_key, item)
|
62
63
|
begin
|
63
64
|
table.insert(item)
|
64
65
|
rescue Sequel::DatabaseError => e
|
65
66
|
raise e unless e.message =~ /duplicate key value violates unique constraint/
|
66
|
-
table.where(
|
67
|
+
table.where(table_primary_key.to_sym => item[table_primary_key]).update(item)
|
67
68
|
end
|
68
69
|
end
|
69
70
|
end
|
data/lib/mosql/version.rb
CHANGED
data/test/_lib.rb
CHANGED
data/test/functional/cli.rb
CHANGED
@@ -11,6 +11,14 @@ mosql_test:
|
|
11
11
|
:columns:
|
12
12
|
- _id: TEXT
|
13
13
|
- var: INTEGER
|
14
|
+
renameid:
|
15
|
+
:meta:
|
16
|
+
:table: sqltable2
|
17
|
+
:columns:
|
18
|
+
- id:
|
19
|
+
:source: _id
|
20
|
+
:type: TEXT
|
21
|
+
- goats: INTEGER
|
14
22
|
EOF
|
15
23
|
|
16
24
|
def fake_cli
|
@@ -29,6 +37,7 @@ EOF
|
|
29
37
|
@adapter = MoSQL::SQLAdapter.new(@map, sql_test_uri)
|
30
38
|
|
31
39
|
@sequel.drop_table?(:sqltable)
|
40
|
+
@sequel.drop_table?(:sqltable2)
|
32
41
|
@map.create_schema(@sequel)
|
33
42
|
|
34
43
|
@cli = fake_cli
|
@@ -73,4 +82,72 @@ EOF
|
|
73
82
|
})
|
74
83
|
assert_equal(100, sequel[:sqltable].where(:_id => o['_id'].to_s).select.first[:var])
|
75
84
|
end
|
85
|
+
|
86
|
+
it 'handle "u" ops with $set and a renamed _id' do
|
87
|
+
o = { '_id' => BSON::ObjectId.new, 'goats' => 96 }
|
88
|
+
@adapter.upsert_ns('mosql_test.renameid', o)
|
89
|
+
|
90
|
+
# $set's are currently a bit of a hack where we read the object
|
91
|
+
# from the db, so make sure the new object exists in mongo
|
92
|
+
connect_mongo['mosql_test']['renameid'].insert(o.merge('goats' => 0),
|
93
|
+
:w => 1)
|
94
|
+
|
95
|
+
@cli.handle_op({ 'ns' => 'mosql_test.renameid',
|
96
|
+
'op' => 'u',
|
97
|
+
'o2' => { '_id' => o['_id'] },
|
98
|
+
'o' => { '$set' => { 'goats' => 0 } },
|
99
|
+
})
|
100
|
+
assert_equal(0, sequel[:sqltable2].where(:id => o['_id'].to_s).select.first[:goats])
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'handles "d" ops with a renamed id' do
|
104
|
+
o = { '_id' => BSON::ObjectId.new, 'goats' => 1 }
|
105
|
+
@adapter.upsert_ns('mosql_test.renameid', o)
|
106
|
+
|
107
|
+
@cli.handle_op({ 'ns' => 'mosql_test.renameid',
|
108
|
+
'op' => 'd',
|
109
|
+
'o' => { '_id' => o['_id'] },
|
110
|
+
})
|
111
|
+
assert_equal(0, sequel[:sqltable2].where(:id => o['_id'].to_s).count)
|
112
|
+
end
|
113
|
+
|
114
|
+
describe '.bulk_upsert' do
|
115
|
+
it 'inserts multiple rows' do
|
116
|
+
objs = [
|
117
|
+
{ '_id' => BSON::ObjectId.new, 'var' => 0 },
|
118
|
+
{ '_id' => BSON::ObjectId.new, 'var' => 1 },
|
119
|
+
{ '_id' => BSON::ObjectId.new, 'var' => 3 },
|
120
|
+
].map { |o| @map.transform('mosql_test.collection', o) }
|
121
|
+
|
122
|
+
@cli.bulk_upsert(sequel[:sqltable], 'mosql_test.collection',
|
123
|
+
objs)
|
124
|
+
|
125
|
+
assert(sequel[:sqltable].where(:_id => objs[0].first, :var => 0).count)
|
126
|
+
assert(sequel[:sqltable].where(:_id => objs[1].first, :var => 1).count)
|
127
|
+
assert(sequel[:sqltable].where(:_id => objs[2].first, :var => 3).count)
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'upserts' do
|
131
|
+
_id = BSON::ObjectId.new
|
132
|
+
objs = [
|
133
|
+
{ '_id' => _id, 'var' => 0 },
|
134
|
+
{ '_id' => BSON::ObjectId.new, 'var' => 1 },
|
135
|
+
{ '_id' => BSON::ObjectId.new, 'var' => 3 },
|
136
|
+
].map { |o| @map.transform('mosql_test.collection', o) }
|
137
|
+
|
138
|
+
@cli.bulk_upsert(sequel[:sqltable], 'mosql_test.collection',
|
139
|
+
objs)
|
140
|
+
|
141
|
+
newobjs = [
|
142
|
+
{ '_id' => _id, 'var' => 117 },
|
143
|
+
{ '_id' => BSON::ObjectId.new, 'var' => 32 },
|
144
|
+
].map { |o| @map.transform('mosql_test.collection', o) }
|
145
|
+
@cli.bulk_upsert(sequel[:sqltable], 'mosql_test.collection',
|
146
|
+
newobjs)
|
147
|
+
|
148
|
+
|
149
|
+
assert(sequel[:sqltable].where(:_id => newobjs[0].first, :var => 117).count)
|
150
|
+
assert(sequel[:sqltable].where(:_id => newobjs[1].first, :var => 32).count)
|
151
|
+
end
|
152
|
+
end
|
76
153
|
end
|
data/test/functional/schema.rb
CHANGED
@@ -15,7 +15,19 @@ db:
|
|
15
15
|
:table: sqltable2
|
16
16
|
:extra_props: true
|
17
17
|
:columns:
|
18
|
-
- _id:
|
18
|
+
- _id: TEXT
|
19
|
+
with_dotted:
|
20
|
+
:meta:
|
21
|
+
:table: sqltable3
|
22
|
+
:extra_props: true
|
23
|
+
:columns:
|
24
|
+
- _id: TEXT
|
25
|
+
- var_a:
|
26
|
+
:source: vars.a
|
27
|
+
:type: TEXT
|
28
|
+
- var_b:
|
29
|
+
:source: vars.b
|
30
|
+
:type: TEXT
|
19
31
|
EOF
|
20
32
|
|
21
33
|
before do
|
@@ -23,11 +35,13 @@ EOF
|
|
23
35
|
|
24
36
|
@sequel.drop_table?(:sqltable)
|
25
37
|
@sequel.drop_table?(:sqltable2)
|
38
|
+
@sequel.drop_table?(:sqltable3)
|
26
39
|
@map.create_schema(@sequel)
|
27
40
|
end
|
28
41
|
|
29
42
|
def table; @sequel[:sqltable]; end
|
30
43
|
def table2; @sequel[:sqltable2]; end
|
44
|
+
def table3; @sequel[:sqltable3]; end
|
31
45
|
|
32
46
|
it 'Creates the tables with the right columns' do
|
33
47
|
assert_equal(Set.new([:_id, :var]),
|
@@ -51,6 +65,30 @@ EOF
|
|
51
65
|
assert_equal(nil, rows[3][:var])
|
52
66
|
end
|
53
67
|
|
68
|
+
it 'Can COPY dotted data' do
|
69
|
+
objects = [
|
70
|
+
{'_id' => "a", 'vars' => {'a' => 1, 'b' => 2}},
|
71
|
+
{'_id' => "b", 'vars' => {}},
|
72
|
+
{'_id' => "c", 'vars' => {'a' => 2, 'c' => 6}},
|
73
|
+
{'_id' => "d", 'vars' => {'a' => 1, 'c' => 7}, 'extra' => 'moo'}
|
74
|
+
]
|
75
|
+
@map.copy_data(@sequel, 'db.with_dotted', objects.map { |o| @map.transform('db.with_dotted', o) } )
|
76
|
+
assert_equal(4, table3.count)
|
77
|
+
o = table3.first(:_id => 'a')
|
78
|
+
assert_equal("1", o[:var_a])
|
79
|
+
assert_equal("2", o[:var_b])
|
80
|
+
|
81
|
+
o = table3.first(:_id => 'b')
|
82
|
+
assert_equal({}, JSON.parse(o[:_extra_props]))
|
83
|
+
|
84
|
+
o = table3.first(:_id => 'c')
|
85
|
+
assert_equal({'vars' => { 'c' => 6} }, JSON.parse(o[:_extra_props]))
|
86
|
+
|
87
|
+
o = table3.first(:_id => 'd')
|
88
|
+
assert_equal({'vars' => { 'c' => 7}, 'extra' => 'moo' }, JSON.parse(o[:_extra_props]))
|
89
|
+
assert_equal(nil, o[:var_b])
|
90
|
+
end
|
91
|
+
|
54
92
|
it 'Can COPY BSON::ObjectIDs' do
|
55
93
|
o = {'_id' => BSON::ObjectId.new, 'var' => 0}
|
56
94
|
@map.copy_data(@sequel, 'db.collection', [ @map.transform('db.collection', o)] )
|
data/test/functional/sql.rb
CHANGED
@@ -16,8 +16,8 @@ class MoSQL::Test::Functional::SQLTest < MoSQL::Test::Functional
|
|
16
16
|
|
17
17
|
describe 'upsert' do
|
18
18
|
it 'inserts new items' do
|
19
|
-
@adapter.upsert!(@table, {'_id' => 0, 'color' => 'red', 'quantity' => 10})
|
20
|
-
@adapter.upsert!(@table, {'_id' => 1, 'color' => 'blue', 'quantity' => 5})
|
19
|
+
@adapter.upsert!(@table, '_id', {'_id' => 0, 'color' => 'red', 'quantity' => 10})
|
20
|
+
@adapter.upsert!(@table, '_id', {'_id' => 1, 'color' => 'blue', 'quantity' => 5})
|
21
21
|
assert_equal(2, @table.count)
|
22
22
|
assert_equal('red', @table[:_id => 0][:color])
|
23
23
|
assert_equal(10, @table[:_id => 0][:quantity])
|
@@ -26,11 +26,11 @@ class MoSQL::Test::Functional::SQLTest < MoSQL::Test::Functional
|
|
26
26
|
end
|
27
27
|
|
28
28
|
it 'updates items' do
|
29
|
-
@adapter.upsert!(@table, {'_id' => 0, 'color' => 'red', 'quantity' => 10})
|
29
|
+
@adapter.upsert!(@table, '_id', {'_id' => 0, 'color' => 'red', 'quantity' => 10})
|
30
30
|
assert_equal(1, @table.count)
|
31
31
|
assert_equal('red', @table[:_id => 0][:color])
|
32
32
|
|
33
|
-
@adapter.upsert!(@table, {'_id' => 0, 'color' => 'blue', 'quantity' => 5})
|
33
|
+
@adapter.upsert!(@table, '_id', {'_id' => 0, 'color' => 'blue', 'quantity' => 5})
|
34
34
|
assert_equal(1, @table.count)
|
35
35
|
assert_equal('blue', @table[:_id => 0][:color])
|
36
36
|
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '../../../_lib.rb')
|
2
|
+
|
3
|
+
class MoSQL::Test::SchemaTest < MoSQL::Test
|
4
|
+
TEST_MAP = <<EOF
|
5
|
+
---
|
6
|
+
db:
|
7
|
+
collection:
|
8
|
+
:meta:
|
9
|
+
:table: sqltable
|
10
|
+
:columns:
|
11
|
+
- id:
|
12
|
+
:source: _id
|
13
|
+
:type: TEXT
|
14
|
+
- var: INTEGER
|
15
|
+
with_extra_props:
|
16
|
+
:meta:
|
17
|
+
:table: sqltable2
|
18
|
+
:extra_props: true
|
19
|
+
:columns:
|
20
|
+
- id:
|
21
|
+
:source: _id
|
22
|
+
:type: TEXT
|
23
|
+
old_conf_syntax:
|
24
|
+
:columns:
|
25
|
+
- _id: TEXT
|
26
|
+
:meta:
|
27
|
+
:table: sqltable3
|
28
|
+
EOF
|
29
|
+
|
30
|
+
before do
|
31
|
+
@map = MoSQL::Schema.new(YAML.load(TEST_MAP))
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'Loads the schema' do
|
35
|
+
assert(@map)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'Can find an ns' do
|
39
|
+
assert(@map.find_ns("db.collection"))
|
40
|
+
assert_nil(@map.find_ns("db.other_collection"))
|
41
|
+
assert_raises(MoSQL::SchemaError) do
|
42
|
+
@map.find_ns!("db.other_collection")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'Converts columns to an array' do
|
47
|
+
table = @map.find_ns("db.collection")
|
48
|
+
assert(table[:columns].is_a?(Array))
|
49
|
+
|
50
|
+
id_mapping = table[:columns].find{|c| c[:source] == '_id'}
|
51
|
+
assert id_mapping
|
52
|
+
assert_equal '_id', id_mapping[:source]
|
53
|
+
assert_equal 'id', id_mapping[:name]
|
54
|
+
assert_equal 'TEXT', id_mapping[:type]
|
55
|
+
|
56
|
+
var_mapping = table[:columns].find{|c| c[:source] == 'var'}
|
57
|
+
assert var_mapping
|
58
|
+
assert_equal 'var', var_mapping[:source]
|
59
|
+
assert_equal 'var', var_mapping[:name]
|
60
|
+
assert_equal 'INTEGER', var_mapping[:type]
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'Can handle the old configuration format' do
|
64
|
+
table = @map.find_ns('db.old_conf_syntax')
|
65
|
+
assert(table[:columns].is_a?(Array))
|
66
|
+
|
67
|
+
id_mapping = table[:columns].find{|c| c[:source] == '_id'}
|
68
|
+
assert id_mapping
|
69
|
+
assert_equal '_id', id_mapping[:source]
|
70
|
+
assert_equal '_id', id_mapping[:name]
|
71
|
+
assert_equal 'TEXT', id_mapping[:type]
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'Can find the primary key of the SQL table' do
|
75
|
+
assert_equal('id', @map.primary_sql_key_for_ns('db.collection'))
|
76
|
+
assert_equal('_id', @map.primary_sql_key_for_ns('db.old_conf_syntax'))
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'can create a SQL schema' do
|
80
|
+
db = stub()
|
81
|
+
db.expects(:create_table?).with('sqltable')
|
82
|
+
db.expects(:create_table?).with('sqltable2')
|
83
|
+
db.expects(:create_table?).with('sqltable3')
|
84
|
+
|
85
|
+
@map.create_schema(db)
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'creates a SQL schema with the right fields' do
|
89
|
+
db = {}
|
90
|
+
stub_1 = stub()
|
91
|
+
stub_1.expects(:column).with('id', 'TEXT')
|
92
|
+
stub_1.expects(:column).with('var', 'INTEGER')
|
93
|
+
stub_1.expects(:column).with('_extra_props').never
|
94
|
+
stub_1.expects(:primary_key).with([:id])
|
95
|
+
stub_2 = stub()
|
96
|
+
stub_2.expects(:column).with('id', 'TEXT')
|
97
|
+
stub_2.expects(:column).with('_extra_props', 'TEXT')
|
98
|
+
stub_2.expects(:primary_key).with([:id])
|
99
|
+
stub_3 = stub()
|
100
|
+
stub_3.expects(:column).with('_id', 'TEXT')
|
101
|
+
stub_3.expects(:column).with('_extra_props').never
|
102
|
+
stub_3.expects(:primary_key).with([:_id])
|
103
|
+
(class << db; self; end).send(:define_method, :create_table?) do |tbl, &blk|
|
104
|
+
case tbl
|
105
|
+
when "sqltable"
|
106
|
+
o = stub_1
|
107
|
+
when "sqltable2"
|
108
|
+
o = stub_2
|
109
|
+
when "sqltable3"
|
110
|
+
o = stub_3
|
111
|
+
else
|
112
|
+
assert(false, "Tried to create an unexpected table: #{tbl}")
|
113
|
+
end
|
114
|
+
o.instance_eval(&blk)
|
115
|
+
end
|
116
|
+
@map.create_schema(db)
|
117
|
+
end
|
118
|
+
|
119
|
+
describe 'when transforming' do
|
120
|
+
it 'transforms rows' do
|
121
|
+
out = @map.transform('db.collection', {'_id' => "row 1", 'var' => 6})
|
122
|
+
assert_equal(["row 1", 6], out)
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'Includes extra props' do
|
126
|
+
out = @map.transform('db.with_extra_props', {'_id' => 7, 'var' => 6, 'other var' => {'key' => 'value'}})
|
127
|
+
assert_equal(2, out.length)
|
128
|
+
assert_equal(7, out[0])
|
129
|
+
assert_equal({'var' => 6, 'other var' => {'key' => 'value'}}, JSON.parse(out[1]))
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'gets all_columns right' do
|
133
|
+
assert_equal(['id', 'var'], @map.all_columns(@map.find_ns('db.collection')))
|
134
|
+
assert_equal(['id', '_extra_props'], @map.all_columns(@map.find_ns('db.with_extra_props')))
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe 'when copying data' do
|
139
|
+
it 'quotes special characters' do
|
140
|
+
assert_equal(%q{\\\\}, @map.quote_copy(%q{\\}))
|
141
|
+
assert_equal(%Q{\\\t}, @map.quote_copy( %Q{\t}))
|
142
|
+
assert_equal(%Q{\\\n}, @map.quote_copy( %Q{\n}))
|
143
|
+
assert_equal(%Q{some text}, @map.quote_copy(%Q{some text}))
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
describe 'fetch_and_delete_dotted' do
|
148
|
+
def check(orig, path, expect, result)
|
149
|
+
assert_equal(expect, @map.fetch_and_delete_dotted(orig, path))
|
150
|
+
assert_equal(result, orig)
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'works on things without dots' do
|
154
|
+
check({'a' => 1, 'b' => 2},
|
155
|
+
'a', 1,
|
156
|
+
{'b' => 2})
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'works if the key does not exist' do
|
160
|
+
check({'a' => 1, 'b' => 2},
|
161
|
+
'c', nil,
|
162
|
+
{'a' => 1, 'b' => 2})
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'fetches nested hashes' do
|
166
|
+
check({'a' => 1, 'b' => { 'c' => 1, 'd' => 2 }},
|
167
|
+
'b.d', 2,
|
168
|
+
{'a' => 1, 'b' => { 'c' => 1 }})
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'fetches deeply nested hashes' do
|
172
|
+
check({'a' => 1, 'b' => { 'c' => { 'e' => 8, 'f' => 9 }, 'd' => 2 }},
|
173
|
+
'b.c.e', 8,
|
174
|
+
{'a' => 1, 'b' => { 'c' => { 'f' => 9 }, 'd' => 2 }})
|
175
|
+
end
|
176
|
+
|
177
|
+
it 'cleans up empty hashes' do
|
178
|
+
check({'a' => { 'b' => 4}},
|
179
|
+
'a.b', 4,
|
180
|
+
{})
|
181
|
+
check({'a' => { 'b' => { 'c' => 5 }, 'd' => 9}},
|
182
|
+
'a.b.c', 5,
|
183
|
+
{'a' => { 'd' => 9 }})
|
184
|
+
end
|
185
|
+
|
186
|
+
it 'recursively cleans' do
|
187
|
+
check({'a' => { 'b' => { 'c' => { 'd' => 99 }}}},
|
188
|
+
'a.b.c.d', 99,
|
189
|
+
{})
|
190
|
+
end
|
191
|
+
|
192
|
+
it 'handles missing path components' do
|
193
|
+
check({'a' => { 'c' => 4 }},
|
194
|
+
'a.b.c.d', nil,
|
195
|
+
{'a' => { 'c' => 4 }})
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mosql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-04-12 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: sequel
|
@@ -180,6 +180,7 @@ extensions: []
|
|
180
180
|
extra_rdoc_files: []
|
181
181
|
files:
|
182
182
|
- .gitignore
|
183
|
+
- .travis.yml
|
183
184
|
- Gemfile
|
184
185
|
- Gemfile.lock
|
185
186
|
- LICENSE
|
@@ -200,7 +201,7 @@ files:
|
|
200
201
|
- test/functional/functional.rb
|
201
202
|
- test/functional/schema.rb
|
202
203
|
- test/functional/sql.rb
|
203
|
-
- test/unit/lib/
|
204
|
+
- test/unit/lib/mosql/schema.rb
|
204
205
|
homepage: https://github.com/stripe/mosql
|
205
206
|
licenses: []
|
206
207
|
post_install_message:
|
@@ -232,4 +233,5 @@ test_files:
|
|
232
233
|
- test/functional/functional.rb
|
233
234
|
- test/functional/schema.rb
|
234
235
|
- test/functional/sql.rb
|
235
|
-
- test/unit/lib/
|
236
|
+
- test/unit/lib/mosql/schema.rb
|
237
|
+
has_rdoc:
|
@@ -1,102 +0,0 @@
|
|
1
|
-
require File.join(File.dirname(__FILE__), '../../../_lib.rb')
|
2
|
-
|
3
|
-
class MoSQL::Test::SchemaTest < MoSQL::Test
|
4
|
-
TEST_MAP = <<EOF
|
5
|
-
---
|
6
|
-
db:
|
7
|
-
collection:
|
8
|
-
:meta:
|
9
|
-
:table: sqltable
|
10
|
-
:columns:
|
11
|
-
- _id: TEXT
|
12
|
-
- var: INTEGER
|
13
|
-
with_extra_props:
|
14
|
-
:meta:
|
15
|
-
:table: sqltable2
|
16
|
-
:extra_props: true
|
17
|
-
:columns:
|
18
|
-
- _id: INTEGER
|
19
|
-
EOF
|
20
|
-
|
21
|
-
before do
|
22
|
-
@map = MoSQL::Schema.new(YAML.load(TEST_MAP))
|
23
|
-
end
|
24
|
-
|
25
|
-
it 'Loads the schema' do
|
26
|
-
assert(@map)
|
27
|
-
end
|
28
|
-
|
29
|
-
it 'Can find an ns' do
|
30
|
-
assert(@map.find_ns("db.collection"))
|
31
|
-
assert_nil(@map.find_ns("db.other_collection"))
|
32
|
-
assert_raises(MoSQL::SchemaError) do
|
33
|
-
@map.find_ns!("db.other_collection")
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
it 'Converts columns to an ordered hash' do
|
38
|
-
table = @map.find_ns("db.collection")
|
39
|
-
assert(table[:columns].is_a?(Hash))
|
40
|
-
assert_equal(['_id', 'var'], table[:columns].keys)
|
41
|
-
end
|
42
|
-
|
43
|
-
it 'can create a SQL schema' do
|
44
|
-
db = stub()
|
45
|
-
db.expects(:create_table?).with('sqltable')
|
46
|
-
db.expects(:create_table?).with('sqltable2')
|
47
|
-
|
48
|
-
@map.create_schema(db)
|
49
|
-
end
|
50
|
-
|
51
|
-
it 'creates a SQL schema with the right fields' do
|
52
|
-
db = {}
|
53
|
-
stub_1 = stub()
|
54
|
-
stub_1.expects(:column).with('_id', 'TEXT')
|
55
|
-
stub_1.expects(:column).with('var', 'INTEGER')
|
56
|
-
stub_1.expects(:column).with('_extra_props').never
|
57
|
-
stub_2 = stub()
|
58
|
-
stub_2.expects(:column).with('_id', 'INTEGER')
|
59
|
-
stub_2.expects(:column).with('_extra_props', 'TEXT')
|
60
|
-
(class << db; self; end).send(:define_method, :create_table?) do |tbl, &blk|
|
61
|
-
case tbl
|
62
|
-
when "sqltable"
|
63
|
-
o = stub_1
|
64
|
-
when "sqltable2"
|
65
|
-
o = stub_2
|
66
|
-
else
|
67
|
-
assert(false, "Tried to create an unexpeced table: #{tbl}")
|
68
|
-
end
|
69
|
-
o.expects(:primary_key).with([:_id])
|
70
|
-
o.instance_eval(&blk)
|
71
|
-
end
|
72
|
-
@map.create_schema(db)
|
73
|
-
end
|
74
|
-
|
75
|
-
describe 'when transforming' do
|
76
|
-
it 'transforms rows' do
|
77
|
-
out = @map.transform('db.collection', {'_id' => "row 1", 'var' => 6})
|
78
|
-
assert_equal(["row 1", 6], out)
|
79
|
-
end
|
80
|
-
|
81
|
-
it 'Includes extra props' do
|
82
|
-
out = @map.transform('db.with_extra_props', {'_id' => 7, 'var' => 6, 'other var' => {'key' => 'value'}})
|
83
|
-
assert_equal(2, out.length)
|
84
|
-
assert_equal(7, out[0])
|
85
|
-
assert_equal({'var' => 6, 'other var' => {'key' => 'value'}}, JSON.parse(out[1]))
|
86
|
-
end
|
87
|
-
|
88
|
-
it 'gets all_columns right' do
|
89
|
-
assert_equal(['_id', 'var'], @map.all_columns(@map.find_ns('db.collection')))
|
90
|
-
assert_equal(['_id', '_extra_props'], @map.all_columns(@map.find_ns('db.with_extra_props')))
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
describe 'when copying data' do
|
95
|
-
it 'quotes special characters' do
|
96
|
-
assert_equal(%q{\\\\}, @map.quote_copy(%q{\\}))
|
97
|
-
assert_equal(%Q{\\\t}, @map.quote_copy( %Q{\t}))
|
98
|
-
assert_equal(%Q{\\\n}, @map.quote_copy( %Q{\n}))
|
99
|
-
assert_equal(%Q{some text}, @map.quote_copy(%Q{some text}))
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|