#-- # Copyright (c) 2009-2010, John Mettraux, jmettraux@gmail.com # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # # Made in Japan. #++ require 'rufus/tokyo/utils' require 'rufus/tokyo/query' require 'rufus/tokyo/transactions' require 'rufus/tokyo/openable' module Rufus::Edo # # Methods common to the two table classes (table + ntyrant) found in # Rufus::Edo # module TableCore include Rufus::Tokyo::HashMethods include Rufus::Tokyo::Transactions # Add the open() method to all Table type classes. def self.included(target) target.extend(Rufus::Tokyo::Openable) end # Closes the table (and frees the datastructure allocated for it), # raises an exception in case of failure. # def close @db.close || raise_error end # Generates a unique id (in the context of this Table instance) # def generate_unique_id @db.genuid end alias :genuid :generate_unique_id INDEX_TYPES = { :lexical => 0, :decimal => 1, :token => 2, :qgram => 3, :opt => 9998, :optimized => 9998, :void => 9999, :remove => 9999, :keep => 1 << 24 } # Sets an index on a column of the table. # # Types maybe be :lexical or :decimal. # # Recently (TC 1.4.26 and 1.4.27) inverted indexes have been added, # they are :token and :qgram. There is an :opt index as well. # # Sorry couldn't find any good doc about those inverted indexes apart from : # # http://alpha.mixi.co.jp/blog/?p=1147 # http://www.excite-webtl.jp/world/english/web/?wb_url=http%3A%2F%2Falpha.mixi.co.jp%2Fblog%2F%3Fp%3D1147&wb_lp=JAEN&wb_dis=2&wb_submit=+%96%7C+%96%F3+ # # Use :keep to "add" and # :remove (or :void) to "remove" an index. # # If column_name is :pk or "", the index will be set on the primary key. # # Returns true in case of success. # def set_index (column_name, *types) column_name = column_name == :pk ? '' : column_name.to_s i = types.inject(0) { |ii, t| ii | INDEX_TYPES[t] } @db.setindex(column_name, i) || raise_error end # Inserts a record in the table db # # table['pk0'] = [ 'name', 'fred', 'age', '45' ] # table['pk1'] = { 'name' => 'jeff', 'age' => '46' } # # Accepts both a hash or an array (expects the array to be of the # form [ key, value, key, value, ... ] else it will raise # an ArgumentError) # # Raises an error in case of failure. # def []= (pk, h_or_a) pk = pk.to_s m = h_or_a.is_a?(Hash) ? h_or_a : Hash[*h_or_a] m = Rufus::Tokyo.h_or_a_to_s(m) #verify_value(m) @db.put(pk, m) || raise_error end # Removes an entry in the table # # (might raise an error if the delete itself failed, but returns nil # if there was no entry for the given key) # # Raises an error if something went wrong # def delete (k) k = k.to_s val = @db[k] return nil unless val @db.out(k) || raise_error val end # Removes all records in this table database # # Raises an error if something went wrong # def clear @db.vanish || raise_error end # Returns an array of all the primary keys in the table # # With no options given, this method will return all the keys (strings) # in a Ruby array. # # :prefix --> returns only the keys who match a given string prefix # # :limit --> returns a limited number of keys # def keys (options={}) pref = options.fetch(:prefix, "") @db.fwmkeys(pref, options[:limit] || -1) end # Deletes all the entries whose key begin with the given prefix. # def delete_keys_with_prefix (prefix) query_delete { |q| q.add('', :strbw, prefix) } end # Returns a hash { key => record } of all the records matching the # given keys. # def lget (*keys) keys = Rufus::Tokyo::h_or_a_to_s(keys.flatten) if @db.respond_to?(:mget) @db.mget(keys) else keys.inject({}) { |h, k| v = self[k]; h[k] = v if v; h } end end alias :mget :lget # Returns the number of records in this table db # def size @db.rnum end # Prepares a query instance (block is optional) # def prepare_query (&block) q = TableQuery.new(table_query_class, self) block.call(q) if block q end # Prepares and runs a query, returns an array of hashes (all Ruby) # (takes care of freeing the query and the result set structures) # def query (&block) prepare_query(&block).run end # Prepares, runs AND delete all the matching records. # def query_delete (&block) prepare_query(&block).delete end # Prepares, runs AND counts all the matching records. # def query_count (&block) prepare_query { |q| q.pk_only # improve efficiency, since we have to do the query }.count end # Warning : this method is low-level, you probably only need # to use #transaction and a block. # # Direct call for 'transaction begin'. # def tranbegin @db.tranbegin || raise_error end # Warning : this method is low-level, you probably only need # to use #transaction and a block. # # Direct call for 'transaction commit'. # def trancommit @db.trancommit || raise_error end # Warning : this method is low-level, you probably only need # to use #transaction and a block. # # Direct call for 'transaction abort'. # def tranabort @db.tranabort || raise_error end # Returns the underlying 'native' Ruby object (of the class devised by # Hirabayashi-san) # def original @db end # Returns the union of the listed queries # # r = table.union( # @t.prepare_query { |q| # q.add 'lang', :includes, 'es' # }, # @t.prepare_query { |q| # q.add 'lang', :includes, 'li' # } # ) # # will return a hash { primary_key => record } of the values matching # the first query OR the second. # # If the last element element passed to this method is the value 'false', # the return value will the array of matching primary keys. # def union (*queries) search(:union, *queries) end # Returns the intersection of the listed queries # # r = table.intersection( # @t.prepare_query { |q| # q.add 'lang', :includes, 'es' # }, # @t.prepare_query { |q| # q.add 'lang', :includes, 'li' # } # ) # # will return a hash { primary_key => record } of the values matching # the first query AND the second. # # If the last element element passed to this method is the value 'false', # the return value will the array of matching primary keys. # def intersection (*queries) search(:intersection, *queries) end # Returns the difference of the listed queries # # r = table.intersection( # @t.prepare_query { |q| # q.add 'lang', :includes, 'es' # }, # @t.prepare_query { |q| # q.add 'lang', :includes, 'li' # } # ) # # will return a hash { primary_key => record } of the values matching # the first query OR the second but not both. # # If the last element element passed to this method is the value 'false', # the return value will the array of matching primary keys. # def difference (*queries) search(:difference, *queries) end # A #search a la ruby-tokyotyrant # (http://github.com/actsasflinn/ruby-tokyotyrant/tree) # # r = table.search( # :intersection, # @t.prepare_query { |q| # q.add 'lang', :includes, 'es' # }, # @t.prepare_query { |q| # q.add 'lang', :includes, 'li' # } # ) # # Accepts the symbols :union, :intersection, :difference or :diff as # first parameter. # # If the last element element passed to this method is the value 'false', # the return value will the array of matching primary keys. # def search (type, *queries) run_query = true run_query = queries.pop if queries.last == false raise( ArgumentError.new("pass at least one prepared query") ) if queries.size < 1 t = META_TYPES[type] raise( ArgumentError.new("no search type #{type.inspect}") ) unless t q = queries.shift.original qs = queries.collect { |qq| qq.original } pks = q.metasearch(qs, META_TYPES[type]) run_query ? lget(pks) : pks end protected META_TYPES = { :union => 0, :intersection => 1, :difference => 2, :diff => 2 } # Returns the value (as a Ruby Hash) else nil # # (the actual #[] method is provided by HashMethods) # def get (k) @db.get(k.to_s) end # Obviously something went wrong, let's ask the db about it and raise # an EdoError # def raise_error err_code = @db.ecode err_msg = @db.errmsg(err_code) raise EdoError.new("(err #{err_code}) #{err_msg}") end #def verify_value (h) # h.each { |k, v| # next if k.is_a?(String) and v.is_a?(String) # raise ArgumentError.new( # "only String keys and values are accepted " + # "( #{k.inspect} => #{v.inspect} )") # } #end end # # A query on a Tokyo Cabinet table db # class TableQuery include Rufus::Tokyo::QueryConstants # Creates a query for a given Rufus::Tokyo::Table # # Queries are usually created via the #query (#prepare_query #do_query) # of the Table instance. # # Methods of interest here are : # # * #add (or #add_condition) # * #order_by # * #limit # # also # # * #pk_only # * #no_pk # def initialize (query_class, table) @table = table @query = query_class.new(table.original) @opts = {} @has_run = false end # Returns the original, underlying RDBQUERY instance. # def original @query end # Adds a condition # # table.query { |q| # q.add 'name', :equals, 'Oppenheimer' # q.add 'age', :numgt, 35 # } # # Understood 'operators' : # # :streq # string equality # :eq # :eql # :equals # # :strinc # string include # :inc # string include # :includes # string include # # :strbw # string begins with # :bw # :starts_with # :strew # string ends with # :ew # :ends_with # # :strand # string which include all the tokens in the given exp # :and # # :stror # string which include at least one of the tokens # :or # # :stroreq # string which is equal to at least one token # # :strorrx # string which matches the given regex # :regex # :matches # # # numbers... # # :numeq # equal # :numequals # :numgt # greater than # :gt # :numge # greater or equal # :ge # :gte # :numlt # greater or equal # :lt # :numle # greater or equal # :le # :lte # :numbt # a number between two tokens in the given exp # :bt # :between # # :numoreq # number which is equal to at least one token # # :ftsph # full-text phrase search # :ftsphrase # :phrase # :ftsand # full-text AND # :ftsor # full-text OR # :ftsex # full-text with 'compound' expression # def add (colname, operator, val, affirmative=true, no_index=false) colname = colname.to_s val = val.to_s op = operator.is_a?(Fixnum) ? operator : OPERATORS[operator] op = op | TDBQCNEGATE unless affirmative op = op | TDBQCNOIDX if no_index @query.addcond(colname, op, val) end alias :add_condition :add # Sets the max number of records to return for this query. # # (If you're using TC >= 1.4.10 the optional 'offset' (skip) parameter # is accepted) # def limit (i, offset=-1) @query.setlimit(i, offset) end # Sets the sort order for the result of the query # # The 'direction' may be : # # :strasc # string ascending # :strdesc # :asc # string ascending # :desc # :numasc # number ascending # :numdesc # def order_by (colname, direction=:strasc) @query.setorder(colname.to_s, DIRECTIONS[direction]) end # When set to true, only the primary keys of the matching records will # be returned. # def pk_only (on=true) @opts[:pk_only] = on end # When set to true, the :pk (primary key) is not inserted in the record # (hashes) returned # def no_pk (on=true) @opts[:no_pk] = on end # Process each record using the supplied block, which will be passed # two parameters, the primary key and the value hash. # # The block passed to this method accepts two parameters : the [String] # primary key and a Hash of the values for the record. # # The return value of the passed block does matter. Three different # values are expected :stop, :delete or a Hash instance. # # :stop will make the iteration stop, further matching records will not # be passed to the block # # :delete will let Tokyo Cabinet delete the record just seen. # # a Hash is passed to let TC update the values for the record just seen. # # Passing an array is possible : [ :stop, { 'name' => 'Toto' } ] will # update the record just seen to a unique column 'name' and will stop the # iteration. Likewise, returning [ :stop, :delete ] will work as well. # def process (&block) @query.proc() do |key, val| r = block.call(key, val) r = [ r ] unless r.is_a?(Array) if updated_value = r.find { |e| e.is_a?(Hash) } val.merge!(updated_value) end r.inject(0) { |i, v| case v when :stop then i = i | 1 << 24 when :delete then i = i | 2 when Hash then i = i | 1 end i } end self end # Runs this query (returns a TableResultSet instance) # def run @has_run = true @last_resultset = TableResultSet.new(@table, @query.search, @opts) end # Runs this query AND immediately let the matching records get deleted. # def delete @query.searchout || @table.raise_error end # Returns the count of results this query return when last run. # Returns 0 if the query was not yet run. # def count #@query.count # not yet implemented by Hirabayashi-san run.free unless @has_run @last_resultset ? @last_resultset.size : 0 end # Frees this data structure # def free # nothing ... :( I hope there's no memory leak end alias :close :free alias :destroy :free end # # The thing queries return # class TableResultSet include Enumerable def initialize (table, primary_keys, query_opts) @table = table @keys = primary_keys @opts = query_opts end # Returns the count of element in this result set # def size @keys.size end alias :length :size # The classical each # def each @keys.each do |pk| if @opts[:pk_only] yield(pk) else val = @table[pk] val[:pk] = pk unless @opts[:no_pk] yield(val) end end end # Returns an array of hashes # def to_a self.collect { |m| m } end # Frees this query (the underlying Tokyo Cabinet list structure) # def free # nothing to do, kept for similarity with Rufus::Tokyo end alias :close :free alias :destroy :free end end