squealer 2.1.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.rvmrc CHANGED
@@ -1,6 +1,6 @@
1
1
  # These are the rubies that squealer has been tested on. Uncomment the one you want to use right now.
2
- #rvm use 1.9.1@squealer
3
- #rvm use 1.8.7@squealer
4
- rvm use 1.9.2
5
- #rvm use 1.9.1
6
- #rvm use 1.8.7
2
+ # 1.8.7-p249 segfaults postgres.
3
+ #
4
+ # rvm use 1.8.7-p174@squealer
5
+ rvm use 1.9.1@squealer #-p378
6
+ # rvm use 1.9.2@squealer #-preview3
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Joshua A. Graham and authors
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -9,10 +9,29 @@ To run standalone, simply make your data squeal thusly:
9
9
 
10
10
  where the squeal script includes a `require 'squealer'`.
11
11
 
12
+ ## Rationale
13
+ * For some reason cranky old data guys think there exists no other than the relational theory for modelling data
14
+ * Josh Graham is crankier and in many cases older (although much better looking) than your cranky old DBA, so he remembers when RDBMS were not prolific, and one had to construct queries that explicitly traversed the network or hierarchical databases of the time (or even the indexed file systems). CODASYL, can you spell it?
15
+ * Although many business problems are best expressed in terms of a spreadsheet (a tuple space), and despite the somewhat disturbing fact that the majority of the world's critical commercial systems hinge on Excel spreadsheets, not every problem is best modelled this way
16
+ * MongoDB (along with a growing number of other noSQL == "not only SQL" databases) provides an alternate mechanism to store data in a way that naturally reflects the real-world problem. Simpler application code, higher performance and straight-forward scalabiltity are natural benefits of modelling in a way that most closely reflects reality
17
+ * At the inaugural QCon San Francisco in a discussion with Martin Fowler and Ola Bini, Josh postulated that ORMs had it the wrong way around: that the application should be persisting its data in a manner natural to it, and that external systems (like reporting and decision support systems - or even numbskull integration at the persistence layer) should bear the cost of mapping. With the huge efforts put into noSQL engines like MongoDB, neo4j, Redis, Hadoop, CouchDB, Memcached, et cetera, has come a rise in popularity. With this increased and broader usage comes people who are looking for tools to make these data stores more accessible. The application is no longer bearing the cost of mapping - it's now time for the ancillary and external systems to pick up the bill!
18
+ * squealer provides a simple, declarative language for mapping values from trees into relations. It is inherently flexibile by being an internal Ruby DSL, so any imperative traversal or mapping logic can be expressed
19
+ * It can be used both in bulk operations on many documents (e.g. a periodic batch job) or executed for one document asynchronously as part of an after_save method (e.g. via a Resque job). It is possible that more work may done on this event-driven approach, perhaps in the form of a squealerd, to reduce latency.
20
+
12
21
  ## Release Notes
22
+ ### v2.2
23
+ * Adds support for PostgreSQL database as an export target
24
+ * Uses EXPORT_DBMS environment variable to specify database adapter
25
+ ** EXPORT_DMBS=mysql
26
+ ** EXPORT_DBMS=postgres
27
+ ** MySQL is default if not specified
28
+ * Switched to using DataMapper's DataObjects SQL wrapper
29
+ * Removed the need for some typecasting and export schema restrictions (e.g. true/false maps to whatever is idiomatic for the specified DBMS)
30
+ * NB: The pg gem for PostgreSQL segfaults on Ruby 1.8.7-p249 so we've reverted to supporting up to 1.8.7-p174
31
+
13
32
  ### v2.1
14
33
  * Ruby 1.8.6 back-compatibility added. Using `eval "", binding, __FILE__, __LINE__` instead of `binding.eval`
15
- * Target SQL script using backtick-quoted (mySQL) identifiers to avoid column-name / keyword conflict
34
+ * Target SQL script using backtick-quoted (MySQL) identifiers to avoid column-name / keyword conflict
16
35
  * Automatically typecast Ruby `Boolean` (to integer), `Symbol` (to string), `Array` (to comma-seperated string)
17
36
  * Improved handling and reporting of Target SQL errors
18
37
  * Schaefer's Special "skewer" Script to reflect on Mongoid models and generate an initial squeal script and SQL schema DDL script. This tool is intended to build the _initial_ scripts only. It is extremely useful to get you going, but do think about the needs of the consumer of the export database, and adjust the scripts to suit. [How do you make something squeal? You skewer it!]
@@ -32,30 +51,35 @@ where the squeal script includes a `require 'squealer'`.
32
51
  ## Warning
33
52
  Squealer is for _standalone_ operation. DO NOT use it directly from within your Ruby application. To make the DSL easy to use, we alter some core types:
34
53
 
35
- * `FalseClass#to_i` - You'll be storing booleans as a `tinyint(1)`, or similar. `false` is `0`.
36
54
  * `Hash#method_missing` - You prefer dot notation. JSON uses dot notation. You are importing from a data store which represents collections as arrays of hashmaps. Dot notation for navigating those collections is convenient. If you use a field name that happens to be a method on Hash you will have to use index notation. (e.g. `kitten.toys` is good, however `kitten.freeze` is not good. Use `kitten['freeze']` instead.)
37
55
  * `NilClass#each` - As you are importing from schemaless repositories and you may be trying to iterate on fields that contain embedded collections, if a specific parent does not contain one of those child collections, the driver will be returning `nil` as the value for that field. Having `NilClass#each` return a `[]` for a nil is convenient, semantically correct in this context, and removes the need for many `nil` checks in the block you provide to `Object#assign`
38
56
  * `Object` - `#import`, `#export`, `#target`, and `#assign` "keywords" are provided for convenience
39
- * `Time#to_s` - As you are exporting to a SQL database, we represent your timestamp in a format that it will parse unequivocally (MongoDB stores all temporal data as a timestamp)
40
- * `TrueClass#to_i` - You'll be storing booleans as a `tinyint(1)`, or similar. `true` is `1`.
57
+ * You need to remember that all temporal data (date, time, datetime, timestamp, whatever) are all converted to a full UTC date and time. This means that if you want to use the simple assign expression (with no block), the target column must be defined as a SQL type that can automatically accept a full date and time. If you just want to store the date or time portion, or do any other manipulation, you must use a block to convert the source value.
41
58
 
42
59
  ## It is a data mapper, it doesn't use one.
43
- Squealer doesn't use your application classes. It doesn't use your ActiveRecord models. It doesn't use mongoid (as awesome as that is), or mongomapper. It's an ETL tool. It could even be called a HRM (Hashmap-Relational-Mapper), but only in hushed tones in the corner boothes of dark pubs. It directly uses the Ruby driver for MongoDB and the Ruby driver for mySQL.
60
+ Squealer doesn't use your application classes. It doesn't use your ActiveRecord models. It doesn't use mongoid (as awesome as that is), mongodoc, or mongomapper. It's an ETL tool. It could even be called a HRM (Hashmap-Relational-Mapper), but only in hushed tones in the corner boothes of dark pubs. It directly uses the Ruby driver for MongoDB and the Ruby driver for MySQL.
44
61
 
45
62
  ## Databases supported
46
- For now, this is specifically for _MongoDB_ exporting to _mySQL_.
47
-
48
- ## Deprecation Warning
49
- Since version 1.1, the primary key value is inferred from the source document `_id` field based on the `Object#target` `table_name` argument matching the name of a variable holding the source document, `row_id` is no longer a parameter on `Object#target`. It will be invalid in version 1.3 and above.
63
+ For now, this is specifically for importing _MongoDB_ documents and exporting to either _MySQL_ or _PostgreSQL_.
50
64
 
51
65
  ## Notes
66
+ Tested on Ruby 1.8.7(-p174) and Ruby 1.9.1(-p378)
67
+
52
68
  The target SQL database _must_ have no foreign keys (because it can't rely on the primary key values and referential integrity is the responsibility of the source data store or the application that uses it).
53
69
 
54
- The target SQL database must use a primary key of `char(24)`. For now, we've assumed that column name is `id`. Each record's `id` value will get the source document `_id` value.
70
+ The target SQL database must use a primary key of `CHAR(24)`. For now, we've assumed that column name is `id`. Each record's `id` value will get the source document `_id` value. There are some plans to make this more flexible. If you are actively requiring this, let Josh know.
55
71
 
56
72
  It is assumed the target data will be quite denormalized - particularly that the hierarchy keys for embedded documents are flattened. This means that a document from `office.room.box` will be exported to a record containing the `id` for `office`, the `id` for `room` and the `id` for `box`.
57
73
 
58
- It is assumed no indexes are present in the target database table (performance drag). You may want to create indexes for pulling data out of the database Squealer exports to. Run a SQL DDL script on your mySQL database after squealing to add the indexes. You should drop the indexes before squealing again.
74
+ It is assumed no indexes are present in the target database table (performance drag). You may want to create indexes for pulling data out of the database Squealer exports to. Run a SQL DDL script on your MySQL database after squealing to add the indexes. You should drop the indexes before squealing again.
75
+
76
+ The target row is inserted, or updated if present. When MySQL is the export DBMS, we are using it's non-standard `INSERT ... UPDATE ON DUPLICATE KEY` extended syntax to achieve this. For PostgreSQL, we use an UPDATE followed by an INSERT. Doing update-or-insert allows an idempotent event-driven update of exported data (e.g. through redis queues) as well as a bulk batch process.
77
+
78
+ ## Copyright
79
+
80
+ Copyright © 2010 Joshua A Graham and authors.
81
+
82
+ ## License
59
83
 
60
- The target row is inserted, or updated if present. We are using MySQL `INSERT ... UPDATE ON DUPLICATE KEY` extended syntax to achieve this for now. This allows an event-driven update of exported data (e.g. through redis queues) as well as a bulk batch process.
84
+ See [LICENSE](blob/master/LICENSE "License").
61
85
 
data/Rakefile CHANGED
@@ -18,9 +18,18 @@ begin
18
18
  gemspec.default_executable = "skewer"
19
19
  gemspec.executables = ["skewer"]
20
20
 
21
- gemspec.add_dependency('mysql', '>= 2.8.1')
21
+ # import DBMS
22
22
  gemspec.add_dependency('mongo', '>= 0.18.3')
23
23
  gemspec.add_dependency('bson_ext', '>= 1.0.1')
24
+
25
+ # export DBMS
26
+ gemspec.add_dependency('data_objects', '>= 0.10.2')
27
+ gemspec.add_dependency('mysql', '>= 2.8.1')
28
+ gemspec.add_dependency('do_mysql', '>= 0.10.2')
29
+ gemspec.add_dependency('pg', '>= 0.9.0')
30
+ gemspec.add_dependency('do_postgres', '>= 0.10.2')
31
+
32
+ gemspec.add_development_dependency('rspec', '>= 1.3.0')
24
33
  end
25
34
  Jeweler::GemcutterTasks.new
26
35
  rescue LoadError
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.1.1
1
+ 2.2.0
data/bin/skewer CHANGED
@@ -1,19 +1,15 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  model = ARGV[0]
4
- unless model
5
- $stderr.puts "usage: skewer ModelName"
6
- exit 1
7
- end
4
+ abort "usage: skewer ModelName" unless model
8
5
 
9
6
  require 'config/boot'
10
7
  require 'config/environment'
11
8
 
12
9
  model = Object.const_get(model)
13
10
 
14
- unless defined?(Mongoid) and Mongoid::Document > model
15
- $stderr.puts "#{model} must be a Mongoid::Document"
16
- exit 1
11
+ unless defined?(Mongoid) && Mongoid::Document > model
12
+ abort "#{model} must be a Mongoid::Document"
17
13
  end
18
14
 
19
15
  write_schema = true
@@ -150,8 +146,8 @@ if write_squeal
150
146
  file.write <<-EOS
151
147
  require 'squealer'
152
148
 
153
- import('localhost', 27017, 'development') # <--- Change this as needed
154
- export('localhost', 'root', '', 'export') # <--- Change this as needed
149
+ import('mysql', 'localhost', 27017, 'development') # <--- Change this as needed
150
+ export('mysql', 'localhost', 'root', '', 'export') # <--- Change this as needed
155
151
 
156
152
  EOS
157
153
  file.write(squeal)
@@ -3,8 +3,9 @@ require 'squealer'
3
3
  # connect to the source mongodb database
4
4
  import('localhost', 27017, 'development')
5
5
 
6
+
6
7
  # connect to the target mysql database
7
- export('localhost', 'root', '', 'reporting_export')
8
+ export('mysql', 'localhost', 'root', '', 'reporting_export')
8
9
 
9
10
  # Here we extract, transform and load all documents in a collection...
10
11
 
@@ -17,6 +18,12 @@ export('localhost', 'root', '', 'reporting_export')
17
18
  #
18
19
  # Defaults to find all...
19
20
  import.source('users').each do |user|
21
+
22
+ # TODO: Allow specification of import row_id (export still uses "id" column as primary key):
23
+ # import_row_id { user['_id'] }
24
+ # import_row_id :_id
25
+ # import_row_id :name, :dob # Digest::SHA1.hexdigest(concatenated keys)[0,24]
26
+
20
27
  # Insert or Update on table 'user' where 'id' is the column name of the primary key.
21
28
  #
22
29
  # The primary key value is taken from the '_id' field of the source document,
@@ -41,7 +48,7 @@ import.source('users').each do |user|
41
48
  # You can normalize the export...
42
49
  # home_address and work_address are a formatted string like: "661 W Lake St, Suite 3NE, Chicago IL, 60611, USA"
43
50
  addresses = []
44
- addresses << atomize_address(user.home_address)
51
+ addresses << atomize_address(user.home_address) # atomize_address is some custom method of yours
45
52
  addresses << atomize_address(user.work_address)
46
53
  addresses.each do |address|
47
54
  target(:address) do
@@ -55,7 +62,7 @@ import.source('users').each do |user|
55
62
  #
56
63
  # You can denormalize the export...
57
64
  # user.home_address = { street: '661 W Lake St', city: 'Chicago', state: 'IL' }
58
- assign(:home_address) { flatten_address(user.home_address) }
65
+ assign(:home_address) { flatten_address(user.home_address) } # flatten_address is some custom method of yours
59
66
  assign(:work_address) { flatten_address(user.work_address) }
60
67
 
61
68
  user.activities.each do |activity|
@@ -1,5 +1,9 @@
1
- require 'mysql'
2
1
  require 'mongo'
2
+ require 'data_objects'
3
+ require 'mysql'
4
+ require 'do_mysql'
5
+ require 'do_postgres'
6
+
3
7
  require 'singleton'
4
8
 
5
9
  module Squealer
@@ -11,8 +15,12 @@ module Squealer
11
15
  @import_connection = Connection.new(@import_dbc)
12
16
  end
13
17
 
14
- def export_to(host, username, password, name)
15
- @export_dbc = Mysql.connect(host, username, password, name)
18
+ def export_to(adapter, host, username, password, name)
19
+ @@all_export_connections ||= []
20
+ @export_do.dispose if @export_do
21
+
22
+ @export_do = DataObjects::Connection.new("#{adapter}://#{username}:#{password}@#{host}/#{name}")
23
+ @@all_export_connections << @export_do
16
24
  end
17
25
 
18
26
  def import
@@ -20,7 +28,11 @@ module Squealer
20
28
  end
21
29
 
22
30
  def export
23
- @export_dbc
31
+ @export_do
32
+ end
33
+
34
+ def upsertable?
35
+ @export_do.is_a? DataObjects::Mysql::Connection
24
36
  end
25
37
 
26
38
  class Connection
@@ -68,5 +80,11 @@ module Squealer
68
80
  @progress_bar.finish if @progress_bar
69
81
  end
70
82
  end
83
+
84
+ private
85
+
86
+ def dispose_all_connections
87
+ @@all_export_connections.each {|c| c.dispose if c} if defined?(@@all_export_connections)
88
+ end
71
89
  end
72
90
  end
data/lib/squealer/hash.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  class Hash
2
2
  def method_missing(name, *args, &block)
3
3
  super if args.size > 0 || block_given?
4
+ #TODO: Warn if key doesn't exist - it's probably a typo in their squealer script
4
5
  self[name.to_s]
5
6
  end
6
7
  end
@@ -1,7 +1,7 @@
1
1
  class Object
2
2
 
3
- def target(table_name, row_id=nil, &block)
4
- Squealer::Target.new(Squealer::Database.instance.export, table_name, row_id, &block)
3
+ def target(table_name, &block)
4
+ Squealer::Target.new(Squealer::Database.instance.export, table_name, &block)
5
5
  end
6
6
 
7
7
  def assign(column_name, &block)
@@ -2,7 +2,6 @@ require 'delegate'
2
2
  require 'singleton'
3
3
 
4
4
  #TODO: Use logger and log throughout
5
- #TODO: Counters and timers
6
5
 
7
6
  module Squealer
8
7
  class Target
@@ -11,16 +10,16 @@ module Squealer
11
10
  Queue.instance.current
12
11
  end
13
12
 
14
- def initialize(database_connection, table_name, row_id=nil, &block)
13
+ def initialize(database_connection, table_name, &block)
15
14
  raise BlockRequired, "Block must be given to target (otherwise, there's no work to do)" unless block_given?
16
15
  raise ArgumentError, "Table name must be supplied" if table_name.to_s.strip.empty?
17
16
 
17
+ @dbc = database_connection
18
18
  @table_name = table_name.to_s
19
19
  @binding = block.binding
20
20
 
21
21
  verify_table_name_in_scope
22
-
23
- @row_id = obtain_row_id(row_id)
22
+ @row_id = infer_row_id
24
23
  @column_names = []
25
24
  @column_values = []
26
25
  @sql = ''
@@ -44,24 +43,17 @@ module Squealer
44
43
 
45
44
  private
46
45
 
47
- def obtain_row_id(row_id)
48
- #TODO: Remove in version 1.3 - just call infer_row_id in initialize
49
- if row_id != nil
50
- puts "\033[33mWARNING - squealer:\033[0m the 'target' row_id parameter is deprecated and will be invalid in version 1.3 and above. Remove it, and ensure the table_name matches a variable containing a hashmap with an _id key"
51
- row_id
52
- else
53
- infer_row_id
54
- end
55
- end
56
-
57
46
  def infer_row_id
58
- eval "#{@table_name}._id", @binding, __FILE__, __LINE__
47
+ (
48
+ (eval "#{@table_name}[:_id]", @binding, __FILE__, __LINE__) ||
49
+ (eval "#{@table_name}['_id']", @binding, __FILE__, __LINE__)
50
+ ).to_s
59
51
  end
60
-
52
+ 3
61
53
  def verify_table_name_in_scope
62
54
  table = eval "#{@table_name}", @binding, __FILE__, __LINE__
63
55
  raise ArgumentError, "The variable '#{@table_name}' is not a hashmap" unless table.is_a? Hash
64
- raise ArgumentError, "The hashmap '#{@table_name}' must have an '_id' key" unless table.has_key? '_id'
56
+ raise ArgumentError, "The hashmap '#{@table_name}' must have an '_id' key" unless table.has_key?('_id') || table.has_key?(:_id)
65
57
  rescue NameError
66
58
  raise NameError, "A variable named '#{@table_name}' must be in scope, and reference a hashmap with at least an '_id' key."
67
59
  end
@@ -84,11 +76,18 @@ module Squealer
84
76
 
85
77
  yield self
86
78
 
87
- @sql = "INSERT #{@table_name}"
88
- @sql << " (#{pk_name}#{column_names}) VALUES (?#{column_value_markers})"
89
- @sql << " ON DUPLICATE KEY UPDATE #{column_markers}"
79
+ insert_statement = %{INSERT INTO "#{@table_name}"}
80
+ insert_statement << %{ (#{pk_name}#{column_names}) VALUES ('#{@row_id}'#{column_value_markers})}
81
+ if Database.instance.upsertable?
82
+ insert_statement << %{ ON DUPLICATE KEY UPDATE #{column_markers}}
83
+ @sql = insert_statement
84
+ else
85
+ update_statement = %{UPDATE "#{@table_name}" SET #{column_markers} WHERE #{pk_name}='#{@row_id}'}
86
+ process_sql(update_statement)
87
+ @sql = update_statement + "; " + insert_statement
88
+ end
90
89
 
91
- execute_sql(@sql)
90
+ process_sql(insert_statement)
92
91
 
93
92
  Queue.instance.pop
94
93
  end
@@ -101,12 +100,16 @@ module Squealer
101
100
  @@targets
102
101
  end
103
102
 
104
- def execute_sql(sql)
105
- statement = Database.instance.export.prepare(sql)
106
- values = typecast_values * 2
103
+ def process_sql(sql)
104
+ values = Database.instance.upsertable? ? typecast_values * 2 : typecast_values
105
+ execute_sql(sql, values)
106
+ end
107
107
 
108
- statement.send(:execute, @row_id, *values) #expand values into distinct arguments
109
- rescue Mysql::Error, TypeError
108
+ def execute_sql(sql, values)
109
+ @dbc.create_command(sql).execute_non_query(*values)
110
+ rescue DataObjects::IntegrityError
111
+ raise "Failed to execute statement: #{sql} with #{values.inspect}.\nOriginal Exception was: #{$!.to_s}" if Database.instance.upsertable?
112
+ rescue
110
113
  raise "Failed to execute statement: #{sql} with #{values.inspect}.\nOriginal Exception was: #{$!.to_s}"
111
114
  end
112
115
 
@@ -140,14 +143,10 @@ module Squealer
140
143
  def typecast_values
141
144
  column_values.map do |value|
142
145
  case value
143
- when true
144
- 1
145
- when false
146
- 0
147
- when Symbol
148
- value.to_s
149
146
  when Array
150
147
  value.join(",")
148
+ when BSON::ObjectID
149
+ value.to_s
151
150
  else
152
151
  value
153
152
  end
@@ -155,7 +154,7 @@ module Squealer
155
154
  end
156
155
 
157
156
  def quote_identifier(name)
158
- "`#{name}`"
157
+ %{"#{name}"}
159
158
  end
160
159
 
161
160
  class Queue < DelegateClass(Array)
data/lib/squealer.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  require 'squealer/hash'
2
2
  require 'squealer/object'
3
- require 'squealer/time'
4
3
 
5
4
  require 'squealer/database'
6
5
  require 'squealer/progress_bar'
@@ -0,0 +1,148 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Exporting" do
4
+ before do
5
+ truncate_export_tables
6
+ end
7
+
8
+ let(:databases) { Squealer::Database.instance }
9
+ let(:today) { Date.today }
10
+
11
+ def prepare_export_database
12
+ databases.export_to($db_adapter, 'localhost', $db_user, '', $db_name)
13
+ end
14
+
15
+ def squeal_basic_users_document(user=users_document)
16
+ target(:user) do
17
+ assign(:name)
18
+ assign(:organization_id)
19
+ assign(:dob)
20
+ assign(:gender)
21
+ assign(:foreign)
22
+ assign(:dull)
23
+ assign(:symbolic)
24
+ assign(:interests)
25
+ end
26
+ end
27
+
28
+ def squeal_users_document_with_activities(user=users_document)
29
+ target(:user) do
30
+ assign(:name)
31
+ assign(:organization_id)
32
+ assign(:dob)
33
+ assign(:gender)
34
+ assign(:foreign)
35
+ assign(:dull)
36
+ assign(:symbolic)
37
+ assign(:interests)
38
+
39
+ user.activities.each do |activity|
40
+ target(:activity) do
41
+ assign(:name)
42
+ assign(:due_date)
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ let :users_document do
49
+ { :_id => 'ABCDEFGHIJKLMNOPQRSTUVWX',
50
+ 'name' => 'Test User', 'dob' => as_time(Date.parse('04-Jul-1776')), 'gender' => 'M',
51
+ 'foreign' => true,
52
+ 'dull' => false,
53
+ 'symbolic' => :of_course,
54
+ 'interests' => ['health', 'education'],
55
+ 'organization_id' => '123456789012345678901234',
56
+ 'activities' => [
57
+ { :_id => 'a1', 'name' => 'Be independent', 'due_date' => as_time(today + 1) },
58
+ { :_id => 'a2', 'name' => 'Fight each other', 'due_date' => as_time(today + 7) }
59
+ ]
60
+ }
61
+ end
62
+
63
+ let :first_users_record do
64
+ dbc = databases.instance_variable_get('@export_do')
65
+ reader = dbc.create_command(%{SELECT * FROM "user"}).execute_reader
66
+ result = reader.each { |x| break x }
67
+ reader.close
68
+ result
69
+ end
70
+
71
+ let :first_activity_record do
72
+ dbc = databases.instance_variable_get('@export_do')
73
+ reader = dbc.create_command(%{SELECT * FROM "activity"}).execute_reader
74
+ result = reader.each { |x| break x }
75
+ reader.close
76
+ result
77
+ end
78
+
79
+ context "a new record" do
80
+ it "saves the data correctly" do
81
+ prepare_export_database
82
+ squeal_basic_users_document
83
+ result = first_users_record
84
+
85
+ result['name'].should == 'Test User'
86
+
87
+ result['dob'].mday.should == 4
88
+ result['dob'].mon.should == 7
89
+ result['dob'].year.should == 1776
90
+
91
+ result['gender'].should == 'M'
92
+
93
+ result['foreign'].should be_true
94
+ result['dull'].should be_false
95
+
96
+ result['symbolic'].should == :of_course.to_s
97
+
98
+ result['interests'].should == 'health,education'
99
+ end
100
+
101
+ it "saves embedded documents correctly" do
102
+ prepare_export_database
103
+ squeal_users_document_with_activities
104
+ result = first_activity_record
105
+
106
+ result['name'].should == 'Be independent'
107
+ result['due_date'].mday.should == (today + 1).mday
108
+ result['due_date'].mon.should == (today + 1).mon
109
+ result['due_date'].year.should == (today + 1).year
110
+ end
111
+ end
112
+
113
+ context "an existing record" do
114
+ it "updates the data correctly" do
115
+ prepare_export_database
116
+ squeal_basic_users_document
117
+ squeal_basic_users_document(users_document.merge('foreign' => false, 'gender' => 'F'))
118
+
119
+ result = first_users_record
120
+
121
+ result['name'].should == 'Test User'
122
+
123
+ result['dob'].mday.should == 4
124
+ result['dob'].mon.should == 7
125
+ result['dob'].year.should == 1776
126
+
127
+ result['gender'].should == 'F'
128
+
129
+ result['foreign'].should be_false
130
+ result['dull'].should be_false
131
+
132
+ result['symbolic'].should == :of_course.to_s
133
+
134
+ result['interests'].should == 'health,education'
135
+ end
136
+
137
+ it "updates the child record correctly" do
138
+ prepare_export_database
139
+ squeal_users_document_with_activities(users_document.merge('activities' => [{ :_id => 'a1', 'name' => 'Be expansionist', 'due_date' => as_time(today + 1) }]))
140
+ result = first_activity_record
141
+
142
+ result['name'].should == 'Be expansionist'
143
+ result['due_date'].mday.should == (today + 1).mday
144
+ result['due_date'].mon.should == (today + 1).mon
145
+ result['due_date'].year.should == (today + 1).year
146
+ end
147
+ end
148
+ end