groovy 0.3.0 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a25e5062d4c0acc9b6df07b5684ee43a2c0db55e2d2cf4fc2778c9034a60223f
4
- data.tar.gz: 2aebe2f7c50b0d44594e8f1f2933e2af5060e882186162268aadc70fbb2c4d80
3
+ metadata.gz: c3c4f437e2e27b7c9d405155bb305d02d989da2beb16a0c70b08ef28c5c07d0b
4
+ data.tar.gz: 47c29f69d84a6c012f395d728fa7a6a680936592741bdeef00c921e274547872
5
5
  SHA512:
6
- metadata.gz: 54f57cd2730644aae24f09073446d137a6da6fffc2affa4d96ff24dea075d0fbf6660841e6508d27a0e3133b0b85e360e27e61612baaac37c86a70012955bd3f
7
- data.tar.gz: 1534c7da96deb58c5845d4c7c22b5d61d0f41637e996b4ef97ffc835e7ef5f6f94d97a4f24407d3677f2cafb745b6764497dd7f51403ee5979a371372fbe35a1
6
+ metadata.gz: e1353493e18aaced9276d9c82236c1b2ef4529731f41f379d28415d3ce99bf63741152813eab5c2e613e7d9da8fd088c9bddf4bf3677f27e86019156c7187ced
7
+ data.tar.gz: 536c0c4180b8207444a3000e4dd7a5d7a3a9a516963721c2fbd8fe956aa7aa7bb0fdbb76c1599af5ec4a26c7b41c61ca1ecf05f0d3cf6815c838d0e734e95cdc
@@ -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
- self.id == other.id
409
+ self.class == other.class && self.id == other.id
410
+ end
411
+
412
+ def <=>(other)
413
+ self.class == other.class && self.id <=> other.id
331
414
  end
332
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.3.0'.freeze
2
+ VERSION = '0.4.4'.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.3.0
4
+ version: 0.4.4
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: []