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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 51ebfa4af97ba85e2b162fdf64eeec5f0eda2a3829a12759e6d8fb01420b2c6a
4
- data.tar.gz: 2b814b93565a791d87fb9cb77121d211c18d709c862b02c9c2c14feed54d8e2d
3
+ metadata.gz: 5ebda26142ef67b09916291cafb3941fd34db0310b259aaf5c2ecf1b5fee3596
4
+ data.tar.gz: 5ee7f4f6c39b0237b281c1d04f27c3cd77ce1d9bb840a34fe8de88aa5114d28b
5
5
  SHA512:
6
- metadata.gz: bbdcd0ef05d17dc23cfd6a1ee1886bb4b827397ff2385cf62e16edb66eaf0a68041774572797c11333774d3fa902c617c334de47044e88221c71f2c9d414e930
7
- data.tar.gz: b12cae355c2a12aa9e46d181ca05d94c9002cc90e0adbdec5b7194ab4ada0d65ce072cde9a777c119644079c9eedb3723f0754375d36d143fa23347ab0937085
6
+ metadata.gz: 4a10d18486ab2f434fd83b46c2d74da3851a80a227af4a42caff4a070d3d7701716a202e52b26591041cd7c57cdb144b9c2abee06186415aeebbdb0b276df55d
7
+ data.tar.gz: af55ec1bb42eaf1fec2de9431fe9b16369348f000a3d8740abb6ec1b02297297826b97cec34c1955615901366427d87bf1b02192320faf92c8d1e1a255c44477
@@ -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,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.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.find_each { |child| child.delete }
107
- # table.delete { |record| record.id > -1 }
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
- def similar_search(col, q)
116
- unless schema.index_columns.include?(col.to_sym)
117
- 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!"
118
162
  end
119
163
 
120
- table.select { |r| r[col].similar_search(q) }
121
- # 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
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(key, attributes = nil)
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 # key is attributes
177
- set_timestamp(key, :created_at)
178
- set_timestamp(key, :updated_at)
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] or raise "Context not defined: #{context_name}"
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, :refs, :record, :changes
296
+ attr_reader :id, :attributes, :record, :changes
229
297
 
230
- def initialize(attrs = nil, record = nil)
231
- @attributes, @refs, @vectors = {}, {}, {}
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
- # TODO: lazy load this
235
- # self.class.schema.singular_references.each do |col|
236
- # set_ref(col, record.public_send(col))
237
- # end
238
- end
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
- attrs ||= {}
241
- unless attrs.is_a?(Hash)
242
- raise ArgumentError.new("Attributes should be a hash")
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
- return set_ref(key, val) if val.respond_to?(:record) || val.is_a?(Groonga::Record)
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
- # self.class.table.delete(_key)
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
- record = self.class.table[id] # _key
319
- # TODO: fix duplication
320
- set_attributes(record.attributes)
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
- @refs[name]
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
- unless obj.nil? || obj.respond_to?(:record)
351
- obj = Model.initialize_from_record(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
- set_record(self.class.insert(attributes))
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|
@@ -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,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 == false || val.to_s.strip == '' ? '\\' : val.to_s
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 == false || val.to_s.strip == '' ? '\\' : val.to_s
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) * PER_PAGE
138
- offset(offset).limit(PER_PAGE) # returns self
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
- 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
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
- # FIXME: figure out the right way to do this.
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 parameters.include?(param)
221
- raise "Duplicate param: #{param}"
222
- end
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
- # puts "Offset is higher than table size!"
248
+ puts "Offset is higher than table size!"
232
249
  []
233
250
  end
234
251
 
235
252
  def execute
236
- 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?
237
257
  query = prepare_query
238
- puts "Finding records with query: #{query}" if ENV['DEBUG']
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
- puts "Sorting with #{sort_key_and_order}, #{sorting.inspect}" if ENV['DEBUG']
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
@@ -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,17 +41,40 @@ 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? })
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
- log "Adding index on #{col}"
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([table_name, col].join('.'))
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}")
@@ -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.9'.freeze
2
+ VERSION = '0.4.3'.freeze
3
3
  end
@@ -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)
@@ -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
@@ -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.9
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-02-25 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: []