lib/gitmodel/persistable.rb in gitmodel-0.0.2 vs lib/gitmodel/persistable.rb in gitmodel-0.0.3

- old
+ new

@@ -11,10 +11,13 @@ include ActiveModel::Observing include ActiveModel::Translation define_model_callbacks :initialize, :find, :touch, :only => :after define_model_callbacks :save, :create, :update, :destroy + + cattr_accessor :index, true + self.index = GitModel::Index.new(self) end base.extend(ClassMethods) end @@ -82,35 +85,36 @@ # OR: # :branch # :commit_message # Returns false if validations failed, otherwise returns the SHA of the commit def save(options = {}) - raise GitModel::NullId unless self.id + _run_save_callbacks do + raise GitModel::NullId unless self.id - if new_record? - raise GitModel::RecordExists if self.class.exists?(self.id) - else - raise GitModel::RecordDoesntExist unless self.class.exists?(self.id) - end + if new_record? + raise GitModel::RecordExists if self.class.exists?(self.id) + else + raise GitModel::RecordDoesntExist unless self.class.exists?(self.id) + end - dir = File.join(self.class.db_subdir, self.id) + GitModel.logger.debug "Saving #{self.class.name} with id: #{id}" - transaction = options.delete(:transaction) || GitModel::Transaction.new(options) - result = transaction.execute do |t| - # Write the attributes to the attributes file - # NOTE: using the redundant attributes.to_hash to work around a bug in - # active_support 3.0.4, remove when - # JSON.generate(HashWithIndifferentAccess.new) no longer fails. - t.index.add(File.join(dir, 'attributes.json'), JSON.pretty_generate(attributes.to_hash)) + dir = File.join(self.class.db_subdir, self.id) - # Write the blob files - blobs.each do |name, data| - t.index.add(File.join(dir, name), data) + transaction = options.delete(:transaction) || GitModel::Transaction.new(options) + result = transaction.execute do |t| + # Write the attributes to the attributes file + t.index.add(File.join(dir, 'attributes.json'), Yajl::Encoder.encode(attributes, nil, :pretty => true)) + + # Write the blob files + blobs.each do |name, data| + t.index.add(File.join(dir, name), data) + end end - end - return result + result + end end # Same as #save but raises an exception on error def save!(options = {}) save(options) || raise(GitModel::RecordNotSaved) @@ -137,15 +141,17 @@ raise GitModel::RecordNotFound if GitModel.current_tree.nil? self.id = File.basename(dir) @new_record = false + GitModel.logger.debug "Loading #{self.class.name} with id: #{id}" + # load the attributes object = GitModel.current_tree / File.join(dir, 'attributes.json') raise GitModel::RecordNotFound if object.nil? - self.attributes = JSON.parse(object.data, :max_nesting => false) + self.attributes = Yajl::Parser.parse(object.data) # load all other non-hidden files in the dir as blobs blobs = (GitModel.current_tree / dir).blobs.reject{|b| b.name[0] == '.' || b.name == 'attributes.json'} blobs.each do |b| self.blobs[b.name] = b.data @@ -182,29 +188,96 @@ o.send :load, dir return o end def exists?(id) + GitModel.logger.debug "Checking existence of #{name} with id: #{id}" GitModel.repo.commits.any? && !(GitModel.current_tree / File.join(db_subdir, id, 'attributes.json')).nil? end def find_all(conditions = {}) + # TODO Refactor this spaghetti GitModel.logger.debug "Finding all #{name.pluralize} with conditions: #{conditions.inspect}" - results = [] - return results unless GitModel.current_tree - dirs = (GitModel.current_tree / db_subdir).trees - dirs.each do |dir| - if dir.blobs.any? - o = new - o.send :load, File.join(db_subdir, dir.name) - results << o + return [] unless GitModel.current_tree + + order = conditions.delete(:order) || :asc + order_by = conditions.delete(:order_by) || :id + limit = conditions.delete(:limit) + + matching_ids = [] + if conditions.empty? # load all objects + trees = (GitModel.current_tree / db_subdir).trees + trees.each do |t| + matching_ids << t.name if t.blobs.any? end + else # only load objects that match conditions + matching_ids_for_condition = {} + conditions.each do |k,v| + matching_ids_for_condition[k] = [] + if k == :id # id isn't indexed + if v.is_a?(Proc) + trees = (GitModel.current_tree / db_subdir).trees + trees.each do |t| + matching_ids_for_condition[k] << t.name if t.blobs.any? && v.call(t.name) + end + else + # an unlikely use case but supporting it for completeness + matching_ids_for_condition[k] << v if (GitModel.current_tree / db_subdir / v) + end + else + raise GitModel::IndexRequired unless index.generated? + attr_index = index.attr_index(k) + if v.is_a?(Proc) + attr_index.each do |value, ids| + matching_ids_for_condition[k] += ids.to_a if v.call(value) + end + else + matching_ids_for_condition[k] += attr_index[v].to_a + end + end + end + matching_ids += matching_ids_for_condition.values.inject{|memo, obj| memo & obj} end + results = nil + if order_by != :id + GitModel.logger.warn "Ordering by an attribute other than id requires loading all matching objects before applying limit, this will be slow" if limit + results = matching_ids.map{|k| find(k)} + + if order == :asc + results = results.sort{|a,b| a.send(order_by) <=> b.send(order_by)} + elsif order == :desc + results = results.sort{|b,a| a.send(order_by) <=> b.send(order_by)} + else + raise GitModel::InvalidParams("invalid order: '#{order}'") + end + + if limit + results = results[0, limit] + end + else + if limit + matching_ids = matching_ids[0, limit] + end + if order == :asc + matching_ids = matching_ids.sort{|a,b| a <=> b} + elsif order == :desc + matching_ids = matching_ids.sort{|b,a| a <=> b} + else + raise GitModel::InvalidParams("invalid order: '#{order}'") + end + results = matching_ids.map{|k| find(k)} + end + return results end + def all_values_for_attr(attr) + attr_index = index.attr_index(attr.to_s) + values = attr_index ? attr_index.keys : [] + end + def create(args) if args.is_a?(Array) args.map{|arg| create(arg)} else o = self.new(args) @@ -222,21 +295,28 @@ end return o end def delete(id, options = {}) + GitModel.logger.debug "Deleting #{name} with id: #{id}" path = File.join(db_subdir, id) transaction = options.delete(:transaction) || GitModel::Transaction.new(options) result = transaction.execute do |t| delete_tree(path, t.index, options) end end def delete_all(options = {}) + GitModel.logger.debug "Deleting all #{name.pluralize}" transaction = options.delete(:transaction) || GitModel::Transaction.new(options) result = transaction.execute do |t| delete_tree(db_subdir, t.index, options) end + end + + def index! + index.generate! + index.save end private