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 +5 -5
- data/LICENSE +20 -0
- data/README.md +36 -12
- data/Rakefile +10 -1
- data/VERSION +1 -1
- data/bin/skewer +5 -9
- data/lib/example_squeal.rb +10 -3
- data/lib/squealer/database.rb +22 -4
- data/lib/squealer/hash.rb +1 -0
- data/lib/squealer/object.rb +2 -2
- data/lib/squealer/target.rb +32 -33
- data/lib/squealer.rb +0 -1
- data/spec/integration/export_a_record_spec.rb +148 -0
- data/spec/spec_helper.rb +34 -54
- data/spec/spec_helper_dbms_mysql.rb +56 -0
- data/spec/spec_helper_dbms_postgres.rb +55 -0
- data/spec/squealer/database_spec.rb +41 -11
- data/spec/squealer/object_spec.rb +4 -4
- data/spec/squealer/target_spec.rb +77 -108
- data/squealer.gemspec +31 -12
- metadata +109 -25
- data/lib/.example_squeal.rb.swp +0 -0
- data/lib/squealer/time.rb +0 -5
- data/spec/squealer/time_spec.rb +0 -10
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
|
-
#
|
3
|
-
#
|
4
|
-
rvm use 1.
|
5
|
-
|
6
|
-
#rvm use 1.
|
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 (
|
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
|
-
*
|
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
|
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
|
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 `
|
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
|
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
|
-
|
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
|
-
|
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
|
+
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)
|
15
|
-
|
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)
|
data/lib/example_squeal.rb
CHANGED
@@ -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|
|
data/lib/squealer/database.rb
CHANGED
@@ -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
|
-
|
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
|
-
@
|
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
data/lib/squealer/object.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
class Object
|
2
2
|
|
3
|
-
def target(table_name,
|
4
|
-
Squealer::Target.new(Squealer::Database.instance.export, table_name,
|
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)
|
data/lib/squealer/target.rb
CHANGED
@@ -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,
|
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
|
-
|
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?
|
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
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
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
|
105
|
-
|
106
|
-
values
|
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
|
-
|
109
|
-
|
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
|
-
"
|
157
|
+
%{"#{name}"}
|
159
158
|
end
|
160
159
|
|
161
160
|
class Queue < DelegateClass(Array)
|
data/lib/squealer.rb
CHANGED
@@ -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
|