mosql 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|