georgepalmer-couch_foo 0.7.1
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/README.rdoc +113 -0
- data/VERSION.yml +4 -0
- data/lib/boolean.rb +3 -0
- data/lib/couch_foo/associations/association_collection.rb +346 -0
- data/lib/couch_foo/associations/association_proxy.rb +204 -0
- data/lib/couch_foo/associations/belongs_to_association.rb +57 -0
- data/lib/couch_foo/associations/belongs_to_polymorphic_association.rb +48 -0
- data/lib/couch_foo/associations/has_and_belongs_to_many_association.rb +111 -0
- data/lib/couch_foo/associations/has_many_association.rb +97 -0
- data/lib/couch_foo/associations/has_one_association.rb +95 -0
- data/lib/couch_foo/associations.rb +1118 -0
- data/lib/couch_foo/attribute_methods.rb +316 -0
- data/lib/couch_foo/base.rb +2117 -0
- data/lib/couch_foo/calculations.rb +117 -0
- data/lib/couch_foo/callbacks.rb +311 -0
- data/lib/couch_foo/database.rb +157 -0
- data/lib/couch_foo/dirty.rb +142 -0
- data/lib/couch_foo/named_scope.rb +168 -0
- data/lib/couch_foo/observer.rb +195 -0
- data/lib/couch_foo/reflection.rb +239 -0
- data/lib/couch_foo/timestamp.rb +41 -0
- data/lib/couch_foo/validations.rb +927 -0
- data/lib/couch_foo/view_methods.rb +234 -0
- data/lib/couch_foo.rb +43 -0
- data/test/couch_foo_test.rb +7 -0
- data/test/test_helper.rb +10 -0
- metadata +116 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
module CouchFoo
|
2
|
+
module Associations
|
3
|
+
class BelongsToAssociation < AssociationProxy #:nodoc:
|
4
|
+
def create(attributes = {})
|
5
|
+
replace(@reflection.klass.create(attributes))
|
6
|
+
end
|
7
|
+
|
8
|
+
def build(attributes = {})
|
9
|
+
replace(@reflection.klass.new(attributes))
|
10
|
+
end
|
11
|
+
|
12
|
+
def replace(record)
|
13
|
+
counter_cache_name = @reflection.counter_cache_property
|
14
|
+
|
15
|
+
if record.nil?
|
16
|
+
if counter_cache_name && !@owner.new_record?
|
17
|
+
@reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name]
|
18
|
+
end
|
19
|
+
|
20
|
+
@target = @owner[@reflection.primary_key_name] = nil
|
21
|
+
else
|
22
|
+
raise_on_type_mismatch(record)
|
23
|
+
|
24
|
+
if counter_cache_name && !@owner.new_record?
|
25
|
+
@reflection.klass.increment_counter(counter_cache_name, record.id)
|
26
|
+
@reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name]
|
27
|
+
end
|
28
|
+
|
29
|
+
@target = (AssociationProxy === record ? record.target : record)
|
30
|
+
@owner[@reflection.primary_key_name] = record.id unless record.new_record?
|
31
|
+
@updated = true
|
32
|
+
end
|
33
|
+
|
34
|
+
loaded
|
35
|
+
record
|
36
|
+
end
|
37
|
+
|
38
|
+
def updated?
|
39
|
+
@updated
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def find_target
|
44
|
+
@reflection.klass.find(
|
45
|
+
@owner[@reflection.primary_key_name],
|
46
|
+
:conditions => conditions,
|
47
|
+
:include => @reflection.options[:include],
|
48
|
+
:readonly => @reflection.options[:readonly]
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
def foreign_key_present
|
53
|
+
!@owner[@reflection.primary_key_name].nil?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module CouchFoo
|
2
|
+
module Associations
|
3
|
+
class BelongsToPolymorphicAssociation < AssociationProxy #:nodoc:
|
4
|
+
def replace(record)
|
5
|
+
if record.nil?
|
6
|
+
@target = @owner[@reflection.primary_key_name] = @owner[@reflection.options[:foreign_type]] = nil
|
7
|
+
else
|
8
|
+
@target = (AssociationProxy === record ? record.target : record)
|
9
|
+
|
10
|
+
@owner[@reflection.primary_key_name] = record.id
|
11
|
+
@owner[@reflection.options[:foreign_type]] = record.class.name.to_s
|
12
|
+
|
13
|
+
@updated = true
|
14
|
+
end
|
15
|
+
|
16
|
+
loaded
|
17
|
+
record
|
18
|
+
end
|
19
|
+
|
20
|
+
def updated?
|
21
|
+
@updated
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def find_target
|
26
|
+
return nil if association_class.nil?
|
27
|
+
|
28
|
+
if @reflection.options[:conditions]
|
29
|
+
association_class.find(
|
30
|
+
@owner[@reflection.primary_key_name],
|
31
|
+
:conditions => conditions,
|
32
|
+
:include => @reflection.options[:include]
|
33
|
+
)
|
34
|
+
else
|
35
|
+
association_class.find(@owner[@reflection.primary_key_name], :include => @reflection.options[:include])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def foreign_key_present
|
40
|
+
!@owner[@reflection.primary_key_name].nil?
|
41
|
+
end
|
42
|
+
|
43
|
+
def association_class
|
44
|
+
@owner[@reflection.options[:foreign_type]] ? @owner[@reflection.options[:foreign_type]].constantize : nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
#TODO column, sql, record and table search
|
2
|
+
module CouchFoo
|
3
|
+
module Associations
|
4
|
+
class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
|
5
|
+
def create(attributes = {})
|
6
|
+
create_record(attributes) { |record| insert_record(record, true) }
|
7
|
+
end
|
8
|
+
|
9
|
+
def create!(attributes = {})
|
10
|
+
create_record(attributes) { |record| insert_record(record, true) }
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
def construct_find_options!(options)
|
15
|
+
options[:joins] = @join_sql
|
16
|
+
options[:readonly] = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select])
|
17
|
+
end
|
18
|
+
|
19
|
+
def count_records
|
20
|
+
load_target.size
|
21
|
+
end
|
22
|
+
|
23
|
+
def insert_record(record, force=true)
|
24
|
+
if record.new_record?
|
25
|
+
if force
|
26
|
+
record.save!
|
27
|
+
else
|
28
|
+
return false unless record.save
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
if @reflection.options[:insert_sql]
|
33
|
+
@owner.connection.insert(interpolate_sql(@reflection.options[:insert_sql], record))
|
34
|
+
else
|
35
|
+
columns = @owner.connection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns")
|
36
|
+
|
37
|
+
attributes = columns.inject({}) do |attrs, column|
|
38
|
+
case column.name.to_s
|
39
|
+
when @reflection.primary_key_name.to_s
|
40
|
+
attrs[column.name] = @owner.quoted_id
|
41
|
+
when @reflection.association_foreign_key.to_s
|
42
|
+
attrs[column.name] = record.quoted_id
|
43
|
+
else
|
44
|
+
if record.has_attribute?(column.name)
|
45
|
+
value = @owner.send(:quote_value, record[column.name], column)
|
46
|
+
attrs[column.name] = value unless value.nil?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
attrs
|
50
|
+
end
|
51
|
+
|
52
|
+
sql =
|
53
|
+
"INSERT INTO #{@owner.connection.quote_table_name @reflection.options[:join_table]} (#{@owner.send(:quoted_column_names, attributes).join(', ')}) " +
|
54
|
+
"VALUES (#{attributes.values.join(', ')})"
|
55
|
+
|
56
|
+
@owner.connection.insert(sql)
|
57
|
+
end
|
58
|
+
|
59
|
+
return true
|
60
|
+
end
|
61
|
+
|
62
|
+
def delete_records(records)
|
63
|
+
if sql = @reflection.options[:delete_sql]
|
64
|
+
records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) }
|
65
|
+
else
|
66
|
+
ids = quoted_record_ids(records)
|
67
|
+
sql = "DELETE FROM #{@owner.connection.quote_table_name @reflection.options[:join_table]} WHERE #{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.association_foreign_key} IN (#{ids})"
|
68
|
+
@owner.connection.delete(sql)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def construct_sql
|
73
|
+
if @reflection.options[:finder_sql]
|
74
|
+
@finder_sql = interpolate_sql(@reflection.options[:finder_sql])
|
75
|
+
else
|
76
|
+
@finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{@owner.quoted_id} "
|
77
|
+
@finder_sql << " AND (#{conditions})" if conditions
|
78
|
+
end
|
79
|
+
|
80
|
+
@join_sql = "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}"
|
81
|
+
end
|
82
|
+
|
83
|
+
def construct_scope
|
84
|
+
{ :find => { :conditions => @finder_sql,
|
85
|
+
:joins => @join_sql,
|
86
|
+
:readonly => false,
|
87
|
+
:order => @reflection.options[:order],
|
88
|
+
:include => @reflection.options[:include],
|
89
|
+
:limit => @reflection.options[:limit] } }
|
90
|
+
end
|
91
|
+
|
92
|
+
# Join tables with additional columns on top of the two foreign keys must be considered ambiguous unless a select
|
93
|
+
# clause has been explicitly defined. Otherwise you can get broken records back, if, for example, the join column also has
|
94
|
+
# an id column. This will then overwrite the id column of the records coming back.
|
95
|
+
def finding_with_ambiguous_select?(select_clause)
|
96
|
+
!select_clause && @owner.connection.columns(@reflection.options[:join_table], "Join Table Columns").size != 2
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
def create_record(attributes, &block)
|
101
|
+
# Can't use Base.create because the foreign key may be a protected attribute.
|
102
|
+
ensure_owner_is_not_new
|
103
|
+
if attributes.is_a?(Array)
|
104
|
+
attributes.collect { |attr| create(attr) }
|
105
|
+
else
|
106
|
+
build_record(attributes, &block)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module CouchFoo
|
2
|
+
module Associations
|
3
|
+
class HasManyAssociation < AssociationCollection #:nodoc:
|
4
|
+
# Count the number of associated records.
|
5
|
+
# With CouchDB it does not make sense to have a second view for this count as it is likely
|
6
|
+
# that at some point the developer will access the objects themselves via find and thus create
|
7
|
+
# a suitable view. With CouchDB > 0.8 we can use the reduce function but for earlier
|
8
|
+
# versions we just do a find and count the results
|
9
|
+
def count(*args)
|
10
|
+
options = args.extract_options!
|
11
|
+
options[:conditions] = @association_conditions.merge(options[:conditions] || {})
|
12
|
+
if database.version > 0.8
|
13
|
+
value = count_view(options)
|
14
|
+
else
|
15
|
+
value = find(:all, options).size
|
16
|
+
end
|
17
|
+
|
18
|
+
limit = @reflection.options[:limit]
|
19
|
+
offset = @reflection.options[:offset]
|
20
|
+
|
21
|
+
if limit || offset
|
22
|
+
[ [value - offset.to_i, 0].max, limit.to_i ].min
|
23
|
+
else
|
24
|
+
value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
def count_records
|
30
|
+
count = if has_cached_counter?
|
31
|
+
@owner.send(:read_attribute, cached_counter_attribute_name)
|
32
|
+
else
|
33
|
+
@reflection.klass.count(:conditions => @association_conditions, :include => @reflection.options[:include])
|
34
|
+
end
|
35
|
+
|
36
|
+
# If there's nothing in the database and @target has no new records
|
37
|
+
# we are certain the current target is an empty array. This is a
|
38
|
+
# documented side-effect of the method that may avoid an extra SELECT.
|
39
|
+
@target ||= [] and loaded if count == 0
|
40
|
+
|
41
|
+
if @reflection.options[:limit]
|
42
|
+
count = [ @reflection.options[:limit], count ].min
|
43
|
+
end
|
44
|
+
|
45
|
+
return count
|
46
|
+
end
|
47
|
+
|
48
|
+
def has_cached_counter?
|
49
|
+
@owner.attribute_present?(cached_counter_attribute_name)
|
50
|
+
end
|
51
|
+
|
52
|
+
def cached_counter_attribute_name
|
53
|
+
"#{@reflection.name}_count"
|
54
|
+
end
|
55
|
+
|
56
|
+
def insert_record(record)
|
57
|
+
set_belongs_to_association_for(record)
|
58
|
+
record.save
|
59
|
+
end
|
60
|
+
|
61
|
+
def delete_records(documents)
|
62
|
+
case @reflection.options[:dependent]
|
63
|
+
when :destroy
|
64
|
+
documents.each(&:destroy)
|
65
|
+
when :delete_all
|
66
|
+
@reflection.klass.delete(documents.map(&:id))
|
67
|
+
else
|
68
|
+
find(documents.map{|d| d.id }, :conditions => @association_conditions).each {|doc| doc.update_attributes({@reflection.primary_key_name => nil}, true)}
|
69
|
+
database.commit
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def target_obsolete?
|
74
|
+
false
|
75
|
+
end
|
76
|
+
|
77
|
+
def construct_conditions
|
78
|
+
if @reflection.options[:as]
|
79
|
+
@association_conditions = {"#{@reflection.options[:as]}_id".to_sym => @owner.id,
|
80
|
+
"#{@reflection.options[:as]}_type".to_sym => @owner.class.name.to_s}
|
81
|
+
else
|
82
|
+
@association_conditions = {@reflection.primary_key_name => @owner.id}
|
83
|
+
end
|
84
|
+
@association_conditions.merge!(conditions) if conditions
|
85
|
+
end
|
86
|
+
|
87
|
+
def construct_scope
|
88
|
+
create_scoping = {}
|
89
|
+
set_belongs_to_association_for(create_scoping)
|
90
|
+
{
|
91
|
+
:find => { :conditions => @association_conditions, :readonly => false, :order => @reflection.options[:order], :offset => @reflection.options[:offset], :limit => @reflection.options[:limit], :include => @reflection.options[:include]},
|
92
|
+
:create => create_scoping
|
93
|
+
}
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module CouchFoo
|
2
|
+
module Associations
|
3
|
+
class HasOneAssociation < BelongsToAssociation #:nodoc:
|
4
|
+
def initialize(owner, reflection)
|
5
|
+
super
|
6
|
+
construct_conditions
|
7
|
+
end
|
8
|
+
|
9
|
+
def create(attrs = {}, replace_existing = true)
|
10
|
+
new_record(replace_existing) { |klass| klass.create(attrs) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def create!(attrs = {}, replace_existing = true)
|
14
|
+
new_record(replace_existing) { |klass| klass.create!(attrs) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def build(attrs = {}, replace_existing = true)
|
18
|
+
new_record(replace_existing) { |klass| klass.new(attrs) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def replace(obj, dont_save = false)
|
22
|
+
load_target
|
23
|
+
|
24
|
+
unless @target.nil? || @target == obj
|
25
|
+
if dependent? && !dont_save
|
26
|
+
@target.destroy unless @target.new_record?
|
27
|
+
@owner.clear_association_cache
|
28
|
+
else
|
29
|
+
@target[@reflection.primary_key_name] = nil
|
30
|
+
@target.save unless @owner.new_record? || @target.new_record?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
if obj.nil?
|
35
|
+
@target = nil
|
36
|
+
else
|
37
|
+
raise_on_type_mismatch(obj)
|
38
|
+
set_belongs_to_association_for(obj)
|
39
|
+
@target = (AssociationProxy === obj ? obj.target : obj)
|
40
|
+
end
|
41
|
+
|
42
|
+
@loaded = true
|
43
|
+
|
44
|
+
unless @owner.new_record? or obj.nil? or dont_save
|
45
|
+
return (obj.save ? self : false)
|
46
|
+
else
|
47
|
+
return (obj.nil? ? nil : self)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
def find_target
|
53
|
+
@reflection.klass.find(:first,
|
54
|
+
:conditions => @association_conditions,
|
55
|
+
:order => @reflection.options[:order],
|
56
|
+
:include => @reflection.options[:include],
|
57
|
+
:readonly => @reflection.options[:readonly]
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
def construct_conditions
|
62
|
+
if @reflection.options[:as]
|
63
|
+
@association_conditions = {"#{@reflection.options[:as]}_id".to_sym => @owner.id,
|
64
|
+
"#{@reflection.options[:as]}_type".to_sym => @owner.class.name.to_s}
|
65
|
+
else
|
66
|
+
@association_conditions = {@reflection.primary_key_name => @owner.id}
|
67
|
+
end
|
68
|
+
@association_conditions.merge!(conditions) if conditions
|
69
|
+
end
|
70
|
+
|
71
|
+
def construct_scope
|
72
|
+
create_scoping = {}
|
73
|
+
set_belongs_to_association_for(create_scoping)
|
74
|
+
{ :create => create_scoping }
|
75
|
+
end
|
76
|
+
|
77
|
+
def new_record(replace_existing)
|
78
|
+
# Make sure we load the target first, if we plan on replacing the existing
|
79
|
+
# instance. Otherwise, if the target has not previously been loaded
|
80
|
+
# elsewhere, the instance we create will get orphaned.
|
81
|
+
load_target if replace_existing
|
82
|
+
record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { yield @reflection.klass }
|
83
|
+
|
84
|
+
if replace_existing
|
85
|
+
replace(record, true)
|
86
|
+
else
|
87
|
+
record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
|
88
|
+
self.target = record
|
89
|
+
end
|
90
|
+
|
91
|
+
record
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|