nobrainer 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|