tylerkovacs-hypertable_adapter 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 0
@@ -0,0 +1,334 @@
1
+ require 'active_record/connection_adapters/abstract_adapter'
2
+ require 'active_record/connection_adapters/qualified_column'
3
+
4
+ module ActiveRecord
5
+ class Base
6
+ def self.require_hypertable_thrift_client
7
+ # Include the hypertools driver if one hasn't already been loaded
8
+ unless defined? Hypertable::ThriftClient
9
+ gem 'hypertable-thrift-client'
10
+ require_dependency 'thrift_client'
11
+ end
12
+ end
13
+
14
+ def self.hypertable_connection(config)
15
+ config = config.symbolize_keys
16
+ require_hypertable_thrift_client
17
+
18
+ raise "Hypertable/ThriftBroker config missing :host" if !config[:host]
19
+ connection = Hypertable::ThriftClient.new(config[:host], config[:port])
20
+
21
+ ConnectionAdapters::HypertableAdapter.new(connection, logger, config)
22
+ end
23
+ end
24
+
25
+ module ConnectionAdapters
26
+ class HypertableAdapter < AbstractAdapter
27
+ @@read_latency = 0.0
28
+ @@write_latency = 0.0
29
+ cattr_accessor :read_latency, :write_latency
30
+
31
+ CELL_FLAG_DELETE_ROW = 0
32
+ CELL_FLAG_DELETE_COLUMN_FAMILY = 1
33
+ CELL_FLAG_DELETE_CELL = 2
34
+ CELL_FLAG_INSERT = 255
35
+
36
+ def initialize(connection, logger, config)
37
+ super(connection, logger)
38
+ @config = config
39
+ @hypertable_column_names = {}
40
+ end
41
+
42
+ def self.reset_timing
43
+ @@read_latency = 0.0
44
+ @@write_latency = 0.0
45
+ end
46
+
47
+ def self.get_timing
48
+ [@@read_latency, @@write_latency]
49
+ end
50
+
51
+ def convert_select_columns_to_array_of_columns(s, columns=nil)
52
+ select_rows = s.class == String ? s.split(',').map{|s| s.strip} : s
53
+ select_rows = select_rows.reject{|s| s == '*'}
54
+
55
+ if select_rows.empty? and !columns.blank?
56
+ for c in columns
57
+ next if c.name == 'ROW' # skip over the ROW key, always included
58
+ if c.is_a?(QualifiedColumn)
59
+ for q in c.qualifiers
60
+ select_rows << qualified_column_name(c.name, q.to_s)
61
+ end
62
+ else
63
+ select_rows << c.name
64
+ end
65
+ end
66
+ end
67
+
68
+ select_rows
69
+ end
70
+
71
+ def adapter_name
72
+ 'Hypertable'
73
+ end
74
+
75
+ def supports_migrations?
76
+ true
77
+ end
78
+
79
+ def native_database_types
80
+ {
81
+ :string => { :name => "varchar", :limit => 255 }
82
+ }
83
+ end
84
+
85
+ def sanitize_conditions(options)
86
+ case options[:conditions]
87
+ when Hash
88
+ # requires Hypertable API to support query by arbitrary cell value
89
+ raise "HyperRecord does not support specifying conditions by Hash"
90
+ when NilClass
91
+ # do nothing
92
+ else
93
+ raise "Only hash conditions are supported"
94
+ end
95
+ end
96
+
97
+ def execute_with_options(options)
98
+ # Rows can be specified using a number of different options:
99
+ # row ranges (start_row and end_row)
100
+ options[:row_intervals] ||= []
101
+
102
+ if options[:row_keys]
103
+ options[:row_keys].flatten.each do |rk|
104
+ row_interval = Hypertable::ThriftGen::RowInterval.new
105
+ row_interval.start_row = rk
106
+ row_interval.start_inclusive = true
107
+ row_interval.end_row = rk
108
+ row_interval.end_inclusive = true
109
+ options[:row_intervals] << row_interval
110
+ end
111
+ elsif options[:start_row]
112
+ raise "missing :end_row" if !options[:end_row]
113
+
114
+ options[:start_inclusive] = options.has_key?(:start_inclusive) ? options[:start_inclusive] : true
115
+ options[:end_inclusive] = options.has_key?(:end_inclusive) ? options[:end_inclusive] : true
116
+
117
+ row_interval = Hypertable::ThriftGen::RowInterval.new
118
+ row_interval.start_row = options[:start_row]
119
+ row_interval.start_inclusive = options[:start_inclusive]
120
+ row_interval.end_row = options[:end_row]
121
+ row_interval.end_inclusive = options[:end_inclusive]
122
+ options[:row_intervals] << row_interval
123
+ end
124
+
125
+ sanitize_conditions(options)
126
+
127
+ select_rows = convert_select_columns_to_array_of_columns(options[:select], options[:columns])
128
+
129
+ t1 = Time.now
130
+ table_name = options[:table_name]
131
+ scan_spec = convert_options_to_scan_spec(options)
132
+ cells = @connection.get_cells(table_name, scan_spec)
133
+ @@read_latency += Time.now - t1
134
+
135
+ cells
136
+ end
137
+
138
+ def convert_options_to_scan_spec(options={})
139
+ scan_spec = Hypertable::ThriftGen::ScanSpec.new
140
+ options[:revs] ||= 1
141
+ options[:return_deletes] ||= false
142
+
143
+ for key in options.keys
144
+ case key.to_sym
145
+ when :row_intervals
146
+ scan_spec.row_intervals = options[key]
147
+ when :cell_intervals
148
+ scan_spec.cell_intervals = options[key]
149
+ when :start_time
150
+ scan_spec.start_time = options[key]
151
+ when :end_time
152
+ scan_spec.end_time = options[key]
153
+ when :limit
154
+ scan_spec.row_limit = options[key]
155
+ when :revs
156
+ scan_spec.revs = options[key]
157
+ when :return_deletes
158
+ scan_spec.return_deletes = options[key]
159
+ when :table_name, :start_row, :end_row, :start_inclusive, :end_inclusive, :select, :columns, :row_keys, :conditions, :include
160
+ # ignore
161
+ else
162
+ raise "Unrecognized scan spec option: #{key}"
163
+ end
164
+ end
165
+
166
+ scan_spec
167
+ end
168
+
169
+ def execute(hql, name=nil)
170
+ log(hql, name) { @connection.hql_query(hql) }
171
+ end
172
+
173
+ # Returns array of column objects for table associated with this class.
174
+ # Hypertable allows columns to include dashes in the name. This doesn't
175
+ # play well with Ruby (can't have dashes in method names), so we must
176
+ # maintain a mapping of original column names to Ruby-safe names.
177
+ def columns(table_name, name = nil)#:nodoc:
178
+ # Each table always has a row key called 'ROW'
179
+ columns = [
180
+ Column.new('ROW', '')
181
+ ]
182
+ schema = describe_table(table_name)
183
+ doc = REXML::Document.new(schema)
184
+ column_families = doc.elements['Schema/AccessGroup[@name="default"]'].elements.to_a
185
+
186
+ @hypertable_column_names[table_name] ||= {}
187
+ for cf in column_families
188
+ column_name = cf.elements['Name'].text
189
+ rubified_name = rubify_column_name(column_name)
190
+ @hypertable_column_names[table_name][rubified_name] = column_name
191
+ columns << new_column(rubified_name, '')
192
+ end
193
+
194
+ columns
195
+ end
196
+
197
+ def remove_column_from_name_map(table_name, name)
198
+ @hypertable_column_names[table_name].delete(rubify_column_name(name))
199
+ end
200
+
201
+ def add_column_to_name_map(table_name, name)
202
+ @hypertable_column_names[table_name][rubify_column_name(name)] = name
203
+ end
204
+
205
+ def add_qualified_column(table_name, column_family, qualifiers=[], default='', sql_type=nil, null=true)
206
+ qc = QualifiedColumn.new(column_family, default, sql_type, null)
207
+ qc.qualifiers = qualifiers
208
+ qualifiers.each{|q| add_column_to_name_map(table_name, qualified_column_name(column_family, q))}
209
+ qc
210
+ end
211
+
212
+ def new_column(column_name, default_value='')
213
+ Column.new(rubify_column_name(column_name), default_value)
214
+ end
215
+
216
+ def qualified_column_name(column_family, qualifier=nil)
217
+ [column_family, qualifier].compact.join(':')
218
+ end
219
+
220
+ def rubify_column_name(column_name)
221
+ column_name.to_s.gsub(/-+/, '_')
222
+ end
223
+
224
+ def is_qualified_column_name?(column_name)
225
+ column_family, qualifier = column_name.split(':', 2)
226
+ if qualifier
227
+ [true, column_family, qualifier]
228
+ else
229
+ [false, nil, nil]
230
+ end
231
+ end
232
+
233
+ def quote(value, column = nil)
234
+ case value
235
+ when NilClass then ''
236
+ when String then value
237
+ else super(value, column)
238
+ end
239
+ end
240
+
241
+ def quote_column_name(name)
242
+ "'#{name}'"
243
+ end
244
+
245
+ def quote_column_name_for_table(name, table_name)
246
+ quote_column_name(hypertable_column_name(name, table_name))
247
+ end
248
+
249
+ def hypertable_column_name(name, table_name, declared_columns_only=false)
250
+ n = @hypertable_column_names[table_name][name]
251
+ n ||= name if !declared_columns_only
252
+ n
253
+ end
254
+
255
+ def describe_table(table_name)
256
+ @connection.get_schema(table_name)
257
+ end
258
+
259
+ def tables(name=nil)
260
+ @connection.get_tables
261
+ end
262
+
263
+ def drop_table(table_name, options = {})
264
+ @connection.drop_table(table_name, options[:if_exists] || false)
265
+ end
266
+
267
+ def write_cells(table_name, cells)
268
+ return if cells.blank?
269
+
270
+ @connection.with_mutator(table_name) do |mutator|
271
+ t1 = Time.now
272
+ @connection.set_cells(mutator, cells.map{|c| cell_from_array(c)})
273
+ @@write_latency += Time.now - t1
274
+ end
275
+ end
276
+
277
+ # Cell passed in as [row_key, column_name, value]
278
+ def cell_from_array(array)
279
+ cell = Hypertable::ThriftGen::Cell.new
280
+ cell.row_key = array[0]
281
+ column_family, column_qualifier = array[1].split(':')
282
+ cell.column_family = column_family
283
+ cell.column_qualifier = column_qualifier if column_qualifier
284
+ cell.value = array[2] if array[2]
285
+ cell
286
+ end
287
+
288
+ def delete_cells(table_name, cells)
289
+ t1 = Time.now
290
+
291
+ @connection.with_mutator(table_name) do |mutator|
292
+ @connection.set_cells(mutator, cells.map{|c|
293
+ cell = cell_from_array(c)
294
+ cell.flag = CELL_FLAG_DELETE_CELL
295
+ cell
296
+ })
297
+ end
298
+
299
+ @@write_latency += Time.now - t1
300
+ end
301
+
302
+ def delete_rows(table_name, row_keys)
303
+ t1 = Time.now
304
+ cells = row_keys.map do |row_key|
305
+ cell = Hypertable::ThriftGen::Cell.new
306
+ cell.row_key = row_key
307
+ cell.flag = CELL_FLAG_DELETE_ROW
308
+ cell
309
+ end
310
+
311
+ @connection.with_mutator(table_name) do |mutator|
312
+ @connection.set_cells(mutator, cells)
313
+ end
314
+
315
+ @@write_latency += Time.now - t1
316
+ end
317
+
318
+ def insert_fixture(fixture, table_name)
319
+ fixture_hash = fixture.to_hash
320
+ row_key = fixture_hash.delete('ROW')
321
+ cells = []
322
+ fixture_hash.keys.each{|k| cells << [row_key, k, fixture_hash[k]]}
323
+ write_cells(table_name, cells)
324
+ end
325
+
326
+ private
327
+
328
+ def select(hql, name=nil)
329
+ # TODO: need hypertools run_hql to return result set
330
+ raise "not yet implemented"
331
+ end
332
+ end
333
+ end
334
+ end
@@ -0,0 +1,59 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ # Like a regular database, each table in Hypertable has a fixed list
4
+ # of columns. However, Hypertable allows flexible schemas through the
5
+ # use of column qualifiers. Suppose a table is defined to have a single
6
+ # column called misc.
7
+ #
8
+ # CREATE TABLE pages (
9
+ # 'misc'
10
+ # )
11
+ #
12
+ # In Hypertable, each traditional database column is referred to as
13
+ # a column family. Each column family can have a theoretically infinite
14
+ # number of qualified instances. An instance of a qualified column
15
+ # is referred to using the column_family:qualifer notation. e.g.,
16
+ #
17
+ # misc:red
18
+ # misc:green
19
+ # misc:blue
20
+ #
21
+ # These qualified column instances do not need to be declared as part
22
+ # of the table schema. The table schema itself does not provide
23
+ # an indication of whether a column family has been used with qualifiers.
24
+ # As a results, we must explicitly declare intent to use a column family
25
+ # in a qualified manner in our class definition. The resulting AR
26
+ # object models the column family as a Hash.
27
+ #
28
+ # class Page < ActiveRecord::HyperBase
29
+ # qualified_column :misc
30
+ # end
31
+ #
32
+ # p = Page.new
33
+ # p.ROW = 'page_1'
34
+ # p.misc['url'] = 'http://www.zvents.com/'
35
+ # p.misc['hits'] = 127
36
+ # p.save
37
+
38
+ class QualifiedColumn < Column
39
+ attr_accessor :qualifiers
40
+
41
+ def initialize(name, default, sql_type = nil, null = true)
42
+ @qualifiers ||= []
43
+ super
44
+ end
45
+
46
+ def klass
47
+ Hash
48
+ end
49
+
50
+ def default
51
+ # Unlike regular AR objects, the default value for a column must
52
+ # be cloned. This is to avoid copy-by-reference issues with {}
53
+ # objects. Without clone, all instances of the class will share
54
+ # a reference to the same object.
55
+ @default.clone
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,117 @@
1
+ require File.join(File.dirname(__FILE__), '../spec_helper.rb')
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ describe HypertableAdapter do
6
+ before do
7
+ @h = HypertableAdapter.new(nil, nil, {})
8
+ end
9
+
10
+ describe HypertableAdapter, '.describe_table' do
11
+ before do
12
+ @describe_table_text = '<Schema generation="1">\n <AccessGroup name="default">\n <ColumnFamily id="1">\n <Name>message</Name> </ColumnFamily>\n <ColumnFamily id="2">\n <Name>date-time</Name>\n </ColumnFamily>\n </AccessGroup>\n </Schema>\n'
13
+ end
14
+
15
+ it "should return a string describing a table" do
16
+ @h.should_receive(:describe_table).with('name').and_return(@describe_table_text)
17
+ @h.describe_table('name').should == @describe_table_text
18
+ end
19
+ end
20
+
21
+ describe HypertableAdapter, '.column' do
22
+ before do
23
+ @describe_table_text = '<Schema generation="1">\n <AccessGroup name="default">\n <ColumnFamily id="1">\n <Name>message</Name> </ColumnFamily>\n <ColumnFamily id="2">\n <Name>date-time</Name>\n </ColumnFamily>\n </AccessGroup>\n </Schema>\n'
24
+ end
25
+
26
+ it "should return an array of columns representing the table schema" do
27
+ @h.stub!(:describe_table).with('name').and_return(@describe_table_text)
28
+ columns = @h.columns('name')
29
+ columns.should be_is_a(Array)
30
+ columns.should have_exactly(3).columns
31
+ # The first column within a Hypertable is always the row key.
32
+ columns[0].name.should == "ROW"
33
+ columns[1].name.should == "message"
34
+ # notice that the original column name "date-time" is converted
35
+ # to a Ruby-friendly column name "date_time"
36
+ columns[2].name.should == "date_time"
37
+ end
38
+
39
+ it "should set up the name mappings between ruby and hypertable" do
40
+ @h.stub!(:describe_table).with('name').and_return(@describe_table_text)
41
+ columns = @h.columns('name')
42
+ @h.hypertable_column_name('date_time', 'name').should == 'date-time'
43
+ end
44
+ end
45
+
46
+ describe HypertableAdapter, '.quote_column_name' do
47
+ it "should surround column name in single quotes" do
48
+ @h.quote_column_name("date_time").should == "'date_time'"
49
+ end
50
+ end
51
+
52
+ describe HypertableAdapter, '.rubify_column_name' do
53
+ it "should change dashes to underscores in column names" do
54
+ @h.rubify_column_name("date-time").should == "date_time"
55
+ end
56
+ end
57
+
58
+ describe HypertableAdapter, '.tables' do
59
+ before do
60
+ @tables = ["table1", "table2"]
61
+ end
62
+
63
+ it "should return an array of table names" do
64
+ @h.should_receive(:tables).and_return(@tables)
65
+ @h.tables.should == @tables
66
+ end
67
+ end
68
+
69
+ describe HypertableAdapter, '.quote' do
70
+ it "should return empty string for nil values" do
71
+ @h.quote(nil).should == ''
72
+ end
73
+ end
74
+
75
+ describe HypertableAdapter, '.quote' do
76
+ it "should return a quoted string for all non-nil values" do
77
+ @h.quote(1).should == "1"
78
+ @h.quote('happy').should == "happy"
79
+ end
80
+ end
81
+
82
+ describe HypertableAdapter, '.is_qualified_column_name?' do
83
+ it "should return false for regular columns" do
84
+ status, family, qualifier = @h.is_qualified_column_name?("col1")
85
+ status.should be_false
86
+ family.should be_nil
87
+ qualifier.should be_nil
88
+ end
89
+
90
+ it "should return true for qualified columns" do
91
+ status, family, qualifier = @h.is_qualified_column_name?("col1:red")
92
+ status.should be_true
93
+ family.should == 'col1'
94
+ qualifier.should == 'red'
95
+ end
96
+ end
97
+
98
+ describe HypertableAdapter, '.convert_select_columns_to_array_of_columns(' do
99
+ it "should accept an array as input" do
100
+ @h.convert_select_columns_to_array_of_columns(["one", "two", "three"]).should == ["one", "two", "three"]
101
+ end
102
+
103
+ it "should accept a string as input and split the results on commas" do
104
+ @h.convert_select_columns_to_array_of_columns("one,two,three").should == ["one", "two", "three"]
105
+ end
106
+
107
+ it "should strip whitespace from column names" do
108
+ @h.convert_select_columns_to_array_of_columns(" one,two , three ").should == ["one", "two", "three"]
109
+ end
110
+
111
+ it "should return [] for a request on * columns" do
112
+ @h.convert_select_columns_to_array_of_columns("*").should == []
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,38 @@
1
+ ENV["RAILS_ENV"] = "test"
2
+ require File.expand_path(File.join(File.dirname(__FILE__), "../../../../config/environment"))
3
+ require 'spec'
4
+ require 'spec/rails'
5
+
6
+ Spec::Runner.configure do |config|
7
+ # If you're not using ActiveRecord you should remove these
8
+ # lines, delete config/database.yml and disable :active_record
9
+ # in your config/boot.rb
10
+ config.use_transactional_fixtures = true
11
+ config.use_instantiated_fixtures = false
12
+ config.fixture_path = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures'))
13
+
14
+ # == Fixtures
15
+ #
16
+ # You can declare fixtures for each example_group like this:
17
+ # describe "...." do
18
+ # fixtures :table_a, :table_b
19
+ #
20
+ # Alternatively, if you prefer to declare them only once, you can
21
+ # do so right here. Just uncomment the next line and replace the fixture
22
+ # names with your fixtures.
23
+ #
24
+ config.global_fixtures = []
25
+
26
+ #
27
+ # If you declare global fixtures, be aware that they will be declared
28
+ # for all of your examples, even those that don't use them.
29
+ #
30
+ # == Mock Framework
31
+ #
32
+ # RSpec uses it's own mocking framework by default. If you prefer to
33
+ # use mocha, flexmock or RR, uncomment the appropriate line:
34
+ #
35
+ # config.mock_with :mocha
36
+ # config.mock_with :flexmock
37
+ # config.mock_with :rr
38
+ end
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tylerkovacs-hypertable_adapter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - tylerkovacs
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-02-01 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Hypertable Adapter allows ActiveRecord to communicate with Hypertable.
17
+ email: tyler.kovacs@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - VERSION.yml
26
+ - lib/active_record
27
+ - lib/active_record/connection_adapters
28
+ - lib/active_record/connection_adapters/hypertable_adapter.rb
29
+ - lib/active_record/connection_adapters/qualified_column.rb
30
+ - spec/spec_helper.rb
31
+ - spec/lib
32
+ - spec/lib/hypertable_adapter_spec.rb
33
+ has_rdoc: true
34
+ homepage: http://github.com/tylerkovacs/hypertable_adapter
35
+ post_install_message:
36
+ rdoc_options:
37
+ - --inline-source
38
+ - --charset=UTF-8
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ version:
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ version:
53
+ requirements: []
54
+
55
+ rubyforge_project:
56
+ rubygems_version: 1.2.0
57
+ signing_key:
58
+ specification_version: 2
59
+ summary: See README
60
+ test_files: []
61
+