nobrainer 0.8.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 (61) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +7 -0
  3. data/README.md +6 -0
  4. data/lib/no_brainer/autoload.rb +14 -0
  5. data/lib/no_brainer/config.rb +61 -0
  6. data/lib/no_brainer/connection.rb +53 -0
  7. data/lib/no_brainer/criteria.rb +20 -0
  8. data/lib/no_brainer/criteria/chainable/core.rb +67 -0
  9. data/lib/no_brainer/criteria/chainable/limit.rb +31 -0
  10. data/lib/no_brainer/criteria/chainable/order_by.rb +76 -0
  11. data/lib/no_brainer/criteria/chainable/raw.rb +25 -0
  12. data/lib/no_brainer/criteria/chainable/scope.rb +41 -0
  13. data/lib/no_brainer/criteria/chainable/where.rb +198 -0
  14. data/lib/no_brainer/criteria/termination/cache.rb +71 -0
  15. data/lib/no_brainer/criteria/termination/count.rb +19 -0
  16. data/lib/no_brainer/criteria/termination/delete.rb +11 -0
  17. data/lib/no_brainer/criteria/termination/eager_loading.rb +64 -0
  18. data/lib/no_brainer/criteria/termination/enumerable.rb +24 -0
  19. data/lib/no_brainer/criteria/termination/first.rb +25 -0
  20. data/lib/no_brainer/criteria/termination/inc.rb +14 -0
  21. data/lib/no_brainer/criteria/termination/update.rb +13 -0
  22. data/lib/no_brainer/database.rb +41 -0
  23. data/lib/no_brainer/decorated_symbol.rb +15 -0
  24. data/lib/no_brainer/document.rb +18 -0
  25. data/lib/no_brainer/document/association.rb +41 -0
  26. data/lib/no_brainer/document/association/belongs_to.rb +64 -0
  27. data/lib/no_brainer/document/association/core.rb +64 -0
  28. data/lib/no_brainer/document/association/has_many.rb +68 -0
  29. data/lib/no_brainer/document/attributes.rb +124 -0
  30. data/lib/no_brainer/document/core.rb +20 -0
  31. data/lib/no_brainer/document/criteria.rb +62 -0
  32. data/lib/no_brainer/document/dirty.rb +88 -0
  33. data/lib/no_brainer/document/dynamic_attributes.rb +12 -0
  34. data/lib/no_brainer/document/id.rb +49 -0
  35. data/lib/no_brainer/document/index.rb +102 -0
  36. data/lib/no_brainer/document/injection_layer.rb +12 -0
  37. data/lib/no_brainer/document/persistance.rb +124 -0
  38. data/lib/no_brainer/document/polymorphic.rb +43 -0
  39. data/lib/no_brainer/document/serialization.rb +9 -0
  40. data/lib/no_brainer/document/store_in.rb +33 -0
  41. data/lib/no_brainer/document/timestamps.rb +18 -0
  42. data/lib/no_brainer/document/validation.rb +35 -0
  43. data/lib/no_brainer/error.rb +10 -0
  44. data/lib/no_brainer/fork.rb +14 -0
  45. data/lib/no_brainer/index_manager.rb +6 -0
  46. data/lib/no_brainer/loader.rb +5 -0
  47. data/lib/no_brainer/locale/en.yml +4 -0
  48. data/lib/no_brainer/query_runner.rb +37 -0
  49. data/lib/no_brainer/query_runner/connection.rb +17 -0
  50. data/lib/no_brainer/query_runner/database_on_demand.rb +26 -0
  51. data/lib/no_brainer/query_runner/driver.rb +8 -0
  52. data/lib/no_brainer/query_runner/logger.rb +29 -0
  53. data/lib/no_brainer/query_runner/run_options.rb +34 -0
  54. data/lib/no_brainer/query_runner/table_on_demand.rb +44 -0
  55. data/lib/no_brainer/query_runner/write_error.rb +28 -0
  56. data/lib/no_brainer/railtie.rb +36 -0
  57. data/lib/no_brainer/railtie/database.rake +34 -0
  58. data/lib/no_brainer/util.rb +23 -0
  59. data/lib/no_brainer/version.rb +3 -0
  60. data/lib/nobrainer.rb +59 -0
  61. metadata +152 -0
@@ -0,0 +1,71 @@
1
+ module NoBrainer::Criteria::Termination::Cache
2
+ extend ActiveSupport::Concern
3
+
4
+ included { attr_accessor :_with_cache }
5
+
6
+ def with_cache
7
+ chain { |criteria| criteria._with_cache = true }
8
+ end
9
+
10
+ def without_cache
11
+ chain { |criteria| criteria._with_cache = false }
12
+ end
13
+
14
+ def inspect
15
+ msg = super
16
+ msg = "#{msg} \e[1;37m# #{@cache.size} results cached\e[0m" if @cache && with_cache?
17
+ msg
18
+ end
19
+
20
+ def merge!(criteria)
21
+ super
22
+ self._with_cache = criteria._with_cache unless criteria._with_cache.nil?
23
+ self.reload
24
+ self
25
+ end
26
+
27
+ def with_cache?
28
+ @_with_cache.nil? ? NoBrainer::Config.cache_documents : @_with_cache
29
+ end
30
+
31
+ def reload
32
+ @cache = nil
33
+ end
34
+
35
+ def each(options={}, &block)
36
+ return super unless with_cache? && !options[:no_cache] && block
37
+ return @cache.each(&block) if @cache
38
+
39
+ cache = []
40
+ super(options.merge(:no_cache => true)) do |instance|
41
+ block.call(instance)
42
+ cache << instance
43
+ end
44
+ @cache = cache
45
+ self
46
+ end
47
+
48
+ def _override_cache(cache)
49
+ @cache = cache
50
+ end
51
+
52
+ def self.reload_on(*methods)
53
+ methods.each do |method|
54
+ define_method(method) do |*args, &block|
55
+ reload
56
+ super(*args, &block).tap { reload }
57
+ end
58
+ end
59
+ end
60
+
61
+ def self.use_cache_for(*methods)
62
+ methods.each do |method|
63
+ define_method(method) do |*args, &block|
64
+ @cache ? @cache.__send__(method, *args, &block) : super(*args, &block)
65
+ end
66
+ end
67
+ end
68
+
69
+ use_cache_for :first, :last, :count, :empty?, :any?
70
+ reload_on :update_all, :destroy_all, :delete_all
71
+ end
@@ -0,0 +1,19 @@
1
+ module NoBrainer::Criteria::Termination::Count
2
+ extend ActiveSupport::Concern
3
+
4
+ def count
5
+ run(to_rql.count)
6
+ end
7
+
8
+ def empty?
9
+ count == 0
10
+ end
11
+
12
+ def any?
13
+ if block_given?
14
+ to_a.any? { |*args| yield(*args) }
15
+ else
16
+ !empty?
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ module NoBrainer::Criteria::Termination::Delete
2
+ extend ActiveSupport::Concern
3
+
4
+ def delete_all
5
+ run(to_rql.delete)['deleted']
6
+ end
7
+
8
+ def destroy_all
9
+ to_a.each { |doc| doc.destroy }
10
+ end
11
+ end
@@ -0,0 +1,64 @@
1
+ module NoBrainer::Criteria::Termination::EagerLoading
2
+ extend ActiveSupport::Concern
3
+
4
+ included { attr_accessor :_includes }
5
+
6
+ def initialize(options={})
7
+ super
8
+ self._includes = []
9
+ end
10
+
11
+ def includes(*values)
12
+ raise "Please enable caching with NoBrainer::Config.cache_documents = true" unless NoBrainer::Config.cache_documents
13
+ chain { |criteria| criteria._includes = values }
14
+ end
15
+
16
+ def merge!(criteria)
17
+ super
18
+ self._includes = self._includes + criteria._includes
19
+ end
20
+
21
+ def each(options={}, &block)
22
+ return super unless should_eager_load? && !options[:no_eager_loading] && block
23
+
24
+ docs = []
25
+ super(options.merge(:no_eager_loading => true)) { |doc| docs << doc }
26
+ eager_load(docs, self._includes)
27
+ docs.each(&block)
28
+ self
29
+ end
30
+
31
+ private
32
+
33
+ def should_eager_load?
34
+ self._includes.present? && !raw?
35
+ end
36
+
37
+ def get_one(criteria)
38
+ super.tap { |doc| eager_load([doc], self._includes) if should_eager_load? }
39
+ end
40
+
41
+ def eager_load_association(docs, association_name, criteria=nil)
42
+ docs = docs.compact
43
+ return if docs.empty?
44
+ association = docs.first.root_class.association_metadata[association_name.to_sym]
45
+ raise "Unknown association #{association_name}" unless association
46
+ association.eager_load(docs, criteria)
47
+ end
48
+
49
+ def eager_load(docs, includes)
50
+ case includes
51
+ when Hash then includes.each do |k,v|
52
+ if v.is_a?(NoBrainer::Criteria)
53
+ v = v.dup
54
+ nested_includes, v._includes = v._includes, []
55
+ eager_load(eager_load_association(docs, k, v), nested_includes)
56
+ else
57
+ eager_load(eager_load_association(docs, k), v)
58
+ end
59
+ end
60
+ when Array then includes.each { |v| eager_load(docs, v) }
61
+ else eager_load_association(docs, includes)
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,24 @@
1
+ module NoBrainer::Criteria::Termination::Enumerable
2
+ extend ActiveSupport::Concern
3
+
4
+ def each(options={}, &block)
5
+ return enum_for(:each, options) unless block
6
+ self.run.each { |attrs| block.call(instantiate_doc(attrs)) }
7
+ self
8
+ end
9
+
10
+ def to_a
11
+ each.to_a.freeze
12
+ end
13
+
14
+ # TODO test that
15
+ def respond_to?(name, include_private = false)
16
+ super || [].respond_to?(name)
17
+ end
18
+
19
+ # TODO Make something a bit more efficent ?
20
+ def method_missing(name, *args, &block)
21
+ return super unless [].respond_to?(name)
22
+ to_a.__send__(name, *args, &block)
23
+ end
24
+ end
@@ -0,0 +1,25 @@
1
+ module NoBrainer::Criteria::Termination::First
2
+ extend ActiveSupport::Concern
3
+
4
+ def first
5
+ get_one(self)
6
+ end
7
+
8
+ def last
9
+ get_one(self.reverse_order)
10
+ end
11
+
12
+ def first!
13
+ first.tap { |doc| raise NoBrainer::Error::DocumentNotFound unless doc }
14
+ end
15
+
16
+ def last!
17
+ last.tap { |doc| raise NoBrainer::Error::DocumentNotFound unless doc }
18
+ end
19
+
20
+ private
21
+
22
+ def get_one(criteria)
23
+ instantiate_doc(criteria.limit(1).run.first)
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ module NoBrainer::Criteria::Termination::Inc
2
+ extend ActiveSupport::Concern
3
+
4
+ def inc_all(field, value=1)
5
+ # TODO The useful inc() is on a model instance.
6
+ # But then do we want to postpone the inc() to the next save?
7
+ # It might make sense (because we don't have transactions).
8
+ update_all { |doc| { field => doc[field] + value } }
9
+ end
10
+
11
+ def dec_all(field, value=1)
12
+ inc_all(field, -value)
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module NoBrainer::Criteria::Termination::Update
2
+ extend ActiveSupport::Concern
3
+
4
+ def update_all(attrs={}, &block)
5
+ block = proc { attrs } unless block_given?
6
+ run(to_rql.update(&block))['replaced']
7
+ end
8
+
9
+ def replace_all(attrs={}, &block)
10
+ block = proc { attrs } unless block_given?
11
+ run(to_rql.replace(&block))['replaced']
12
+ end
13
+ end
@@ -0,0 +1,41 @@
1
+ class NoBrainer::Database
2
+ attr_accessor :connection
3
+
4
+ # FIXME This class is a bit weird as we don't use it to represent the current
5
+ # database.
6
+
7
+ delegate :database_name, :to => :connection
8
+
9
+ def initialize(connection)
10
+ self.connection = connection
11
+ end
12
+
13
+ def raw
14
+ @raw ||= RethinkDB::RQL.new.db(database_name)
15
+ end
16
+
17
+ def drop!
18
+ # FIXME Sad hack.
19
+ db = (Thread.current[:nobrainer_options] || {})[:db] || database_name
20
+ connection.db_drop(db)['dropped'] == 1
21
+ end
22
+
23
+ # Note that truncating each table (purge) is much faster than dropping the
24
+ # database (drop)
25
+ def purge!(options={})
26
+ table_list.each do |table_name|
27
+ NoBrainer.run { |r| r.table(table_name).delete }
28
+ end
29
+ true
30
+ rescue RuntimeError => e
31
+ raise e unless e.message =~ /No entry with that name/
32
+ end
33
+
34
+ [:table_create, :table_drop, :table_list].each do |cmd|
35
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
36
+ def #{cmd}(*args)
37
+ NoBrainer.run { |r| r.#{cmd}(*args) }
38
+ end
39
+ RUBY
40
+ end
41
+ end
@@ -0,0 +1,15 @@
1
+ class NoBrainer::DecoratedSymbol < Struct.new(:symbol, :modifier, :args)
2
+ MODIFIERS = { :ne => :ne, :not => :ne, :in => :in, :eq => :eq,
3
+ :gt => :gt, :ge => :ge, :gte => :ge,
4
+ :lt => :lt, :le => :le, :lte => :le}
5
+
6
+ def self.hook
7
+ Symbol.class_eval do
8
+ MODIFIERS.each do |modifier_name, modifier|
9
+ define_method modifier_name do |*args|
10
+ NoBrainer::DecoratedSymbol.new(self, modifier, args)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ require 'active_model'
2
+
3
+ module NoBrainer::Document
4
+ extend ActiveSupport::Concern
5
+ extend NoBrainer::Autoload
6
+
7
+ autoload_and_include :Core, :StoreIn, :InjectionLayer, :Attributes, :Persistance, :Dirty,
8
+ :Id, :Association, :Serialization, :Criteria, :Validation,
9
+ :Polymorphic, :Index
10
+
11
+ autoload :DynamicAttributes, :Timestamps
12
+
13
+ included do
14
+ include Timestamps if NoBrainer::Config.auto_include_timestamps
15
+ end
16
+
17
+ singleton_class.delegate :all, :to => Core
18
+ end
@@ -0,0 +1,41 @@
1
+ module NoBrainer::Document::Association
2
+ extend NoBrainer::Autoload
3
+ autoload :Core, :BelongsTo, :HasMany
4
+
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class << self; attr_accessor :association_metadata; end
9
+ self.association_metadata = {}
10
+ end
11
+
12
+ def association(metadata)
13
+ @associations ||= {}
14
+ @associations[metadata] ||= metadata.new(self)
15
+ end
16
+
17
+ module ClassMethods
18
+ def inherited(subclass)
19
+ super
20
+ subclass.association_metadata = self.association_metadata.dup
21
+ end
22
+
23
+ [:belongs_to, :has_many].each do |association|
24
+ define_method(association) do |target, options={}|
25
+ target = target.to_sym
26
+
27
+ if r = self.association_metadata[target]
28
+ r.options.merge!(options)
29
+ else
30
+ metadata_klass = NoBrainer::Document::Association.const_get(association.to_s.camelize).const_get(:Metadata)
31
+ r = metadata_klass.new(self, target, options)
32
+ ([self] + descendants).each do |klass|
33
+ klass.association_metadata[target] = r
34
+ end
35
+ end
36
+ r.hook
37
+ r
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,64 @@
1
+ class NoBrainer::Document::Association::BelongsTo
2
+ include NoBrainer::Document::Association::Core
3
+
4
+ class Metadata
5
+ VALID_BELONGS_TO_OPTIONS = [:foreign_key, :class_name, :index]
6
+ include NoBrainer::Document::Association::Core::Metadata
7
+
8
+ def foreign_key
9
+ # TODO test :foreign_key
10
+ options[:foreign_key].try(:to_sym) || :"#{target_name}_id"
11
+ end
12
+
13
+ def target_klass
14
+ # TODO test :class_name
15
+ (options[:class_name] || target_name.to_s.camelize).constantize
16
+ end
17
+
18
+ def hook
19
+ super
20
+ options.assert_valid_keys(*VALID_BELONGS_TO_OPTIONS)
21
+
22
+ owner_klass.field foreign_key, :index => options[:index]
23
+ delegate("#{foreign_key}=", :assign_foreign_key, :call_super => true)
24
+ add_callback_for(:after_validation)
25
+ end
26
+
27
+ def eager_load(docs, criteria=nil)
28
+ target_criteria = target_klass.all
29
+ target_criteria = target_criteria.merge(criteria) if criteria
30
+ docs_fks = Hash[docs.map { |doc| [doc, doc.read_attribute(foreign_key)] }]
31
+ fks = docs_fks.values.compact.uniq
32
+ fk_targets = Hash[target_criteria.where(:id.in => fks).map { |doc| [doc.id, doc] }]
33
+ docs_fks.each { |doc, fk| doc.association(self)._write(fk_targets[fk]) if fk_targets[fk] }
34
+ fk_targets.values
35
+ end
36
+ end
37
+
38
+ def assign_foreign_key(value)
39
+ @target = nil
40
+ end
41
+
42
+ def read
43
+ return @target if @target && NoBrainer::Config.cache_documents
44
+ if fk = instance.read_attribute(foreign_key)
45
+ @target = target_klass.find(fk)
46
+ end
47
+ end
48
+
49
+ def _write(target)
50
+ @target = target
51
+ end
52
+
53
+ def write(target)
54
+ assert_target_type(target)
55
+ instance.write_attribute(foreign_key, target.try(:id))
56
+ _write(target)
57
+ end
58
+
59
+ def after_validation_callback
60
+ if @target && !@target.persisted?
61
+ raise NoBrainer::Error::AssociationNotSaved.new("#{target_name} must be saved first")
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,64 @@
1
+ module NoBrainer::Document::Association::Core
2
+ extend ActiveSupport::Concern
3
+
4
+ module Metadata
5
+ extend ActiveSupport::Concern
6
+
7
+ attr_accessor :owner_klass, :target_name, :options
8
+
9
+ def initialize(owner_klass, target_name, options={})
10
+ @owner_klass = owner_klass
11
+ @target_name = target_name
12
+ @options = options
13
+ end
14
+
15
+ def association_klass
16
+ @association_klass ||= self.class.name.deconstantize.constantize
17
+ end
18
+
19
+ def new(instance)
20
+ association_klass.new(self, instance)
21
+ end
22
+
23
+ def delegate(method_name, target, options={})
24
+ metadata = self
25
+ owner_klass.inject_in_layer :associations do
26
+ define_method(method_name) do |*args, &block|
27
+ super(*args, &block) if options[:call_super]
28
+ association(metadata).__send__(target, *args, &block)
29
+ end
30
+ end
31
+ end
32
+
33
+ def hook
34
+ delegate("#{target_name}=", :write)
35
+ delegate("#{target_name}", :read)
36
+ end
37
+
38
+ def add_callback_for(what)
39
+ instance_eval <<-RUBY, __FILE__, __LINE__+1
40
+ if !@added_#{what}
41
+ metadata = self
42
+ owner_klass.#{what} { association(metadata).#{what}_callback }
43
+ @added_#{what} = true
44
+ end
45
+ RUBY
46
+ end
47
+ end
48
+
49
+ included { attr_accessor :metadata, :instance }
50
+
51
+ delegate :foreign_key, :target_name, :target_klass, :to => :metadata
52
+
53
+ def initialize(metadata, instance)
54
+ @metadata = metadata
55
+ @instance = instance
56
+ end
57
+
58
+ def assert_target_type(value)
59
+ unless value.is_a?(target_klass) || value.nil?
60
+ msg = "Trying to use a #{value.class} as a #{target_name}"
61
+ raise NoBrainer::Error::InvalidType.new(msg)
62
+ end
63
+ end
64
+ end