purview 1.0.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
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