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.
@@ -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