schron 0.0.3 → 0.0.4
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 +4 -4
- data/Rakefile +4 -3
- data/lib/schron/archive.rb +17 -0
- data/lib/schron/datastore/mongo.rb +50 -22
- data/lib/schron/db.rb +36 -0
- data/lib/schron/error.rb +9 -1
- data/lib/schron/identity_map.rb +9 -1
- data/lib/schron/identity_map_repository.rb +168 -0
- data/lib/schron/logged_repository.rb +58 -0
- data/lib/schron/query.rb +14 -4
- data/lib/schron/test/entity.rb +3 -2
- data/lib/schron/test/repository_examples.rb +1 -1
- data/schron.gemspec +1 -1
- data/spec/lib/schron/datastore/mongo_spec.rb +18 -1
- data/spec/lib/schron/db_spec.rb +35 -0
- data/spec/lib/schron/identity_map_repository_spec.rb +181 -0
- data/spec/lib/schron/query_spec.rb +22 -0
- metadata +9 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 8f4bf73bf955f3f82baa2bab5bc3acc1b812cd61
         | 
| 4 | 
            +
              data.tar.gz: 334dc42717de4a920f6c698a031829d6113615c3
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 978dfca8ba3c7ea74efd6ad4825fcc34f6cf9641c2b8417185a9242c15e9a68122442c3736711288ccbb5b2fb48ce73364aaf602598ab517d5391d8be5b4dac1
         | 
| 7 | 
            +
              data.tar.gz: 4385936c35b1df5c92c15d00fef37d12641f613766305d78baf319f1a3317618205f7f1d736734f03ab6027bf0f6ff0abba6dd3dd10be757f0b8d10d6d94d0b0
         | 
    
        data/Rakefile
    CHANGED
    
    | @@ -12,8 +12,9 @@ task :bundle do | |
| 12 12 | 
             
              system "cd #{root} && bundle install"
         | 
| 13 13 | 
             
            end
         | 
| 14 14 |  | 
| 15 | 
            -
             | 
| 16 | 
            -
             | 
| 15 | 
            +
            require 'rspec/core/rake_task'
         | 
| 16 | 
            +
            RSpec::Core::RakeTask.new do |t|
         | 
| 17 | 
            +
              t.rspec_opts = '--color'
         | 
| 17 18 | 
             
            end
         | 
| 18 19 |  | 
| 19 | 
            -
            task :default => [:bundle, :spec]
         | 
| 20 | 
            +
            task :default => [:bundle, :spec]
         | 
    
        data/lib/schron/archive.rb
    CHANGED
    
    | @@ -27,11 +27,28 @@ module Schron | |
| 27 27 | 
             
                def query(&block)
         | 
| 28 28 | 
             
                  Query.new(self, &block)
         | 
| 29 29 | 
             
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def exec_query(query)
         | 
| 32 | 
            +
                  raw_results = datastore.exec_query(query)
         | 
| 33 | 
            +
                  load_all(raw_results)
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def exec_count(query)
         | 
| 37 | 
            +
                  datastore.exec_count(query)
         | 
| 38 | 
            +
                end
         | 
| 30 39 |  | 
| 31 40 | 
             
                def all
         | 
| 32 41 | 
             
                  query.all
         | 
| 33 42 | 
             
                end
         | 
| 34 43 |  | 
| 44 | 
            +
                def batches(size, &block)
         | 
| 45 | 
            +
                  query.batches(size, &block)
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
                
         | 
| 48 | 
            +
                def batched_each(size, &block)
         | 
| 49 | 
            +
                  query.batched_each(size, &block)
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
                
         | 
| 35 52 | 
             
                def first
         | 
| 36 53 | 
             
                  query.first
         | 
| 37 54 | 
             
                end
         | 
| @@ -30,43 +30,55 @@ module Schron | |
| 30 30 | 
             
                  end
         | 
| 31 31 |  | 
| 32 32 | 
             
                  def insert(kind, hash)
         | 
| 33 | 
            -
                     | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 33 | 
            +
                    with_write_errors! do
         | 
| 34 | 
            +
                      hash[:id] ||= Schron::Id.generate
         | 
| 35 | 
            +
                      serialized = serialize(kind, hash)
         | 
| 36 | 
            +
                      coll(kind).insert(serialized)
         | 
| 37 | 
            +
                      hash
         | 
| 38 | 
            +
                    end
         | 
| 37 39 | 
             
                  end
         | 
| 38 40 |  | 
| 39 41 | 
             
                  def multi_insert(kind, hashes)
         | 
| 40 | 
            -
                     | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 42 | 
            +
                    with_write_errors! do
         | 
| 43 | 
            +
                      hashes.each { |h| h[:id] ||= Schron::Id.generate }
         | 
| 44 | 
            +
                      docs = hashes.map { |h| serialize(kind, h) }
         | 
| 45 | 
            +
                      coll(kind).insert(docs)
         | 
| 46 | 
            +
                      hashes
         | 
| 47 | 
            +
                    end
         | 
| 44 48 | 
             
                  end
         | 
| 45 49 |  | 
| 46 50 | 
             
                  def update(kind, hash)
         | 
| 47 | 
            -
                     | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            +
                    with_write_errors! do
         | 
| 52 | 
            +
                      Schron::Id.require!(hash)
         | 
| 53 | 
            +
                      doc = serialize(kind, hash)
         | 
| 54 | 
            +
                      coll(kind).update(id_selector(hash[:id]), doc)
         | 
| 55 | 
            +
                      hash
         | 
| 56 | 
            +
                    end
         | 
| 51 57 | 
             
                  end
         | 
| 52 58 |  | 
| 53 59 | 
             
                  def multi_update(kind, hashes)
         | 
| 54 | 
            -
                     | 
| 55 | 
            -
             | 
| 56 | 
            -
                       | 
| 57 | 
            -
             | 
| 60 | 
            +
                    with_write_errors! do
         | 
| 61 | 
            +
                      Schron::Id.require_all!(hashes)
         | 
| 62 | 
            +
                      hashes.each do |hash|
         | 
| 63 | 
            +
                        doc = serialize(kind, hash)
         | 
| 64 | 
            +
                        coll(kind).update(id_selector(hash[:id]), doc)
         | 
| 65 | 
            +
                      end
         | 
| 66 | 
            +
                      hashes
         | 
| 58 67 | 
             
                    end
         | 
| 59 | 
            -
                    hashes
         | 
| 60 68 | 
             
                  end
         | 
| 61 69 |  | 
| 62 70 | 
             
                  def remove(kind, id)
         | 
| 63 | 
            -
                     | 
| 64 | 
            -
             | 
| 71 | 
            +
                    with_write_errors! do
         | 
| 72 | 
            +
                      coll(kind).remove(id_selector(id))
         | 
| 73 | 
            +
                      nil
         | 
| 74 | 
            +
                    end
         | 
| 65 75 | 
             
                  end
         | 
| 66 76 |  | 
| 67 77 | 
             
                  def multi_remove(kind, ids)
         | 
| 68 | 
            -
                     | 
| 69 | 
            -
             | 
| 78 | 
            +
                    with_write_errors! do
         | 
| 79 | 
            +
                      coll(kind).remove(multiple_id_selector(ids))
         | 
| 80 | 
            +
                      nil
         | 
| 81 | 
            +
                    end
         | 
| 70 82 | 
             
                  end
         | 
| 71 83 |  | 
| 72 84 | 
             
                  def exec_query(query)
         | 
| @@ -138,6 +150,22 @@ module Schron | |
| 138 150 | 
             
                    {_id: {"$in" => serialized }}
         | 
| 139 151 | 
             
                  end
         | 
| 140 152 |  | 
| 153 | 
            +
             | 
| 154 | 
            +
                  def with_write_errors!(&block)
         | 
| 155 | 
            +
                    block.call
         | 
| 156 | 
            +
                  rescue ::Mongo::OperationFailure => detail
         | 
| 157 | 
            +
                    if detail.result &&
         | 
| 158 | 
            +
                        detail.result['writeErrors'] &&
         | 
| 159 | 
            +
                        !detail.result['writeErrors'].empty?
         | 
| 160 | 
            +
                      case detail.result['writeErrors'].first['code']
         | 
| 161 | 
            +
                      when 11000
         | 
| 162 | 
            +
                        raise Schron::DuplicateKeyError.new(nil, detail.result)
         | 
| 163 | 
            +
                      end
         | 
| 164 | 
            +
                    end
         | 
| 165 | 
            +
             | 
| 166 | 
            +
                    raise detail
         | 
| 167 | 
            +
                  end
         | 
| 168 | 
            +
             | 
| 141 169 | 
             
                  def selector_for(query)
         | 
| 142 170 | 
             
                    selector = {}
         | 
| 143 171 | 
             
                    query.filters.each do |(field, op, filter_value)|
         | 
| @@ -186,4 +214,4 @@ module Schron | |
| 186 214 |  | 
| 187 215 | 
             
                end
         | 
| 188 216 | 
             
              end
         | 
| 189 | 
            -
            end
         | 
| 217 | 
            +
            end
         | 
    
        data/lib/schron/db.rb
    ADDED
    
    | @@ -0,0 +1,36 @@ | |
| 1 | 
            +
            module Schron
         | 
| 2 | 
            +
              class DB
         | 
| 3 | 
            +
             | 
| 4 | 
            +
                def initialize(repos={})
         | 
| 5 | 
            +
                  @repos = repos.each_with_object({}) do |(k,v), hash|
         | 
| 6 | 
            +
                    hash[k.to_sym] = v
         | 
| 7 | 
            +
                  end
         | 
| 8 | 
            +
                  @repos.each do |name, repo|
         | 
| 9 | 
            +
                    define_singleton_method(name) { self[name] }
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def [](name)
         | 
| 14 | 
            +
                  @repos[name.to_sym]
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def session_begin
         | 
| 18 | 
            +
                  @repos.each do |name, repo|
         | 
| 19 | 
            +
                    repo.session_begin if repo.respond_to?(:session_begin)
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def session_end
         | 
| 24 | 
            +
                  @repos.each do |name, repo|
         | 
| 25 | 
            +
                    repo.session_end if repo.respond_to?(:session_end)
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def session(&block)
         | 
| 30 | 
            +
                  session_begin
         | 
| 31 | 
            +
                  block.call
         | 
| 32 | 
            +
                ensure
         | 
| 33 | 
            +
                  session_end
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
              end
         | 
| 36 | 
            +
            end
         | 
    
        data/lib/schron/error.rb
    CHANGED
    
    
    
        data/lib/schron/identity_map.rb
    CHANGED
    
    
| @@ -0,0 +1,168 @@ | |
| 1 | 
            +
            require 'schron/identity_map'
         | 
| 2 | 
            +
            require 'schron/repository/interface'
         | 
| 3 | 
            +
            require 'schron/repository'
         | 
| 4 | 
            +
            require 'schron/archive/interface'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Schron
         | 
| 7 | 
            +
              module IdentityMapRepository
         | 
| 8 | 
            +
                include Schron::Archive::Interface
         | 
| 9 | 
            +
                include Schron::Repository::Interface
         | 
| 10 | 
            +
                
         | 
| 11 | 
            +
                include Schron::Repository
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def self.included(archive_class)
         | 
| 14 | 
            +
                  archive_class.extend Schron::DSL
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
             | 
| 18 | 
            +
                # def initialize(*args)
         | 
| 19 | 
            +
                #   super(*args)
         | 
| 20 | 
            +
                #   @id_map = nil
         | 
| 21 | 
            +
                # end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            #     def method_missing(method_name, *args, &block)
         | 
| 24 | 
            +
            #       @repo.method(method_name).unbind.bind(self).call(*args, &block)
         | 
| 25 | 
            +
            # #      @repo.__send__(method_name, *args, &block)
         | 
| 26 | 
            +
            #     end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                # def respond_to_missing?(method_name, include_private = false)
         | 
| 29 | 
            +
                #   @repo.respond_to?(method_name, include_private)
         | 
| 30 | 
            +
                # end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def session_begin
         | 
| 33 | 
            +
                  @id_map ||= IdentityMap.new
         | 
| 34 | 
            +
                  @sessions ||= 0
         | 
| 35 | 
            +
                  @sessions += 1
         | 
| 36 | 
            +
                  nil
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                def session_end
         | 
| 40 | 
            +
                  raise 'no session is open' unless @id_map
         | 
| 41 | 
            +
                  @sessions -= 1
         | 
| 42 | 
            +
                  @id_map = nil if @sessions == 0
         | 
| 43 | 
            +
                  nil
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                def session(&block)
         | 
| 47 | 
            +
                  session_begin
         | 
| 48 | 
            +
                  block.call
         | 
| 49 | 
            +
                ensure
         | 
| 50 | 
            +
                  session_end
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                # [:datastore, :kind, :entity_class, :indexed_fields,
         | 
| 54 | 
            +
                #   :identity].each do |repo_method|
         | 
| 55 | 
            +
                #   define_method(repo_method) do |*args, &block|
         | 
| 56 | 
            +
                #     @repo.__send__(repo_method, *args, &block)
         | 
| 57 | 
            +
                #   end
         | 
| 58 | 
            +
                # end
         | 
| 59 | 
            +
                
         | 
| 60 | 
            +
                # def query(&block)
         | 
| 61 | 
            +
                #   Query.new(self, &block)
         | 
| 62 | 
            +
                # end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                def exec_query(query)
         | 
| 65 | 
            +
                  results = super
         | 
| 66 | 
            +
                  results.map do |object|
         | 
| 67 | 
            +
                    if identity_map.has?(object.id)
         | 
| 68 | 
            +
                      id_map_object = identity_map.get(object.id)
         | 
| 69 | 
            +
                      assign_attributes(from: object, to: id_map_object)
         | 
| 70 | 
            +
                      id_map_object
         | 
| 71 | 
            +
                    else
         | 
| 72 | 
            +
                      identity_map.put(object.id, object)
         | 
| 73 | 
            +
                      object
         | 
| 74 | 
            +
                    end
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
                end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                def get(id)
         | 
| 79 | 
            +
                  identity_map.fetch(id) do
         | 
| 80 | 
            +
                    super
         | 
| 81 | 
            +
                    # @repo.get(id)
         | 
| 82 | 
            +
                  end
         | 
| 83 | 
            +
                end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                def multi_get(ids)
         | 
| 86 | 
            +
                  ids = ids.to_a unless ids.kind_of?(Array)
         | 
| 87 | 
            +
                  result = Array.new(ids.size)
         | 
| 88 | 
            +
                  needed_ids = []
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                  ids.each_with_index do |id, index|
         | 
| 91 | 
            +
                    if identity_map.has?(id)
         | 
| 92 | 
            +
                      result[index] = identity_map.get(id)
         | 
| 93 | 
            +
                    else
         | 
| 94 | 
            +
                      needed_ids << id
         | 
| 95 | 
            +
                    end
         | 
| 96 | 
            +
                  end
         | 
| 97 | 
            +
                  
         | 
| 98 | 
            +
                  needed_objects = super(needed_ids)#@repo.multi_get(needed_ids)
         | 
| 99 | 
            +
                  needed_objects.each do |object|
         | 
| 100 | 
            +
                    if object
         | 
| 101 | 
            +
                      identity_map.put(object.id, object)
         | 
| 102 | 
            +
                      result[ids.index(object.id)] = object
         | 
| 103 | 
            +
                    end
         | 
| 104 | 
            +
                  end
         | 
| 105 | 
            +
                  
         | 
| 106 | 
            +
                  result.compact
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                def insert(object)
         | 
| 110 | 
            +
                  result = super
         | 
| 111 | 
            +
                  # result = @repo.insert(object)
         | 
| 112 | 
            +
                  assign_attributes(from: result, to: object)
         | 
| 113 | 
            +
                  identity_map.put(object.id, object)
         | 
| 114 | 
            +
                  object
         | 
| 115 | 
            +
                end
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                def multi_insert(objects)
         | 
| 118 | 
            +
                  # results = @repo.multi_insert(objects)
         | 
| 119 | 
            +
                  results = super
         | 
| 120 | 
            +
                  objects.each_with_index do |o, i|
         | 
| 121 | 
            +
                    assign_attributes(from: results[i], to: o)
         | 
| 122 | 
            +
                    identity_map.put(o.id, o)
         | 
| 123 | 
            +
                  end
         | 
| 124 | 
            +
                  objects
         | 
| 125 | 
            +
                end
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                def update(object)
         | 
| 128 | 
            +
                  super
         | 
| 129 | 
            +
                  # @repo.update(object)
         | 
| 130 | 
            +
                  object
         | 
| 131 | 
            +
                end
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                def multi_update(objects)
         | 
| 134 | 
            +
                  super
         | 
| 135 | 
            +
                  # @repo.multi_update(objects)
         | 
| 136 | 
            +
                  objects
         | 
| 137 | 
            +
                end
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                def remove(id)
         | 
| 140 | 
            +
                  super
         | 
| 141 | 
            +
                  object = identity_map.delete(id)
         | 
| 142 | 
            +
                  object.freeze if object
         | 
| 143 | 
            +
                  nil
         | 
| 144 | 
            +
                end
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                def multi_remove(ids)
         | 
| 147 | 
            +
                  super
         | 
| 148 | 
            +
                  ids.each do |id|
         | 
| 149 | 
            +
                    object = identity_map.delete(id)
         | 
| 150 | 
            +
                    object.freeze if object
         | 
| 151 | 
            +
                  end
         | 
| 152 | 
            +
                  nil
         | 
| 153 | 
            +
                end
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                def identity_map
         | 
| 156 | 
            +
                  @id_map || raise('Must start a session before accessing data from an IdentityMapRepository')
         | 
| 157 | 
            +
                end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                private
         | 
| 160 | 
            +
             | 
| 161 | 
            +
                def assign_attributes(from: from, to: to)
         | 
| 162 | 
            +
                  if to.respond_to?(:attributes=)
         | 
| 163 | 
            +
                    to.attributes = from.attributes
         | 
| 164 | 
            +
                  end
         | 
| 165 | 
            +
                end
         | 
| 166 | 
            +
             | 
| 167 | 
            +
              end
         | 
| 168 | 
            +
            end
         | 
| @@ -0,0 +1,58 @@ | |
| 1 | 
            +
            require 'delegate'
         | 
| 2 | 
            +
            require 'logger'
         | 
| 3 | 
            +
            require 'benchmark'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Schron
         | 
| 6 | 
            +
              class LoggedRepository
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                def initialize(repo,
         | 
| 9 | 
            +
                    logger: nil,
         | 
| 10 | 
            +
                    log_level: :info,
         | 
| 11 | 
            +
                    exclude_regexp: nil,
         | 
| 12 | 
            +
                    include_regexp: nil)
         | 
| 13 | 
            +
                  @repo = repo
         | 
| 14 | 
            +
                  @logger = logger || Logger.new(STDOUT)
         | 
| 15 | 
            +
                  @log_level = log_level
         | 
| 16 | 
            +
                  @exclude = exclude_regexp
         | 
| 17 | 
            +
                  @include = include_regexp
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def method_missing(method_name, *args, &block)
         | 
| 21 | 
            +
                  if @repo.respond_to?(method_name)
         | 
| 22 | 
            +
                    log(method_name, *args) do
         | 
| 23 | 
            +
                      @repo.__send__(method_name, *args, &block)
         | 
| 24 | 
            +
                    end
         | 
| 25 | 
            +
                  else
         | 
| 26 | 
            +
                    super
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def respond_to_missing?(method_name, include_private=false)
         | 
| 31 | 
            +
                  @repo.respond_to?(method_name, include_private)
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                private
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def log(method_name, *args, &block)
         | 
| 37 | 
            +
                  if should_log_method?(method_name)
         | 
| 38 | 
            +
                    label = "#{@repo.class.name}##{method_name}(#{args.inspect})"
         | 
| 39 | 
            +
                    return_val = nil
         | 
| 40 | 
            +
                    time = Benchmark.realtime { return_val = block.call }
         | 
| 41 | 
            +
                    @logger.__send__(@log_level, ":repo: finished #{label} (#{time}s)")
         | 
| 42 | 
            +
                    return_val
         | 
| 43 | 
            +
                  else
         | 
| 44 | 
            +
                    block.call
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                def should_log_method?(method_name)
         | 
| 49 | 
            +
                  if @exclude && method_name =~ @exclude
         | 
| 50 | 
            +
                    false
         | 
| 51 | 
            +
                  elsif @include && method_name !~ @include
         | 
| 52 | 
            +
                    false
         | 
| 53 | 
            +
                  else
         | 
| 54 | 
            +
                    true
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
              end
         | 
| 58 | 
            +
            end
         | 
    
        data/lib/schron/query.rb
    CHANGED
    
    | @@ -71,9 +71,7 @@ module Schron | |
| 71 71 | 
             
                ## Query Terminators
         | 
| 72 72 |  | 
| 73 73 | 
             
                def all
         | 
| 74 | 
            -
                   | 
| 75 | 
            -
                  loaded_results = archive.load_all(raw_results)
         | 
| 76 | 
            -
                  with_pagination(loaded_results)
         | 
| 74 | 
            +
                  with_pagination(archive.exec_query(self))
         | 
| 77 75 | 
             
                end
         | 
| 78 76 |  | 
| 79 77 | 
             
                def first
         | 
| @@ -81,13 +79,25 @@ module Schron | |
| 81 79 | 
             
                end
         | 
| 82 80 |  | 
| 83 81 | 
             
                def count
         | 
| 84 | 
            -
                   | 
| 82 | 
            +
                  archive.exec_count(self)
         | 
| 85 83 | 
             
                end
         | 
| 86 84 |  | 
| 87 85 | 
             
                def exists?
         | 
| 88 86 | 
             
                  count > 0
         | 
| 89 87 | 
             
                end
         | 
| 90 88 |  | 
| 89 | 
            +
                def batched_each(size, &block)
         | 
| 90 | 
            +
                  batches(size) { |group| group.each(&block) }
         | 
| 91 | 
            +
                end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                def batches(size, &block)
         | 
| 94 | 
            +
                  current_page = 1
         | 
| 95 | 
            +
                  begin
         | 
| 96 | 
            +
                    results = page(current_page, per_page: size).all
         | 
| 97 | 
            +
                    block.call(results)
         | 
| 98 | 
            +
                    current_page = results.paging.next_page
         | 
| 99 | 
            +
                  end while current_page
         | 
| 100 | 
            +
                end
         | 
| 91 101 |  | 
| 92 102 | 
             
                protected
         | 
| 93 103 |  | 
    
        data/lib/schron/test/entity.rb
    CHANGED
    
    
    
        data/schron.gemspec
    CHANGED
    
    | @@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) | |
| 4 4 |  | 
| 5 5 | 
             
            Gem::Specification.new do |spec|
         | 
| 6 6 | 
             
              spec.name          = "schron"
         | 
| 7 | 
            -
              spec.version       = "0.0. | 
| 7 | 
            +
              spec.version       = "0.0.4"
         | 
| 8 8 | 
             
              spec.authors       = ["David Faber"]
         | 
| 9 9 | 
             
              spec.email         = ["david@1bios.co"]
         | 
| 10 10 | 
             
              spec.summary       = %q{Repository implementation for entity persistence}
         | 
| @@ -20,4 +20,21 @@ describe Schron::Datastore::Mongo do | |
| 20 20 |  | 
| 21 21 | 
             
              include_examples 'schron datastore'
         | 
| 22 22 |  | 
| 23 | 
            -
             | 
| 23 | 
            +
              describe 'error handling' do
         | 
| 24 | 
            +
                context 'duplicate key error' do
         | 
| 25 | 
            +
                  let(:coll_name) { 'books' }
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  before(:each) do
         | 
| 28 | 
            +
                    db[coll_name].create_index(:title, unique: true)
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  it 'raises a Schron::DuplicateKeyError' do
         | 
| 32 | 
            +
                    ds.insert('books', title: 'Hyperion')
         | 
| 33 | 
            +
                    expect do
         | 
| 34 | 
            +
                      ds.insert('books', title: 'Hyperion')
         | 
| 35 | 
            +
                    end.to raise_error(Schron::DuplicateKeyError)
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
              end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
            end
         | 
| @@ -0,0 +1,35 @@ | |
| 1 | 
            +
            require 'spec_helper'
         | 
| 2 | 
            +
            require 'schron/db'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            describe Schron::DB do
         | 
| 5 | 
            +
              subject(:db) do
         | 
| 6 | 
            +
                described_class.new(
         | 
| 7 | 
            +
                  users: id_map_repo,
         | 
| 8 | 
            +
                  foos: non_id_map_repo)
         | 
| 9 | 
            +
              end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              let(:id_map_repo) { double }
         | 
| 12 | 
            +
             | 
| 13 | 
            +
              let(:non_id_map_repo) { double }
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              it 'defines accessors for each repository' do
         | 
| 16 | 
            +
                expect(db.users).to be(id_map_repo)
         | 
| 17 | 
            +
                expect(db.foos).to be(non_id_map_repo)
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              it 'access repos by name as a symbol with []' do
         | 
| 21 | 
            +
                expect(db[:users]).to eq(id_map_repo)
         | 
| 22 | 
            +
                expect(db[:foos]).to eq(non_id_map_repo)
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
              it 'access repos by name as string with []' do
         | 
| 26 | 
            +
                expect(db['users']).to eq(id_map_repo)
         | 
| 27 | 
            +
                expect(db['foos']).to eq(non_id_map_repo)    
         | 
| 28 | 
            +
              end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
              it 'opens sessions on all repos at once' do
         | 
| 31 | 
            +
                expect(id_map_repo).to receive(:session_begin)
         | 
| 32 | 
            +
                expect(id_map_repo).to receive(:session_end)
         | 
| 33 | 
            +
                db.session {}
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
            end
         | 
| @@ -0,0 +1,181 @@ | |
| 1 | 
            +
            require 'spec_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'schron/repository'
         | 
| 4 | 
            +
            require 'schron/datastore/memory'
         | 
| 5 | 
            +
            require 'schron/identity_map_repository'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            describe Schron::IdentityMapRepository do
         | 
| 8 | 
            +
              include Schron::Test
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              subject(:repo) { repo_class.new(ds) }
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              let(:archive) { repo }
         | 
| 13 | 
            +
              
         | 
| 14 | 
            +
              let(:repo_class) do
         | 
| 15 | 
            +
                Class.new do
         | 
| 16 | 
            +
                  include Schron::IdentityMapRepository
         | 
| 17 | 
            +
                  entity_class Schron::Test::Entity
         | 
| 18 | 
            +
                  kind 'test'
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
              end
         | 
| 21 | 
            +
              # let(:underlying_repo) do
         | 
| 22 | 
            +
              #   underlying_repo_class.new(ds,
         | 
| 23 | 
            +
              #     entity_class: Schron::Test::Entity,
         | 
| 24 | 
            +
              #     kind: 'test')
         | 
| 25 | 
            +
              # end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
              let(:ds) { Schron::Datastore::Memory.new }
         | 
| 28 | 
            +
             | 
| 29 | 
            +
              describe 'session' do
         | 
| 30 | 
            +
                it 'creates an identity map' do
         | 
| 31 | 
            +
                  expect { repo.identity_map }.to raise_error
         | 
| 32 | 
            +
                  repo.session do
         | 
| 33 | 
            +
                    expect(repo.identity_map).not_to be_nil
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                it 'creates a new one for each session' do
         | 
| 38 | 
            +
                  id_map = nil
         | 
| 39 | 
            +
                  repo.session { id_map = repo.identity_map }
         | 
| 40 | 
            +
                  repo.session do
         | 
| 41 | 
            +
                    expect(id_map).not_to be(repo.identity_map)
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                it 'uses the same identity map if a session has already started' do
         | 
| 46 | 
            +
                  repo.session_begin
         | 
| 47 | 
            +
                  id_map = repo.identity_map
         | 
| 48 | 
            +
                  repo.session_begin
         | 
| 49 | 
            +
                  expect(id_map).to be(repo.identity_map)
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                it 'closes sessions after an equal number of begin and end calls have been made' do
         | 
| 53 | 
            +
                  repo.session_begin
         | 
| 54 | 
            +
                  repo.session_begin
         | 
| 55 | 
            +
                  repo.session_end
         | 
| 56 | 
            +
                  expect(repo.identity_map).not_to be_nil
         | 
| 57 | 
            +
                  repo.session_end
         | 
| 58 | 
            +
                  expect{ repo.identity_map }.to raise_error
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
              end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
              describe 'shared examples' do
         | 
| 63 | 
            +
                around(:each) do |example|
         | 
| 64 | 
            +
                  repo.session { example.run }
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
                
         | 
| 67 | 
            +
                include_examples 'schron archive'
         | 
| 68 | 
            +
                include_examples 'schron repository'
         | 
| 69 | 
            +
              end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
              context 'within a session' do
         | 
| 72 | 
            +
                around(:each) do |example|
         | 
| 73 | 
            +
                  repo.session { example.run }
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                let(:object) { repo.insert(repo.entity_class.new) }
         | 
| 77 | 
            +
                let(:object2) { repo.insert(repo.entity_class.new) }
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                it 'returns the same object after insertion' do
         | 
| 80 | 
            +
                  obj = repo.entity_class.new
         | 
| 81 | 
            +
                  inserted = repo.insert(obj)
         | 
| 82 | 
            +
                  expect(inserted).to be(obj)
         | 
| 83 | 
            +
                end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                it 'returns the same objects after multi insertion' do
         | 
| 86 | 
            +
                  obj = repo.entity_class.new
         | 
| 87 | 
            +
                  inserted = repo.multi_insert([obj]).first
         | 
| 88 | 
            +
                  expect(obj).to be(inserted)
         | 
| 89 | 
            +
                end
         | 
| 90 | 
            +
                
         | 
| 91 | 
            +
                it 'returns one object ref when retreived with get' do
         | 
| 92 | 
            +
                  loaded = repo.get(object.id)
         | 
| 93 | 
            +
                  expect(object).to be(loaded)
         | 
| 94 | 
            +
                end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                it 'returns the same object ref when retrieved by multi get' do
         | 
| 97 | 
            +
                  loaded = repo.multi_get([object.id]).first
         | 
| 98 | 
            +
                  expect(object).to be(loaded)
         | 
| 99 | 
            +
                end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                it 'can multi_get a set of ids' do
         | 
| 102 | 
            +
                  object
         | 
| 103 | 
            +
                  object2
         | 
| 104 | 
            +
                  repo.identity_map.clear
         | 
| 105 | 
            +
                  result = repo.multi_get(Set.new([object.id, object2.id]))
         | 
| 106 | 
            +
                  expect(result.map(&:id)).to include(object.id, object2.id)
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                it 'syncs updates' do
         | 
| 110 | 
            +
                  loaded = repo.get(object.id)
         | 
| 111 | 
            +
                  loaded.attributes[:thing] = 'thing'
         | 
| 112 | 
            +
                  updated = repo.update(loaded)
         | 
| 113 | 
            +
                  expect(updated).to be(object)
         | 
| 114 | 
            +
                  expect(object.attributes[:thing]).to eq('thing')
         | 
| 115 | 
            +
                  reloaded = repo.get(object.id)
         | 
| 116 | 
            +
                  expect(reloaded.attributes[:thing]).to eq('thing')
         | 
| 117 | 
            +
                  expect(reloaded).to be(object)
         | 
| 118 | 
            +
                end
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                it 'syncs multi updates' do
         | 
| 121 | 
            +
                  l1 = repo.get(object.id)
         | 
| 122 | 
            +
                  l2 = repo.get(object2.id)
         | 
| 123 | 
            +
                  l1.attributes[:a] = 'b'
         | 
| 124 | 
            +
                  l2.attributes[:b] = 'c'
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                  results = repo.multi_update([l1, l2])
         | 
| 127 | 
            +
                  expect(results[0]).to be(object)
         | 
| 128 | 
            +
                  expect(results[1]).to be(object2)
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                  expect(object.attributes[:a]).to eq('b')
         | 
| 131 | 
            +
                  expect(object2.attributes[:b]).to eq('c')
         | 
| 132 | 
            +
                end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                it 'freezes removed objects' do
         | 
| 135 | 
            +
                  loaded = repo.get(object.id)
         | 
| 136 | 
            +
                  repo.remove(loaded.id)
         | 
| 137 | 
            +
                  expect(object).to be_frozen
         | 
| 138 | 
            +
                end
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                it 'freezes multi-removed objects' do
         | 
| 141 | 
            +
                  loaded = repo.get(object.id)
         | 
| 142 | 
            +
                  repo.multi_remove([loaded.id])
         | 
| 143 | 
            +
                  expect(object).to be_frozen
         | 
| 144 | 
            +
                end
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                # it 'calls custom repo methods on the original repo' do
         | 
| 147 | 
            +
                #   repo.define_singleton_method(:my_query) do |a1, &block|
         | 
| 148 | 
            +
                #     block.call(a1)
         | 
| 149 | 
            +
                #   end
         | 
| 150 | 
            +
                #   results = []
         | 
| 151 | 
            +
                #   repo.my_query('testing') do |val|
         | 
| 152 | 
            +
                #     results << val
         | 
| 153 | 
            +
                #   end
         | 
| 154 | 
            +
                #   expect(results).to eq(['testing'])
         | 
| 155 | 
            +
                # end
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                # it 'responds_to? methods on the original repo' do
         | 
| 158 | 
            +
                #   underlying_repo.define_singleton_method(:custom_thing) {}
         | 
| 159 | 
            +
                #   expect(repo).to respond_to(:custom_thing)
         | 
| 160 | 
            +
                # end
         | 
| 161 | 
            +
             | 
| 162 | 
            +
              end
         | 
| 163 | 
            +
             | 
| 164 | 
            +
              describe 'queries' do
         | 
| 165 | 
            +
                it 'puts query results into the identiy map' do
         | 
| 166 | 
            +
                  repo.session { repo.insert(repo.entity_class.new) }
         | 
| 167 | 
            +
                  repo.session do
         | 
| 168 | 
            +
                    obj = repo.query.all.first
         | 
| 169 | 
            +
                    expect(obj).to be(repo.get(obj.id))
         | 
| 170 | 
            +
                  end
         | 
| 171 | 
            +
                end
         | 
| 172 | 
            +
             | 
| 173 | 
            +
                it 'pulls query results from the identity map' do
         | 
| 174 | 
            +
                  repo.session do
         | 
| 175 | 
            +
                    obj = repo.insert(repo.entity_class.new)
         | 
| 176 | 
            +
                    expect(obj).to be(repo.query.first)
         | 
| 177 | 
            +
                  end
         | 
| 178 | 
            +
                end
         | 
| 179 | 
            +
                
         | 
| 180 | 
            +
              end
         | 
| 181 | 
            +
            end
         | 
| @@ -62,4 +62,26 @@ describe Schron::Query do | |
| 62 62 | 
             
                  expect(query.first).to eq('loaded')
         | 
| 63 63 | 
             
                end
         | 
| 64 64 | 
             
              end
         | 
| 65 | 
            +
              
         | 
| 66 | 
            +
              describe 'batches' do
         | 
| 67 | 
            +
                it 'iterates over all results in groups, only loading the specified amount into memory at a time' do
         | 
| 68 | 
            +
                  expect(ds).to receive(:exec_query).at_least(2).and_call_original
         | 
| 69 | 
            +
                  2.times { archive.insert(Schron::GenericEntity.new) }
         | 
| 70 | 
            +
                  query.batches(1) do |group|
         | 
| 71 | 
            +
                    expect(group.size).to eq(1)
         | 
| 72 | 
            +
                  end
         | 
| 73 | 
            +
                end
         | 
| 74 | 
            +
              end
         | 
| 75 | 
            +
              
         | 
| 76 | 
            +
              describe 'batched each' do
         | 
| 77 | 
            +
                it 'iterates over each result, only loading the specified amount into memory at a time' do
         | 
| 78 | 
            +
                  expect(ds).to receive(:exec_query).at_least(2).and_call_original
         | 
| 79 | 
            +
                  2.times { archive.insert(Schron::GenericEntity.new) }
         | 
| 80 | 
            +
                  i = 0
         | 
| 81 | 
            +
                  query.batches(1) do |item|
         | 
| 82 | 
            +
                    i += 1
         | 
| 83 | 
            +
                  end
         | 
| 84 | 
            +
                  expect(i).to eq(2)
         | 
| 85 | 
            +
                end
         | 
| 86 | 
            +
              end
         | 
| 65 87 | 
             
            end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: schron
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.0. | 
| 4 | 
            +
              version: 0.0.4
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - David Faber
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2014- | 
| 11 | 
            +
            date: 2014-08-22 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: rake
         | 
| @@ -128,12 +128,15 @@ files: | |
| 128 128 | 
             
            - lib/schron/datastore/mongo/serializer.rb
         | 
| 129 129 | 
             
            - lib/schron/datastore/sequel.rb
         | 
| 130 130 | 
             
            - lib/schron/datastore/serializer.rb
         | 
| 131 | 
            +
            - lib/schron/db.rb
         | 
| 131 132 | 
             
            - lib/schron/dsl.rb
         | 
| 132 133 | 
             
            - lib/schron/error.rb
         | 
| 133 134 | 
             
            - lib/schron/generic_archive.rb
         | 
| 134 135 | 
             
            - lib/schron/generic_entity.rb
         | 
| 135 136 | 
             
            - lib/schron/id.rb
         | 
| 136 137 | 
             
            - lib/schron/identity_map.rb
         | 
| 138 | 
            +
            - lib/schron/identity_map_repository.rb
         | 
| 139 | 
            +
            - lib/schron/logged_repository.rb
         | 
| 137 140 | 
             
            - lib/schron/paginated_results.rb
         | 
| 138 141 | 
             
            - lib/schron/paging.rb
         | 
| 139 142 | 
             
            - lib/schron/query.rb
         | 
| @@ -150,7 +153,9 @@ files: | |
| 150 153 | 
             
            - spec/lib/schron/datastore/memory_spec.rb
         | 
| 151 154 | 
             
            - spec/lib/schron/datastore/mongo_spec.rb
         | 
| 152 155 | 
             
            - spec/lib/schron/datastore/sequel_spec.rb
         | 
| 156 | 
            +
            - spec/lib/schron/db_spec.rb
         | 
| 153 157 | 
             
            - spec/lib/schron/dsl_spec.rb
         | 
| 158 | 
            +
            - spec/lib/schron/identity_map_repository_spec.rb
         | 
| 154 159 | 
             
            - spec/lib/schron/identity_map_spec.rb
         | 
| 155 160 | 
             
            - spec/lib/schron/paginaged_results_spec.rb
         | 
| 156 161 | 
             
            - spec/lib/schron/query_spec.rb
         | 
| @@ -185,7 +190,9 @@ test_files: | |
| 185 190 | 
             
            - spec/lib/schron/datastore/memory_spec.rb
         | 
| 186 191 | 
             
            - spec/lib/schron/datastore/mongo_spec.rb
         | 
| 187 192 | 
             
            - spec/lib/schron/datastore/sequel_spec.rb
         | 
| 193 | 
            +
            - spec/lib/schron/db_spec.rb
         | 
| 188 194 | 
             
            - spec/lib/schron/dsl_spec.rb
         | 
| 195 | 
            +
            - spec/lib/schron/identity_map_repository_spec.rb
         | 
| 189 196 | 
             
            - spec/lib/schron/identity_map_spec.rb
         | 
| 190 197 | 
             
            - spec/lib/schron/paginaged_results_spec.rb
         | 
| 191 198 | 
             
            - spec/lib/schron/query_spec.rb
         |