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.
- checksums.yaml +7 -0
- data/LICENSE.md +7 -0
- data/README.md +6 -0
- data/lib/no_brainer/autoload.rb +14 -0
- data/lib/no_brainer/config.rb +61 -0
- data/lib/no_brainer/connection.rb +53 -0
- data/lib/no_brainer/criteria.rb +20 -0
- data/lib/no_brainer/criteria/chainable/core.rb +67 -0
- data/lib/no_brainer/criteria/chainable/limit.rb +31 -0
- data/lib/no_brainer/criteria/chainable/order_by.rb +76 -0
- data/lib/no_brainer/criteria/chainable/raw.rb +25 -0
- data/lib/no_brainer/criteria/chainable/scope.rb +41 -0
- data/lib/no_brainer/criteria/chainable/where.rb +198 -0
- data/lib/no_brainer/criteria/termination/cache.rb +71 -0
- data/lib/no_brainer/criteria/termination/count.rb +19 -0
- data/lib/no_brainer/criteria/termination/delete.rb +11 -0
- data/lib/no_brainer/criteria/termination/eager_loading.rb +64 -0
- data/lib/no_brainer/criteria/termination/enumerable.rb +24 -0
- data/lib/no_brainer/criteria/termination/first.rb +25 -0
- data/lib/no_brainer/criteria/termination/inc.rb +14 -0
- data/lib/no_brainer/criteria/termination/update.rb +13 -0
- data/lib/no_brainer/database.rb +41 -0
- data/lib/no_brainer/decorated_symbol.rb +15 -0
- data/lib/no_brainer/document.rb +18 -0
- data/lib/no_brainer/document/association.rb +41 -0
- data/lib/no_brainer/document/association/belongs_to.rb +64 -0
- data/lib/no_brainer/document/association/core.rb +64 -0
- data/lib/no_brainer/document/association/has_many.rb +68 -0
- data/lib/no_brainer/document/attributes.rb +124 -0
- data/lib/no_brainer/document/core.rb +20 -0
- data/lib/no_brainer/document/criteria.rb +62 -0
- data/lib/no_brainer/document/dirty.rb +88 -0
- data/lib/no_brainer/document/dynamic_attributes.rb +12 -0
- data/lib/no_brainer/document/id.rb +49 -0
- data/lib/no_brainer/document/index.rb +102 -0
- data/lib/no_brainer/document/injection_layer.rb +12 -0
- data/lib/no_brainer/document/persistance.rb +124 -0
- data/lib/no_brainer/document/polymorphic.rb +43 -0
- data/lib/no_brainer/document/serialization.rb +9 -0
- data/lib/no_brainer/document/store_in.rb +33 -0
- data/lib/no_brainer/document/timestamps.rb +18 -0
- data/lib/no_brainer/document/validation.rb +35 -0
- data/lib/no_brainer/error.rb +10 -0
- data/lib/no_brainer/fork.rb +14 -0
- data/lib/no_brainer/index_manager.rb +6 -0
- data/lib/no_brainer/loader.rb +5 -0
- data/lib/no_brainer/locale/en.yml +4 -0
- data/lib/no_brainer/query_runner.rb +37 -0
- data/lib/no_brainer/query_runner/connection.rb +17 -0
- data/lib/no_brainer/query_runner/database_on_demand.rb +26 -0
- data/lib/no_brainer/query_runner/driver.rb +8 -0
- data/lib/no_brainer/query_runner/logger.rb +29 -0
- data/lib/no_brainer/query_runner/run_options.rb +34 -0
- data/lib/no_brainer/query_runner/table_on_demand.rb +44 -0
- data/lib/no_brainer/query_runner/write_error.rb +28 -0
- data/lib/no_brainer/railtie.rb +36 -0
- data/lib/no_brainer/railtie/database.rake +34 -0
- data/lib/no_brainer/util.rb +23 -0
- data/lib/no_brainer/version.rb +3 -0
- data/lib/nobrainer.rb +59 -0
- 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,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
|