massive_record 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +15 -0
- data/.gitignore +6 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +38 -0
- data/Manifest +24 -0
- data/README.md +225 -0
- data/Rakefile +16 -0
- data/TODO.md +8 -0
- data/autotest/discover.rb +1 -0
- data/lib/massive_record.rb +18 -0
- data/lib/massive_record/exceptions.rb +11 -0
- data/lib/massive_record/orm/attribute_methods.rb +61 -0
- data/lib/massive_record/orm/attribute_methods/dirty.rb +80 -0
- data/lib/massive_record/orm/attribute_methods/read.rb +23 -0
- data/lib/massive_record/orm/attribute_methods/write.rb +24 -0
- data/lib/massive_record/orm/base.rb +176 -0
- data/lib/massive_record/orm/callbacks.rb +52 -0
- data/lib/massive_record/orm/column.rb +18 -0
- data/lib/massive_record/orm/config.rb +47 -0
- data/lib/massive_record/orm/errors.rb +47 -0
- data/lib/massive_record/orm/finders.rb +125 -0
- data/lib/massive_record/orm/id_factory.rb +133 -0
- data/lib/massive_record/orm/persistence.rb +199 -0
- data/lib/massive_record/orm/schema.rb +4 -0
- data/lib/massive_record/orm/schema/column_families.rb +48 -0
- data/lib/massive_record/orm/schema/column_family.rb +102 -0
- data/lib/massive_record/orm/schema/column_interface.rb +91 -0
- data/lib/massive_record/orm/schema/common_interface.rb +48 -0
- data/lib/massive_record/orm/schema/field.rb +128 -0
- data/lib/massive_record/orm/schema/fields.rb +37 -0
- data/lib/massive_record/orm/schema/table_interface.rb +96 -0
- data/lib/massive_record/orm/table.rb +9 -0
- data/lib/massive_record/orm/validations.rb +52 -0
- data/lib/massive_record/spec/support/simple_database_cleaner.rb +52 -0
- data/lib/massive_record/thrift/hbase.rb +2307 -0
- data/lib/massive_record/thrift/hbase_constants.rb +14 -0
- data/lib/massive_record/thrift/hbase_types.rb +225 -0
- data/lib/massive_record/version.rb +3 -0
- data/lib/massive_record/wrapper/base.rb +28 -0
- data/lib/massive_record/wrapper/cell.rb +45 -0
- data/lib/massive_record/wrapper/column_families_collection.rb +19 -0
- data/lib/massive_record/wrapper/column_family.rb +22 -0
- data/lib/massive_record/wrapper/connection.rb +71 -0
- data/lib/massive_record/wrapper/row.rb +170 -0
- data/lib/massive_record/wrapper/scanner.rb +50 -0
- data/lib/massive_record/wrapper/table.rb +148 -0
- data/lib/massive_record/wrapper/tables_collection.rb +13 -0
- data/massive_record.gemspec +28 -0
- data/spec/config.yml.example +4 -0
- data/spec/orm/cases/attribute_methods_spec.rb +47 -0
- data/spec/orm/cases/auto_generate_id_spec.rb +54 -0
- data/spec/orm/cases/base_spec.rb +176 -0
- data/spec/orm/cases/callbacks_spec.rb +309 -0
- data/spec/orm/cases/column_spec.rb +49 -0
- data/spec/orm/cases/config_spec.rb +103 -0
- data/spec/orm/cases/dirty_spec.rb +129 -0
- data/spec/orm/cases/encoding_spec.rb +49 -0
- data/spec/orm/cases/finders_spec.rb +208 -0
- data/spec/orm/cases/hbase/connection_spec.rb +13 -0
- data/spec/orm/cases/i18n_spec.rb +32 -0
- data/spec/orm/cases/id_factory_spec.rb +75 -0
- data/spec/orm/cases/persistence_spec.rb +479 -0
- data/spec/orm/cases/table_spec.rb +81 -0
- data/spec/orm/cases/validation_spec.rb +92 -0
- data/spec/orm/models/address.rb +7 -0
- data/spec/orm/models/person.rb +15 -0
- data/spec/orm/models/test_class.rb +5 -0
- data/spec/orm/schema/column_families_spec.rb +186 -0
- data/spec/orm/schema/column_family_spec.rb +131 -0
- data/spec/orm/schema/column_interface_spec.rb +115 -0
- data/spec/orm/schema/field_spec.rb +196 -0
- data/spec/orm/schema/fields_spec.rb +126 -0
- data/spec/orm/schema/table_interface_spec.rb +171 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/connection_helpers.rb +76 -0
- data/spec/support/mock_massive_record_connection.rb +80 -0
- data/spec/thrift/cases/encoding_spec.rb +48 -0
- data/spec/wrapper/cases/connection_spec.rb +53 -0
- data/spec/wrapper/cases/table_spec.rb +231 -0
- metadata +228 -0
@@ -0,0 +1,125 @@
|
|
1
|
+
module MassiveRecord
|
2
|
+
module ORM
|
3
|
+
module Finders
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
#
|
8
|
+
# Interface for retrieving objects based on key.
|
9
|
+
# Has some convenience behaviour like find :first, :last, :all.
|
10
|
+
#
|
11
|
+
def find(*args)
|
12
|
+
options = args.extract_options!.to_options
|
13
|
+
raise ArgumentError.new("At least one argument required!") if args.empty?
|
14
|
+
raise RecordNotFound.new("Can't find a #{model_name.human} without an ID.") if args.first.nil?
|
15
|
+
raise ArgumentError.new("Sorry, conditions are not supported!") if options.has_key? :conditions
|
16
|
+
args << options
|
17
|
+
|
18
|
+
type = args.shift if args.first.is_a? Symbol
|
19
|
+
find_many = type == :all
|
20
|
+
expected_result_size = nil
|
21
|
+
|
22
|
+
return (find_many ? [] : nil) unless table.exists?
|
23
|
+
|
24
|
+
result_from_table = if type
|
25
|
+
table.send(type, *args) # first() / all()
|
26
|
+
else
|
27
|
+
options = args.extract_options!
|
28
|
+
what_to_find = args.first
|
29
|
+
expected_result_size = 1
|
30
|
+
|
31
|
+
if args.first.kind_of?(Array)
|
32
|
+
find_many = true
|
33
|
+
elsif args.length > 1
|
34
|
+
find_many = true
|
35
|
+
what_to_find = args
|
36
|
+
end
|
37
|
+
|
38
|
+
expected_result_size = what_to_find.length if what_to_find.is_a? Array
|
39
|
+
table.find(what_to_find, options)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Filter out unexpected IDs (unless type is set (all/first), in that case
|
43
|
+
# we have no expectations on the returned rows' ids)
|
44
|
+
unless type || result_from_table.blank?
|
45
|
+
if find_many
|
46
|
+
result_from_table.select! { |result| what_to_find.include? result.id }
|
47
|
+
else
|
48
|
+
if result_from_table.id != what_to_find
|
49
|
+
result_from_table = nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
raise RecordNotFound.new("Could not find #{model_name} with id=#{what_to_find}") if result_from_table.blank? && type.nil?
|
55
|
+
|
56
|
+
if find_many && expected_result_size && expected_result_size != result_from_table.length
|
57
|
+
raise RecordNotFound.new("Expected to find #{expected_result_size} records, but found only #{result_from_table.length}")
|
58
|
+
end
|
59
|
+
|
60
|
+
records = [result_from_table].compact.flatten.collect do |row|
|
61
|
+
instantiate(transpose_hbase_columns_to_record_attributes(row))
|
62
|
+
end
|
63
|
+
|
64
|
+
find_many ? records : records.first
|
65
|
+
end
|
66
|
+
|
67
|
+
def first(*args)
|
68
|
+
find(:first, *args)
|
69
|
+
end
|
70
|
+
|
71
|
+
def last(*args)
|
72
|
+
raise "Sorry, not implemented!"
|
73
|
+
end
|
74
|
+
|
75
|
+
def all(*args)
|
76
|
+
find(:all, *args)
|
77
|
+
end
|
78
|
+
|
79
|
+
def find_in_batches(*args)
|
80
|
+
table.find_in_batches(*args) do |rows|
|
81
|
+
records = rows.collect do |row|
|
82
|
+
instantiate(transpose_hbase_columns_to_record_attributes(row))
|
83
|
+
end
|
84
|
+
yield records
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def find_each(*args)
|
89
|
+
find_in_batches(*args) do |rows|
|
90
|
+
rows.each do |row|
|
91
|
+
yield row
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
def exists?(id)
|
98
|
+
!!find(id) rescue false
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def transpose_hbase_columns_to_record_attributes(row)
|
105
|
+
attributes = {:id => row.id}
|
106
|
+
|
107
|
+
autoload_column_families_and_fields_with(row.columns.keys)
|
108
|
+
|
109
|
+
# Parse the schema to populate the instance attributes
|
110
|
+
attributes_schema.each do |key, field|
|
111
|
+
cell = row.columns[field.unique_name]
|
112
|
+
attributes[field.name] = cell.nil? ? nil : cell.deserialize_value
|
113
|
+
end
|
114
|
+
attributes
|
115
|
+
end
|
116
|
+
|
117
|
+
def instantiate(record)
|
118
|
+
allocate.tap do |model|
|
119
|
+
model.init_with('attributes' => record)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module MassiveRecord
|
4
|
+
module ORM
|
5
|
+
|
6
|
+
#
|
7
|
+
# A factory class for unique IDs for any given tables.
|
8
|
+
#
|
9
|
+
# Usage:
|
10
|
+
# IdFactory.next_for(:cars) # => 1
|
11
|
+
# IdFactory.next_for(:cars) # => 2
|
12
|
+
# IdFactory.next_for(AClassRespondingToTableName) # => 1
|
13
|
+
# IdFactory.next_for("a_class_responding_to_table_names") # => 2
|
14
|
+
#
|
15
|
+
#
|
16
|
+
# Storage:
|
17
|
+
# Stored in id_factories table, under column family named tables.
|
18
|
+
# Field name equals to tables it has generated ids for, and it's
|
19
|
+
# values is integers (if the adapter supports it).
|
20
|
+
#
|
21
|
+
class IdFactory < Table
|
22
|
+
include Singleton
|
23
|
+
|
24
|
+
COLUMN_FAMILY_FOR_TABLES = :tables
|
25
|
+
ID = "id_factory"
|
26
|
+
|
27
|
+
column_family COLUMN_FAMILY_FOR_TABLES do
|
28
|
+
autoload_fields
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Returns the factory, singleton class.
|
33
|
+
# It will be a reloaded version each time instance
|
34
|
+
# is retrieved, or else it will fetch self from the
|
35
|
+
# database, or if all other fails return a new of self.
|
36
|
+
#
|
37
|
+
def self.instance
|
38
|
+
if table_exists?
|
39
|
+
begin
|
40
|
+
if @instance
|
41
|
+
@instance.reload
|
42
|
+
else
|
43
|
+
@instance = find(ID)
|
44
|
+
end
|
45
|
+
rescue RecordNotFound
|
46
|
+
@instance = nil
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
@instance = new unless @instance
|
51
|
+
@instance
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Delegates to the instance, just a shout cut.
|
56
|
+
#
|
57
|
+
def self.next_for(table)
|
58
|
+
instance.next_for(table)
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
|
63
|
+
#
|
64
|
+
# Returns a new and unique id for a given table name
|
65
|
+
# Table can a symbol, string or an object responding to table_name
|
66
|
+
#
|
67
|
+
def next_for(table)
|
68
|
+
table = table.respond_to?(:table_name) ? table.table_name : table.to_s
|
69
|
+
next_id :table => table
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
|
74
|
+
def id
|
75
|
+
ID
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
#
|
81
|
+
# Method which actually does the increment work for
|
82
|
+
# a given table name as string
|
83
|
+
#
|
84
|
+
def next_id(options = {})
|
85
|
+
options.assert_valid_keys(:table)
|
86
|
+
table_name = options.delete :table
|
87
|
+
|
88
|
+
create_field_or_ensure_type_integer_for(table_name)
|
89
|
+
atomic_increment!(table_name)
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
|
94
|
+
|
95
|
+
def create_field_or_ensure_type_integer_for(table_name)
|
96
|
+
if has_field_for? table_name
|
97
|
+
ensure_type_integer_for(table_name)
|
98
|
+
else
|
99
|
+
create_field_for(table_name)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
#
|
105
|
+
# Creates a field for a table name which is new
|
106
|
+
# Feels a bit hackish, hooking in and doing some of what the
|
107
|
+
# autoload-functionality of column_family block above does too.
|
108
|
+
# But at least, we can "dynamicly" assign new attributes to this object.
|
109
|
+
#
|
110
|
+
def create_field_for(table_name)
|
111
|
+
add_field_to_column_family COLUMN_FAMILY_FOR_TABLES, table_name, :integer, :default => 0
|
112
|
+
end
|
113
|
+
|
114
|
+
#
|
115
|
+
# Just makes sure that definition of a field is set to integer.
|
116
|
+
# This is needed as the autoload functionlaity sets all types to strings.
|
117
|
+
#
|
118
|
+
def ensure_type_integer_for(table_name)
|
119
|
+
column_family_for_tables.field_by_name(table_name).type = :integer
|
120
|
+
self[table_name] = 0 if self[table_name].blank?
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
def has_field_for?(table_name)
|
125
|
+
respond_to? table_name
|
126
|
+
end
|
127
|
+
|
128
|
+
def column_family_for_tables
|
129
|
+
@column_family_for_tables ||= column_families.family_by_name(COLUMN_FAMILY_FOR_TABLES)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
module MassiveRecord
|
2
|
+
module ORM
|
3
|
+
module Persistence
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def create(attributes = {})
|
8
|
+
new(attributes).tap do |record|
|
9
|
+
record.save
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def destroy_all
|
14
|
+
all.each { |record| record.destroy }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def new_record?
|
20
|
+
@new_record
|
21
|
+
end
|
22
|
+
|
23
|
+
def persisted?
|
24
|
+
!(new_record? || destroyed?)
|
25
|
+
end
|
26
|
+
|
27
|
+
def destroyed?
|
28
|
+
@destroyed
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
def reload
|
33
|
+
self.attributes_raw = self.class.find(id).attributes
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
def save(*)
|
38
|
+
create_or_update
|
39
|
+
end
|
40
|
+
|
41
|
+
def save!(*)
|
42
|
+
create_or_update or raise RecordNotSaved
|
43
|
+
end
|
44
|
+
|
45
|
+
def update_attribute(attr_name, value)
|
46
|
+
send("#{attr_name}=", value)
|
47
|
+
save(:validate => false)
|
48
|
+
end
|
49
|
+
|
50
|
+
def update_attributes(attributes)
|
51
|
+
self.attributes = attributes
|
52
|
+
save
|
53
|
+
end
|
54
|
+
|
55
|
+
def update_attributes!(attributes)
|
56
|
+
self.attributes = attributes
|
57
|
+
save!
|
58
|
+
end
|
59
|
+
|
60
|
+
# TODO This actually does nothing atm, but it's here and callbacks on it
|
61
|
+
# is working.
|
62
|
+
def touch
|
63
|
+
true
|
64
|
+
end
|
65
|
+
|
66
|
+
def destroy
|
67
|
+
@destroyed = row_for_record.destroy and freeze
|
68
|
+
end
|
69
|
+
alias_method :delete, :destroy
|
70
|
+
|
71
|
+
|
72
|
+
|
73
|
+
|
74
|
+
def increment(attr_name, by = 1)
|
75
|
+
raise NotNumericalFieldError unless attributes_schema[attr_name.to_s].type == :integer
|
76
|
+
self[attr_name] ||= 0
|
77
|
+
self[attr_name] += by
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
def increment!(attr_name, by = 1)
|
82
|
+
increment(attr_name, by).update_attribute(attr_name, self[attr_name])
|
83
|
+
end
|
84
|
+
|
85
|
+
# Atomic increment of an attribute. Please note that it's the
|
86
|
+
# adapter (or the wrapper) which needs to guarantee that the update
|
87
|
+
# is atomic, and as of writing this the Thrift adapter / wrapper does
|
88
|
+
# not do this anatomic.
|
89
|
+
def atomic_increment!(attr_name, by = 1)
|
90
|
+
ensure_that_we_have_table_and_column_families!
|
91
|
+
attr_name = attr_name.to_s
|
92
|
+
|
93
|
+
row = row_for_record
|
94
|
+
row.values = attributes_to_row_values_hash([attr_name])
|
95
|
+
self[attr_name] = row.atomic_increment(attributes_schema[attr_name].unique_name, by).to_i
|
96
|
+
end
|
97
|
+
|
98
|
+
def decrement(attr_name, by = 1)
|
99
|
+
raise NotNumericalFieldError unless attributes_schema[attr_name.to_s].type == :integer
|
100
|
+
self[attr_name] ||= 0
|
101
|
+
self[attr_name] -= by
|
102
|
+
self
|
103
|
+
end
|
104
|
+
|
105
|
+
def decrement!(attr_name, by = 1)
|
106
|
+
decrement(attr_name, by).update_attribute(attr_name, self[attr_name])
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
|
113
|
+
def create_or_update
|
114
|
+
!!(new_record? ? create : update)
|
115
|
+
end
|
116
|
+
|
117
|
+
def create
|
118
|
+
ensure_that_we_have_table_and_column_families!
|
119
|
+
|
120
|
+
if saved = store_record_to_database
|
121
|
+
@new_record = false
|
122
|
+
end
|
123
|
+
saved
|
124
|
+
end
|
125
|
+
|
126
|
+
def update(attribute_names_to_update = attributes.keys)
|
127
|
+
ensure_that_we_have_table_and_column_families!
|
128
|
+
|
129
|
+
store_record_to_database(attribute_names_to_update)
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
|
134
|
+
|
135
|
+
#
|
136
|
+
# Takes care of the actual storing of the record to the database
|
137
|
+
# Both update and create is using this
|
138
|
+
#
|
139
|
+
def store_record_to_database(attribute_names_to_update = [])
|
140
|
+
row = row_for_record
|
141
|
+
row.values = attributes_to_row_values_hash(attribute_names_to_update)
|
142
|
+
row.save
|
143
|
+
end
|
144
|
+
|
145
|
+
|
146
|
+
#
|
147
|
+
# Iterates over tables and column families and ensure that we
|
148
|
+
# have what we need
|
149
|
+
#
|
150
|
+
def ensure_that_we_have_table_and_column_families!
|
151
|
+
if !self.class.connection.tables.include? self.class.table_name
|
152
|
+
missing_family_names = calculate_missing_family_names
|
153
|
+
self.class.table.create_column_families(missing_family_names) unless missing_family_names.empty?
|
154
|
+
self.class.table.save
|
155
|
+
end
|
156
|
+
|
157
|
+
raise ColumnFamiliesMissingError.new(calculate_missing_family_names) if !calculate_missing_family_names.empty?
|
158
|
+
end
|
159
|
+
|
160
|
+
#
|
161
|
+
# Calculate which column families are missing in the database in
|
162
|
+
# context of what the schema instructs.
|
163
|
+
#
|
164
|
+
def calculate_missing_family_names
|
165
|
+
existing_family_names = self.class.table.fetch_column_families.collect(&:name) rescue []
|
166
|
+
expected_family_names = column_families ? column_families.collect(&:name) : []
|
167
|
+
|
168
|
+
expected_family_names.collect(&:to_s) - existing_family_names.collect(&:to_s)
|
169
|
+
end
|
170
|
+
|
171
|
+
#
|
172
|
+
# Returns a Wrapper::Row class which we can manipulate this
|
173
|
+
# record in the database with
|
174
|
+
#
|
175
|
+
def row_for_record
|
176
|
+
raise IdMissing.new("You must set an ID before save.") if id.blank?
|
177
|
+
|
178
|
+
MassiveRecord::Wrapper::Row.new({
|
179
|
+
:id => id,
|
180
|
+
:table => self.class.table
|
181
|
+
})
|
182
|
+
end
|
183
|
+
|
184
|
+
#
|
185
|
+
# Returns attributes on a form which Wrapper::Row expects
|
186
|
+
#
|
187
|
+
def attributes_to_row_values_hash(only_attr_names = [])
|
188
|
+
values = Hash.new { |hash, key| hash[key] = Hash.new }
|
189
|
+
|
190
|
+
attributes_schema.each do |attr_name, orm_field|
|
191
|
+
next unless only_attr_names.empty? || only_attr_names.include?(attr_name)
|
192
|
+
values[orm_field.column_family.name][orm_field.column] = send(attr_name)
|
193
|
+
end
|
194
|
+
|
195
|
+
values
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|