groovy 0.2.8 → 0.4.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 59f58f216c8fc9465c4eb8569145b401f55e71682e6c5361addb623eb3ad36aa
4
- data.tar.gz: bdacfc59885d00a8f6457ebc707c653fd63a8251ed09ddcb133eed50f6a4d8df
3
+ metadata.gz: c2e735dd501cd86a3ee2ab21b111bf7aa4ab3d76d9d14acf8be86b97e34335b1
4
+ data.tar.gz: 97b3b88d1f297ab6d74b9818858272c8fa711a27a3c0d64964508988a3304b85
5
5
  SHA512:
6
- metadata.gz: 7890d931e4ce3c20b6fb7c25772c7a3c078022640090fe18a2b2c5215a4e819951ee97ceb1485df5cdad5e148c7670f6de6e4f84e69f329db41a01f12b3e67ee
7
- data.tar.gz: 823259a7565bad47388f1fedade8a0e2c0aa277f15d55fc1921f003c333d9be76fb01d98001dc002316941b3364ca9ca9bd92a199a95fd7221efaca8a28cfb8e
6
+ metadata.gz: deb85e6549f328555d795e542e282c6e3a50c574fdac769530648c69292f02ed469bc293304db863b7c9ac28a5bb653bd85eb79a7652f6390b3f5309fc492552
7
+ data.tar.gz: 7546011290684eca615cf4823df5886908fe823c9bee086a80504a01a7d3aa846a0fd34dc05efc023a061078ac3645d352618c64a03b614989cb51bc3ca4c5c9
@@ -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
@@ -1,12 +1,12 @@
1
1
  require 'bundler/setup'
2
2
  require 'groovy'
3
3
 
4
- Groovy.open('./db/2019', :current)
5
- # Groovy.open('./db/2020', :next)
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 == 2019 ? :current : :next
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
@@ -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
@@ -20,11 +20,16 @@ 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
- def self.included(base)
32
+ def self.included(base)
28
33
  base.extend(ClassMethods)
29
34
  base.include(Forwardable)
30
35
  base.table_name = base.name.sub(/y$/, 'ie') + 's'
@@ -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,63 +74,104 @@ module Groovy
61
74
  s.sync
62
75
 
63
76
  extend(PatriciaTrieMethods) if table.is_a?(Groonga::PatriciaTrie)
64
- s.attribute_columns.each { |col| add_attr_accessors(col) }
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
- # called from Query, so needs to be public
72
- def new_from_record(record)
73
- new(record.attributes, record)
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 find_and_init_record(id)
77
- if found = table[id]
78
- new_from_record(found)
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
- def find_records(&block)
83
- records = table.select(&block)
84
- records.map do|r|
85
- find_and_init_record(r.attributes['_key']['_id'])
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.id
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(key, attributes = nil)
96
- if record = insert(key, attributes)
97
- new_from_record(record)
98
- end
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 create!(key, attributes = nil)
102
- create(key, attributes) or raise "Invalid!"
135
+ def update_all(attrs)
136
+ find_each { |child| child.update_attributes(attrs) }
103
137
  end
104
138
 
105
139
  def delete_all
106
- all.each { |child| child.delete }.count
107
- # schema.rebuild!
140
+ # find_each { |child| child.delete }
141
+ table.delete { |record| record._id > -1 }
108
142
  end
109
143
 
144
+ # def dump_table!
145
+ # Groonga::TableDumper.new(table).dump
146
+ # # schema.rebuild!
147
+ # end
148
+
110
149
  # def column(name)
111
150
  # Groonga["#{table_name}.#{name}"] # .search, .similar_search, etc
112
151
  # end
113
152
 
114
- def similar_search(col, q)
115
- unless schema.index_columns.include?(col.to_sym)
116
- raise "Column '#{col}' doesn't have an index set!"
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!"
117
162
  end
118
163
 
119
- table.select { |r| r[col].similar_search(q) }
120
- # table.select("#{col}:#{q}", operator: Groonga::Operation::SIMILAR)
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
121
175
  end
122
176
 
123
177
  def unique_values_for(column, limit: -1, cache: false)
@@ -144,7 +198,7 @@ module Groovy
144
198
  end
145
199
 
146
200
  def query
147
- query_class.new(self, table)
201
+ query_class.new(self, table)
148
202
  end
149
203
 
150
204
  def scope(name, obj)
@@ -154,28 +208,25 @@ module Groovy
154
208
  end
155
209
  end
156
210
 
157
- [: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|
158
212
  define_method scope_method do |*args, &block|
159
213
  query.public_send(scope_method, *args, &block)
160
214
  end
161
215
  end
162
-
163
- # this seems to be the same as: `table[id]`
164
- # def search(key, options = nil)
165
- # raise "Not supported!" unless table.respond_to?(:search)
166
- # table.search(key, options)
167
- # end
168
216
 
169
217
  def_instance_delegators :table, :count, :size
170
218
 
171
219
  # called from instance too, so must by public
172
- def insert(key, attributes = nil)
220
+ def insert(attributes, key = nil)
221
+ set_timestamp(attributes, :created_at)
222
+ set_timestamp(attributes, :updated_at)
223
+
173
224
  if table.support_key?
225
+ raise "Key required" if key.nil?
174
226
  table.add(key, attributes)
175
- else # key is attributes
176
- set_timestamp(key, :created_at)
177
- set_timestamp(key, :updated_at)
178
- table.add(key)
227
+ else
228
+ raise "Key present, but unsupported" if key
229
+ table.add(attributes)
179
230
  end
180
231
  end
181
232
 
@@ -183,6 +234,17 @@ module Groovy
183
234
  obj[key_name] = Time.now if attribute_names.include?(key_name.to_sym)
184
235
  end
185
236
 
237
+ def callbacks
238
+ @callbacks ||= {}
239
+ end
240
+
241
+ [:before_create, :after_create].each do |event|
242
+ define_method(event) do |*method_names|
243
+ callbacks[:before_create] ||= []
244
+ callbacks[:before_create].push(*method_names)
245
+ end
246
+ end
247
+
186
248
  private
187
249
 
188
250
  def query_class
@@ -194,7 +256,8 @@ module Groovy
194
256
  end
195
257
 
196
258
  def db_context
197
- Groovy.contexts[context_name.to_sym] or raise "Context not defined: #{context_name}"
259
+ Groovy.contexts[context_name.to_sym] \
260
+ or raise "Context not defined: #{context_name} Please call Groovy.open('./db/path') first."
198
261
  end
199
262
 
200
263
  def add_attr_accessors(col)
@@ -224,30 +287,38 @@ module Groovy
224
287
  end
225
288
  end
226
289
 
227
- attr_reader :id, :attributes, :refs, :record, :changes
290
+ attr_reader :id, :attributes, :record, :changes
228
291
 
229
- def initialize(attrs = nil, record = nil)
230
- @attributes, @refs, @vectors = {}, {}, {}
292
+ def initialize(attrs = nil, record = nil, key = nil)
293
+ @attributes, @vectors, @_key = {}, {}, key # key is used on creation only
231
294
 
232
295
  if set_record(record)
233
- # TODO: lazy load this
234
- # self.class.schema.singular_references.each do |col|
235
- # set_ref(col, record.public_send(col))
236
- # end
237
- end
296
+ set_attributes_from_record(record)
297
+ else
298
+ attrs ||= {}
299
+ unless attrs.is_a?(Hash)
300
+ raise ArgumentError.new("Attributes should be a Hash")
301
+ end
238
302
 
239
- attrs ||= {}
240
- unless attrs.is_a?(Hash)
241
- raise ArgumentError.new("Attributes should be a hash")
303
+ # don't call set_attributes since we don't want to call
304
+ # setters, that might be overriden with custom logic.
305
+ # attrs.each { |k,v| self[k] = v }
306
+ set_attributes(attrs)
242
307
  end
243
308
 
244
- # don't call set_attributes since we don't want to call
245
- # setters, that might be overriden with custom logic.
246
- # attrs.each { |k,v| self[k] = v }
247
- set_attributes(attrs)
248
309
  @changes = {}
249
310
  end
250
311
 
312
+ # get reference to the actual record in the Groonga table,
313
+ # not the temporary one we get as part of a search result.
314
+ def load_record
315
+ self.class.table[id]
316
+ end
317
+
318
+ def inspect
319
+ "#<#{self.class.name} id:#{id.inspect} attributes:[#{self.class.attribute_names.join(', ')}]>"
320
+ end
321
+
251
322
  def new_record?
252
323
  id.nil?
253
324
  # _key.nil?
@@ -258,10 +329,12 @@ module Groovy
258
329
  end
259
330
 
260
331
  def []=(key, val)
261
- return set_ref(key, val) if val.respond_to?(:record) || val.is_a?(Groonga::Record)
332
+ if self.class.schema.singular_references.include?(key.to_sym) # val.respond_to?(:record) || val.is_a?(Groonga::Record)
333
+ return set_ref(key, val)
334
+ end
262
335
 
263
336
  unless self.class.attribute_names.include?(key.to_sym)
264
- raise "Invalid attribute: #{key}"
337
+ raise "Invalid attribute: #{key}"
265
338
  end
266
339
 
267
340
  set_attribute(key, val)
@@ -299,58 +372,85 @@ module Groovy
299
372
 
300
373
  def save!(options = {})
301
374
  raise "Invalid!" unless save
375
+ self
302
376
  end
303
377
 
304
378
  def delete
305
- record.delete # doesn't work if record.id doesn't match _key
306
- # self.class.table.delete(_key)
379
+ # record.delete # doesn't work if record.id doesn't match _key
380
+ self.class.table.delete(record._id) # record.record_id
307
381
  set_record(nil)
308
382
  self
309
383
  rescue Groonga::InvalidArgument => e
310
- puts "Error: #{e.inspect}"
311
- raise RecordNotPersisted
384
+ # puts "Error: #{e.inspect}"
385
+ raise RecordNotPersisted, e.message
312
386
  end
313
387
 
314
388
  def reload
389
+ raise RecordNotPersisted if id.nil?
315
390
  ensure_persisted!
316
- record = self.class.table[id] # _key
317
- # TODO: fix duplication
318
- set_attributes(record.attributes)
391
+ rec = self.class.table[id] # _key
392
+ # set_record(rec)
393
+ set_attributes_from_record(rec)
319
394
  @changes = {}
320
395
  self
321
396
  end
322
397
 
323
398
  def as_json(options = {})
324
- attributes
399
+ options[:only] ? attributes.slice(*options[:only]) : attributes
325
400
  end
326
401
 
327
402
  def ==(other)
328
403
  self.id == other.id
329
404
  end
330
405
 
406
+ def <=>(other)
407
+ self.id <=> other.id
408
+ end
409
+
331
410
  private
332
411
 
412
+ def get_record_attribute(key)
413
+ val = record[key]
414
+ if self.class.schema.time_column?(key)
415
+ fix_time_value(val)
416
+ else
417
+ val
418
+ end
419
+ end
420
+
421
+ def fix_time_value(val)
422
+ return val.to_i == 0 ? nil : val
423
+ end
424
+
333
425
  # def _key
334
426
  # return unless record
335
427
  # record.respond_to?(:_key) ? record._key : id
336
428
  # end
337
429
 
430
+ def set_attributes_from_record(rec)
431
+ self.class.attribute_names.each do |col|
432
+ public_send("#{col}=", get_record_attribute(col))
433
+ end
434
+ end
435
+
338
436
  def set_attribute(key, val)
339
437
  changes[key.to_sym] = [self[key], val] if changes # nil when initializing
340
438
  attributes[key.to_sym] = val
341
439
  end
342
440
 
343
441
  def get_ref(name)
344
- @refs[name]
442
+ if record and obj = record[name]
443
+ Model.initialize_from_record(obj)
444
+ end
345
445
  end
346
446
 
347
447
  def set_ref(name, obj)
348
- unless obj.nil? || obj.respond_to?(:record)
349
- obj = Model.initialize_from_record(obj)
448
+ if record.nil?
449
+ set_attribute(name, obj.id) # obj should be a groovy model or groonga record
450
+ else
451
+ obj = obj.record if obj.respond_to?(:record)
452
+ record[name] = obj
350
453
  end
351
-
352
- @refs[name] = obj
353
- set_attribute(name, obj.nil? ? nil : obj.key)
354
454
  end
355
455
 
356
456
  def set_record(obj)
@@ -359,10 +459,18 @@ module Groovy
359
459
  end
360
460
 
361
461
  def create
362
- set_record(self.class.insert(attributes))
462
+ fire_callbacks(:before_create)
463
+ set_record(self.class.insert(attributes, @_key))
464
+ fire_callbacks(:after_create)
363
465
  self
364
466
  end
365
467
 
468
+ def fire_callbacks(name)
469
+ if arr = self.class.callbacks[name] and arr.any?
470
+ arr.each { |fn| send(fn) }
471
+ end
472
+ end
473
+
366
474
  def update
367
475
  ensure_persisted!
368
476
  changes.each do |key, values|
@@ -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
- raise "Not an index column, so cannot do fulltext search: #{col}"
32
- end
33
- parameters.push(AND + "(#{col}:@#{q})")
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
- # def inspect
38
- # "<#{self.class.name} #{parameters}>"
39
- # end
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,10 +60,12 @@ module Groovy
46
60
  where(conditions).limit(1).first
47
61
  end
48
62
 
49
- def find_each(&block)
50
- in_batches(of: 10) do |group|
51
- group.each { |item| yield(item) }
63
+ def find_each(opts = {}, &block)
64
+ count = 0
65
+ in_batches({ of: 10 }.merge(opts)) do |group|
66
+ group.each { |item| count += 1; yield(item) }
52
67
  end
68
+ count
53
69
  end
54
70
 
55
71
  # http://groonga.org/docs/reference/grn_expr/query_syntax.html
@@ -74,7 +90,7 @@ module Groovy
74
90
  add_param(AND + str)
75
91
 
76
92
  else
77
- str = val.nil? || val.to_s.strip == '' ? '\\' : val.to_s
93
+ str = val.nil? || val === false || val.to_s.strip == '' ? "\"\"" : escape_val(val)
78
94
  add_param(AND + [key, str].join(':'))
79
95
  end
80
96
  end
@@ -108,7 +124,7 @@ module Groovy
108
124
  add_param(AND + str)
109
125
 
110
126
  else
111
- str = val.nil? || val.to_s.strip == '' ? '\\' : val.to_s
127
+ str = val.nil? || val === false || val.to_s.strip == '' ? "\"\"" : escape_val(val)
112
128
  add_param(AND + [key, str].join(':!')) # not
113
129
  end
114
130
  end
@@ -130,18 +146,18 @@ module Groovy
130
146
  self
131
147
  end
132
148
 
133
- def paginate(page = 1)
149
+ def paginate(page = 1, per_page: PER_PAGE)
134
150
  page = 1 if page.to_i < 1
135
- offset = ((page.to_i)-1) * PER_PAGE
136
- offset(offset).limit(PER_PAGE) # returns self
151
+ offset = ((page.to_i)-1) * per_page
152
+ offset(offset).limit(per_page) # returns self
137
153
  end
138
154
 
139
155
  # sort_by(title: :asc)
140
156
  def sort_by(hash)
141
- if hash.is_a?(String) # e.g. title.desc
142
- 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('.')
143
159
  hash = {}
144
- hash[param] = dir
160
+ hash[param] = dir || 'asc'
145
161
  end
146
162
 
147
163
  sorting[:by] = hash.keys.map do |key|
@@ -181,13 +197,21 @@ module Groovy
181
197
  records.each { |r| block.call(r) }
182
198
  end
183
199
 
200
+ def update_all(attrs)
201
+ each { |r| r.update_attributes(attrs) }
202
+ end
203
+
184
204
  def total_entries
185
205
  results # ensure query has been run
186
206
  @total_entries
187
207
  end
188
208
 
189
- def last
190
- records[size-1]
209
+ def last(count = 1)
210
+ if count > 1
211
+ records[(size-count)..-1]
212
+ else
213
+ records[size-1]
214
+ end
191
215
  end
192
216
 
193
217
  def in_batches(of: 1000, from: nil, &block)
@@ -205,46 +229,45 @@ module Groovy
205
229
 
206
230
  def records
207
231
  @records ||= results.map do |r|
208
- # FIXME: figure out the right way to do this.
209
- id = r.attributes['_value']['_key']['_id']
210
- model.find_and_init_record(id)
232
+ model.new_from_record(r)
211
233
  end
212
234
  end
213
235
 
214
236
  private
215
- attr_reader :model, :table, :options
237
+ attr_reader :model, :table, :options, :select_block
216
238
 
217
239
  def add_param(param)
218
- if parameters.include?(param)
219
- raise "Duplicate param: #{param}"
220
- end
221
-
222
- # if param matches blank/nil, put at the end of the stack
223
- 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)
224
243
  end
225
244
 
226
245
  def results
227
246
  @results ||= execute
228
247
  rescue Groonga::TooLargeOffset
229
- # puts "Offset is higher than table size!"
248
+ puts "Offset is higher than table size!"
230
249
  []
231
250
  end
232
251
 
233
252
  def execute
234
- set = if parameters.any?
253
+ set = if select_block
254
+ debug "Finding records with select block"
255
+ table.select { |record| select_block.call(record) }
256
+ elsif parameters.any?
235
257
  query = prepare_query
236
- puts "Finding records with query: #{query}" if ENV['DEBUG']
258
+ debug "Finding records with query: #{query}"
237
259
  table.select(query, options)
238
260
  else
261
+ debug "Finding records with options: #{options.inspect}"
239
262
  table.select(options)
240
263
  end
241
264
 
242
265
  @total_entries = set.size
243
266
 
244
- puts "Sorting with #{sort_key_and_order}, #{sorting.inspect}" if ENV['DEBUG']
267
+ debug "Sorting with #{sort_key_and_order}, #{sorting.inspect}"
245
268
  set = set.sort(sort_key_and_order, {
246
269
  limit: sorting[:limit],
247
- offset: sorting[:offset]
270
+ offset: sorting[:offset], # [sorting[:offset], @total_entries].min
248
271
  })
249
272
 
250
273
  sorting[:group_by] ? set.group(group_by) : set
@@ -264,6 +287,10 @@ module Groovy
264
287
  sorting[:by] or [{ key: @default_sort_key, order: :asc }]
265
288
  end
266
289
 
290
+ def escape_val(val)
291
+ val.to_s.gsub(':', '\:')
292
+ end
293
+
267
294
  def prepare_query
268
295
  space_regex = Regexp.new('\s([' + VALID_QUERY_CHARS + '])')
269
296
  query = parameters.join(' ').split(/ or /i).map do |part|
@@ -272,6 +299,11 @@ module Groovy
272
299
  .gsub(/(\d\d):(\d\d):(\d\d)/, '\1\:\2\:\3') # escape hh:mm:ss in timestamps
273
300
  end.join(' OR ').sub(/^-/, '_id:>0 -') #.gsub(' OR -', ' -')
274
301
  end
302
+
303
+ def debug(str)
304
+ puts str if ENV['DEBUG']
305
+ end
306
+
275
307
  end
276
308
 
277
309
  end
@@ -16,7 +16,9 @@ module Groovy
16
16
  'boolean' => 'boolean',
17
17
  'integer' => 'int32',
18
18
  'big_integer' => 'int64',
19
- 'time' => 'time'
19
+ 'date' => 'date',
20
+ 'time' => 'time',
21
+ 'datetime' => 'time'
20
22
  }.freeze
21
23
 
22
24
  attr_reader :index_columns
@@ -39,15 +41,27 @@ module Groovy
39
41
  end
40
42
 
41
43
  def singular_references
42
- get_names table.columns.select(&:reference_column?).reject(&:vector?)
44
+ # @singular_references ||=
45
+ get_names(table.columns.select(&:reference_column?).reject(&:vector?))
43
46
  end
44
47
 
45
48
  def plural_references
46
- get_names table.columns.select(&:vector?)
49
+ # @plural_references ||=
50
+ get_names(table.columns.select(&:vector?))
47
51
  end
48
52
 
49
53
  def attribute_columns
50
- get_names table.columns.select(&:column?)
54
+ # @attribute_columns ||=
55
+ get_names(table.columns.select { |c| c.column? && !c.reference_column? && !c.vector? })
56
+ end
57
+
58
+ def time_columns
59
+ # @time_columns ||=
60
+ get_names(table.columns.select { |c| c.column? && c.range.name == 'Time' })
61
+ end
62
+
63
+ def time_column?(name)
64
+ time_columns.include?(name)
51
65
  end
52
66
 
53
67
  def rebuild!
@@ -91,6 +105,7 @@ module Groovy
91
105
  @index_columns.each do |col|
92
106
  add_index_on(col)
93
107
  end
108
+ self
94
109
  end
95
110
 
96
111
  private
@@ -102,13 +117,15 @@ module Groovy
102
117
  @table = @search_table = nil # clear cached vars
103
118
  end
104
119
 
105
- def add_index_on(col)
120
+ def add_index_on(col, opts = {})
106
121
  ensure_search_table!
107
122
  return false if search_table.have_column?([table_name, col].join('_'))
108
123
 
109
- log "Adding index on #{col}"
124
+ name_col = [table_name, col].join('.')
125
+ log "Adding index on #{name_col}"
110
126
  Groonga::Schema.change_table(SEARCH_TABLE_NAME, context: context) do |table|
111
- table.index([table_name, col].join('.'))
127
+ # table.index(name_col, name: name_col, with_position: true, with_section: true)
128
+ table.index(name_col, name: name_col.sub('.', '_'))
112
129
  end
113
130
  end
114
131
 
@@ -117,6 +134,7 @@ module Groovy
117
134
  opts = (@opts[:search_table] || {}).merge({
118
135
  type: :patricia_trie,
119
136
  normalizer: :NormalizerAuto,
137
+ key_type: "ShortText",
120
138
  default_tokenizer: "TokenBigram"
121
139
  })
122
140
  log("Creating search table with options: #{opts.inspect}")
@@ -9,16 +9,37 @@ module Groovy
9
9
  end
10
10
 
11
11
  def size
12
- # records.count
13
- items.count # so we filter out removed ones
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
- items.to_s
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
@@ -1,3 +1,3 @@
1
1
  module Groovy
2
- VERSION = '0.2.8'.freeze
2
+ VERSION = '0.4.2'.freeze
3
3
  end
@@ -58,6 +58,23 @@ describe Groovy::Model do
58
58
  end
59
59
 
60
60
  describe '.delete_all' do
61
+
62
+ before do
63
+ @first = TestProduct.create!(name: 'A product', price: 100)
64
+ @second = TestProduct.create!(name: 'Another product', price: 200)
65
+ expect(TestProduct.count).to eq(2)
66
+ end
67
+
68
+ it 'deletes all' do
69
+ TestProduct.delete_all
70
+ expect(TestProduct.count).to eq(0)
71
+
72
+ # expect { @first.reload }.to raise_error
73
+ # expect { @second.reload }.to raise_error
74
+ # expect { TestProduct.find(@first.id) }.to raise_error
75
+ # expect { TestProduct.find(@second.id) }.to raise_error
76
+ end
77
+
61
78
  end
62
79
 
63
80
  describe '#[]' do
@@ -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
@@ -17,11 +17,36 @@ describe Groovy::Query do
17
17
  end
18
18
 
19
19
  describe '#where' do
20
+ describe 'boolean value' do
21
+ it 'finds expected records' do
22
+ res = TestProduct.where(visible: true)
23
+ expect(res.map(&:id)).to eq([@p1.id, @p3.id, @p5.id])
24
+
25
+ res = TestProduct.where(visible: false)
26
+ expect(res.map(&:id)).to eq([@p2.id, @p4.id])
27
+ end
28
+
29
+ it 'works with other args too' do
30
+ res = TestProduct.where(visible: true).where(name: 'Product 5')
31
+ expect(res.map(&:id)).to eq([@p5.id])
32
+
33
+ res = TestProduct.where(visible: false).where(name: 'Product 2')
34
+ expect(res.map(&:id)).to eq([@p2.id])
35
+ end
36
+ end
37
+
20
38
  describe 'nil value' do
21
39
  it 'finds expected records' do
22
40
  res = TestProduct.where(tag_list: nil)
23
41
  expect(res.map(&:id)).to eq([@p3.id, @p5.id])
24
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
25
50
  it 'works with other args too' do
26
51
  res = TestProduct.where(name: 'Product 5').where(tag_list: nil)
27
52
  expect(res.map(&:id)).to eq([@p5.id])
@@ -53,6 +78,11 @@ describe Groovy::Query do
53
78
  res = TestProduct.where(tag_list: 'one, number two & three')
54
79
  expect(res.map(&:id)).to eq([@p1.id])
55
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
56
86
  end
57
87
 
58
88
  describe 'lower/greater than search (timestamps)' do
@@ -141,11 +171,36 @@ describe Groovy::Query do
141
171
  end
142
172
 
143
173
  describe '#not' do
174
+ describe 'boolean value' do
175
+ it 'finds expected records' do
176
+ res = TestProduct.where.not(visible: true)
177
+ expect(res.map(&:id)).to eq([@p2.id, @p4.id])
178
+
179
+ res = TestProduct.where.not(visible: false)
180
+ expect(res.map(&:id)).to eq([@p1.id, @p3.id, @p5.id])
181
+ end
182
+
183
+ it 'works with other args too' do
184
+ res = TestProduct.where.not(visible: true).where(name: 'Product 2')
185
+ expect(res.map(&:id)).to eq([@p2.id])
186
+
187
+ res = TestProduct.where.not(visible: false).where(name: 'Product 5')
188
+ expect(res.map(&:id)).to eq([@p5.id])
189
+ end
190
+ end
191
+
144
192
  describe 'nil value' do
145
193
  it 'finds expected records' do
146
194
  res = TestProduct.where.not(tag_list: nil)
147
195
  expect(res.map(&:id)).to eq([@p1.id, @p2.id, @p4.id])
148
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
149
204
  it 'works with other args too' do
150
205
  res = TestProduct.where.not(name: 'Product 2').where.not(tag_list: nil)
151
206
  expect(res.map(&:id)).to eq([@p1.id, @p4.id])
@@ -177,6 +232,10 @@ describe Groovy::Query do
177
232
  res = TestProduct.not(tag_list: 'one, number two & three')
178
233
  expect(res.map(&:id)).to eq([@p2.id, @p3.id, @p4.id, @p5.id])
179
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
180
239
  end
181
240
 
182
241
  context 'basic regex' do
@@ -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
@@ -4,6 +4,9 @@ Bundler.require(:default, :test)
4
4
  require 'rspec/core'
5
5
  require './lib/groovy'
6
6
 
7
+ # Groonga::Logger.path = "/tmp/groonga.log"
8
+ # Groonga::Logger.max_level = :debug
9
+
7
10
  RSpec.configure do |config|
8
11
  config.before(:each) do
9
12
  # Groonga::Context.default = nil
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.2.8
4
+ version: 0.4.2
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: 2019-10-16 00:00:00.000000000 Z
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: []