purview 1.0.0.alpha

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.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +33 -0
  3. data/.travis.yml +18 -0
  4. data/Gemfile +3 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +143 -0
  7. data/Rakefile +11 -0
  8. data/TODO +81 -0
  9. data/lib/purview/columns/base.rb +65 -0
  10. data/lib/purview/columns/boolean.rb +11 -0
  11. data/lib/purview/columns/created_timestamp.rb +11 -0
  12. data/lib/purview/columns/date.rb +11 -0
  13. data/lib/purview/columns/float.rb +11 -0
  14. data/lib/purview/columns/id.rb +11 -0
  15. data/lib/purview/columns/integer.rb +11 -0
  16. data/lib/purview/columns/money.rb +11 -0
  17. data/lib/purview/columns/string.rb +11 -0
  18. data/lib/purview/columns/text.rb +11 -0
  19. data/lib/purview/columns/time.rb +11 -0
  20. data/lib/purview/columns/timestamp.rb +11 -0
  21. data/lib/purview/columns/updated_timestamp.rb +11 -0
  22. data/lib/purview/columns/uuid.rb +11 -0
  23. data/lib/purview/columns.rb +14 -0
  24. data/lib/purview/connections/base.rb +55 -0
  25. data/lib/purview/connections/mysql.rb +39 -0
  26. data/lib/purview/connections/postgresql.rb +27 -0
  27. data/lib/purview/connections.rb +3 -0
  28. data/lib/purview/databases/base.rb +559 -0
  29. data/lib/purview/databases/mysql.rb +207 -0
  30. data/lib/purview/databases/postgresql.rb +210 -0
  31. data/lib/purview/databases.rb +3 -0
  32. data/lib/purview/exceptions/base.rb +5 -0
  33. data/lib/purview/exceptions/could_not_acquire_lock.rb +9 -0
  34. data/lib/purview/exceptions/lock_already_released.rb +9 -0
  35. data/lib/purview/exceptions/no_table.rb +9 -0
  36. data/lib/purview/exceptions/no_window.rb +9 -0
  37. data/lib/purview/exceptions/rows_outside_window.rb +18 -0
  38. data/lib/purview/exceptions/table.rb +13 -0
  39. data/lib/purview/exceptions.rb +7 -0
  40. data/lib/purview/loaders/base.rb +154 -0
  41. data/lib/purview/loaders/mysql.rb +81 -0
  42. data/lib/purview/loaders/postgresql.rb +81 -0
  43. data/lib/purview/loaders.rb +3 -0
  44. data/lib/purview/loggers/base.rb +99 -0
  45. data/lib/purview/loggers/console.rb +11 -0
  46. data/lib/purview/loggers.rb +2 -0
  47. data/lib/purview/mixins/helpers.rb +21 -0
  48. data/lib/purview/mixins/logger.rb +21 -0
  49. data/lib/purview/mixins.rb +2 -0
  50. data/lib/purview/parsers/base.rb +39 -0
  51. data/lib/purview/parsers/csv.rb +49 -0
  52. data/lib/purview/parsers/tsv.rb +11 -0
  53. data/lib/purview/parsers.rb +3 -0
  54. data/lib/purview/pullers/base.rb +19 -0
  55. data/lib/purview/pullers/uri.rb +66 -0
  56. data/lib/purview/pullers.rb +2 -0
  57. data/lib/purview/refinements/object.rb +5 -0
  58. data/lib/purview/refinements/time.rb +5 -0
  59. data/lib/purview/refinements.rb +2 -0
  60. data/lib/purview/structs/base.rb +10 -0
  61. data/lib/purview/structs/result.rb +7 -0
  62. data/lib/purview/structs/window.rb +7 -0
  63. data/lib/purview/structs.rb +3 -0
  64. data/lib/purview/tables/base.rb +140 -0
  65. data/lib/purview/tables/raw.rb +13 -0
  66. data/lib/purview/tables.rb +2 -0
  67. data/lib/purview/types/base.rb +9 -0
  68. data/lib/purview/types/boolean.rb +9 -0
  69. data/lib/purview/types/date.rb +9 -0
  70. data/lib/purview/types/float.rb +9 -0
  71. data/lib/purview/types/integer.rb +9 -0
  72. data/lib/purview/types/money.rb +9 -0
  73. data/lib/purview/types/string.rb +9 -0
  74. data/lib/purview/types/text.rb +9 -0
  75. data/lib/purview/types/time.rb +9 -0
  76. data/lib/purview/types/timestamp.rb +9 -0
  77. data/lib/purview/types/uuid.rb +9 -0
  78. data/lib/purview/types.rb +11 -0
  79. data/lib/purview/version.rb +3 -0
  80. data/lib/purview.rb +27 -0
  81. data/purview.gemspec +29 -0
  82. data/spec/spec_helper.rb +5 -0
  83. metadata +210 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4753e561eece41d8838e916c7914ae3b3d155b3b
4
+ data.tar.gz: 8430637788951088580baa1f533540968974f28c
5
+ SHA512:
6
+ metadata.gz: 742e3ab26e898924c9054b461d41c700144859910b96ccfd2df6ac6e9d51bb2945a61f73282445f7c239c8adfeb4cc53bdf518aad195b91cc931cdeca24639bb
7
+ data.tar.gz: 43d8b1a2b5f6cf401a33e254a8b63169b1764fe3a88b83391a91b6068203b0bd02df339746982580864aba2a3c3f9ae0ba079fd938f31fe687d37e38b7aff987
data/.gitignore ADDED
@@ -0,0 +1,33 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /InstalledFiles
5
+ /coverage/
6
+ /pkg/
7
+ /script/
8
+ /spec/reports/
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+ tags
13
+
14
+ ## Specific to RubyMotion:
15
+ .dat*
16
+ .repl_history
17
+ build/
18
+
19
+ ## Documentation cache and generated files:
20
+ /.yardoc/
21
+ /_yardoc/
22
+ /doc/
23
+ /rdoc/
24
+
25
+ ## Environment normalisation:
26
+ /.bundle/
27
+ /lib/bundler/man/
28
+
29
+ ## Ruby artifacts:
30
+ .ruby-gemset
31
+ .ruby-version
32
+ .rvmrc
33
+ Gemfile.lock
data/.travis.yml ADDED
@@ -0,0 +1,18 @@
1
+ language: ruby
2
+
3
+ matrix:
4
+ allow_failures:
5
+ - rvm: jruby-19mode
6
+
7
+ rvm:
8
+ - 1.9.3
9
+ - 2.0.0
10
+ - 2.1.0
11
+ - 2.1.1
12
+ - 2.1.2
13
+ - 2.1.3
14
+ - 2.1.4
15
+ - 2.1.5
16
+ - 2.2.0
17
+ - jruby-19mode
18
+ - rbx-2
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Jonathan W. Zaleski
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # Purview
2
+
3
+ [![Build Status](https://secure.travis-ci.org/jzaleski/purview.png?branch=master)](http://travis-ci.org/jzaleski/purview)
4
+ [![Dependency Status](https://gemnasium.com/jzaleski/purview.png)](https://gemnasium.com/jzaleski/purview)
5
+
6
+ A framework designed to simplify data warehousing
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'purview'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install purview
21
+
22
+ ## Usage
23
+
24
+ Load the `MySQL` client (for `PostgreSQL` simply change 'mysql2' to 'pg')
25
+ ```ruby
26
+ require 'mysql2'
27
+ ```
28
+
29
+ Set the database-name (this can be anything, but it must exist)
30
+ ```ruby
31
+ database_name = :data_warehouse
32
+ ```
33
+
34
+ Combine all the configuration options and instantiate the `Database` (for
35
+ `PostgreSQL` simply change `MySQL` to `PostgreSQL`)
36
+ ```ruby
37
+ database_opts = {}
38
+
39
+ database = Purview::Databases::MySQL.new(
40
+ database_name,
41
+ database_opts
42
+ )
43
+ ```
44
+
45
+ Set the table-name (this can be anything, but it must exist)
46
+ ```ruby
47
+ table_name = :users
48
+ ```
49
+
50
+ Define the `Column(s)` (available column-types: `Boolean`, `CreatedTimestamp`,
51
+ `Date`, `Float`, `Id`, `Integer`, `Money`, `String`, `Text`, `Time`, `Timestamp`,
52
+ `UpdatedTimestamp` & `UUID` -- the `Id`, `CreatedTimestamp` & `UpdatedTimestamp`
53
+ columns are required for all tables)
54
+ ```ruby
55
+ columns = [
56
+ Purview::Columns::Id.new(:id),
57
+ Purview::Columns::String.new(:name, :nullable => false),
58
+ Purview::Columns::String.new(:email, :nullable => false, :limit => 100),
59
+ Purview::Columns::CreatedTimestamp.new(:created_at),
60
+ Purview::Columns::UpdatedTimestamp.new(:updated_at),
61
+ ]
62
+ ```
63
+
64
+ Configure the `Puller` (available puller-types: `URI`)
65
+ ```ruby
66
+ puller_opts = {
67
+ :type => Purview::Pullers::URI,
68
+ :uri => 'http://feed.test.com/users',
69
+ }
70
+ ```
71
+
72
+ Configure the `Parser` (available parser-types: `CSV` & `TSV`)
73
+ ```ruby
74
+ parser_opts = {
75
+ :type => Purview::Parsers::TSV,
76
+ }
77
+ ```
78
+
79
+ Configure the `Loader` (for `PostgreSQL` simply change `MySQL` to `PostgreSQL`)
80
+ ```ruby
81
+ loader_opts = {
82
+ :type => Purview::Loaders::MySQL,
83
+ }
84
+ ```
85
+
86
+ Configure the `starting_timestamp` (this is the min-date to pull and can vary
87
+ between `Table(s)`)
88
+ ```ruby
89
+ starting_timestamp = Time.parse('2012-01-01 00:00:00Z')
90
+ ```
91
+
92
+ Combine all the configuration options and instantiate the `Table`
93
+ ```ruby
94
+ table_opts = {
95
+ :columns => columns,
96
+ :database => database,
97
+ :loader => loader_opts,
98
+ :parser => parser_opts,
99
+ :puller => puller_opts,
100
+ :starting_timestamp => starting_timestamp,
101
+ }
102
+
103
+ table = Purview::Tables::Raw.new(
104
+ table_name,
105
+ table_opts
106
+ )
107
+ ```
108
+
109
+ Add the `Table` to the `Database` (schema). In order for [the] `Table` to be
110
+ `sync[ed]` it *must* be added to [the] `Database`
111
+ ```ruby
112
+ database.add_table(table)
113
+ ```
114
+
115
+ Create the `Table`. Recommended for testing purposes *only*. For production
116
+ environments you will likely want an external process to manage the schema (for
117
+ `PostgreSQL` simply change `Mysql2::Error` to `PG::DuplicateTable`)
118
+ ```ruby
119
+ begin
120
+ database.create_table(
121
+ database.connect,
122
+ table
123
+ )
124
+ rescue Mysql2::Error; end
125
+ ```
126
+
127
+ Sync the `Database`. This process will select a [candidate] `Table`, pull data
128
+ from its [remote-]source and reconcile the new data against the main-table (e.g.
129
+ perform `INSERTs`, `UPDATEs` and `DELETEs`). When multiple `Table(s)` are
130
+ configured the least recently pulled and available (`enabled` and not `locked`)
131
+ `Table` will be selected (you will likely want to configure some process to load
132
+ the schema run the `sync` at regularly scheduled intervals)
133
+ ```ruby
134
+ database.sync
135
+ ```
136
+
137
+ ## Contributing
138
+
139
+ 1. Fork it ( http://github.com/jzaleski/purview/fork )
140
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
141
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
142
+ 4. Push to the branch (`git push origin my-new-feature`)
143
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec) do |task|
6
+ task.rspec_opts = %w[--format progress]
7
+ end
8
+
9
+ task :test => :spec
10
+
11
+ task :default => :test
data/TODO ADDED
@@ -0,0 +1,81 @@
1
+ ... CLOSED TASKS ....
2
+
3
+ * Handle blank values a bit more intelligently (all blanks -> nil)
4
+ * Add support for INSERTs, UPDATEs and DELETEs (of table-data)
5
+ * Deal w/ NULL values
6
+ * Deal w/ BOOL values
7
+ * Build out MVP for PostgreSQL loader
8
+ * Create temporary table from table-metadata and load all of the parsed rows
9
+ into it (build create, update and delete logic atop the temporary table)
10
+ * Map [object-]types to database types (in the loader?)
11
+ * Add windowing to pulls (simulate w/ 2 different data-files if necessary)
12
+ * Store max-timestamp pulled (NULL or previous window + window size)
13
+ * Add configuration to `Table` for window-size (in the future this can scale
14
+ dynamically)
15
+ * Include [window] timestamps in query-string for pull[er]
16
+ * Figure out an intelligent way to solve the chicken <-> egg problem for
17
+ `table_metadata`
18
+ * Fail fast if there are no candidate tables (`next_table`)
19
+ * Ensure that the puller does not try to go into the future (`next_window`)
20
+ * Add in a primitive logging facility (can be temporary)
21
+ * Enhance `next_table_sql`
22
+ * Lock tables (feeds) during pull, parse and load
23
+ * Verify table-locking mechanism behaves as expected
24
+ * Ability to drop a table
25
+ * Reorder args: "table, table_name" to: "table_name, table"
26
+ * Reorder args: "table, index_name, *columns" to "index_name, table, *columns"
27
+ * Ability to create an index
28
+ * Ability to drop an index
29
+ * Indices on `created_at` and `updated_at` columns
30
+ * Gracefully handle table {,un}locking in case of error
31
+ * Add debugging code
32
+ * Raise no-window & no-table exceptions (making them catchable upstream)
33
+ * Minimize race-condition around table-locking (use where on UPDATE, raise if
34
+ zero rows are updated)
35
+ * Log number of delete, inserts and updates in `loader`
36
+ * `load_temporary_table` should bomb if there is data outside the window
37
+ * Delete from outside window where id in temporary table
38
+ * Rename `data` to `rows` in `execute`
39
+ * Rename `Postgres` to `PostgreSQL`
40
+ * Finish PostgreSQL support (requires `pg` gem)
41
+ * Finish MySQL support (requires `mysql2` gem)
42
+ * Usage/configuration examples in README.md
43
+
44
+ ... ONGOING TASKS ...
45
+
46
+ * Add tests
47
+ * Add comments where appropriate
48
+ * DRY/Refactor
49
+
50
+ ... OPEN TASKS ....
51
+
52
+ * Create table class to encapulate `table_metadata` logic
53
+ * Build out change-log tables
54
+ * Build out canonical, fact and aggregate tables (and related transforms)
55
+ * Configurable re-pull window (do this automatically once up to current?)
56
+ * Add schema management capabilities (detect schema-deltas and suggestion
57
+ modifications)
58
+
59
+ ... DEFERRED TASKS ...
60
+
61
+ * Fix JRuby bundle/build
62
+
63
+ ... CLOSED QUESTIONS ...
64
+
65
+ * Where is the best place to create the connection?
66
+ * Immediately? Then it's open during the pull, etc.
67
+ * Lazily? Makes for connection logic all over the place
68
+ * Perhaps it can be centralized entirely into the `Database` class?
69
+ * Consider using ISO-8601 timestamps in query-string (not sure on this one, it
70
+ is slightly more fragile because of URL-encoding, etc.)
71
+
72
+ ... OPEN QUESTIONS ...
73
+
74
+ * Parallel pulls from the same "source"? Can this be done in one request?
75
+ * Rename columns during pull/parse `source_name` & `target_name` perhaps? Maybe
76
+ create a new type of column?
77
+
78
+ ... INTEGRATION CONSIDERATIONS ...
79
+
80
+ * Deployment
81
+ * Scheduling (Daemon? CRON?)
@@ -0,0 +1,65 @@
1
+ module Purview
2
+ module Columns
3
+ class Base
4
+ attr_reader :name
5
+
6
+ def initialize(name, opts={})
7
+ @name = name.to_sym
8
+ @opts = default_opts.merge(opts)
9
+ end
10
+
11
+ def default
12
+ opts[:default]
13
+ end
14
+
15
+ def default?
16
+ !!default
17
+ end
18
+
19
+ def limit
20
+ opts[:limit]
21
+ end
22
+
23
+ def limit?
24
+ !!limit
25
+ end
26
+
27
+ def nullable
28
+ coalesce(opts[:nullable], true)
29
+ end
30
+
31
+ def nullable?
32
+ !!nullable
33
+ end
34
+
35
+ def parse(value)
36
+ blank = blank?(value)
37
+ return nil if blank && nullable?
38
+ raise %{Unexpected blank value for column: "#{name}"} if blank
39
+ type.parse(value)
40
+ end
41
+
42
+ def primary_key
43
+ opts[:primary_key]
44
+ end
45
+
46
+ def primary_key?
47
+ !!primary_key
48
+ end
49
+
50
+ def type
51
+ coalesce(opts[:type], Purview::Types::String)
52
+ end
53
+
54
+ private
55
+
56
+ include Purview::Mixins::Helpers
57
+
58
+ attr_reader :opts
59
+
60
+ def default_opts
61
+ {}
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,11 @@
1
+ module Purview
2
+ module Columns
3
+ class Boolean < Base
4
+ private
5
+
6
+ def default_opts
7
+ super.merge(:type => Purview::Types::Boolean)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Purview
2
+ module Columns
3
+ class CreatedTimestamp < Timestamp
4
+ private
5
+
6
+ def default_opts
7
+ super.merge(:nullable => false)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Purview
2
+ module Columns
3
+ class Date < Base
4
+ private
5
+
6
+ def default_opts
7
+ super.merge(:type => Purview::Types::Date)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Purview
2
+ module Columns
3
+ class Float < Base
4
+ private
5
+
6
+ def default_opts
7
+ super.merge(:type => Purview::Types::Float)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Purview
2
+ module Columns
3
+ class Id < Integer
4
+ private
5
+
6
+ def default_opts
7
+ super.merge(:nullable => false, :primary_key => true)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Purview
2
+ module Columns
3
+ class Integer < Base
4
+ private
5
+
6
+ def default_opts
7
+ super.merge(:type => Purview::Types::Integer)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Purview
2
+ module Columns
3
+ class Money < Base
4
+ private
5
+
6
+ def default_opts
7
+ super.merge(:type => Purview::Types::Float)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Purview
2
+ module Columns
3
+ class String < Base
4
+ private
5
+
6
+ def default_opts
7
+ super.merge(:type => Purview::Types::String, :limit => 255)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Purview
2
+ module Columns
3
+ class Text < Base
4
+ private
5
+
6
+ def default_opts
7
+ super.merge(:type => Purview::Types::Text)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Purview
2
+ module Columns
3
+ class Time < Base
4
+ private
5
+
6
+ def default_opts
7
+ super.merge(:type => Purview::Types::Time)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Purview
2
+ module Columns
3
+ class Timestamp < Base
4
+ private
5
+
6
+ def default_opts
7
+ super.merge(:type => Purview::Types::Timestamp)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Purview
2
+ module Columns
3
+ class UpdatedTimestamp < Timestamp
4
+ private
5
+
6
+ def default_opts
7
+ super.merge(:nullable => false)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Purview
2
+ module Columns
3
+ class UUID < Base
4
+ private
5
+
6
+ def default_opts
7
+ super.merge(:type => Purview::Types::UUID, :limit => 36)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ require 'purview/columns/base'
2
+ require 'purview/columns/boolean'
3
+ require 'purview/columns/date'
4
+ require 'purview/columns/float'
5
+ require 'purview/columns/integer'
6
+ require 'purview/columns/id'
7
+ require 'purview/columns/money'
8
+ require 'purview/columns/string'
9
+ require 'purview/columns/text'
10
+ require 'purview/columns/time'
11
+ require 'purview/columns/timestamp'
12
+ require 'purview/columns/created_timestamp'
13
+ require 'purview/columns/updated_timestamp'
14
+ require 'purview/columns/uuid'
@@ -0,0 +1,55 @@
1
+ module Purview
2
+ module Connections
3
+ class Base
4
+ def initialize(opts={})
5
+ @opts = opts
6
+ end
7
+
8
+ def connect
9
+ @connection ||= new_connection
10
+ self
11
+ end
12
+
13
+ def disconnect
14
+ connection.close
15
+ @connection = nil
16
+ self
17
+ end
18
+
19
+ def execute(sql)
20
+ logger.debug("Executing: #{sql}")
21
+ result = execute_sql(sql)
22
+ Purview::Structs::Result.new(
23
+ :rows => extract_rows(result),
24
+ :rows_affected => extract_rows_affected(result)
25
+ )
26
+ end
27
+
28
+ def with_transaction
29
+ raise %{All "#{Base}(s)" must override the "with_transaction" method}
30
+ end
31
+
32
+ private
33
+
34
+ include Purview::Mixins::Logger
35
+
36
+ attr_reader :opts, :connection
37
+
38
+ def execute_sql(sql)
39
+ raise %{All "#{Base}(s)" must override the "execute_sql" method}
40
+ end
41
+
42
+ def extract_rows(result)
43
+ raise %{All "#{Base}(s)" must override the "extract_rows" method}
44
+ end
45
+
46
+ def extract_rows_affected(result)
47
+ raise %{All "#{Base}(s)" must override the "extract_rows_affected" method}
48
+ end
49
+
50
+ def new_connection
51
+ raise %{All "#{Base}(s)" must override the "new_connection" method}
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,39 @@
1
+ module Purview
2
+ module Connections
3
+ class MySQL < Base
4
+ def with_transaction
5
+ connection.query(BEGIN_TRANSACTION)
6
+ yield.tap { |result| connection.query(COMMIT_TRANSACTION) }
7
+ rescue Mysql2::Error
8
+ connection.query(ROLLBACK_TRANSACTION)
9
+ raise
10
+ end
11
+
12
+ private
13
+
14
+ BEGIN_TRANSACTION = 'BEGIN'
15
+ COMMIT_TRANSACTION = 'COMMIT'
16
+ ROLLBACK_TRANSACTION = 'ROLLBACK'
17
+
18
+ def execute_sql(sql)
19
+ connection.query(sql, query_opts)
20
+ end
21
+
22
+ def extract_rows(result)
23
+ result && result.to_a
24
+ end
25
+
26
+ def extract_rows_affected(result)
27
+ connection.affected_rows
28
+ end
29
+
30
+ def new_connection
31
+ Mysql2::Client.new(opts)
32
+ end
33
+
34
+ def query_opts
35
+ { :cast => false }
36
+ end
37
+ end
38
+ end
39
+ end