groovy 0.2.9 → 0.4.3
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/example/basic.rb +3 -2
- data/example/relations.rb +20 -4
- data/lib/groovy.rb +12 -4
- data/lib/groovy/model.rb +187 -75
- data/lib/groovy/query.rb +64 -34
- data/lib/groovy/schema.rb +36 -7
- data/lib/groovy/vector.rb +26 -5
- data/lib/groovy/version.rb +1 -1
- data/spec/model_spec.rb +9 -1
- data/spec/query_spec.rb +24 -1
- data/spec/search_spec.rb +54 -0
- data/spec/spec_helper.rb +3 -0
- metadata +3 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 5ebda26142ef67b09916291cafb3941fd34db0310b259aaf5c2ecf1b5fee3596
         | 
| 4 | 
            +
              data.tar.gz: 5ee7f4f6c39b0237b281c1d04f27c3cd77ce1d9bb840a34fe8de88aa5114d28b
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 4a10d18486ab2f434fd83b46c2d74da3851a80a227af4a42caff4a070d3d7701716a202e52b26591041cd7c57cdb144b9c2abee06186415aeebbdb0b276df55d
         | 
| 7 | 
            +
              data.tar.gz: af55ec1bb42eaf1fec2de9431fe9b16369348f000a3d8740abb6ec1b02297297826b97cec34c1955615901366427d87bf1b02192320faf92c8d1e1a255c44477
         | 
    
        data/example/basic.rb
    CHANGED
    
    | @@ -9,6 +9,7 @@ class Product | |
| 9 9 | 
             
              schema do |t|
         | 
| 10 10 | 
             
                t.string :name
         | 
| 11 11 | 
             
                t.integer :price
         | 
| 12 | 
            +
                t.boolean :published
         | 
| 12 13 | 
             
                t.timestamps
         | 
| 13 14 | 
             
              end
         | 
| 14 15 |  | 
| @@ -19,11 +20,11 @@ end | |
| 19 20 | 
             
            def populate
         | 
| 20 21 | 
             
              5_000.times do |i|
         | 
| 21 22 | 
             
                puts "Creating product #{i}" if i % 1000 == 0
         | 
| 22 | 
            -
                Product.create!(name: "A product with index #{i}", price: 10000 + i % 10)
         | 
| 23 | 
            +
                Product.create!(name: "A product with index #{i}", published: true, price: 10000 + i % 10)
         | 
| 23 24 | 
             
              end
         | 
| 24 25 | 
             
            end
         | 
| 25 26 |  | 
| 26 | 
            -
            populate if Product.count == 0 | 
| 27 | 
            +
            populate if Product.count == 0
         | 
| 27 28 |  | 
| 28 29 | 
             
            # 50_000 products: 50M
         | 
| 29 30 | 
             
            # 100_000 products: 50M
         | 
    
        data/example/relations.rb
    CHANGED
    
    | @@ -1,12 +1,12 @@ | |
| 1 1 | 
             
            require 'bundler/setup'
         | 
| 2 2 | 
             
            require 'groovy'
         | 
| 3 3 |  | 
| 4 | 
            -
            Groovy.open('./db/ | 
| 5 | 
            -
            # Groovy.open('./db/ | 
| 4 | 
            +
            Groovy.open('./db/2020', :current)
         | 
| 5 | 
            +
            # Groovy.open('./db/2021', :next)
         | 
| 6 6 |  | 
| 7 7 | 
             
            module Groovy::Model::ClassMethods
         | 
| 8 8 | 
             
              def context_name
         | 
| 9 | 
            -
                Time.now.year ==  | 
| 9 | 
            +
                Time.now.year == 2020 ? :current : :next
         | 
| 10 10 | 
             
              end
         | 
| 11 11 |  | 
| 12 12 | 
             
              def table
         | 
| @@ -34,6 +34,7 @@ class Place | |
| 34 34 | 
             
                t.reference :categories, "Categories", type: :vector
         | 
| 35 35 | 
             
                t.string :name
         | 
| 36 36 | 
             
                t.string :description, index: true
         | 
| 37 | 
            +
                t.boolean :visible
         | 
| 37 38 | 
             
                t.timestamps
         | 
| 38 39 | 
             
              end
         | 
| 39 40 |  | 
| @@ -50,6 +51,21 @@ class Location | |
| 50 51 | 
             
              end
         | 
| 51 52 | 
             
            end
         | 
| 52 53 |  | 
| 54 | 
            +
            # from groonga tests
         | 
| 55 | 
            +
            #
         | 
| 56 | 
            +
            # schema.create_table("Users", :type => :hash, :key_type => "UInt32") { |table| }
         | 
| 57 | 
            +
            #
         | 
| 58 | 
            +
            # schema.create_table("Communities", :type => :hash, :key_type => "ShortText") do |table|
         | 
| 59 | 
            +
            #   table.reference("users", "Users", :type => :vector)
         | 
| 60 | 
            +
            # end
         | 
| 61 | 
            +
            #
         | 
| 62 | 
            +
            # groonga = @communities.add("groonga")
         | 
| 63 | 
            +
            # morita = @users.add(29)
         | 
| 64 | 
            +
            # groonga["users"] = [morita]
         | 
| 65 | 
            +
            #
         | 
| 66 | 
            +
            # assert_equal([29], @users.collect {|record| record.key})
         | 
| 67 | 
            +
            # assert_equal([29], groonga["users"].collect {|record| record.key})
         | 
| 68 | 
            +
             | 
| 53 69 | 
             
            def insert_places(count = 1000)
         | 
| 54 70 | 
             
              puts "Inserting #{count} places!"
         | 
| 55 71 | 
             
              count.times do |i|
         | 
| @@ -57,4 +73,4 @@ def insert_places(count = 1000) | |
| 57 73 | 
             
              end
         | 
| 58 74 | 
             
            end
         | 
| 59 75 |  | 
| 60 | 
            -
            insert_places if Place.count == 0
         | 
| 76 | 
            +
            insert_places if Place.count == 0
         | 
    
        data/lib/groovy.rb
    CHANGED
    
    | @@ -1,6 +1,14 @@ | |
| 1 1 | 
             
            require 'groonga'
         | 
| 2 2 | 
             
            require File.expand_path(File.dirname(__FILE__)) + '/groovy/model'
         | 
| 3 3 |  | 
| 4 | 
            +
            # overwrite Groonga::Record#inspect because the #attributes part is
         | 
| 5 | 
            +
            # making debugging take ages
         | 
| 6 | 
            +
            class Groonga::Record
         | 
| 7 | 
            +
              def inspect
         | 
| 8 | 
            +
                super
         | 
| 9 | 
            +
              end
         | 
| 10 | 
            +
            end
         | 
| 11 | 
            +
             | 
| 4 12 | 
             
            module Groovy
         | 
| 5 13 |  | 
| 6 14 | 
             
              class Error < StandardError; end
         | 
| @@ -16,18 +24,18 @@ module Groovy | |
| 16 24 | 
             
                def [](name)
         | 
| 17 25 | 
             
                  contexts[name.to_sym]
         | 
| 18 26 | 
             
                end
         | 
| 19 | 
            -
             | 
| 27 | 
            +
             | 
| 20 28 | 
             
                def first_context_name
         | 
| 21 29 | 
             
                  contexts.keys.first
         | 
| 22 30 | 
             
                end
         | 
| 23 31 |  | 
| 24 32 | 
             
                def open(db_path, name = :default, opts = {})
         | 
| 25 33 | 
             
                  unless db_path.is_a?(String)
         | 
| 26 | 
            -
                    raise ArgumentError, "Invalid db_path: #{db_path}" | 
| 34 | 
            +
                    raise ArgumentError, "Invalid db_path: #{db_path}"
         | 
| 27 35 | 
             
                  end
         | 
| 28 36 |  | 
| 29 37 | 
             
                  if contexts[name.to_sym]
         | 
| 30 | 
            -
                    raise ArgumentError, "Context already defined: #{name}" | 
| 38 | 
            +
                    raise ArgumentError, "Context already defined: #{name}"
         | 
| 31 39 | 
             
                  end
         | 
| 32 40 |  | 
| 33 41 | 
             
                  contexts[name.to_sym] = if name == :default
         | 
| @@ -40,7 +48,7 @@ module Groovy | |
| 40 48 | 
             
                def close(name = :default)
         | 
| 41 49 | 
             
                  ctx = contexts[name.to_sym] or raise ContextNotFound.new(name)
         | 
| 42 50 | 
             
                  contexts.delete(name.to_sym)
         | 
| 43 | 
            -
                  ctx.close | 
| 51 | 
            +
                  ctx.close
         | 
| 44 52 | 
             
                rescue Groonga::Closed => e
         | 
| 45 53 | 
             
                  raise ContextAlreadyClosed
         | 
| 46 54 | 
             
                end
         | 
    
        data/lib/groovy/model.rb
    CHANGED
    
    | @@ -20,8 +20,13 @@ module Groovy | |
| 20 20 | 
             
                  model.new_from_record(obj)
         | 
| 21 21 | 
             
                end
         | 
| 22 22 |  | 
| 23 | 
            +
                # def self.initialize_from_hash(key, obj)
         | 
| 24 | 
            +
                #   model = model_from_table(key)
         | 
| 25 | 
            +
                #   model.find(obj['_id'])
         | 
| 26 | 
            +
                # end
         | 
| 27 | 
            +
             | 
| 23 28 | 
             
                def self.model_from_table(table_name)
         | 
| 24 | 
            -
                  Kernel.const_get(table_name.sub(/ies$/, 'y').sub(/s$/, ''))
         | 
| 29 | 
            +
                  Kernel.const_get(table_name.sub(/ies$/, 'y').sub(/s$/, '').capitalize)
         | 
| 25 30 | 
             
                end
         | 
| 26 31 |  | 
| 27 32 | 
             
                def self.included(base)
         | 
| @@ -44,9 +49,17 @@ module Groovy | |
| 44 49 | 
             
                  end
         | 
| 45 50 |  | 
| 46 51 | 
             
                  def attribute_names
         | 
| 47 | 
            -
                    schema.attribute_columns
         | 
| 52 | 
            +
                    @attribute_names ||= schema.attribute_columns
         | 
| 48 53 | 
             
                  end
         | 
| 49 54 |  | 
| 55 | 
            +
                  # def singular_refs
         | 
| 56 | 
            +
                  #   schema.singular_references
         | 
| 57 | 
            +
                  # end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  # def plural_refs
         | 
| 60 | 
            +
                  #   schema.plural_references
         | 
| 61 | 
            +
                  # end
         | 
| 62 | 
            +
             | 
| 50 63 | 
             
                  def schema(options = {}, &block)
         | 
| 51 64 | 
             
                    @schema ||= load_schema(options, &block)
         | 
| 52 65 | 
             
                  end
         | 
| @@ -61,64 +74,104 @@ module Groovy | |
| 61 74 | 
             
                      s.sync
         | 
| 62 75 |  | 
| 63 76 | 
             
                      extend(PatriciaTrieMethods) if table.is_a?(Groonga::PatriciaTrie)
         | 
| 64 | 
            -
                      s. | 
| 65 | 
            -
                      s.singular_references.each { |col| add_ref_accessors(col) }
         | 
| 66 | 
            -
                      s.plural_references.each { |col| add_vector_accessors(col) }
         | 
| 77 | 
            +
                      s.column_names.each { |col| add_accessors_for(col, s) }
         | 
| 67 78 | 
             
                      s
         | 
| 68 79 | 
             
                    end
         | 
| 69 80 | 
             
                  end
         | 
| 70 81 |  | 
| 71 | 
            -
                   | 
| 72 | 
            -
             | 
| 73 | 
            -
                     | 
| 82 | 
            +
                  def add_column(name, type, options = {})
         | 
| 83 | 
            +
                    @attribute_names = nil # ensure cache is cleared
         | 
| 84 | 
            +
                    schema.column(name, type, options)
         | 
| 85 | 
            +
                    schema.sync
         | 
| 86 | 
            +
                    add_accessors_for(name)
         | 
| 74 87 | 
             
                  end
         | 
| 75 88 |  | 
| 76 | 
            -
                  def  | 
| 77 | 
            -
                    if  | 
| 78 | 
            -
                       | 
| 89 | 
            +
                  def add_accessors_for(col, s = schema)
         | 
| 90 | 
            +
                    if s.attribute_columns.include?(col)
         | 
| 91 | 
            +
                      add_attr_accessors(col)
         | 
| 92 | 
            +
                    elsif s.singular_references.include?(col)
         | 
| 93 | 
            +
                      add_ref_accessors(col)
         | 
| 94 | 
            +
                    elsif s.plural_references.include?(col)
         | 
| 95 | 
            +
                      add_vector_accessors(col)
         | 
| 96 | 
            +
                    else
         | 
| 97 | 
            +
                      puts "WARNING: Unknown column type: #{col}"
         | 
| 79 98 | 
             
                    end
         | 
| 80 99 | 
             
                  end
         | 
| 81 100 |  | 
| 82 | 
            -
                   | 
| 83 | 
            -
             | 
| 84 | 
            -
                     | 
| 85 | 
            -
             | 
| 86 | 
            -
                    end
         | 
| 101 | 
            +
                  # called from Query, so needs to be public
         | 
| 102 | 
            +
                  def new_from_record(record)
         | 
| 103 | 
            +
                    # new(record.attributes, record)
         | 
| 104 | 
            +
                    new(nil, record)
         | 
| 87 105 | 
             
                  end
         | 
| 88 106 |  | 
| 107 | 
            +
                  # def find_and_init_record(id)
         | 
| 108 | 
            +
                  #   if found = table[id.to_i]
         | 
| 109 | 
            +
                  #     new_from_record(found)
         | 
| 110 | 
            +
                  #   end
         | 
| 111 | 
            +
                  # end
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                  # def find_records(&block)
         | 
| 114 | 
            +
                  #   records = table.select(&block)
         | 
| 115 | 
            +
                  #   records.map do |r|
         | 
| 116 | 
            +
                  #     find_and_init_record(r.attributes['_key']['_id'])
         | 
| 117 | 
            +
                  #   end
         | 
| 118 | 
            +
                  # end
         | 
| 119 | 
            +
             | 
| 89 120 | 
             
                  def find(id)
         | 
| 90 | 
            -
                    if record = table[id] and record. | 
| 121 | 
            +
                    if record = table[id.to_i] and record.record_id
         | 
| 91 122 | 
             
                      new_from_record(record)
         | 
| 92 123 | 
             
                    end
         | 
| 93 124 | 
             
                  end
         | 
| 94 125 |  | 
| 95 | 
            -
                  def create( | 
| 96 | 
            -
                     | 
| 97 | 
            -
             | 
| 98 | 
            -
             | 
| 126 | 
            +
                  def create(attributes, key = nil)
         | 
| 127 | 
            +
                    obj = new(attributes, nil, key)
         | 
| 128 | 
            +
                    obj.save ? obj : false
         | 
| 129 | 
            +
                  end
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                  def create!(attributes, key = nil)
         | 
| 132 | 
            +
                    create(attributes, key) or raise Error, "Invalid"
         | 
| 99 133 | 
             
                  end
         | 
| 100 134 |  | 
| 101 | 
            -
                  def  | 
| 102 | 
            -
                     | 
| 135 | 
            +
                  def update_all(attrs)
         | 
| 136 | 
            +
                    find_each { |child| child.update_attributes(attrs) }
         | 
| 103 137 | 
             
                  end
         | 
| 104 138 |  | 
| 105 139 | 
             
                  def delete_all
         | 
| 106 | 
            -
                     | 
| 107 | 
            -
                     | 
| 108 | 
            -
                    # schema.rebuild!
         | 
| 140 | 
            +
                    # find_each { |child| child.delete }
         | 
| 141 | 
            +
                    table.delete { |record| record._id > -1 }
         | 
| 109 142 | 
             
                  end
         | 
| 110 143 |  | 
| 144 | 
            +
                  # def dump_table!
         | 
| 145 | 
            +
                  #   Groonga::TableDumper.new(table).dump
         | 
| 146 | 
            +
                  #   # schema.rebuild!
         | 
| 147 | 
            +
                  # end
         | 
| 148 | 
            +
             | 
| 111 149 | 
             
                  # def column(name)
         | 
| 112 150 | 
             
                  #   Groonga["#{table_name}.#{name}"] # .search, .similar_search, etc
         | 
| 113 151 | 
             
                  # end
         | 
| 114 152 |  | 
| 115 | 
            -
             | 
| 116 | 
            -
             | 
| 117 | 
            -
             | 
| 153 | 
            +
             | 
| 154 | 
            +
                  def index_search(column, query, options = {}, &block)
         | 
| 155 | 
            +
                    results = table.select { |rec| rec[column].match(query) }
         | 
| 156 | 
            +
                    render_results(results, &block)
         | 
| 157 | 
            +
                  end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                  def similar_search(column, query, options = {}, &block)
         | 
| 160 | 
            +
                    unless schema.index_columns.include?(column.to_sym)
         | 
| 161 | 
            +
                      raise Error, "Column '#{column}' doesn't have an index set!"
         | 
| 118 162 | 
             
                    end
         | 
| 119 163 |  | 
| 120 | 
            -
                    table.select | 
| 121 | 
            -
                     | 
| 164 | 
            +
                    # results = table.select("#{col}:#{q}", operator: Groonga::Operation::SIMILAR)
         | 
| 165 | 
            +
                    results = table.select { |rec| rec[column].similar_search(query) }
         | 
| 166 | 
            +
                    render_results(results, &block)
         | 
| 167 | 
            +
                  end
         | 
| 168 | 
            +
             | 
| 169 | 
            +
                  def render_results(results, &block)
         | 
| 170 | 
            +
                    if block_given?
         | 
| 171 | 
            +
                      results.each { |rec| yield new_from_record(rec) }
         | 
| 172 | 
            +
                    else
         | 
| 173 | 
            +
                      results.map { |rec| new_from_record(rec) }
         | 
| 174 | 
            +
                    end
         | 
| 122 175 | 
             
                  end
         | 
| 123 176 |  | 
| 124 177 | 
             
                  def unique_values_for(column, limit: -1, cache: false)
         | 
| @@ -155,28 +208,31 @@ module Groovy | |
| 155 208 | 
             
                    end
         | 
| 156 209 | 
             
                  end
         | 
| 157 210 |  | 
| 158 | 
            -
                  [:find_by, :search, :where, :not, :sort_by, :limit, :offset, :paginate, :in_batches].each do |scope_method|
         | 
| 211 | 
            +
                  [:select, :find_each, :find_by, :search, :where, :not, :sort_by, :limit, :offset, :paginate, :in_batches].each do |scope_method|
         | 
| 159 212 | 
             
                    define_method scope_method do |*args, &block|
         | 
| 160 213 | 
             
                      query.public_send(scope_method, *args, &block)
         | 
| 161 214 | 
             
                    end
         | 
| 162 215 | 
             
                  end
         | 
| 163 216 |  | 
| 164 | 
            -
                  # this seems to be the same as: `table[id]`
         | 
| 165 | 
            -
                  # def search(key, options = nil)
         | 
| 166 | 
            -
                  #   raise "Not supported!" unless table.respond_to?(:search)
         | 
| 167 | 
            -
                  #   table.search(key, options)
         | 
| 168 | 
            -
                  # end
         | 
| 169 | 
            -
             | 
| 170 217 | 
             
                  def_instance_delegators :table, :count, :size
         | 
| 171 218 |  | 
| 172 219 | 
             
                  # called from instance too, so must by public
         | 
| 173 | 
            -
                  def insert( | 
| 220 | 
            +
                  def insert(attributes, key = nil)
         | 
| 221 | 
            +
                    set_timestamp(attributes, :created_at)
         | 
| 222 | 
            +
                    set_timestamp(attributes, :updated_at)
         | 
| 223 | 
            +
             | 
| 224 | 
            +
                    # remove nil attributes for integer columns, otherwise
         | 
| 225 | 
            +
                    # we get a TypeError (no implicit conversion from nil to integer)
         | 
| 226 | 
            +
                    attributes.each do |k, v|
         | 
| 227 | 
            +
                      attributes.delete(k) if v.nil? # && schema.integer_columns.include?(k)
         | 
| 228 | 
            +
                    end
         | 
| 229 | 
            +
             | 
| 174 230 | 
             
                    if table.support_key?
         | 
| 231 | 
            +
                      raise "Key required" if key.nil?
         | 
| 175 232 | 
             
                      table.add(key, attributes)
         | 
| 176 | 
            -
                    else | 
| 177 | 
            -
                       | 
| 178 | 
            -
                       | 
| 179 | 
            -
                      table.add(key)
         | 
| 233 | 
            +
                    else
         | 
| 234 | 
            +
                      raise "Key present, but unsupported" if key
         | 
| 235 | 
            +
                      table.add(attributes)
         | 
| 180 236 | 
             
                    end
         | 
| 181 237 | 
             
                  end
         | 
| 182 238 |  | 
| @@ -184,6 +240,17 @@ module Groovy | |
| 184 240 | 
             
                    obj[key_name] = Time.now if attribute_names.include?(key_name.to_sym)
         | 
| 185 241 | 
             
                  end
         | 
| 186 242 |  | 
| 243 | 
            +
                  def callbacks
         | 
| 244 | 
            +
                    @callbacks ||= {}
         | 
| 245 | 
            +
                  end
         | 
| 246 | 
            +
             | 
| 247 | 
            +
                  [:before_create, :after_create].each do |event|
         | 
| 248 | 
            +
                    define_method(event) do |*method_names|
         | 
| 249 | 
            +
                      callbacks[:before_create] ||= []
         | 
| 250 | 
            +
                      callbacks[:before_create].push(*method_names)
         | 
| 251 | 
            +
                    end
         | 
| 252 | 
            +
                  end
         | 
| 253 | 
            +
             | 
| 187 254 | 
             
                  private
         | 
| 188 255 |  | 
| 189 256 | 
             
                  def query_class
         | 
| @@ -195,7 +262,8 @@ module Groovy | |
| 195 262 | 
             
                  end
         | 
| 196 263 |  | 
| 197 264 | 
             
                  def db_context
         | 
| 198 | 
            -
                    Groovy.contexts[context_name.to_sym]  | 
| 265 | 
            +
                    Groovy.contexts[context_name.to_sym] \
         | 
| 266 | 
            +
                      or raise "Context not defined: #{context_name} Please call Groovy.open('./db/path') first."
         | 
| 199 267 | 
             
                  end
         | 
| 200 268 |  | 
| 201 269 | 
             
                  def add_attr_accessors(col)
         | 
| @@ -225,30 +293,38 @@ module Groovy | |
| 225 293 | 
             
                  end
         | 
| 226 294 | 
             
                end
         | 
| 227 295 |  | 
| 228 | 
            -
                attr_reader :id, :attributes, : | 
| 296 | 
            +
                attr_reader :id, :attributes, :record, :changes
         | 
| 229 297 |  | 
| 230 | 
            -
                def initialize(attrs = nil, record = nil)
         | 
| 231 | 
            -
                  @attributes, @ | 
| 298 | 
            +
                def initialize(attrs = nil, record = nil, key = nil)
         | 
| 299 | 
            +
                  @attributes, @vectors, @_key = {}, {}, key # key is used on creation only
         | 
| 232 300 |  | 
| 233 301 | 
             
                  if set_record(record)
         | 
| 234 | 
            -
                     | 
| 235 | 
            -
             | 
| 236 | 
            -
                     | 
| 237 | 
            -
                     | 
| 238 | 
            -
             | 
| 302 | 
            +
                    set_attributes_from_record(record)
         | 
| 303 | 
            +
                  else
         | 
| 304 | 
            +
                    attrs ||= {}
         | 
| 305 | 
            +
                    unless attrs.is_a?(Hash)
         | 
| 306 | 
            +
                      raise ArgumentError.new("Attributes should be a Hash, not a #{attrs.class}")
         | 
| 307 | 
            +
                    end
         | 
| 239 308 |  | 
| 240 | 
            -
             | 
| 241 | 
            -
             | 
| 242 | 
            -
                     | 
| 309 | 
            +
                    # don't call set_attributes since we don't want to call
         | 
| 310 | 
            +
                    # setters, that might be overriden with custom logic.
         | 
| 311 | 
            +
                    # attrs.each { |k,v| self[k] = v }
         | 
| 312 | 
            +
                    set_attributes(attrs)
         | 
| 243 313 | 
             
                  end
         | 
| 244 314 |  | 
| 245 | 
            -
                  # don't call set_attributes since we don't want to call
         | 
| 246 | 
            -
                  # setters, that might be overriden with custom logic.
         | 
| 247 | 
            -
                  # attrs.each { |k,v| self[k] = v }
         | 
| 248 | 
            -
                  set_attributes(attrs)
         | 
| 249 315 | 
             
                  @changes = {}
         | 
| 250 316 | 
             
                end
         | 
| 251 317 |  | 
| 318 | 
            +
                # get reference to the actual record in the Groonga table,
         | 
| 319 | 
            +
                # not the temporary one we get as part of a search result.
         | 
| 320 | 
            +
                def load_record
         | 
| 321 | 
            +
                  self.class.table[id]
         | 
| 322 | 
            +
                end
         | 
| 323 | 
            +
             | 
| 324 | 
            +
                def inspect
         | 
| 325 | 
            +
                  "#<#{self.class.name} id:#{id.inspect} attributes:[#{self.class.attribute_names.join(', ')}]>"
         | 
| 326 | 
            +
                end
         | 
| 327 | 
            +
             | 
| 252 328 | 
             
                def new_record?
         | 
| 253 329 | 
             
                  id.nil?
         | 
| 254 330 | 
             
                  # _key.nil?
         | 
| @@ -259,7 +335,9 @@ module Groovy | |
| 259 335 | 
             
                end
         | 
| 260 336 |  | 
| 261 337 | 
             
                def []=(key, val)
         | 
| 262 | 
            -
                   | 
| 338 | 
            +
                  if self.class.schema.singular_references.include?(key.to_sym) # val.respond_to?(:record) || val.is_a?(Groonga::Record)
         | 
| 339 | 
            +
                    return set_ref(key, val)
         | 
| 340 | 
            +
                  end
         | 
| 263 341 |  | 
| 264 342 | 
             
                  unless self.class.attribute_names.include?(key.to_sym)
         | 
| 265 343 | 
             
                    raise "Invalid attribute: #{key}"
         | 
| @@ -300,59 +378,85 @@ module Groovy | |
| 300 378 |  | 
| 301 379 | 
             
                def save!(options = {})
         | 
| 302 380 | 
             
                  raise "Invalid!" unless save
         | 
| 381 | 
            +
                  self
         | 
| 303 382 | 
             
                end
         | 
| 304 383 |  | 
| 305 384 | 
             
                def delete
         | 
| 306 | 
            -
                  record.delete # doesn't work if record.id doesn't match _key
         | 
| 307 | 
            -
                   | 
| 385 | 
            +
                  # record.delete # doesn't work if record.id doesn't match _key
         | 
| 386 | 
            +
                  self.class.table.delete(record._id) # record.record_id
         | 
| 308 387 | 
             
                  set_record(nil)
         | 
| 309 388 | 
             
                  self
         | 
| 310 389 | 
             
                rescue Groonga::InvalidArgument => e
         | 
| 311 | 
            -
                  puts "Error: #{e.inspect}"
         | 
| 312 | 
            -
                  raise RecordNotPersisted
         | 
| 390 | 
            +
                  # puts "Error: #{e.inspect}"
         | 
| 391 | 
            +
                  raise RecordNotPersisted, e.message
         | 
| 313 392 | 
             
                end
         | 
| 314 393 |  | 
| 315 394 | 
             
                def reload
         | 
| 316 395 | 
             
                  raise RecordNotPersisted if id.nil?
         | 
| 317 396 | 
             
                  ensure_persisted!
         | 
| 318 | 
            -
                   | 
| 319 | 
            -
                  #  | 
| 320 | 
            -
                   | 
| 397 | 
            +
                  rec = self.class.table[id] # _key
         | 
| 398 | 
            +
                  # set_record(rec)
         | 
| 399 | 
            +
                  set_attributes_from_record(rec)
         | 
| 321 400 | 
             
                  @changes = {}
         | 
| 322 401 | 
             
                  self
         | 
| 323 402 | 
             
                end
         | 
| 324 403 |  | 
| 325 404 | 
             
                def as_json(options = {})
         | 
| 326 | 
            -
                  attributes
         | 
| 405 | 
            +
                  options[:only] ? attributes.slice(*options[:only]) : attributes
         | 
| 327 406 | 
             
                end
         | 
| 328 407 |  | 
| 329 408 | 
             
                def ==(other)
         | 
| 330 409 | 
             
                  self.id == other.id
         | 
| 331 410 | 
             
                end
         | 
| 332 411 |  | 
| 412 | 
            +
                def <=>(other)
         | 
| 413 | 
            +
                  self.id <=> other.id
         | 
| 414 | 
            +
                end
         | 
| 415 | 
            +
             | 
| 333 416 | 
             
                private
         | 
| 334 417 |  | 
| 418 | 
            +
                def get_record_attribute(key)
         | 
| 419 | 
            +
                  val = record[key]
         | 
| 420 | 
            +
                  if self.class.schema.time_columns.include?(key)
         | 
| 421 | 
            +
                    fix_time_value(val)
         | 
| 422 | 
            +
                  else
         | 
| 423 | 
            +
                    val
         | 
| 424 | 
            +
                  end
         | 
| 425 | 
            +
                end
         | 
| 426 | 
            +
             | 
| 427 | 
            +
                def fix_time_value(val)
         | 
| 428 | 
            +
                  return val.to_i == 0 ? nil : val
         | 
| 429 | 
            +
                end
         | 
| 430 | 
            +
             | 
| 335 431 | 
             
                # def _key
         | 
| 336 432 | 
             
                #   return unless record
         | 
| 337 433 | 
             
                #   record.respond_to?(:_key) ? record._key : id
         | 
| 338 434 | 
             
                # end
         | 
| 339 435 |  | 
| 436 | 
            +
                def set_attributes_from_record(rec)
         | 
| 437 | 
            +
                  self.class.attribute_names.each do |col|
         | 
| 438 | 
            +
                    public_send("#{col}=", get_record_attribute(col))
         | 
| 439 | 
            +
                  end
         | 
| 440 | 
            +
                end
         | 
| 441 | 
            +
             | 
| 340 442 | 
             
                def set_attribute(key, val)
         | 
| 341 443 | 
             
                  changes[key.to_sym] = [self[key], val] if changes # nil when initializing
         | 
| 342 444 | 
             
                  attributes[key.to_sym] = val
         | 
| 343 445 | 
             
                end
         | 
| 344 446 |  | 
| 345 447 | 
             
                def get_ref(name)
         | 
| 346 | 
            -
                   | 
| 448 | 
            +
                  if record and obj = record[name]
         | 
| 449 | 
            +
                    Model.initialize_from_record(obj)
         | 
| 450 | 
            +
                  end
         | 
| 347 451 | 
             
                end
         | 
| 348 452 |  | 
| 349 453 | 
             
                def set_ref(name, obj)
         | 
| 350 | 
            -
                   | 
| 351 | 
            -
                    obj  | 
| 454 | 
            +
                  if record.nil?
         | 
| 455 | 
            +
                    set_attribute(name, obj.id) # obj should be a groovy model or groonga record
         | 
| 456 | 
            +
                  else
         | 
| 457 | 
            +
                    obj = obj.record if obj.respond_to?(:record)
         | 
| 458 | 
            +
                    record[name] = obj
         | 
| 352 459 | 
             
                  end
         | 
| 353 | 
            -
             | 
| 354 | 
            -
                  @refs[name] = obj
         | 
| 355 | 
            -
                  set_attribute(name, obj.nil? ? nil : obj.key)
         | 
| 356 460 | 
             
                end
         | 
| 357 461 |  | 
| 358 462 | 
             
                def set_record(obj)
         | 
| @@ -361,10 +465,18 @@ module Groovy | |
| 361 465 | 
             
                end
         | 
| 362 466 |  | 
| 363 467 | 
             
                def create
         | 
| 364 | 
            -
                   | 
| 468 | 
            +
                  fire_callbacks(:before_create)
         | 
| 469 | 
            +
                  set_record(self.class.insert(attributes, @_key))
         | 
| 470 | 
            +
                  fire_callbacks(:after_create)
         | 
| 365 471 | 
             
                  self
         | 
| 366 472 | 
             
                end
         | 
| 367 473 |  | 
| 474 | 
            +
                def fire_callbacks(name)
         | 
| 475 | 
            +
                  if arr = self.class.callbacks[name] and arr.any?
         | 
| 476 | 
            +
                    arr.each { |fn| send(fn) }
         | 
| 477 | 
            +
                  end
         | 
| 478 | 
            +
                end
         | 
| 479 | 
            +
             | 
| 368 480 | 
             
                def update
         | 
| 369 481 | 
             
                  ensure_persisted!
         | 
| 370 482 | 
             
                  changes.each do |key, values|
         | 
    
        data/lib/groovy/query.rb
    CHANGED
    
    | @@ -25,18 +25,32 @@ module Groovy | |
| 25 25 | 
             
                  @default_sort_key = table.is_a?(Groonga::Hash) ? '_key' : '_id'
         | 
| 26 26 | 
             
                end
         | 
| 27 27 |  | 
| 28 | 
            +
                # def inspect
         | 
| 29 | 
            +
                #   "<#{self.class.name} #{parameters}>"
         | 
| 30 | 
            +
                # end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def as_json(options = {})
         | 
| 33 | 
            +
                  Array.new.tap do |arr|
         | 
| 34 | 
            +
                    each { |record| arr.push(record.as_json(options)) }
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
             | 
| 28 38 | 
             
                def search(obj)
         | 
| 29 39 | 
             
                  obj.each do |col, q|
         | 
| 30 | 
            -
                    unless model.index_columns.include?(col)
         | 
| 31 | 
            -
             | 
| 32 | 
            -
                    end
         | 
| 33 | 
            -
                     | 
| 40 | 
            +
                    # unless model.schema.index_columns.include?(col)
         | 
| 41 | 
            +
                    #   raise "Not an index column, so cannot do fulltext search: #{col}"
         | 
| 42 | 
            +
                    # end
         | 
| 43 | 
            +
                    q.split(' ').each do |word|
         | 
| 44 | 
            +
                      parameters.push(AND + "(#{col}:@#{word})")
         | 
| 45 | 
            +
                    end if q.is_a?(String) && q.strip != ''
         | 
| 34 46 | 
             
                  end
         | 
| 47 | 
            +
                  self
         | 
| 35 48 | 
             
                end
         | 
| 36 49 |  | 
| 37 | 
            -
                 | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 50 | 
            +
                def select(&block)
         | 
| 51 | 
            +
                  @select_block = block
         | 
| 52 | 
            +
                  self
         | 
| 53 | 
            +
                end
         | 
| 40 54 |  | 
| 41 55 | 
             
                def find(id)
         | 
| 42 56 | 
             
                  find_by(_id: id)
         | 
| @@ -46,9 +60,9 @@ module Groovy | |
| 46 60 | 
             
                  where(conditions).limit(1).first
         | 
| 47 61 | 
             
                end
         | 
| 48 62 |  | 
| 49 | 
            -
                def find_each(&block)
         | 
| 63 | 
            +
                def find_each(opts = {}, &block)
         | 
| 50 64 | 
             
                  count = 0
         | 
| 51 | 
            -
                  in_batches(of: 10) do |group|
         | 
| 65 | 
            +
                  in_batches({ of: 10 }.merge(opts)) do |group|
         | 
| 52 66 | 
             
                    group.each { |item| count += 1; yield(item) }
         | 
| 53 67 | 
             
                  end
         | 
| 54 68 | 
             
                  count
         | 
| @@ -76,7 +90,7 @@ module Groovy | |
| 76 90 | 
             
                          add_param(AND + str)
         | 
| 77 91 |  | 
| 78 92 | 
             
                        else
         | 
| 79 | 
            -
                          str = val.nil? || val  | 
| 93 | 
            +
                          str = val.nil? || val === false || val.to_s.strip == '' ? "\"\"" : escape_val(val)
         | 
| 80 94 | 
             
                          add_param(AND + [key, str].join(':'))
         | 
| 81 95 | 
             
                        end
         | 
| 82 96 | 
             
                      end
         | 
| @@ -110,7 +124,7 @@ module Groovy | |
| 110 124 | 
             
                          add_param(AND + str)
         | 
| 111 125 |  | 
| 112 126 | 
             
                        else
         | 
| 113 | 
            -
                          str = val.nil? || val  | 
| 127 | 
            +
                          str = val.nil? || val === false || val.to_s.strip == '' ? "\"\"" : escape_val(val)
         | 
| 114 128 | 
             
                          add_param(AND + [key, str].join(':!')) # not
         | 
| 115 129 | 
             
                        end
         | 
| 116 130 | 
             
                      end
         | 
| @@ -132,18 +146,18 @@ module Groovy | |
| 132 146 | 
             
                  self
         | 
| 133 147 | 
             
                end
         | 
| 134 148 |  | 
| 135 | 
            -
                def paginate(page = 1)
         | 
| 149 | 
            +
                def paginate(page = 1, per_page: PER_PAGE)
         | 
| 136 150 | 
             
                  page = 1 if page.to_i < 1
         | 
| 137 | 
            -
                  offset = ((page.to_i)-1) *  | 
| 138 | 
            -
                  offset(offset).limit( | 
| 151 | 
            +
                  offset = ((page.to_i)-1) * per_page
         | 
| 152 | 
            +
                  offset(offset).limit(per_page) # returns self
         | 
| 139 153 | 
             
                end
         | 
| 140 154 |  | 
| 141 155 | 
             
                # sort_by(title: :asc)
         | 
| 142 156 | 
             
                def sort_by(hash)
         | 
| 143 | 
            -
                  if hash.is_a?(String) # e.g. title.desc
         | 
| 144 | 
            -
                    param, dir = hash.split('.')
         | 
| 157 | 
            +
                  if hash.is_a?(String) || hash.is_a?(Symbol) # e.g. 'title.desc' or :title (asc by default)
         | 
| 158 | 
            +
                    param, dir = hash.to_s.split('.')
         | 
| 145 159 | 
             
                    hash = {}
         | 
| 146 | 
            -
                    hash[param] = dir
         | 
| 160 | 
            +
                    hash[param] = dir || 'asc'
         | 
| 147 161 | 
             
                  end
         | 
| 148 162 |  | 
| 149 163 | 
             
                  sorting[:by] = hash.keys.map do |key|
         | 
| @@ -183,13 +197,21 @@ module Groovy | |
| 183 197 | 
             
                  records.each { |r| block.call(r) }
         | 
| 184 198 | 
             
                end
         | 
| 185 199 |  | 
| 200 | 
            +
                def update_all(attrs)
         | 
| 201 | 
            +
                  each { |r| r.update_attributes(attrs) }
         | 
| 202 | 
            +
                end
         | 
| 203 | 
            +
             | 
| 186 204 | 
             
                def total_entries
         | 
| 187 205 | 
             
                  results # ensure query has been run
         | 
| 188 206 | 
             
                  @total_entries
         | 
| 189 207 | 
             
                end
         | 
| 190 208 |  | 
| 191 | 
            -
                def last
         | 
| 192 | 
            -
                   | 
| 209 | 
            +
                def last(count = 1)
         | 
| 210 | 
            +
                  if count > 1
         | 
| 211 | 
            +
                    records[(size-count)..-1]
         | 
| 212 | 
            +
                  else
         | 
| 213 | 
            +
                    records[size-1]
         | 
| 214 | 
            +
                  end
         | 
| 193 215 | 
             
                end
         | 
| 194 216 |  | 
| 195 217 | 
             
                def in_batches(of: 1000, from: nil, &block)
         | 
| @@ -207,46 +229,45 @@ module Groovy | |
| 207 229 |  | 
| 208 230 | 
             
                def records
         | 
| 209 231 | 
             
                  @records ||= results.map do |r|
         | 
| 210 | 
            -
                     | 
| 211 | 
            -
                    id = r.attributes['_value']['_key']['_id']
         | 
| 212 | 
            -
                    model.find_and_init_record(id)
         | 
| 232 | 
            +
                    model.new_from_record(r)
         | 
| 213 233 | 
             
                  end
         | 
| 214 234 | 
             
                end
         | 
| 215 235 |  | 
| 216 236 | 
             
                private
         | 
| 217 | 
            -
                attr_reader :model, :table, :options
         | 
| 237 | 
            +
                attr_reader :model, :table, :options, :select_block
         | 
| 218 238 |  | 
| 219 239 | 
             
                def add_param(param)
         | 
| 220 | 
            -
                  if  | 
| 221 | 
            -
             | 
| 222 | 
            -
                   | 
| 223 | 
            -
             | 
| 224 | 
            -
                  # if param matches blank/nil, put at the end of the stack
         | 
| 225 | 
            -
                  param[/:\!?\\/] ? parameters.push(param) : parameters.unshift(param)
         | 
| 240 | 
            +
                  raise "Select block already given!" if select_block
         | 
| 241 | 
            +
                  raise "Duplicate param: #{param}" if parameters.include?(param)
         | 
| 242 | 
            +
                  parameters.push(param)
         | 
| 226 243 | 
             
                end
         | 
| 227 244 |  | 
| 228 245 | 
             
                def results
         | 
| 229 246 | 
             
                  @results ||= execute
         | 
| 230 247 | 
             
                rescue Groonga::TooLargeOffset
         | 
| 231 | 
            -
                   | 
| 248 | 
            +
                  puts "Offset is higher than table size!"
         | 
| 232 249 | 
             
                  []
         | 
| 233 250 | 
             
                end
         | 
| 234 251 |  | 
| 235 252 | 
             
                def execute
         | 
| 236 | 
            -
                  set = if  | 
| 253 | 
            +
                  set = if select_block
         | 
| 254 | 
            +
                    debug "Finding records with select block"
         | 
| 255 | 
            +
                    table.select { |record| select_block.call(record) }
         | 
| 256 | 
            +
                  elsif parameters.any?
         | 
| 237 257 | 
             
                    query = prepare_query
         | 
| 238 | 
            -
                     | 
| 258 | 
            +
                    debug "Finding records with query: #{query}"
         | 
| 239 259 | 
             
                    table.select(query, options)
         | 
| 240 260 | 
             
                  else
         | 
| 261 | 
            +
                    debug "Finding records with options: #{options.inspect}"
         | 
| 241 262 | 
             
                    table.select(options)
         | 
| 242 263 | 
             
                  end
         | 
| 243 264 |  | 
| 244 265 | 
             
                  @total_entries = set.size
         | 
| 245 266 |  | 
| 246 | 
            -
                   | 
| 267 | 
            +
                  debug "Sorting with #{sort_key_and_order}, #{sorting.inspect}"
         | 
| 247 268 | 
             
                  set = set.sort(sort_key_and_order, {
         | 
| 248 269 | 
             
                    limit: sorting[:limit],
         | 
| 249 | 
            -
                    offset: sorting[:offset]
         | 
| 270 | 
            +
                    offset: sorting[:offset], # [sorting[:offset], @total_entries].min
         | 
| 250 271 | 
             
                  })
         | 
| 251 272 |  | 
| 252 273 | 
             
                  sorting[:group_by] ? set.group(group_by) : set
         | 
| @@ -266,6 +287,10 @@ module Groovy | |
| 266 287 | 
             
                  sorting[:by] or [{ key: @default_sort_key, order: :asc }]
         | 
| 267 288 | 
             
                end
         | 
| 268 289 |  | 
| 290 | 
            +
                def escape_val(val)
         | 
| 291 | 
            +
                  val.to_s.gsub(':', '\:')
         | 
| 292 | 
            +
                end
         | 
| 293 | 
            +
             | 
| 269 294 | 
             
                def prepare_query
         | 
| 270 295 | 
             
                  space_regex = Regexp.new('\s([' + VALID_QUERY_CHARS + '])')
         | 
| 271 296 | 
             
                  query = parameters.join(' ').split(/ or /i).map do |part|
         | 
| @@ -274,6 +299,11 @@ module Groovy | |
| 274 299 | 
             
                        .gsub(/(\d\d):(\d\d):(\d\d)/, '\1\:\2\:\3') # escape hh:mm:ss in timestamps
         | 
| 275 300 | 
             
                  end.join(' OR ').sub(/^-/, '_id:>0 -') #.gsub(' OR -', ' -')
         | 
| 276 301 | 
             
                end
         | 
| 302 | 
            +
             | 
| 303 | 
            +
                def debug(str)
         | 
| 304 | 
            +
                  puts str if ENV['DEBUG']
         | 
| 305 | 
            +
                end
         | 
| 306 | 
            +
             | 
| 277 307 | 
             
              end
         | 
| 278 308 |  | 
| 279 309 | 
             
            end
         | 
    
        data/lib/groovy/schema.rb
    CHANGED
    
    | @@ -16,7 +16,9 @@ module Groovy | |
| 16 16 | 
             
                  'boolean' => 'boolean',
         | 
| 17 17 | 
             
                  'integer' => 'int32',
         | 
| 18 18 | 
             
                  'big_integer' => 'int64',
         | 
| 19 | 
            -
                  ' | 
| 19 | 
            +
                  'date' => 'date',
         | 
| 20 | 
            +
                  'time' => 'time',
         | 
| 21 | 
            +
                  'datetime' => 'time'
         | 
| 20 22 | 
             
                }.freeze
         | 
| 21 23 |  | 
| 22 24 | 
             
                attr_reader :index_columns
         | 
| @@ -39,17 +41,40 @@ module Groovy | |
| 39 41 | 
             
                end
         | 
| 40 42 |  | 
| 41 43 | 
             
                def singular_references
         | 
| 42 | 
            -
                   | 
| 44 | 
            +
                  # @singular_references ||=
         | 
| 45 | 
            +
                  get_names(table.columns.select(&:reference_column?).reject(&:vector?))
         | 
| 43 46 | 
             
                end
         | 
| 44 47 |  | 
| 45 48 | 
             
                def plural_references
         | 
| 46 | 
            -
                   | 
| 49 | 
            +
                  # @plural_references ||=
         | 
| 50 | 
            +
                  get_names(table.columns.select(&:vector?))
         | 
| 47 51 | 
             
                end
         | 
| 48 52 |  | 
| 49 53 | 
             
                def attribute_columns
         | 
| 50 | 
            -
                   | 
| 54 | 
            +
                  # @attribute_columns ||=
         | 
| 55 | 
            +
                  get_names(table.columns.select { |c| c.column? && !c.reference_column? && !c.vector? })
         | 
| 51 56 | 
             
                end
         | 
| 52 57 |  | 
| 58 | 
            +
                def time_columns
         | 
| 59 | 
            +
                  columns_by_type('Time')
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                def integer_columns
         | 
| 63 | 
            +
                  columns_by_type('Int32')
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                def boolean_columns
         | 
| 67 | 
            +
                  columns_by_type('Bool')
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                def columns_by_type(type)
         | 
| 71 | 
            +
                  get_names(table.columns.select { |c| c.column? && c.range.name == type })
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                # def time_column?(name)
         | 
| 75 | 
            +
                #   time_columns.include?(name)
         | 
| 76 | 
            +
                # end
         | 
| 77 | 
            +
             | 
| 53 78 | 
             
                def rebuild!
         | 
| 54 79 | 
             
                  log("Rebuilding!")
         | 
| 55 80 | 
             
                  # remove_table! if table
         | 
| @@ -91,6 +116,7 @@ module Groovy | |
| 91 116 | 
             
                  @index_columns.each do |col|
         | 
| 92 117 | 
             
                    add_index_on(col)
         | 
| 93 118 | 
             
                  end
         | 
| 119 | 
            +
                  self
         | 
| 94 120 | 
             
                end
         | 
| 95 121 |  | 
| 96 122 | 
             
                private
         | 
| @@ -102,13 +128,15 @@ module Groovy | |
| 102 128 | 
             
                  @table = @search_table = nil # clear cached vars
         | 
| 103 129 | 
             
                end
         | 
| 104 130 |  | 
| 105 | 
            -
                def add_index_on(col)
         | 
| 131 | 
            +
                def add_index_on(col, opts = {})
         | 
| 106 132 | 
             
                  ensure_search_table!
         | 
| 107 133 | 
             
                  return false if search_table.have_column?([table_name, col].join('_'))
         | 
| 108 134 |  | 
| 109 | 
            -
                   | 
| 135 | 
            +
                  name_col = [table_name, col].join('.')
         | 
| 136 | 
            +
                  log "Adding index on #{name_col}"
         | 
| 110 137 | 
             
                  Groonga::Schema.change_table(SEARCH_TABLE_NAME, context: context) do |table|
         | 
| 111 | 
            -
                    table.index( | 
| 138 | 
            +
                    # table.index(name_col, name: name_col, with_position: true, with_section: true)
         | 
| 139 | 
            +
                    table.index(name_col, name: name_col.sub('.', '_'))
         | 
| 112 140 | 
             
                  end
         | 
| 113 141 | 
             
                end
         | 
| 114 142 |  | 
| @@ -117,6 +145,7 @@ module Groovy | |
| 117 145 | 
             
                  opts = (@opts[:search_table] || {}).merge({
         | 
| 118 146 | 
             
                    type: :patricia_trie,
         | 
| 119 147 | 
             
                    normalizer: :NormalizerAuto,
         | 
| 148 | 
            +
                    key_type: "ShortText",
         | 
| 120 149 | 
             
                    default_tokenizer: "TokenBigram"
         | 
| 121 150 | 
             
                  })
         | 
| 122 151 | 
             
                  log("Creating search table with options: #{opts.inspect}")
         | 
    
        data/lib/groovy/vector.rb
    CHANGED
    
    | @@ -9,16 +9,37 @@ module Groovy | |
| 9 9 | 
             
                end
         | 
| 10 10 |  | 
| 11 11 | 
             
                def size
         | 
| 12 | 
            -
                   | 
| 13 | 
            -
                   | 
| 12 | 
            +
                  return 0 unless obj.record
         | 
| 13 | 
            +
                  records.count
         | 
| 14 | 
            +
                  # items.count # so we filter out removed ones
         | 
| 14 15 | 
             
                end
         | 
| 15 16 |  | 
| 16 17 | 
             
                def inspect
         | 
| 17 | 
            -
                   | 
| 18 | 
            +
                  "#<#{obj.class}->#{key.capitalize} size:#{size}>"
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def as_json(options = {})
         | 
| 22 | 
            +
                  Array.new.tap do |arr|
         | 
| 23 | 
            +
                    each { |record| arr.push(record.as_json(options)) }
         | 
| 24 | 
            +
                  end
         | 
| 18 25 | 
             
                end
         | 
| 19 26 |  | 
| 20 27 | 
             
                alias_method :count, :size
         | 
| 21 28 |  | 
| 29 | 
            +
                # we redefine first and last to avoid having to perform a full query
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def first
         | 
| 32 | 
            +
                  if obj = records.first
         | 
| 33 | 
            +
                    Model.initialize_from_record(obj)
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                def last
         | 
| 38 | 
            +
                  if obj = records.last
         | 
| 39 | 
            +
                    Model.initialize_from_record(obj)
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 22 43 | 
             
                def each(&block)
         | 
| 23 44 | 
             
                  items.each { |r| block.call(r) }
         | 
| 24 45 | 
             
                end
         | 
| @@ -71,8 +92,8 @@ module Groovy | |
| 71 92 |  | 
| 72 93 | 
             
                def items
         | 
| 73 94 | 
             
                  return [] unless obj.record
         | 
| 74 | 
            -
                  records.map do |r| | 
| 75 | 
            -
                    if !exists?(r) | 
| 95 | 
            +
                  records.map do |r|
         | 
| 96 | 
            +
                    if !exists?(r)
         | 
| 76 97 | 
             
                      remove_record(r) if REMOVE_MISSING
         | 
| 77 98 | 
             
                      nil
         | 
| 78 99 | 
             
                    else
         | 
    
        data/lib/groovy/version.rb
    CHANGED
    
    
    
        data/spec/model_spec.rb
    CHANGED
    
    | @@ -17,7 +17,7 @@ describe Groovy::Model do | |
| 17 17 | 
             
              describe '.scope' do
         | 
| 18 18 |  | 
| 19 19 | 
             
                before :all do
         | 
| 20 | 
            -
                  TestProduct.class_eval do | 
| 20 | 
            +
                  TestProduct.class_eval do
         | 
| 21 21 | 
             
                    scope :with_name, -> (name) { where(name: name) if name }
         | 
| 22 22 | 
             
                    scope :by_price_asc, -> { sort_by(price: :asc) }
         | 
| 23 23 | 
             
                    scope :cheapest, -> { by_price_asc }
         | 
| @@ -49,6 +49,13 @@ describe Groovy::Model do | |
| 49 49 | 
             
              end
         | 
| 50 50 |  | 
| 51 51 | 
             
              describe '.create' do
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                it 'does not explode when inserting nil values for columns' do
         | 
| 54 | 
            +
                  expect do
         | 
| 55 | 
            +
                    TestProduct.create({ price: nil })
         | 
| 56 | 
            +
                  end.not_to raise_error
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 52 59 | 
             
              end
         | 
| 53 60 |  | 
| 54 61 | 
             
              describe '.find' do
         | 
| @@ -60,6 +67,7 @@ describe Groovy::Model do | |
| 60 67 | 
             
              describe '.delete_all' do
         | 
| 61 68 |  | 
| 62 69 | 
             
                before do
         | 
| 70 | 
            +
                  TestProduct.delete_all
         | 
| 63 71 | 
             
                  @first  = TestProduct.create!(name: 'A product', price: 100)
         | 
| 64 72 | 
             
                  @second = TestProduct.create!(name: 'Another product', price: 200)
         | 
| 65 73 | 
             
                  expect(TestProduct.count).to eq(2)
         | 
    
        data/spec/query_spec.rb
    CHANGED
    
    | @@ -7,7 +7,7 @@ describe Groovy::Query do | |
| 7 7 | 
             
                load_schema! 'query_spec'
         | 
| 8 8 | 
             
                @p1 = TestProduct.create!(name: "Product 1", visible: true, price: 10, tag_list: 'one, number two & three')
         | 
| 9 9 | 
             
                @p2 = TestProduct.create!(name: "Product 2", visible: false, price: 20, tag_list: 'number two, three')
         | 
| 10 | 
            -
                @p3 = TestProduct.create!(name: "Product 3", visible: true, price: 30, tag_list: nil)
         | 
| 10 | 
            +
                @p3 = TestProduct.create!(name: "Product 3: The Best", visible: true, price: 30, tag_list: nil)
         | 
| 11 11 | 
             
                @p4 = TestProduct.create!(name: "Product 4", visible: false, price: 40, tag_list: 'one, number two')
         | 
| 12 12 | 
             
                @p5 = TestProduct.create!(name: "Product 5", visible: true, price: 50, tag_list: '')
         | 
| 13 13 | 
             
              end
         | 
| @@ -40,6 +40,13 @@ describe Groovy::Query do | |
| 40 40 | 
             
                    res = TestProduct.where(tag_list: nil)
         | 
| 41 41 | 
             
                    expect(res.map(&:id)).to eq([@p3.id, @p5.id])
         | 
| 42 42 | 
             
                  end
         | 
| 43 | 
            +
                  it 'works with other nil values too' do
         | 
| 44 | 
            +
                    res = TestProduct.where(visible: nil).where(tag_list: nil)
         | 
| 45 | 
            +
                    expect(res.map(&:id)).to eq([])
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    res = TestProduct.where(tag_list: nil).where(name: nil)
         | 
| 48 | 
            +
                    expect(res.map(&:id)).to eq([])
         | 
| 49 | 
            +
                  end
         | 
| 43 50 | 
             
                  it 'works with other args too' do
         | 
| 44 51 | 
             
                    res = TestProduct.where(name: 'Product 5').where(tag_list: nil)
         | 
| 45 52 | 
             
                    expect(res.map(&:id)).to eq([@p5.id])
         | 
| @@ -71,6 +78,11 @@ describe Groovy::Query do | |
| 71 78 | 
             
                    res = TestProduct.where(tag_list: 'one, number two & three')
         | 
| 72 79 | 
             
                    expect(res.map(&:id)).to eq([@p1.id])
         | 
| 73 80 | 
             
                  end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                  it 'escapes required chars' do
         | 
| 83 | 
            +
                    res = TestProduct.where(name: 'Product 3: The Best')
         | 
| 84 | 
            +
                    expect(res.map(&:id)).to eq([@p3.id])
         | 
| 85 | 
            +
                  end
         | 
| 74 86 | 
             
                end
         | 
| 75 87 |  | 
| 76 88 | 
             
                describe 'lower/greater than search (timestamps)' do
         | 
| @@ -182,6 +194,13 @@ describe Groovy::Query do | |
| 182 194 | 
             
                    res = TestProduct.where.not(tag_list: nil)
         | 
| 183 195 | 
             
                    expect(res.map(&:id)).to eq([@p1.id, @p2.id, @p4.id])
         | 
| 184 196 | 
             
                  end
         | 
| 197 | 
            +
                  it 'works with other nil values too' do
         | 
| 198 | 
            +
                    res = TestProduct.where.not(visible: nil).where(tag_list: nil)
         | 
| 199 | 
            +
                    expect(res.map(&:id)).to eq([@p3.id, @p5.id])
         | 
| 200 | 
            +
             | 
| 201 | 
            +
                    res = TestProduct.where.not(tag_list: nil).where(name: nil)
         | 
| 202 | 
            +
                    expect(res.map(&:id)).to eq([])
         | 
| 203 | 
            +
                  end
         | 
| 185 204 | 
             
                  it 'works with other args too' do
         | 
| 186 205 | 
             
                    res = TestProduct.where.not(name: 'Product 2').where.not(tag_list: nil)
         | 
| 187 206 | 
             
                    expect(res.map(&:id)).to eq([@p1.id, @p4.id])
         | 
| @@ -213,6 +232,10 @@ describe Groovy::Query do | |
| 213 232 | 
             
                    res = TestProduct.not(tag_list: 'one, number two & three')
         | 
| 214 233 | 
             
                    expect(res.map(&:id)).to eq([@p2.id, @p3.id, @p4.id, @p5.id])
         | 
| 215 234 | 
             
                  end
         | 
| 235 | 
            +
                  it 'escapes required chars' do
         | 
| 236 | 
            +
                    res = TestProduct.not(name: 'Product 3: The Best')
         | 
| 237 | 
            +
                    expect(res.map(&:id)).to eq([@p1.id, @p2.id, @p4.id, @p5.id])
         | 
| 238 | 
            +
                  end
         | 
| 216 239 | 
             
                end
         | 
| 217 240 |  | 
| 218 241 | 
             
                context 'basic regex' do
         | 
    
        data/spec/search_spec.rb
    ADDED
    
    | @@ -0,0 +1,54 @@ | |
| 1 | 
            +
            require_relative './spec_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            describe Groovy::Model, 'searching' do
         | 
| 4 | 
            +
             | 
| 5 | 
            +
              before :all do
         | 
| 6 | 
            +
                Groovy.open('tmp/search', 'search_spec')
         | 
| 7 | 
            +
                load_schema! 'search_spec'
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                TestProduct.add_column :description, :string, index: true
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                TestProduct.create!(name: 'First product', description: 'Lorem ipsum dolor sit amet')
         | 
| 12 | 
            +
                TestProduct.create!(name: 'Second product', description: 'Lorea el ipsum poh loco')
         | 
| 13 | 
            +
              end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              after :all do
         | 
| 16 | 
            +
                Groovy.close('search_spec')
         | 
| 17 | 
            +
              end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
              describe 'single word, exact match' do
         | 
| 20 | 
            +
                it 'returns results' do
         | 
| 21 | 
            +
                  # res = TestProduct.search(description: 'sit')
         | 
| 22 | 
            +
                  res = TestProduct.search(description: 'sit')
         | 
| 23 | 
            +
                  expect(res.first.name).to eq('First product')
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  # res = TestProduct.search(description: 'loco')
         | 
| 26 | 
            +
                  res = TestProduct.search(description: 'loco')
         | 
| 27 | 
            +
                  expect(res.first.name).to eq('Second product')
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
              end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
              describe 'two consecutive words, exact match' do
         | 
| 32 | 
            +
                it 'returns results' do
         | 
| 33 | 
            +
                  # res = TestProduct.search(description: 'sit amet')
         | 
| 34 | 
            +
                  res = TestProduct.search(description: 'sit amet')
         | 
| 35 | 
            +
                  expect(res.first.name).to eq('First product')
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  # res = TestProduct.search(description: 'lorea el')
         | 
| 38 | 
            +
                  res = TestProduct.search(description: 'lorea el')
         | 
| 39 | 
            +
                  expect(res.first.name).to eq('Second product')
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
              end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
              describe 'two random words, both match' do
         | 
| 44 | 
            +
                it 'returns results' do
         | 
| 45 | 
            +
                  # res = TestProduct.search(description: 'amet ipsum')
         | 
| 46 | 
            +
                  res = TestProduct.search(description: 'amet ipsum')
         | 
| 47 | 
            +
                  expect(res.first.name).to eq('First product')
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  # res = TestProduct.search(description: 'poh el')
         | 
| 50 | 
            +
                  res = TestProduct.search(description: 'poh el')
         | 
| 51 | 
            +
                  expect(res.first.name).to eq('Second product')
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
              end
         | 
| 54 | 
            +
            end
         | 
    
        data/spec/spec_helper.rb
    CHANGED
    
    
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: groovy
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.4.3
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Tomás Pollak
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2020- | 
| 11 | 
            +
            date: 2020-10-13 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: rroonga
         | 
| @@ -100,6 +100,7 @@ files: | |
| 100 100 | 
             
            - spec/groovy_spec.rb
         | 
| 101 101 | 
             
            - spec/model_spec.rb
         | 
| 102 102 | 
             
            - spec/query_spec.rb
         | 
| 103 | 
            +
            - spec/search_spec.rb
         | 
| 103 104 | 
             
            - spec/spec_helper.rb
         | 
| 104 105 | 
             
            homepage: https://github.com/tomas/groovy
         | 
| 105 106 | 
             
            licenses: []
         |