mosql 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.3
5
+ services:
6
+ - mongodb
7
+ - postgresql
8
+ before_script:
9
+ - psql -c 'create database mosql;' -U postgres
10
+ env:
11
+ - MONGOSQL_TEST_SQL=postgres://localhost/mosql
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mosql (0.1.1)
4
+ mosql (0.1.2)
5
5
  bson_ext
6
6
  json
7
7
  log4r
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
- - _id: TEXT
57
- - author: TEXT
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. `:columns` is a list of one-element hashes, mapping
71
- field-name to SQL type. It is required to include at least an `_id`
72
- mapping. `:meta` contains metadata about this collection/table. It is
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
@@ -1,6 +1,6 @@
1
1
  require 'rake/testtask'
2
2
 
3
- task :default
3
+ task :default => [:test]
4
4
  task :build
5
5
 
6
6
  Rake::TestTask.new do |t|
data/bin/mosql CHANGED
@@ -1,7 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'rubygems'
4
- require 'bundler/setup'
5
3
  require 'mosql/cli'
6
4
 
7
5
  MoSQL::CLI.run(ARGV)
@@ -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. Proceeding anyways...")
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.load(File.read(@options[:collections]))
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
- optail
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
- start_ts = @mongo['local']['oplog.rs'].find_one({}, {:sort => [['$natural', -1]]})['ts']
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
- sqlid = @sql.transform_one_ns(ns, { '_id' => _id })['_id']
257
- obj = collection_for_ns(ns).find_one({:_id => _id})
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(:_id => sqlid).delete()
267
+ @sql.table_for_ns(ns).where(primary_sql_key.to_sym => sqlid).delete()
262
268
  end
263
269
  end
264
270
 
@@ -4,19 +4,44 @@ module MoSQL
4
4
  class Schema
5
5
  include MoSQL::Logging
6
6
 
7
- def to_ordered_hash(lst)
8
- hash = BSON::OrderedHash.new
7
+ def to_array(lst)
8
+ array = []
9
9
  lst.each do |ent|
10
- raise "Invalid ordered hash entry #{ent.inspect}" unless ent.is_a?(Hash) && ent.keys.length == 1
11
- field, type = ent.first
12
- hash[field] = type
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] = to_ordered_hash(spec[: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 |field, type|
39
- column field, type
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 |name, type|
71
- v = obj.delete(name)
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 = schema[:columns].keys
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
@@ -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 _id found in transform of #{obj.inspect}" if h['_id'].nil?
45
- table_for_ns(ns).where(:_id => h['_id']).delete
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 (_id=#{item['_id']}): #{e}")
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(:_id => item['_id']).update(item)
67
+ table.where(table_primary_key.to_sym => item[table_primary_key]).update(item)
67
68
  end
68
69
  end
69
70
  end
@@ -1,3 +1,3 @@
1
1
  module MoSQL
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -1,6 +1,3 @@
1
- require 'rubygems'
2
- require 'bundler/setup'
3
-
4
1
  require 'minitest/autorun'
5
2
  require 'minitest/spec'
6
3
  require 'mocha'
@@ -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
@@ -15,7 +15,19 @@ db:
15
15
  :table: sqltable2
16
16
  :extra_props: true
17
17
  :columns:
18
- - _id: INTEGER
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)] )
@@ -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.1.2
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-02-15 00:00:00.000000000 Z
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/mongo-sql/schema.rb
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/mongo-sql/schema.rb
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