Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

initial checking - hyper_record working with thrift-based hypertable …

…adapter
  • Loading branch information...
commit df1cd3c1ce0336b226650570282b60925f08c78c 1 parent 520a089
@tylerkovacs authored
View
8 Rakefile
@@ -7,10 +7,10 @@ begin
require 'jeweler'
Jeweler::Tasks.new do |s|
s.name = "hyper_record"
- s.summary = %Q{TODO}
+ s.summary = %Q{Fully integrates ActiveRecord with Hypertable.}
s.email = "tyler.kovacs@gmail.com"
s.homepage = "http://github.com/tylerkovacs/hyper_record"
- s.description = "TODO"
+ s.description = "See README"
s.authors = ["tylerkovacs"]
end
rescue LoadError
@@ -32,8 +32,8 @@ Rake::RDocTask.new do |rdoc|
end
Rcov::RcovTask.new do |t|
- t.libs << 'test'
- t.test_files = FileList['test/**/*_test.rb']
+ t.libs << 'spec'
+ t.test_files = FileList['spec/**/*_spec.rb']
t.verbose = true
end
View
1  init.rb
@@ -0,0 +1 @@
+require 'hyper_record'
View
80 lib/associations/hyper_has_and_belongs_to_many_association_extension.rb
@@ -0,0 +1,80 @@
+module ActiveRecord
+ module Associations
+ module HyperHasAndBelongsToManyAssociationExtension
+ def self.included(base)
+ base.class_eval do
+ alias_method :find_without_hypertable, :find
+ alias_method :find, :find_with_hypertable
+
+ alias_method :delete_records_without_hypertable, :delete_records
+ alias_method :delete_records, :delete_records_with_hypertable
+
+ alias_method :insert_record_without_hypertable, :insert_record
+ alias_method :insert_record, :insert_record_with_hypertable
+
+ alias_method :create_record_without_hypertable, :create_record
+ alias_method :create_record, :create_record_with_hypertable
+ end
+ end
+
+ def find_with_hypertable(*args)
+ if @reflection.klass <= ActiveRecord::HyperBase
+ associated_object_ids = @owner.send(@reflection.association_foreign_key).keys
+ @reflection.klass.find(associated_object_ids)
+ else
+ find_without_hypertable(*args)
+ end
+ end
+
+ def insert_record_with_hypertable(record, force=true)
+ if @reflection.klass <= ActiveRecord::HyperBase
+ @owner.send(@reflection.association_foreign_key)[record.ROW] = 1
+ @owner.write_cells([ [@owner.ROW, record.connection.qualified_column_name(@reflection.association_foreign_key, record.ROW), "1"] ])
+ record.send(@reflection.primary_key_name)[@owner.ROW] = 1
+ record.write_cells([ [record.ROW, record.connection.qualified_column_name(@reflection.primary_key_name, @owner.ROW), "1"] ])
+ else
+ insert_record_without_hypertable(record, force)
+ end
+ end
+
+ def delete_records_with_hypertable(records)
+ if @reflection.klass <= ActiveRecord::HyperBase
+ cells_to_delete_by_table = Hash.new{|h,k| h[k] = []}
+
+ records.each {|r|
+ # remove association cells from in memory object
+ @owner.send(@reflection.association_foreign_key).delete(r.ROW)
+ r.send(@reflection.primary_key_name).delete(@owner.ROW)
+
+ # make list of cells that need to be removed from hypertable
+ cells_to_delete_by_table[@owner.class.table_name] << [@owner.ROW, r.connection.qualified_column_name(@reflection.association_foreign_key, r.ROW)]
+ cells_to_delete_by_table[r.class.table_name] << [r.ROW, r.connection.qualified_column_name(@reflection.primary_key_name, @owner.ROW)]
+ }
+
+ for table in cells_to_delete_by_table.keys
+ @owner.delete_cells(cells_to_delete_by_table[table], table)
+ end
+ else
+ delete_records_without_hypertable(records)
+ end
+ end
+
+ private
+ def create_record_with_hypertable(attributes)
+ if @reflection.klass <= ActiveRecord::HyperBase
+ r = @reflection.klass.create(attributes)
+ insert_record_with_hypertable(r)
+ r
+ else
+ create_record_without_hypertable(attributes) {|record|
+ insert_record_without_hypertable(record, true)
+ }
+ end
+ end
+ end
+
+ class HasAndBelongsToManyAssociation
+ include HyperHasAndBelongsToManyAssociationExtension
+ end
+ end
+end
View
78 lib/associations/hyper_has_many_association_extension.rb
@@ -0,0 +1,78 @@
+module ActiveRecord
+ module Associations
+ module HyperHasManyAssociationExtension
+ def self.included(base)
+ base.class_eval do
+ alias_method :find_without_hypertable, :find
+ alias_method :find, :find_with_hypertable
+
+ alias_method :delete_records_without_hypertable, :delete_records
+ alias_method :delete_records, :delete_records_with_hypertable
+
+ alias_method :insert_record_without_hypertable, :insert_record
+ alias_method :insert_record, :insert_record_with_hypertable
+
+ alias_method :create_record_without_hypertable, :create_record
+ alias_method :create_record, :create_record_with_hypertable
+ end
+ end
+
+ def find_with_hypertable(*args)
+ if @reflection.klass <= ActiveRecord::HyperBase
+ associated_object_ids = @owner.send(@reflection.association_foreign_key).keys
+ @reflection.klass.find(associated_object_ids)
+ else
+ find_without_hypertable(*args)
+ end
+ end
+
+ def insert_record_with_hypertable(record)
+ if @reflection.klass <= ActiveRecord::HyperBase
+ raise "missing ROW key on record" if record.ROW.blank?
+ @owner.send(@reflection.association_foreign_key)[record.ROW] = 1
+ @owner.write_cells([ [@owner.ROW, record.connection.qualified_column_name(@reflection.association_foreign_key, record.ROW), "1"] ])
+ record.send("#{@reflection.primary_key_name}=", @owner.ROW)
+ record.save
+ self.reset
+ else
+ insert_record_without_hypertable(record)
+ end
+ end
+
+ def delete_records_with_hypertable(records)
+ if @reflection.klass <= ActiveRecord::HyperBase
+ cells_to_delete = []
+ records.each {|r|
+ # remove association cells from in memory object
+ r.send("#{@reflection.primary_key_name}=", nil)
+ r.save
+
+ # make list of cells that need to be removed from hypertable
+ cells_to_delete << [@owner.ROW, @owner.connection.qualified_column_name(@reflection.association_foreign_key, r.ROW)]
+ }
+
+ @owner.delete_cells(cells_to_delete, @owner.class.table_name)
+ else
+ delete_records_without_hypertable(records)
+ end
+ end
+
+ private
+ def create_record_with_hypertable(attributes)
+ if @reflection.klass <= ActiveRecord::HyperBase
+ r = @reflection.klass.create(attributes)
+ insert_record_with_hypertable(r)
+ r
+ else
+ create_record_without_hypertable(attributes) {|record|
+ insert_record_without_hypertable(record)
+ }
+ end
+ end
+ end
+
+ class HasManyAssociation
+ include HyperHasManyAssociationExtension
+ end
+ end
+end
View
306 lib/hyper_record.rb
@@ -0,0 +1,306 @@
+require 'associations/hyper_has_many_association_extension'
+require 'associations/hyper_has_and_belongs_to_many_association_extension'
+
+module ActiveRecord
+ class Base
+ def self.inherited(child) #:nodoc:
+ return if child == ActiveRecord::HyperBase
+
+ @@subclasses[self] ||= []
+ @@subclasses[self] << child
+ super
+ end
+ end
+
+ class HyperBase < Base
+ # All records must include a ROW key
+ validates_presence_of :ROW
+
+ def initialize(attrs={})
+ super(attrs)
+ self.ROW = attrs[:ROW] if attrs[:ROW]
+ end
+
+ # Instance Methods
+ def update
+ write_quoted_attributes(attributes_with_quotes(false, false))
+ true
+ end
+
+ def create
+ write_quoted_attributes(attributes_with_quotes(false, false))
+ @new_record = false
+ self.attributes[self.class.primary_key]
+ end
+
+ def destroy
+ # check for associations and delete association cells as necessary
+ for reflection_key in self.class.reflections.keys
+ case self.class.reflections[reflection_key].macro
+ when :has_and_belongs_to_many
+ # remove all the association cells from the associated objects
+ cells_to_delete = []
+
+ for row_key in self.send(self.class.reflections[reflection_key].association_foreign_key).keys
+ cells_to_delete << [row_key, self.class.connection.qualified_column_name(self.class.reflections[reflection_key].primary_key_name, self.ROW)]
+ end
+
+ self.delete_cells(cells_to_delete, self.class.reflections[reflection_key].klass.table_name)
+ end
+ end
+
+ self.class.connection.delete_rows(self.class.table_name, [self.ROW])
+ end
+
+ def increment(attribute, by=1)
+ self[attribute] = self[attribute].to_i
+ self[attribute] += by
+ self
+ end
+
+ def increment!(attribute, by=1)
+ increment(attribute, by)
+ self.save
+ end
+
+ def decrement(attribute, by=1)
+ increment(attribute, -by)
+ end
+
+ def decrement!(attribute, by=1)
+ increment!(attribute, -by)
+ end
+
+ # Returns a copy of the attributes hash where all the values have been
+ # safely quoted for insertion. Translated qualified columns from a Hash
+ # value in Ruby to a flat list of attributes.
+ def attributes_with_quotes(include_primary_key = true, include_readonly_attributes = true)
+ quoted = attributes.inject({}) do |quoted, (name, value)|
+ if column = column_for_attribute(name)
+ if column.is_a?(ConnectionAdapters::QualifiedColumn) and value.is_a?(Hash)
+ value.keys.each{|k|
+ quoted[self.class.connection.qualified_column_name(column.name, k)] = quote_value(value[k], column)
+ }
+ else
+ quoted[name] = quote_value(value, column) unless !include_primary_key && column.primary
+ end
+ end
+ quoted
+ end
+ include_readonly_attributes ? quoted : remove_readonly_attributes(quoted)
+ end
+
+ def quoted_attributes_to_cells(quoted_attrs, table=self.class.table_name)
+ cells = []
+ pk = self.attributes[self.class.primary_key]
+ quoted_attrs.keys.each{|key|
+ cells << [pk, connection.hypertable_column_name(key, table), quoted_attrs[key].to_s]
+ }
+ cells
+ end
+
+ def write_quoted_attributes(quoted_attrs, table=self.class.table_name)
+ write_cells(quoted_attributes_to_cells(quoted_attrs, table))
+ end
+
+ # Write an array of cells to Hypertable
+ def write_cells(cells, table=self.class.table_name)
+ connection.write_cells(table, cells)
+ end
+
+ # Delete an array of cells from Hypertable
+ # cells is an array of cell keys [["row", "column"], ...]
+ def delete_cells(cells, table=self.class.table_name)
+ connection.delete_cells(table, cells)
+ end
+
+ # Delete an array of rows from Hypertable
+ # rows is an array of row keys ["row1", "row2", ...]
+ def delete_rows(row_keys, table=self.class.table_name)
+ connection.delete_rows(table, cells)
+ end
+
+ # Class Methods
+ class << self
+ def abstract_class?
+ self == ActiveRecord::HyperBase
+ end
+
+ def exists?(id_or_conditions)
+ case id_or_conditions
+ when Fixnum, String
+ !find(:first, :row_keys => [id_or_conditions]).nil?
+ when Hash
+ !find(:first, :conditions => id_or_conditions).nil?
+ else
+ raise "only Fixnum, String and Hash arguments supported"
+ end
+ end
+
+ def delete(*ids)
+ self.connection.delete_rows(table_name, ids.flatten)
+ end
+
+ def find(*args)
+ options = args.extract_options!
+
+ case args.first
+ when :first then find_initial(options)
+ when :all then find_by_options(options)
+ else find_from_ids(args, options)
+ end
+ end
+
+ def find_initial(options)
+ options.update(:limit => 1)
+ find_by_options(options).first
+ end
+
+ def find_by_options(options)
+ options[:table_name] ||= quoted_table_name
+ options[:columns] ||= columns
+
+ # Don't request the ROW key explicitly, it always comes back
+ options[:select] ||= qualified_column_names_without_row_key.map{|c| connection.hypertable_column_name(c, table_name)}
+
+ rows = Hash.new{|h,k| h[k] = []}
+ for cell in connection.execute_with_options(options)
+ rows[cell.row_key] << cell
+ end
+
+ rows.values.map{|row|
+ row_with_mapped_column_names = {
+ 'ROW' => row.first.row_key
+ }
+
+ for cell in row
+ if cell.column_qualifier
+ family = connection.rubify_column_name(cell.column_family)
+ row_with_mapped_column_names[family] ||= {}
+ row_with_mapped_column_names[family][cell.column_qualifier] = cell.value
+ else
+ family = connection.rubify_column_name(cell.column_family)
+ row_with_mapped_column_names[family] = cell.value
+ end
+ end
+
+ # make sure that the resulting object has attributes for all
+ # columns - even ones that aren't in the response (due to limited
+ # select)
+ for column in column_families_without_row_key
+ if !row_with_mapped_column_names.has_key?(column.name)
+ if column.is_a?(ActiveRecord::ConnectionAdapters::QualifiedColumn)
+ row_with_mapped_column_names[column.name] = {}
+ else
+ row_with_mapped_column_names[column.name] = nil
+ end
+ end
+ end
+
+ instantiate(row_with_mapped_column_names)
+ }
+ end
+
+ def find_from_ids(ids, options)
+ expects_array = ids.first.kind_of?(Array)
+ return ids.first if expects_array && ids.first.empty?
+ ids = ids.flatten.compact.uniq
+
+ case ids.size
+ when 0
+ raise RecordNotFound, "Couldn't find #{name} without an ID"
+ when 1
+ result = find_one(ids.first, options)
+ expects_array ? [ result ] : result
+ else
+ find_some(ids, options)
+ end
+ end
+
+ def find_one(id, options)
+ return nil if id.blank?
+
+ options[:row_keys] = [id.to_s]
+
+ if result = find_initial(options)
+ result
+ else
+ raise ::ActiveRecord::RecordNotFound, "Couldn't find #{name} with ID=#{id}"
+ end
+ end
+
+ def find_some(ids, options)
+ options[:row_keys] = [ids.map{|i| i.to_s}]
+ find_by_options(options)
+ end
+
+ def table_exists?(name=table_name)
+ connection.tables.include?(name)
+ end
+
+ def drop_table
+ connection.drop_table(table_name) if table_exists?
+ end
+
+ # Returns the primary key field for a table. In Hypertable, a single
+ # row key exists for each row. The row key is referred to as ROW
+ # in HQL, so we'll refer to it the same way here.
+ def primary_key
+ "ROW"
+ end
+
+ # Returns array of column objects for table associated with this class.
+ def columns
+ unless @columns
+ @columns = connection.columns(table_name, "#{name} Columns")
+ @qualified_columns ||= []
+ @qualified_columns.each{|qc|
+ # Remove the column family from the column list
+ @columns = @columns.reject{|c| c.name == qc[:column_name].to_s}
+ connection.remove_column_from_name_map(table_name, qc[:column_name].to_s)
+
+ # Add the new qualified column family to the column list
+ @columns << connection.add_qualified_column(table_name, qc[:column_name].to_s, qc[:qualifiers], {}.clone)
+ }
+ @columns.each {|column| column.primary = column.name == primary_key}
+ end
+ @columns
+ end
+
+ def quoted_column_names(attributes=attributes_with_quotes)
+ attributes.keys.collect do |column_name|
+ self.class.connection.quote_column_name_for_table(column_name, table_name)
+ end
+ end
+
+ def column_families_without_row_key
+ columns[1,columns.length]
+ end
+
+ def qualified_column_names_without_row_key
+ cols = column_families_without_row_key.map{|c| c.name}
+ for qc in @qualified_columns
+ cols.delete(qc[:column_name].to_s)
+ for qualifier in qc[:qualifiers]
+ cols << "#{qc[:column_name]}:#{qualifier}"
+ end
+ end
+ cols
+ end
+
+ # qualified_column :misc, :qualifiers => [:name, :url]
+ attr_accessor :qualified_columns
+ def qualified_column(*attrs)
+ @qualified_columns ||= []
+ name = attrs.shift
+
+ qualifiers = attrs.shift
+ qualifiers = qualifiers.symbolize_keys[:qualifiers] if qualifiers
+ @qualified_columns << {
+ :column_name => name,
+ :qualifiers => qualifiers || []
+ }
+ end
+ end
+ end
+end
View
8 spec/fixtures/pages.yml
@@ -0,0 +1,8 @@
+page_1:
+ ROW: page_1
+ name: LOLcats and more
+ url: http://www.icanhascheezburger.com
+page_2:
+ ROW: page_2
+ name: ESPN
+ url: http://espn.go.com
View
1  spec/fixtures/qualified_pages.yml
@@ -0,0 +1 @@
+# intentionally left blank
View
234 spec/lib/associations_spec.rb
@@ -0,0 +1,234 @@
+require File.join(File.dirname(__FILE__), '../spec_helper.rb')
+
+class Book < ActiveRecord::HyperBase
+ has_many :chapters
+ has_and_belongs_to_many :authors
+ qualified_column :author_id, :qualifiers => ['.*']
+ qualified_column :chapter_id, :qualifiers => ['.*']
+
+ def self.create_table
+ hql = "CREATE TABLE #{table_name} (
+ 'title',
+ 'author_id',
+ 'chapter_id'
+ )"
+ connection.execute(hql)
+ end
+end
+
+class Chapter < ActiveRecord::HyperBase
+ belongs_to :book
+
+ def self.create_table
+ hql = "CREATE TABLE #{table_name} (
+ 'title',
+ 'book_id' MAX_VERSIONS=1
+ )"
+ connection.execute(hql)
+ end
+end
+
+class Author < ActiveRecord::HyperBase
+ has_and_belongs_to_many :books
+ qualified_column :book_id, :qualifiers => ['.*']
+
+ def self.create_table
+ hql = "CREATE TABLE #{table_name} (
+ 'name',
+ 'book_id'
+ )"
+ connection.execute(hql)
+ end
+end
+
+module ActiveRecord
+ module HyperRecord
+ describe HyperBase, '.has_and_belongs_to_many' do
+ before(:each) do
+ Book.drop_table
+ Author.drop_table
+ Book.create_table
+ Author.create_table
+
+ @b = Book.new({:title => "Curious George and the Electric Fence"})
+ @b.ROW = 'curious_george'
+ @b.save!
+
+ @a1 = Author.new({:name => 'Irvine Welsh', :ROW => 'irvine_welsh'})
+ @a1.save!
+ @a2 = Author.new({:name => 'Douglas Adams', :ROW => 'douglas_adams'})
+ @a2.save!
+ end
+
+ it "should support addition of association elements using <<" do
+ @b.authors.should be_empty
+ @b.authors << @a1
+ @b.authors.should == [@a1]
+ @b.reload
+ @b.authors.should == [@a1]
+ @a1.books.should == [@b]
+ end
+
+ it "should allow multiple objects to be associated through HABTM" do
+ @b.authors.should be_empty
+ @b.authors << @a1
+ @b.authors << @a2
+ @b.authors.map{|x| x.ROW}.sort.should == [@a1, @a2].map{|x| x.ROW}.sort
+ @b.reload
+ @b.authors.map{|x| x.ROW}.sort.should == [@a1, @a2].map{|x| x.ROW}.sort
+ @a1.books.map{|x| x.ROW}.sort.should == [@b].map{|x| x.ROW}.sort
+ @a2.books.map{|x| x.ROW}.sort.should == [@b].map{|x| x.ROW}.sort
+ end
+
+ it "should allow removal of association elements using clear" do
+ @b.authors.should be_empty
+ @b.authors << @a1
+ @b.authors.should == [@a1]
+ @b.authors.clear
+ @b.reload
+ @b.authors.should be_empty
+ end
+
+ it "should allow an object to be created through the association" do
+ a = @b.authors.create({:name => 'Harper Lee', :ROW => 'harper_lee'})
+ a.new_record?.should be_false
+ a.reload
+ a.books.should == [@b]
+ end
+
+ it "should allow an object to be newed through the association" do
+ @b.authors.should be_empty
+ a = @b.authors.new({:name => 'Harper Lee', :ROW => 'harper_lee'})
+ a.new_record?.should be_true
+ a.save!
+ a.reload
+ a.books.should be_empty
+ end
+
+ it "should allow removal of association elements using delete" do
+ @b.authors.should be_empty
+ @b.authors << @a1
+ @b.authors << @a2
+ @b.authors.delete(@a2)
+ @b.reload
+ @b.authors.should == [@a1]
+ end
+
+ it "should clean up association cells when an object is destroyed" do
+ @b.authors.should be_empty
+ @b.authors << @a1
+ @b.author_id.should == {@a1.ROW => 1}
+ @a1.destroy
+ @b.reload
+ @b.author_id.should == {}
+ end
+ end
+
+ describe HyperBase, '.belongs_to_and_has_many' do
+ before(:each) do
+ Book.drop_table
+ Chapter.drop_table
+ Book.create_table
+ Chapter.create_table
+
+ @b = Book.new({:title => "Curious George and the Electric Fence"})
+ @b.ROW = 'curious_george'
+ @b.save!
+
+ @c1 = Chapter.new({:title => 'Ch 1', :ROW => 'c1'})
+ @c1.save!
+ @b.chapters << @c1
+ @c2 = Chapter.new({:title => 'Ch 2', :ROW => 'c2'})
+ @c2.save!
+ end
+
+ it "should allow belongs_to assocs between two hyperrecord objects" do
+ @c1.book.should == @b
+ @c2.book.should == nil
+ @b.chapters.to_a.should == [@c1]
+ @b.chapters << @c2
+ @b.reload
+ @b.chapters.to_a.should == [@c1, @c2]
+ @c2.book_id.should == @b.ROW
+ end
+
+ it "should clear has_many associations when requested" do
+ @b.chapters.to_a.should == [@c1]
+ @b.chapters.clear
+ @b.reload
+ @b.chapters.to_a.should be_empty
+ @b.chapter_id.should == {}
+ end
+
+ it "should allow new records through has_many but note that the association cells are not written, so this method is to be avoided" do
+ @b.chapters.to_a.should == [@c1]
+ c = @b.chapters.new({:ROW => 'c3', :title => 'Ch 3'})
+ c.new_record?.should be_true
+ c.save!
+ @b.reload
+ @b.chapters.length.should == 1
+ @b.chapters.should == [@c1]
+ @b.chapter_id.should == {@c1.ROW => "1"}
+ @b.chapters << c
+ @b.reload
+ @b.chapters.should == [@c1, c]
+ @b.chapter_id.should == {@c1.ROW => "1", c.ROW => "1"}
+ end
+
+ it "should allow create records through has_many" do
+ @b.chapters.to_a.should == [@c1]
+ c = @b.chapters.create({:ROW => 'c3', :title => 'Ch 3'})
+ c.new_record?.should be_false
+ @b.reload
+ @b.chapters.length.should == 2
+ @b.chapters.should == [@c1, c]
+ end
+
+ it "should allow new records using << has_many" do
+ @b.chapters.to_a.should == [@c1]
+ c = Chapter.new({:ROW => 'c3', :title => 'Ch 3'})
+ c.new_record?.should be_true
+ @b.chapters << c
+ @b.reload
+ @b.chapters.length.should == 2
+ @b.chapters.should == [@c1, c]
+ c.reload
+ c.book.should == @b
+ end
+
+ it "should support remove of has_many records through delete" do
+ @b.chapters.to_a.should == [@c1]
+ c = @b.chapters.create({:ROW => 'c3', :title => 'Ch 3'})
+ @b.reload
+ @b.chapters.should == [@c1, c]
+ @b.chapter_id.should == {@c1.ROW => "1", c.ROW => "1"}
+ @b.chapters.delete(@c1)
+ @b.reload
+ @b.chapters.should == [c]
+ @b.chapter_id.should == {c.ROW => "1"}
+ end
+
+ it "should update belongs_to id value on assignment" do
+ @c2.book_id.should be_blank
+ @c2.book = @b
+ @c2.save!
+ @c2.reload
+ @c2.book_id.should == @b.id
+ end
+
+ it "should silently ignore eager loading of belongs_to associations" do
+ c1 = Chapter.find(@c1.ROW, :include => [:book])
+ # note: no exception, loaded? is marked as true and assoc still works
+ c1.book.loaded?.should be_true
+ c1.book.should == @b
+ end
+
+ it "should silently ignore eager loading of has_many associations" do
+ b = Book.find(@b.ROW, :include => [:chapters])
+ # note: no exception, loaded? is marked as false and assoc still works
+ b.chapters.loaded?.should be_false
+ b.chapters.to_a.should == [@c1]
+ end
+ end
+ end
+end
View
538 spec/lib/hyper_record_spec.rb
@@ -0,0 +1,538 @@
+require File.join(File.dirname(__FILE__), '../spec_helper.rb')
+
+module ActiveRecord
+ module HyperRecord
+ describe HyperBase, '.describe_table' do
+ fixtures :pages
+
+ it "should return a string describing the table schema" do
+ table_description = Page.connection.describe_table(Page.table_name)
+ table_description.should_not be_empty
+ table_description.should include("name")
+ table_description.should include("url")
+ end
+ end
+
+ describe HyperBase, '.table_exists?' do
+ fixtures :pages
+
+ it "should return true if the underlying table exists" do
+ Page.table_exists?.should be_true
+ end
+
+ it "should return false if the underlying table does not exists" do
+ Dummy.table_exists?.should be_false
+ end
+ end
+
+ describe HyperBase, '.drop_table' do
+ fixtures :pages
+
+ it "should remove a table from hypertable" do
+ Page.table_exists?.should be_true
+ Page.drop_table
+ Page.table_exists?.should be_false
+ end
+ end
+
+ describe HyperBase, '.columns' do
+ fixtures :pages
+
+ it "should return an array of columns within the table" do
+ table_columns = Page.columns
+ table_columns.should_not be_empty
+ # column list include the special ROW key.
+ table_columns.map{|c| c.name}.should == ['ROW', 'name', 'url']
+ end
+ end
+
+ describe HyperBase, '.column_families_without_row_key' do
+ fixtures :pages
+
+ it "should return an array of columns within the table but does not include the row key" do
+ columns_without_row_key = Page.column_families_without_row_key
+ columns_without_row_key.should_not be_empty
+ # column list does not include the special ROW key.
+ columns_without_row_key.map{|c| c.name}.should == ['name', 'url']
+ end
+ end
+
+ describe HyperBase, '.qualified_column_names_without_row_key' do
+ fixtures :pages
+
+ it "should return an array of column names where column families are replaced by fully qualified columns" do
+ cols = QualifiedPage.qualified_column_names_without_row_key
+ cols.should_not be_empty
+ cols.should == ['misc:name', 'misc:url', 'misc2:foo', 'misc2:bar']
+ end
+ end
+
+ describe HyperBase, '.qualified_columns' do
+ it "should include qualified columns in the regular column list" do
+ columns = QualifiedPage.columns
+ columns.should_not be_empty
+ columns.map{|c| c.name}.should == ['ROW', 'misc', 'misc2']
+ end
+ end
+
+ describe HyperBase, '.find_initial' do
+ fixtures :pages
+
+ it "should return the first row in the table" do
+ page = Page.find_initial({})
+ page.class.should == Page
+ page.name.should == "LOLcats and more"
+ page.url.should == "http://www.icanhascheezburger.com"
+ end
+ end
+
+ describe HyperBase, '.find_one' do
+ fixtures :pages
+
+ it "should return the requested row from the table" do
+ page = Page.find_one('page_1', {})
+ page.class.should == Page
+ page.name.should == "LOLcats and more"
+ page.url.should == "http://www.icanhascheezburger.com"
+ end
+ end
+
+ describe HyperBase, '.find_some' do
+ fixtures :pages
+
+ it "should return the requested rows from the table" do
+ row_keys = Page.find(:all).map{|p| p.ROW}
+ record_count = row_keys.length
+ record_count.should == 2
+ pages = Page.find(row_keys)
+ pages.length.should == record_count
+ end
+ end
+
+ describe HyperBase, '.find' do
+ fixtures :pages, :qualified_pages
+
+ it "should return the declared list of qualified columns by default" do
+ qp = QualifiedPage.new
+ qp.new_record?.should be_true
+ qp.misc['name'] = 'new page'
+ qp.misc['url']= 'new.com'
+ qp.ROW = 'new_qualified_page'
+ qp.save.should be_true
+
+ qp = QualifiedPage.find('new_qualified_page')
+ qp.misc.keys.sort.should == ['name', 'url']
+ end
+
+ it "should support the limit option" do
+ Page.new({:ROW => 'row key'}).save.should be_true
+ Page.find(:all).length.should == 3
+ pages = Page.find(:all, :limit => 2)
+ pages.length.should == 2
+ end
+
+ it "should not support finder conditions not in Hash format" do
+ lambda {Page.find(:all, :conditions => "value = 1")}.should raise_error
+ end
+
+ it "should not support finder conditions in Hash format" do
+ # NOTE: will be supported in the future when Hypertable supports
+ # efficient lookup on arbitrary columns
+ lambda {
+ pages = Page.find(:all, :conditions => {:name => 'ESPN'})
+ pages.length.should == 1
+ p = pages.first
+ p.name.should == 'ESPN'
+ p.ROW.should == 'page_2'
+ }.should raise_error
+ end
+
+ it "should not support finder conditions in Hash format when the value is an array" do
+ # NOTE: will be supported in the future when Hypertable supports
+ # efficient lookup on arbitrary columns
+ lambda {
+ all_pages = Page.find(:all)
+ all_pages.length.should == 2
+ name_values = all_pages.map{|p| p.name}
+ pages = Page.find(:all, :conditions => {:name => name_values})
+ pages.length.should == 2
+ }.should raise_error
+ end
+
+ it "should return a specific list of qualifiers when requested explicitly in finder options" do
+ qp = QualifiedPage.new
+ qp.new_record?.should be_true
+ qp.misc['name'] = 'new page'
+ qp.misc['url']= 'new.com'
+ qp.ROW = 'new_qualified_page'
+ qp.save.should be_true
+
+ qp = QualifiedPage.find('new_qualified_page', :select => 'misc:url')
+ # NOTE: will be supported in the future when Hypertable supports
+ # efficient lookup on arbitrary columns
+ # qp.misc.keys.sort.should == ['url']
+ # For now, it returns all columns
+ qp.misc.keys.sort.should == ['name', 'url']
+ end
+
+ it "should return an empty hash for all qualified columns even if none are explicitly listed in qualifiers" do
+ qpweq = QualifiedPageWithoutExplicitQualifiers.new
+ qpweq.new_record?.should be_true
+ qpweq.misc['name'] = 'new page'
+ qpweq.misc['url']= 'new.com'
+ qpweq.ROW = 'new_qualified_page'
+ qpweq.save.should be_true
+
+ qpweq2 = QualifiedPageWithoutExplicitQualifiers.find(qpweq.ROW)
+ # NOTE: will be supported in the future when Hypertable supports
+ # efficient lookup on arbitrary columns
+ # qpweq2.misc.should == {}
+ # qpweq2.misc2.should == ""
+ # For now, it returns all columns
+ qpweq2.misc.should == {"name"=>"new page", "url"=>"new.com"}
+ qpweq2.misc2.should be_nil
+ end
+
+ it "should instantiate the object with empty hashes for qualified columns when no explicit select list is supplied" do
+ qp = QualifiedPage.new
+ qp.new_record?.should be_true
+ qp.ROW = 'new_qualified_page'
+ qp.misc2 = 'test'
+ qp.save.should be_true
+
+ qp = QualifiedPage.find(:first)
+ qp.misc.should == {}
+ end
+
+ it "should allow user to specify ROW key as part of initialize attributes" do
+ p = Page.new({:ROW => 'row key'})
+ p.ROW.should == 'row key'
+ end
+
+ it "should not have any residual state between calls to new" do
+ qp = QualifiedPage.new
+ qp.new_record?.should be_true
+ qp.misc['name'] = 'new page'
+ qp.misc['url']= 'new.com'
+ qp.ROW = 'new_qualified_page'
+ qp.save.should be_true
+
+ qp2 = QualifiedPage.new
+ qp2.misc.should == {}
+ qp2.misc2.should == {}
+ qp.misc.object_id.should_not == qp2.misc.object_id
+ end
+ end
+
+ describe HyperBase, '.table_exists?' do
+ fixtures :pages
+
+ it "should return true for a table that does exist" do
+ Page.table_exists?.should be_true
+ end
+
+ it "should return false for a table that does exist" do
+ Dummy.table_exists?.should be_false
+ end
+ end
+
+ describe HyperBase, '.primary_key' do
+ it "should always return the special ROW key" do
+ Dummy.primary_key.should == 'ROW'
+ end
+ end
+
+ describe HyperBase, '.new' do
+ fixtures :pages, :qualified_pages
+
+ it "should an object of correct class" do
+ p = Page.new
+ p.new_record?.should be_true
+ p.class.should == Page
+ p.class.should < ActiveRecord::HyperBase
+ p.attributes.keys.sort.should == ['name', 'url']
+ end
+
+ it "should not allow an object to be saved without a row key" do
+ page_count = Page.find(:all).length
+ p = Page.new
+ p.new_record?.should be_true
+ p.name = "new page"
+ p.url = "new.com"
+ p.valid?.should be_false
+ p.save.should be_false
+ p.new_record?.should be_true
+ p.ROW = "new_page"
+ p.valid?.should be_true
+ p.save.should be_true
+ p.new_record?.should be_false
+ Page.find(:all).length.should == page_count + 1
+ end
+
+ it "should save a table with qualified columns correctly" do
+ qp = QualifiedPage.new
+ qp.new_record?.should be_true
+ qp.misc['name'] = 'new page'
+ qp.misc['url']= 'new.com'
+ qp.ROW = 'new_qualified_page'
+ qp.save.should be_true
+ qp.new_record?.should be_false
+ qp.reload.should == qp
+ qp.misc['name'].should == 'new page'
+ qp.misc['url'].should == 'new.com'
+ qp.misc.keys.sort.should == ['name', 'url']
+ end
+ end
+
+ describe HyperBase, '.reload' do
+ fixtures :pages
+
+ it "should reload an object and revert any changed state" do
+ p = Page.find(:first)
+ p.class.should == Page
+ original_url = p.url.clone
+ p.url = "new url"
+ p.reload.should == p
+ p.url.should == original_url
+ end
+ end
+
+ describe HyperBase, '.save' do
+ fixtures :pages, :qualified_pages
+
+ it "should update an object in hypertable" do
+ p = Page.find(:first)
+ p.class.should == Page
+ original_url = p.url.clone
+ p.url = "new url"
+ p.save.should be_true
+ p.url.should == "new url"
+ p.reload.should == p
+ p.url.should == "new url"
+ end
+
+ it "should allow undeclared qualified columns to be saved, provided that the column family is declared" do
+ qp = QualifiedPage.new
+ qp.new_record?.should be_true
+ qp.misc['name'] = 'new page'
+ qp.misc['url'] = 'new.com'
+ qp.misc['new_column'] = 'value'
+ qp.ROW = 'new_qualified_page'
+ qp.save.should be_true
+ qp.new_record?.should be_false
+ qp.reload.should == qp
+ qp.misc['name'].should == 'new page'
+ qp.misc['url'].should == 'new.com'
+ qp.misc['new_column'].should == 'value'
+ end
+ end
+
+ describe HyperBase, '.update' do
+ fixtures :pages
+
+ it "should update an object in hypertable" do
+ p = Page.find(:first)
+ p.class.should == Page
+ original_url = p.url.clone
+ p.url = "new url"
+ p.update.should be_true
+ p.url.should == "new url"
+ p.reload.should == p
+ p.url.should == "new url"
+ end
+ end
+
+ describe HyperBase, '.destroy' do
+ fixtures :pages
+
+ it "should remove an object from hypertable" do
+ p = Page.find(:first)
+ p.reload.should == p
+ p.destroy
+ lambda {p.reload}.should raise_error(::ActiveRecord::RecordNotFound)
+ end
+
+ it "should remove an object from hypertable based on the id" do
+ p = Page.find(:first)
+ p.reload.should == p
+ Page.destroy(p.ROW)
+ lambda {p.reload}.should raise_error(::ActiveRecord::RecordNotFound)
+ end
+
+ it "should remove multiple objects from hypertable based on ids" do
+ pages = Page.find(:all)
+ pages.length.should == 2
+ Page.destroy(pages.map{|p| p.ROW})
+ pages = Page.find(:all)
+ pages.length.should == 0
+ end
+ end
+
+ describe HyperBase, '.delete' do
+ fixtures :pages
+
+ it "should remove an object from hypertable based on the id" do
+ p = Page.find(:first)
+ p.reload.should == p
+ Page.delete(p.ROW)
+ lambda {p.reload}.should raise_error(::ActiveRecord::RecordNotFound)
+ end
+
+ it "should remove multiple objects from hypertable based on ids" do
+ pages = Page.find(:all)
+ pages.length.should == 2
+ Page.delete(pages.map{|p| p.ROW})
+ pages = Page.find(:all)
+ pages.length.should == 0
+ end
+ end
+
+ describe HyperBase, '.exists' do
+ fixtures :pages
+
+ it "should confirm that a record exists" do
+ p = Page.find(:first)
+ Page.exists?(p.ROW).should be_true
+ end
+
+ it "should refute that a record does not exists" do
+ Page.exists?('foofooofoofoofoofoomonkey').should be_false
+ end
+
+ it "should not support arguments that are not numbers, strings or hashes" do
+ lambda {Page.exists?([1])}.should raise_error
+ end
+
+ it "should not allow a Hash argument for conditions" do
+ lambda {
+ Page.exists?(:name => 'ESPN').should be_true
+ }.should raise_error
+
+
+ lambda {
+ Page.find(:first, :conditions => {:name => 'ESPN'}).should_not be_nil
+ }.should raise_error
+
+ lambda {
+ Page.exists?(:name => 'foofoofoofoofoo').should be_false
+ }.should raise_error
+
+ lambda {
+ Page.find(:first, :conditions => {:name => 'foofoofoofoofoo'}).should be_nil
+ }.should raise_error
+ end
+ end
+
+ describe HyperBase, '.increment' do
+ fixtures :pages
+
+ it "should increment an integer value" do
+ p = Page.find(:first)
+ p.name = 7
+ p.save
+ p.reload
+ p.increment('name')
+ p.name.should == 8
+ p.save
+ p.reload
+ p.name.should == "8"
+ p.increment('name', 2)
+ p.save
+ p.reload
+ p.name.should == "10"
+ end
+ end
+
+ describe HyperBase, '.increment!' do
+ fixtures :pages
+
+ it "should increment an integer value and save" do
+ p = Page.find(:first)
+ p.name = 7
+ p.increment!('name')
+ p.name.should == 8
+ p.reload
+ p.name.should == "8"
+ p.increment!('name', 2)
+ p.reload
+ p.name.should == "10"
+ end
+ end
+
+ describe HyperBase, '.decrement' do
+ fixtures :pages
+
+ it "should decrement an integer value" do
+ p = Page.find(:first)
+ p.name = 7
+ p.save
+ p.reload
+ p.decrement('name')
+ p.name.should == 6
+ p.save
+ p.reload
+ p.name.should == "6"
+ p.decrement('name', 2)
+ p.save
+ p.reload
+ p.name.should == "4"
+ end
+ end
+
+ describe HyperBase, '.decrement!' do
+ fixtures :pages
+
+ it "should decrement an integer value and save" do
+ p = Page.find(:first)
+ p.name = 7
+ p.decrement!('name')
+ p.name.should == 6
+ p.reload
+ p.name.should == "6"
+ p.decrement!('name', 2)
+ p.reload
+ p.name.should == "4"
+ end
+ end
+
+ describe HyperBase, '.update_attribute' do
+ fixtures :pages
+
+ it "should allow a single attribute to be updated" do
+ p = Page.find(:first)
+ p.update_attribute(:name, 'new name value')
+ p.name.should == 'new name value'
+ p.reload
+ p.name.should == 'new name value'
+ end
+
+ it "should save changes to more than the named column because that's the way activerecord works" do
+ p = Page.find(:first)
+ p.name = "name"
+ p.url = "url"
+ p.save!
+ p.url = "new url"
+ p.update_attribute(:name, 'new name value')
+ p.name.should == 'new name value'
+ p.url.should == 'new url'
+ p.reload
+ p.name.should == 'new name value'
+ p.url.should == 'new url'
+ end
+ end
+
+ describe HyperBase, '.update_attributes' do
+ fixtures :pages
+
+ it "should allow multiple attributes to be updated" do
+ p = Page.find(:first)
+ p.update_attributes({:name => 'new name value', :url => 'http://new/'})
+ p.name.should == 'new name value'
+ p.url.should == 'http://new/'
+ p.reload
+ p.name.should == 'new name value'
+ p.url.should == 'http://new/'
+ end
+ end
+ end
+end
View
128 spec/spec_helper.rb
@@ -0,0 +1,128 @@
+ENV["RAILS_ENV"] = "test"
+require File.expand_path(File.join(File.dirname(__FILE__), "../../../../config/environment"))
+require 'spec'
+require 'spec/rails'
+
+ActiveRecord::Base.configurations['hypertable'] = {
+ 'adapter' => 'hypertable',
+ 'host' => 'localhost',
+ 'port' => '38080'
+}
+
+class Dummy < ActiveRecord::HyperBase
+end
+
+class Page < ActiveRecord::HyperBase
+ self.establish_connection(:hypertable)
+
+ def self.create_table
+ hql = "CREATE TABLE #{table_name} (
+ 'name',
+ 'url'
+ )"
+ connection.execute(hql)
+ end
+end
+
+class QualifiedPage < ActiveRecord::HyperBase
+ self.establish_connection(:hypertable)
+ qualified_column :misc, :qualifiers => [:name, :url]
+ qualified_column :misc2, :qualifiers => [:foo, :bar]
+
+ def self.create_table
+ hql = "CREATE TABLE #{table_name} (
+ 'misc',
+ 'misc2'
+ )"
+ connection.execute(hql)
+ end
+end
+
+class QualifiedPageWithoutExplicitQualifiers < ActiveRecord::HyperBase
+ QualifiedPageWithoutExplicitQualifiers.set_table_name "qualified_pages"
+ self.establish_connection(:hypertable)
+ qualified_column :misc
+
+ def self.create_table
+ QualifiedPage.create_table
+ end
+end
+
+ActiveRecord::Base.connection = Page.connection
+
+Spec::Runner.configure do |config|
+ # If you're not using ActiveRecord you should remove these
+ # lines, delete config/database.yml and disable :active_record
+ # in your config/boot.rb
+ config.use_transactional_fixtures = false
+ config.use_instantiated_fixtures = false
+ config.fixture_path = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures'))
+
+ # == Fixtures
+ #
+ # You can declare fixtures for each example_group like this:
+ # describe "...." do
+ # fixtures :table_a, :table_b
+ #
+ # Alternatively, if you prefer to declare them only once, you can
+ # do so right here. Just uncomment the next line and replace the fixture
+ # names with your fixtures.
+ #
+ config.global_fixtures = []
+
+ #
+ # If you declare global fixtures, be aware that they will be declared
+ # for all of your examples, even those that don't use them.
+ #
+ # == Mock Framework
+ #
+ # RSpec uses it's own mocking framework by default. If you prefer to
+ # use mocha, flexmock or RR, uncomment the appropriate line:
+ #
+ # config.mock_with :mocha
+ # config.mock_with :flexmock
+ # config.mock_with :rr
+end
+
+class Fixtures
+ def self.create_fixtures(fixtures_directory, table_names, class_names = {})
+ Page.drop_table
+ Page.create_table
+ QualifiedPage.drop_table
+ QualifiedPage.create_table
+
+ table_names = [table_names].flatten.map { |n| n.to_s }
+ connection = block_given? ? yield : ActiveRecord::Base.connection
+
+ table_names_to_fetch = table_names.reject { |table_name| fixture_is_cached?(connection, table_name) }
+
+ unless table_names_to_fetch.empty?
+ ActiveRecord::Base.silence do
+ connection.disable_referential_integrity do
+ fixtures_map = {}
+
+ fixtures = table_names_to_fetch.map do |table_name|
+ fixtures_map[table_name] = Fixtures.new(connection, File.split(table_name.to_s).last, class_names[table_name.to_sym], File.join(fixtures_directory, table_name.to_s))
+ end
+
+ all_loaded_fixtures.update(fixtures_map)
+
+ connection.transaction(Thread.current['open_transactions'].to_i == 0) do
+ #fixtures.reverse.each {|fixture| fixture.delete_existing_fixtures}
+ fixtures.each {|fixture| fixture.insert_fixtures}
+
+ # Cap primary key sequences to max(pk).
+ if connection.respond_to?(:reset_pk_sequence!)
+ table_names.each do |table_name|
+ connection.reset_pk_sequence!(table_name)
+ end
+ end
+ end
+
+ cache_fixtures(connection, fixtures)
+ end
+ end
+ end
+ cached_fixtures(connection, table_names)
+ end
+end
View
7 test/hyper_record_test.rb
@@ -1,7 +0,0 @@
-require File.dirname(__FILE__) + '/test_helper'
-
-class HyperRecordTest < Test::Unit::TestCase
- should "probably rename this file and start testing for real" do
- flunk "hey buddy, you should probably rename this file and start testing for real"
- end
-end
View
10 test/test_helper.rb
@@ -1,10 +0,0 @@
-require 'rubygems'
-require 'test/unit'
-require 'shoulda'
-require 'mocha'
-
-$LOAD_PATH.unshift(File.dirname(__FILE__))
-require 'hyper_record'
-
-class Test::Unit::TestCase
-end
Please sign in to comment.
Something went wrong with that request. Please try again.