nobrainer 0.13.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/lib/no_brainer/config.rb +20 -4
  3. data/lib/no_brainer/criteria/count.rb +1 -1
  4. data/lib/no_brainer/criteria/delete.rb +2 -2
  5. data/lib/no_brainer/criteria/first.rb +6 -0
  6. data/lib/no_brainer/criteria/order_by.rb +33 -10
  7. data/lib/no_brainer/criteria/update.rb +2 -2
  8. data/lib/no_brainer/criteria/where.rb +12 -6
  9. data/lib/no_brainer/decorated_symbol.rb +2 -1
  10. data/lib/no_brainer/document/association/belongs_to.rb +10 -5
  11. data/lib/no_brainer/document/association/core.rb +1 -1
  12. data/lib/no_brainer/document/association/has_many.rb +10 -4
  13. data/lib/no_brainer/document/attributes.rb +29 -3
  14. data/lib/no_brainer/document/callbacks.rb +2 -10
  15. data/lib/no_brainer/document/core.rb +5 -3
  16. data/lib/no_brainer/document/criteria.rb +11 -11
  17. data/lib/no_brainer/document/dirty.rb +11 -0
  18. data/lib/no_brainer/document/dynamic_attributes.rb +4 -0
  19. data/lib/no_brainer/document/id.rb +49 -4
  20. data/lib/no_brainer/document/index.rb +6 -2
  21. data/lib/no_brainer/document/persistance.rb +7 -6
  22. data/lib/no_brainer/document/readonly.rb +7 -0
  23. data/lib/no_brainer/document/serialization.rb +4 -0
  24. data/lib/no_brainer/document/types/boolean.rb +26 -0
  25. data/lib/no_brainer/document/types/date.rb +26 -0
  26. data/lib/no_brainer/document/types/float.rb +20 -0
  27. data/lib/no_brainer/document/types/integer.rb +18 -0
  28. data/lib/no_brainer/document/types/string.rb +14 -0
  29. data/lib/no_brainer/document/types/symbol.rb +21 -0
  30. data/lib/no_brainer/document/types/time.rb +41 -0
  31. data/lib/no_brainer/document/types.rb +64 -124
  32. data/lib/no_brainer/document/uniqueness.rb +4 -2
  33. data/lib/no_brainer/document/validation.rb +2 -1
  34. data/lib/no_brainer/document.rb +2 -0
  35. data/lib/no_brainer/error.rb +9 -9
  36. data/lib/no_brainer/query_runner/logger.rb +18 -12
  37. data/lib/no_brainer/query_runner/missing_index.rb +15 -3
  38. data/lib/no_brainer/query_runner/reconnect.rb +15 -4
  39. data/lib/no_brainer/query_runner/table_on_demand.rb +14 -25
  40. data/lib/no_brainer/query_runner/write_error.rb +5 -8
  41. data/lib/no_brainer/rql.rb +25 -0
  42. data/lib/nobrainer.rb +9 -6
  43. data/lib/rails/generators/nobrainer/model/model_generator.rb +2 -1
  44. data/lib/rails/generators/nobrainer/model/templates/model.rb.tt +7 -2
  45. metadata +18 -11
  46. data/lib/no_brainer/util.rb +0 -23
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b5ca498dfa914c0c0178f339485d78ba804c5b95
4
- data.tar.gz: c0e0c6f35d8927bb6281f860aac769753df59b88
3
+ metadata.gz: e97dd4f563340a2c8551ffefb21f22fc628954b2
4
+ data.tar.gz: 1fca054e8f966be9e7518f8245c39987d40068c7
5
5
  SHA512:
6
- metadata.gz: ed914e1eee020658197d596245404a2a088acbf033f5fc3a44cb490620cdc5ef110d62e4da927e7579e5e3383bc90e16dd499269a9bd8952dfa2011d3e2f2a12
7
- data.tar.gz: f3204081b4ef7617706ee3c037830bdacbde973ae79e914b782b39fb41f1ec36b0125f78b427d0f21078bdae543ee426f2936ba69a1327bc36176acc6997765e
6
+ metadata.gz: 1cbfebfa6ac2922e733d157522eda4e6c9edf9de9e51601f6892ac2ac2814593ed20807c68441a2ff7391398cf37896eeb739eaf2cdafa0da8e9fe91bb02fce2
7
+ data.tar.gz: ace1719ae85c7593145e81818fb1b93f715ad65d4634130c664b0fdd134c637aba0395066163b9fd3aee91f7ec0b27a53338e857ec2c089a4b425e02f2eba00b
@@ -4,7 +4,8 @@ module NoBrainer::Config
4
4
  class << self
5
5
  mattr_accessor :rethinkdb_url, :logger, :warn_on_active_record,
6
6
  :auto_create_databases, :auto_create_tables,
7
- :max_reconnection_tries, :durability, :colorize_logger,
7
+ :max_reconnection_tries, :durability,
8
+ :user_timezone, :db_timezone, :colorize_logger,
8
9
  :distributed_lock_class
9
10
 
10
11
  def apply_defaults
@@ -15,6 +16,8 @@ module NoBrainer::Config
15
16
  self.auto_create_tables = true
16
17
  self.max_reconnection_tries = 10
17
18
  self.durability = default_durability
19
+ self.user_timezone = :local
20
+ self.db_timezone = :utc
18
21
  self.colorize_logger = true
19
22
  self.distributed_lock_class = nil
20
23
  end
@@ -27,6 +30,7 @@ module NoBrainer::Config
27
30
  def configure(&block)
28
31
  apply_defaults unless configured?
29
32
  block.call(self) if block
33
+ assert_valid_options!
30
34
  @configured = true
31
35
 
32
36
  NoBrainer.disconnect_if_url_changed
@@ -36,9 +40,21 @@ module NoBrainer::Config
36
40
  !!@configured
37
41
  end
38
42
 
43
+ def assert_valid_options!
44
+ assert_array_in :durability, [:hard, :soft]
45
+ assert_array_in :user_timezone, [:unchanged, :utc, :local]
46
+ assert_array_in :db_timezone, [:unchanged, :utc, :local]
47
+ end
48
+
49
+ def assert_array_in(name, values)
50
+ unless __send__(name).in?(values)
51
+ raise ArgumentError.new("Unknown configuration for #{name}: #{__send__(name)}. Valid values are: #{values.inspect}")
52
+ end
53
+ end
54
+
39
55
  def default_rethinkdb_url
40
56
  db = ENV['RETHINKDB_DB'] || ENV['RDB_DB']
41
- db ||= "#{Rails.application.class.parent_name.underscore}_#{Rails.env}" if defined?(Rails)
57
+ db ||= "#{Rails.application.class.parent_name.underscore}_#{Rails.env}" rescue nil
42
58
  host = ENV['RETHINKDB_HOST'] || ENV['RDB_HOST'] || 'localhost'
43
59
  port = ENV['RETHINKDB_PORT'] || ENV['RDB_PORT']
44
60
  auth = ENV['RETHINKDB_AUTH'] || ENV['RDB_AUTH']
@@ -48,11 +64,11 @@ module NoBrainer::Config
48
64
  end
49
65
 
50
66
  def default_logger
51
- defined?(Rails) ? Rails.logger : Logger.new(STDERR).tap { |l| l.level = Logger::WARN }
67
+ defined?(Rails.logger) ? Rails.logger : Logger.new(STDERR).tap { |l| l.level = Logger::WARN }
52
68
  end
53
69
 
54
70
  def default_durability
55
- (defined?(Rails) && (Rails.env.test? || Rails.env.development?)) ? :soft : :hard
71
+ (defined?(Rails.env) && (Rails.env.test? || Rails.env.development?)) ? :soft : :hard
56
72
  end
57
73
  end
58
74
  end
@@ -2,7 +2,7 @@ module NoBrainer::Criteria::Count
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  def count
5
- run(to_rql.count)
5
+ run(without_ordering.to_rql.count)
6
6
  end
7
7
 
8
8
  def empty?
@@ -2,10 +2,10 @@ module NoBrainer::Criteria::Delete
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  def delete_all
5
- run(to_rql.delete)
5
+ run(without_ordering.to_rql.delete)
6
6
  end
7
7
 
8
8
  def destroy_all
9
- to_a.each { |doc| doc.destroy }
9
+ without_ordering.to_a.each { |doc| doc.destroy }
10
10
  end
11
11
  end
@@ -17,6 +17,12 @@ module NoBrainer::Criteria::First
17
17
  last.tap { |doc| raise NoBrainer::Error::DocumentNotFound unless doc }
18
18
  end
19
19
 
20
+ def sample(n=nil)
21
+ result = NoBrainer.run { self.without_ordering.to_rql.sample(n.nil? ? 1 : n) }
22
+ result = result.map(&method(:instantiate_doc))
23
+ n.nil? ? result.first : result
24
+ end
25
+
20
26
  private
21
27
 
22
28
  def get_one(criteria)
@@ -1,7 +1,7 @@
1
1
  module NoBrainer::Criteria::OrderBy
2
2
  extend ActiveSupport::Concern
3
3
 
4
- included { attr_accessor :order, :_reverse_order }
4
+ included { attr_accessor :order, :ordering_mode }
5
5
 
6
6
  def initialize(options={})
7
7
  super
@@ -24,36 +24,55 @@ module NoBrainer::Criteria::OrderBy
24
24
 
25
25
  chain do |criteria|
26
26
  criteria.order = rules
27
- criteria._reverse_order = false
27
+ criteria.ordering_mode = :normal
28
28
  end
29
29
  end
30
30
 
31
+ def without_ordering
32
+ chain { |criteria| criteria.ordering_mode = :disabled }
33
+ end
34
+
31
35
  def merge!(criteria, options={})
32
36
  super
33
37
  # The latest order_by() wins
34
38
  self.order = criteria.order if criteria.order.present?
35
- self._reverse_order = criteria._reverse_order unless criteria._reverse_order.nil?
39
+ self.ordering_mode = criteria.ordering_mode unless criteria.ordering_mode.nil?
36
40
  self
37
41
  end
38
42
 
39
43
  def reverse_order
40
- chain { |criteria| criteria._reverse_order = !self._reverse_order }
44
+ chain do |criteria|
45
+ criteria.ordering_mode =
46
+ case self.ordering_mode
47
+ when nil then :reversed
48
+ when :normal then :reversed
49
+ when :reversed then :normal
50
+ when :disabled then :disabled
51
+ end
52
+ end
41
53
  end
42
54
 
43
55
  private
44
56
 
45
57
  def effective_order
46
- self.order.presence || {:id => :asc}
58
+ self.order.presence || (klass ? {klass.pk_name => :asc} : {})
47
59
  end
48
60
 
49
61
  def reverse_order?
50
- !!self._reverse_order
62
+ self.ordering_mode == :reversed
63
+ end
64
+
65
+ def should_order?
66
+ self.ordering_mode != :disabled
51
67
  end
52
68
 
53
69
  def compile_rql_pass1
54
70
  rql = super
71
+ return rql unless should_order?
72
+ _effective_order = effective_order
73
+ return rql if _effective_order.empty?
55
74
 
56
- rql_rules = effective_order.map do |k,v|
75
+ rql_rules = _effective_order.map do |k,v|
57
76
  case v
58
77
  when :asc then reverse_order? ? RethinkDB::RQL.new.desc(k) : RethinkDB::RQL.new.asc(k)
59
78
  when :desc then reverse_order? ? RethinkDB::RQL.new.asc(k) : RethinkDB::RQL.new.desc(k)
@@ -64,9 +83,10 @@ module NoBrainer::Criteria::OrderBy
64
83
  # We are going to try to go so and if we cannot, we'll simply apply
65
84
  # the ordering in pass2, which will happen after a potential filter().
66
85
 
67
- if rql.body.type == Term::TermType::TABLE && !without_index?
86
+ NoBrainer::RQL.is_table?(rql)
87
+ if NoBrainer::RQL.is_table?(rql) && !without_index?
68
88
  options = {}
69
- first_key = effective_order.first[0]
89
+ first_key = _effective_order.first[0]
70
90
  if (first_key.is_a?(Symbol) || first_key.is_a?(String)) && klass.has_index?(first_key)
71
91
  options[:index] = rql_rules.shift
72
92
  end
@@ -83,7 +103,10 @@ module NoBrainer::Criteria::OrderBy
83
103
 
84
104
  def compile_rql_pass2
85
105
  rql = super
86
- rql = rql.order_by(*@rql_rules_pass2) if @rql_rules_pass2
106
+ if @rql_rules_pass2
107
+ rql = rql.order_by(*@rql_rules_pass2)
108
+ @rql_rules_pass2 = nil
109
+ end
87
110
  rql
88
111
  end
89
112
 
@@ -2,10 +2,10 @@ module NoBrainer::Criteria::Update
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  def update_all(*args, &block)
5
- run(to_rql.update(*args, &block))
5
+ run(without_ordering.to_rql.update(*args, &block))
6
6
  end
7
7
 
8
8
  def replace_all(*args, &block)
9
- run(to_rql.replace(*args, &block))
9
+ run(without_ordering.to_rql.replace(*args, &block))
10
10
  end
11
11
  end
@@ -54,24 +54,26 @@ module NoBrainer::Criteria::Where
54
54
  end
55
55
  end
56
56
 
57
- class BinaryOperator < Struct.new(:key, :op, :value, :criteria)
57
+ class BinaryOperator < Struct.new(:key, :op, :value, :criteria, :casted_values)
58
58
  def simplify
59
59
  key = cast_key(self.key)
60
60
  case op
61
61
  when :in then
62
62
  case value
63
- when Range then BinaryOperator.new(key, :between, (cast_value(value.min)..cast_value(value.max)), criteria)
64
- when Array then BinaryOperator.new(key, :in, value.map(&method(:cast_value)).uniq, criteria)
63
+ when Range then BinaryOperator.new(key, :between, (cast_value(value.min)..cast_value(value.max)), criteria, true)
64
+ when Array then BinaryOperator.new(key, :in, value.map(&method(:cast_value)).uniq, criteria, true)
65
65
  else raise ArgumentError.new ":in takes an array/range, not #{value}"
66
66
  end
67
- else BinaryOperator.new(key, op, cast_value(value), criteria)
67
+ when :between then BinaryOperator.new(key, :between, (cast_value(value.min)..cast_value(value.max)), criteria, true)
68
+ else BinaryOperator.new(key, op, cast_value(value), criteria, true)
68
69
  end
69
70
  end
70
71
 
71
72
  def to_rql(doc)
72
73
  case op
74
+ when :defined then value ? doc.has_fields(key) : doc.has_fields(key).not
73
75
  when :between then (doc[key] >= value.min) & (doc[key] <= value.max)
74
- when :in then RethinkDB::RQL.new.expr(value).contains(doc[key])
76
+ when :in then RethinkDB::RQL.new.expr(value).contains(doc[key])
75
77
  else doc[key].__send__(op, value)
76
78
  end
77
79
  end
@@ -87,18 +89,22 @@ module NoBrainer::Criteria::Where
87
89
  end
88
90
 
89
91
  def cast_value(value)
92
+ return value if casted_values
93
+
90
94
  case association
91
95
  when NoBrainer::Document::Association::BelongsTo::Metadata
92
96
  target_klass = association.target_klass
93
97
  opts = { :attr_name => key, :value => value, :type => target_klass}
94
98
  raise NoBrainer::Error::InvalidType.new(opts) unless value.is_a?(target_klass)
95
- value.id
99
+ value.pk_value
96
100
  else
97
101
  criteria.klass.cast_user_to_db_for(key, value)
98
102
  end
99
103
  end
100
104
 
101
105
  def cast_key(key)
106
+ return key if casted_values
107
+
102
108
  case association
103
109
  when NoBrainer::Document::Association::BelongsTo::Metadata
104
110
  association.foreign_key
@@ -2,7 +2,8 @@ class NoBrainer::DecoratedSymbol < Struct.new(:symbol, :modifier, :args)
2
2
  MODIFIERS = { :in => :in, :nin => :nin,
3
3
  :eq => :eq, :ne => :ne, :not => :ne,
4
4
  :gt => :gt, :ge => :ge, :gte => :ge,
5
- :lt => :lt, :le => :le, :lte => :le}
5
+ :lt => :lt, :le => :le, :lte => :le,
6
+ :defined => :defined }
6
7
 
7
8
  def self.hook
8
9
  Symbol.class_eval do
@@ -2,13 +2,18 @@ class NoBrainer::Document::Association::BelongsTo
2
2
  include NoBrainer::Document::Association::Core
3
3
 
4
4
  class Metadata
5
- VALID_OPTIONS = [:foreign_key, :class_name, :index, :validates, :required]
5
+ VALID_OPTIONS = [:primary_key, :foreign_key, :class_name, :index, :validates, :required]
6
6
  include NoBrainer::Document::Association::Core::Metadata
7
7
  extend NoBrainer::Document::Association::EagerLoader::Generic
8
8
 
9
9
  def foreign_key
10
10
  # TODO test :foreign_key
11
- options[:foreign_key].try(:to_sym) || :"#{target_name}_id"
11
+ options[:foreign_key].try(:to_sym) || :"#{target_name}_#{primary_key}"
12
+ end
13
+
14
+ def primary_key
15
+ # TODO test :primary_key
16
+ options[:primary_key].try(:to_sym) || target_klass.pk_name
12
17
  end
13
18
 
14
19
  def target_klass
@@ -32,7 +37,7 @@ class NoBrainer::Document::Association::BelongsTo
32
37
  add_callback_for(:after_validation)
33
38
  end
34
39
 
35
- eager_load_with :owner_key => ->{ foreign_key }, :target_key => ->{ :id },
40
+ eager_load_with :owner_key => ->{ foreign_key }, :target_key => ->{ primary_key },
36
41
  :unscoped => true
37
42
  end
38
43
 
@@ -49,13 +54,13 @@ class NoBrainer::Document::Association::BelongsTo
49
54
  return target if loaded?
50
55
 
51
56
  if fk = owner.read_attribute(foreign_key)
52
- preload(target_klass.find(fk))
57
+ preload(target_klass.unscoped.where(primary_key => fk).first)
53
58
  end
54
59
  end
55
60
 
56
61
  def write(target)
57
62
  assert_target_type(target)
58
- owner.write_attribute(foreign_key, target.try(:id))
63
+ owner.write_attribute(foreign_key, target.try(:pk_value))
59
64
  preload(target)
60
65
  end
61
66
 
@@ -49,7 +49,7 @@ module NoBrainer::Document::Association::Core
49
49
 
50
50
  included { attr_accessor :metadata, :owner }
51
51
 
52
- delegate :foreign_key, :target_name, :target_klass, :to => :metadata
52
+ delegate :primary_key, :foreign_key, :target_name, :target_klass, :to => :metadata
53
53
 
54
54
  def initialize(metadata, owner)
55
55
  @metadata, @owner = metadata, owner
@@ -2,13 +2,18 @@ class NoBrainer::Document::Association::HasMany
2
2
  include NoBrainer::Document::Association::Core
3
3
 
4
4
  class Metadata
5
- VALID_OPTIONS = [:foreign_key, :class_name, :dependent]
5
+ VALID_OPTIONS = [:primary_key, :foreign_key, :class_name, :dependent]
6
6
  include NoBrainer::Document::Association::Core::Metadata
7
7
  extend NoBrainer::Document::Association::EagerLoader::Generic
8
8
 
9
9
  def foreign_key
10
10
  # TODO test :foreign_key
11
- options[:foreign_key].try(:to_sym) || owner_klass.name.foreign_key.to_sym
11
+ options[:foreign_key].try(:to_sym) || :"#{owner_klass.name.underscore}_#{owner_klass.pk_name}"
12
+ end
13
+
14
+ def primary_key
15
+ # TODO test :primary_key
16
+ options[:primary_key].try(:to_sym) || target_klass.pk_name
12
17
  end
13
18
 
14
19
  def target_klass
@@ -25,6 +30,7 @@ class NoBrainer::Document::Association::HasMany
25
30
  target_klass.association_metadata.values.select do |assoc|
26
31
  assoc.is_a?(NoBrainer::Document::Association::BelongsTo::Metadata) and
27
32
  assoc.foreign_key == self.foreign_key and
33
+ assoc.primary_key == self.primary_key and
28
34
  assoc.target_klass.root_class == owner_klass.root_class
29
35
  end
30
36
  end
@@ -34,11 +40,11 @@ class NoBrainer::Document::Association::HasMany
34
40
  add_callback_for(:before_destroy) if options[:dependent]
35
41
  end
36
42
 
37
- eager_load_with :owner_key => ->{ :id }, :target_key => ->{ foreign_key }
43
+ eager_load_with :owner_key => ->{ primary_key }, :target_key => ->{ foreign_key }
38
44
  end
39
45
 
40
46
  def target_criteria
41
- @target_criteria ||= target_klass.where(foreign_key => owner.id)
47
+ @target_criteria ||= target_klass.where(foreign_key => owner.pk_value)
42
48
  .after_find(set_inverse_proc)
43
49
  end
44
50
 
@@ -1,6 +1,7 @@
1
1
  module NoBrainer::Document::Attributes
2
- VALID_FIELD_OPTIONS = [:index, :default, :type, :cast_user_to_db, :cast_db_to_user, :validates, :required, :unique, :readonly]
3
- RESERVED_FIELD_NAMES = [:index, :default, :and, :or, :selector, :associations] + NoBrainer::DecoratedSymbol::MODIFIERS.keys
2
+ VALID_FIELD_OPTIONS = [:index, :default, :type, :validates, :required, :unique, :in, :readonly, :primary_key]
3
+ RESERVED_FIELD_NAMES = [:index, :default, :and, :or, :selector, :associations, :pk_value] \
4
+ + NoBrainer::DecoratedSymbol::MODIFIERS.keys
4
5
  extend ActiveSupport::Concern
5
6
 
6
7
  included do
@@ -15,8 +16,12 @@ module NoBrainer::Document::Attributes
15
16
  assign_attributes(attrs, options.reverse_merge(:pristine => true))
16
17
  end
17
18
 
19
+ def readable_attributes
20
+ @_attributes.keys & self.class.fields.keys.map(&:to_s)
21
+ end
22
+
18
23
  def attributes
19
- Hash[@_attributes.keys.map { |k| [k, read_attribute(k)] }].with_indifferent_access.freeze
24
+ Hash[readable_attributes.map { |k| [k, read_attribute(k)] }].with_indifferent_access.freeze
20
25
  end
21
26
 
22
27
  def read_attribute(name)
@@ -57,6 +62,10 @@ module NoBrainer::Document::Attributes
57
62
  Hash[@_attributes.sort_by { |k,v| self.class.fields.keys.index(k.to_sym) || 2**10 }]
58
63
  end
59
64
 
65
+ def to_s
66
+ "#<#{self.class} #{self.class.pk_name}: #{self.pk_value.inspect}>"
67
+ end
68
+
60
69
  def inspect
61
70
  "#<#{self.class} #{inspectable_attributes.map { |k,v| "#{k}: #{v.inspect}" }.join(', ')}>"
62
71
  end
@@ -97,6 +106,23 @@ module NoBrainer::Document::Attributes
97
106
  _field(attr, self.fields[attr])
98
107
  end
99
108
 
109
+ def _remove_field(attr, options={})
110
+ inject_in_layer :attributes do
111
+ remove_method("#{attr}=")
112
+ remove_method("#{attr}")
113
+ end
114
+ end
115
+
116
+ def remove_field(attr, options={})
117
+ attr = attr.to_sym
118
+
119
+ _remove_field(attr, options)
120
+
121
+ ([self] + descendants).each do |klass|
122
+ klass.fields.delete(attr)
123
+ end
124
+ end
125
+
100
126
  def has_field?(attr)
101
127
  !!fields[attr.to_sym]
102
128
  end
@@ -1,19 +1,11 @@
1
1
  module NoBrainer::Document::Callbacks
2
2
  extend ActiveSupport::Concern
3
3
 
4
- def self.terminator
5
- if Gem.loaded_specs['activesupport'].version.release >= Gem::Version.new('4.1')
6
- proc { false }
7
- else
8
- 'false'
9
- end
10
- end
11
-
12
4
  included do
13
5
  extend ActiveModel::Callbacks
14
6
 
15
- define_model_callbacks :initialize, :create, :update, :save, :destroy, :terminator => NoBrainer::Document::Callbacks.terminator
16
- define_model_callbacks :find, :only => [:after], :terminator => NoBrainer::Document::Callbacks.terminator
7
+ define_model_callbacks :initialize, :create, :update, :save, :destroy, :terminator => proc { false }
8
+ define_model_callbacks :find, :only => [:after], :terminator => proc { false }
17
9
  end
18
10
 
19
11
  def initialize(*args, &block)
@@ -5,16 +5,18 @@ module NoBrainer::Document::Core
5
5
  attr_accessor :_all
6
6
 
7
7
  def all
8
- Rails.application.eager_load! if defined?(Rails)
8
+ Rails.application.eager_load! if defined?(Rails.application.eager_load!)
9
9
  @_all
10
10
  end
11
11
  end
12
12
  self._all = []
13
13
 
14
- # TODO This assume the primary key is id.
15
- # RethinkDB can have a custom primary key. careful.
16
14
  include ActiveModel::Conversion
17
15
 
16
+ def to_key
17
+ [pk_value]
18
+ end
19
+
18
20
  included do
19
21
  # TODO test these includes
20
22
  extend ActiveModel::Naming
@@ -2,7 +2,7 @@ module NoBrainer::Document::Criteria
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  def selector
5
- self.class.selector_for(id)
5
+ self.class.selector_for(pk_value)
6
6
  end
7
7
 
8
8
  included { cattr_accessor :default_scope_proc, :instance_accessor => false }
@@ -10,7 +10,7 @@ module NoBrainer::Document::Criteria
10
10
  module ClassMethods
11
11
  delegate :to_rql, # Core
12
12
  :limit, :offset, :skip, # Limit
13
- :order_by, :reverse_order, # OrderBy
13
+ :order_by, :reverse_order, :without_ordering, # OrderBy
14
14
  :scoped, :unscoped, # Scope
15
15
  :where, :with_index, :without_index, :used_index, :indexed?, # Where
16
16
  :with_cache, :without_cache, # Cache
@@ -18,7 +18,7 @@ module NoBrainer::Document::Criteria
18
18
  :delete_all, :destroy_all, # Delete
19
19
  :includes, :preload, # Preload
20
20
  :each, :to_a, # Enumerable
21
- :first, :last, :first!, :last!, # First
21
+ :first, :last, :first!, :last!, :sample, # First
22
22
  :update_all, :replace_all, # Update
23
23
  :to => :all
24
24
 
@@ -40,20 +40,20 @@ module NoBrainer::Document::Criteria
40
40
  self.default_scope_proc = criteria.is_a?(Proc) ? criteria : proc { criteria }
41
41
  end
42
42
 
43
- def selector_for(id)
44
- # TODO Pass primary key if not default
45
- unscoped.where(:id => id)
43
+ def selector_for(pk)
44
+ rql_table.get(pk)
46
45
  end
47
46
 
48
47
  # XXX this doesn't have the same semantics as
49
48
  # other ORMs. the equivalent is find!.
50
- def find(id)
51
- selector_for(id).first
49
+ def find(pk)
50
+ attrs = NoBrainer.run { selector_for(pk) }
51
+ new_from_db(attrs).tap { |doc| doc.run_callbacks(:find) } if attrs
52
52
  end
53
53
 
54
- def find!(id)
55
- find(id).tap do |doc|
56
- raise NoBrainer::Error::DocumentNotFound, "#{self.class} id #{id} not found" unless doc
54
+ def find!(pk)
55
+ find(pk).tap do |doc|
56
+ raise NoBrainer::Error::DocumentNotFound, "#{self} #{pk_name}: #{pk} not found" unless doc
57
57
  end
58
58
  end
59
59
  end
@@ -79,5 +79,16 @@ module NoBrainer::Document::Dirty
79
79
  end
80
80
  end
81
81
  end
82
+
83
+ def _remove_field(attr, options={})
84
+ super
85
+ inject_in_layer :dirty_tracking do
86
+ remove_method("#{attr}_change")
87
+ remove_method("#{attr}_changed?")
88
+ remove_method("#{attr}_was")
89
+ remove_method("#{attr}=")
90
+ remove_method("#{attr}")
91
+ end
92
+ end
82
93
  end
83
94
  end
@@ -17,4 +17,8 @@ module NoBrainer::Document::DynamicAttributes
17
17
  @_attributes[name] = value
18
18
  end
19
19
  end
20
+
21
+ def readable_attributes
22
+ @_attributes.keys
23
+ end
20
24
  end
@@ -5,17 +5,23 @@ require 'digest/md5'
5
5
  module NoBrainer::Document::Id
6
6
  extend ActiveSupport::Concern
7
7
 
8
- included do
9
- self.field :id, :type => String, :default => ->{ NoBrainer::Document::Id.generate }, :readonly => true
8
+ DEFAULT_PK_NAME = :id
9
+
10
+ def pk_value
11
+ __send__(self.class.pk_name)
12
+ end
13
+
14
+ def pk_value=(value)
15
+ __send__("#{self.class.pk_name}=", value)
10
16
  end
11
17
 
12
18
  def ==(other)
13
19
  return super unless self.class == other.class
14
- !id.nil? && id == other.id
20
+ !pk_value.nil? && pk_value == other.pk_value
15
21
  end
16
22
  alias_method :eql?, :==
17
23
 
18
- delegate :hash, :to => :id
24
+ delegate :hash, :to => :pk_value
19
25
 
20
26
  # The following code is inspired by the mongo-ruby-driver
21
27
 
@@ -46,4 +52,43 @@ module NoBrainer::Document::Id
46
52
 
47
53
  oid.unpack("C12").map {|e| v=e.to_s(16); v.size == 1 ? "0#{v}" : v }.join
48
54
  end
55
+
56
+ module ClassMethods
57
+ def define_default_pk
58
+ class_variable_set(:@@pk_name, nil)
59
+ field NoBrainer::Document::Id::DEFAULT_PK_NAME, :primary_key => :default,
60
+ :type => String, :default => ->{ NoBrainer::Document::Id.generate }
61
+ end
62
+
63
+ def define_pk(attr)
64
+ if fields[pk_name].try(:[], :primary_key) == :default
65
+ remove_field(pk_name, :set_default_pk => false)
66
+ end
67
+ class_variable_set(:@@pk_name, attr)
68
+ end
69
+
70
+ def pk_name
71
+ class_variable_get(:@@pk_name)
72
+ end
73
+
74
+ def _field(attr, options={})
75
+ super
76
+ define_pk(attr) if options[:primary_key]
77
+ end
78
+
79
+ def field(attr, options={})
80
+ if options[:primary_key]
81
+ options = options.merge(:readonly => true) if options[:readonly].nil?
82
+ options = options.merge(:index => true)
83
+ end
84
+ super
85
+ end
86
+
87
+ def _remove_field(attr, options={})
88
+ super
89
+ if fields[attr][:primary_key] && options[:set_default_pk] != false
90
+ define_default_pk
91
+ end
92
+ end
93
+ end
49
94
  end
@@ -5,7 +5,6 @@ module NoBrainer::Document::Index
5
5
  included do
6
6
  cattr_accessor :indexes, :instance_accessor => false
7
7
  self.indexes = {}
8
- self.index :id
9
8
  end
10
9
 
11
10
  module ClassMethods
@@ -57,6 +56,11 @@ module NoBrainer::Document::Index
57
56
  end
58
57
  end
59
58
 
59
+ def _remove_field(attr, options={})
60
+ super
61
+ remove_index(attr) if fields[attr][:index]
62
+ end
63
+
60
64
  def perform_create_index(index_name, options={})
61
65
  index_name = index_name.to_sym
62
66
  index_args = self.indexes[index_name]
@@ -79,7 +83,7 @@ module NoBrainer::Document::Index
79
83
 
80
84
  def perform_update_indexes(options={})
81
85
  current_indexes = NoBrainer.run(self.rql_table.index_list).map(&:to_sym)
82
- wanted_indexes = self.indexes.keys - [:id] # XXX Primary key?
86
+ wanted_indexes = self.indexes.keys - [self.pk_name]
83
87
 
84
88
  (current_indexes - wanted_indexes).each do |index_name|
85
89
  perform_drop_index(index_name, options)