# `BogoHash` provides a `Hash`-like interface to the database table # corresponding to a {DBStruct} subclass. # # You can think if it as a `Hash` that holds all instances of a # particular {DBStruct} subclass keyed on the objects' `rowid` # attributes. Most of the `Hash` interface is available (including # `Enumerable`) with the exception of `[]=`. `delete` and `delete_if` # work as expected, however. # # You should never explicitly create a `BogoHash`. Instead, use the # class's {DBStruct.items} or {DBStruct.where} methods. # # Since these methods can be used to narrow the range of items, a # `BogoHash` does not necessarily encompass all instances of its # associated `DBStruct` subclass. Rows that do not match the # selection criteria are not available even when explicitly addressed # by row ID. # # Under the hood, a `BogoHash` is a wrapper around a # `Sequel::Dataset`. class DBStruct::BogoHash include Enumerable # @!visibility private # Internal use only! def initialize(dbs_klass, dataset, query) @dbs_klass = dbs_klass @dataset = dataset @query = query.dup.freeze # used for printing only end # Return a human-friendly string describing this object. # # @return [String] def inspect params = "" params = "(#{@query.map(&:inspect).join(", ")})" unless @query.empty? return "#{@dbs_klass}.items#{params}" end alias to_s inspect # Retrieve the item at `rowid` or nil if its not present. def [](rowid) row = @dataset.first(_id: rowid) return nil unless row return @dbs_klass.new(_id: rowid) end # Test if `rowid` is one of the keys in `self`. def has_key?(rowid) = !! self[rowid] alias include? has_key? alias member? has_key? alias key? has_key? # Return the number of items in `self`. def size = @dataset.count alias length size # Test if self has no items def empty? = size() == 0 # Return all keys. # @return [Array[Integer]] - the row IDs def keys = map{|k,v| k} # Return all values. # @return [Array[DBStruct.with(...)]] def values = map{|k,v| v} # Return all keys and values as an array of two-element arrays. # @return [Array[Array[Integer, DBStruct.with(...)]]] def to_a = map{|k,v| [k,v]} # # Deletion # # Delete `rowid` and its associated value from the underlying table # (and `self` by extension). If `rowid` is not present, does # nothing. # # Note that the deleted row object will be returned but can no # longer be used. In particular, it does not hold the contents of # the deleted row. # # @return [DBStruct.with(...)] The deleted value or nil def delete(rowid, &block) transaction { unless has_key?(rowid) return block.value if block return nil end val = self[rowid] @dataset.where(_id: rowid).delete return val } end # Evaluate the block on each key-value pair in `self` end delete # each entry for which the block returns true. # # Like all enumeration operations, the tests and deletions occur in # a single transaction. # # @yield [value] The block to evaluate def delete_if(&block) transaction { self.each{ |k, v| block.call(k,v) and delete(k) } } return self end alias reject! delete_if # Delete all entries def clear @dataset.delete return self end # # Enumeration # # Evaluate `block` over each key-and-value pair (i.e. row and # row-id). If no block is given, returns an Enumerator instead. # # It is safe to perform other database operations inside the block # or enumeration loop. This includes modifications to the database # being enumerated, although whether or not added or removed items # are still visited is undefined. # # If called with a block, the entire operation happens within a # single transaction. # # Since this class imports Enumerable, all of its functionality is # also available. def each(&block) = each_backend(:basic_each, block) alias each_pair each # Like {.each} but only yields the key (i.e. row ID). def each_key(&block) = each_backend(:basic_each_key, block) # Like {.each} but only yields the value. def each_value(&block) = each_backend(:basic_each_value, block) private def row_id_after(prev_rowid) row = @dataset .where{_id > prev_rowid} .limit(1) .select(:_id) .first return row[:_id] if row return nil end # This is slow, but will usually work even if there are database # operations in the block. def basic_each(&block) return if size == 0 curr = -1 while true rowid = row_id_after(curr) return unless rowid block.call(rowid, self[rowid]) curr = rowid end end def basic_each_key(&block) = basic_each{|k,v| block.call(k)} def basic_each_value(&block) = basic_each{|k,v| block.call(v)} # Handle the enumeration methods (each, each_key, each_value) as # expected: if given, call with the block inside a transaction; # otherwise, return an Enumerator for it. def each_backend(each_method, block) return self.to_enum(each_method) unless block transaction { self.send(each_method, &block) } return self end # TODO: unsafe_each via Dataset.each; faster but troublesome if you # modify things within the block. public # # Advanced Querying # # Expose `Sequel::Dataset#where` for more advanced querying. # # The underlying `Sequel::Dataset`'s `where` method is called with # all of the arguments and the result is a new BogoHash containing # only the items that match the query. # # This is the Bigger Hammer you can use to narrow a down to a subset # of your data when groups are not enough. # # @return [BogoHash] def where(*cond, **kwcond, &block) new_dataset = @dataset.where(*cond, **kwcond, &block) desc = ["SQL:#{new_dataset.sql}"] return self.class.new(@dbs_klass, new_dataset, desc) end # # Helpers # # Calls the underling `Sequel::Database#transaction` methods with # all arguments. # # Convenience method. def transaction(*args, **kwargs, &block) = @dbs_klass.transaction(*args, **kwargs, &block) end