tylerkovacs-hypertable_adapter 0.1.0

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