groovy 0.3.0 → 0.4.0

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: a25e5062d4c0acc9b6df07b5684ee43a2c0db55e2d2cf4fc2778c9034a60223f
4
- data.tar.gz: 2aebe2f7c50b0d44594e8f1f2933e2af5060e882186162268aadc70fbb2c4d80
3
+ metadata.gz: 1019bd31fda4785bec67322dfb8aeddfab4d0fa12143b23a8dd75f4a3c84515e
4
+ data.tar.gz: 44ab6a5fd87d136bc3b6915ab323fac95b6a8012f5cdc4ea2fb6498d5236cb3a
5
5
  SHA512:
6
- metadata.gz: 54f57cd2730644aae24f09073446d137a6da6fffc2affa4d96ff24dea075d0fbf6660841e6508d27a0e3133b0b85e360e27e61612baaac37c86a70012955bd3f
7
- data.tar.gz: 1534c7da96deb58c5845d4c7c22b5d61d0f41637e996b4ef97ffc835e7ef5f6f94d97a4f24407d3677f2cafb745b6764497dd7f51403ee5979a371372fbe35a1
6
+ metadata.gz: 2ae8f4bcfb8d6166fa4ad8814521c498a44f3e512de23c4cb986d7ce7cd4ef98722a889c3a8e69e1f55bdfc9651f19a9b03e9d31bcb2f87552c73c194fc8f099
7
+ data.tar.gz: 4a29d7007af10d67f4265ef9e431fa347ebc979745c87037fbf8957e2441fc012904cf5385d8aad8d1da6708e91ffae6b407acecf6e3c8e19f2f0bc787d90639
@@ -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
@@ -50,6 +50,21 @@ class Location
50
50
  end
51
51
  end
52
52
 
53
+ # from groonga tests
54
+ #
55
+ # schema.create_table("Users", :type => :hash, :key_type => "UInt32") { |table| }
56
+ #
57
+ # schema.create_table("Communities", :type => :hash, :key_type => "ShortText") do |table|
58
+ # table.reference("users", "Users", :type => :vector)
59
+ # end
60
+ #
61
+ # groonga = @communities.add("groonga")
62
+ # morita = @users.add(29)
63
+ # groonga["users"] = [morita]
64
+ #
65
+ # assert_equal([29], @users.collect {|record| record.key})
66
+ # assert_equal([29], groonga["users"].collect {|record| record.key})
67
+
53
68
  def insert_places(count = 1000)
54
69
  puts "Inserting #{count} places!"
55
70
  count.times do |i|
@@ -57,4 +72,4 @@ def insert_places(count = 1000)
57
72
  end
58
73
  end
59
74
 
60
- insert_places if Place.count == 0
75
+ insert_places if Place.count == 0
@@ -16,18 +16,18 @@ module Groovy
16
16
  def [](name)
17
17
  contexts[name.to_sym]
18
18
  end
19
-
19
+
20
20
  def first_context_name
21
21
  contexts.keys.first
22
22
  end
23
23
 
24
24
  def open(db_path, name = :default, opts = {})
25
25
  unless db_path.is_a?(String)
26
- raise ArgumentError, "Invalid db_path: #{db_path}"
26
+ raise ArgumentError, "Invalid db_path: #{db_path}"
27
27
  end
28
28
 
29
29
  if contexts[name.to_sym]
30
- raise ArgumentError, "Context already defined: #{name}"
30
+ raise ArgumentError, "Context already defined: #{name}"
31
31
  end
32
32
 
33
33
  contexts[name.to_sym] = if name == :default
@@ -40,7 +40,7 @@ module Groovy
40
40
  def close(name = :default)
41
41
  ctx = contexts[name.to_sym] or raise ContextNotFound.new(name)
42
42
  contexts.delete(name.to_sym)
43
- ctx.close
43
+ ctx.close
44
44
  rescue Groonga::Closed => e
45
45
  raise ContextAlreadyClosed
46
46
  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,25 @@ 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
+ [: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
+
174
224
  if table.support_key?
225
+ raise "Key required" if key.nil?
175
226
  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)
227
+ else
228
+ raise "Key present, but unsupported" if key
229
+ table.add(attributes)
180
230
  end
181
231
  end
182
232
 
@@ -184,6 +234,17 @@ module Groovy
184
234
  obj[key_name] = Time.now if attribute_names.include?(key_name.to_sym)
185
235
  end
186
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
+
187
248
  private
188
249
 
189
250
  def query_class
@@ -195,7 +256,8 @@ module Groovy
195
256
  end
196
257
 
197
258
  def db_context
198
- 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."
199
261
  end
200
262
 
201
263
  def add_attr_accessors(col)
@@ -225,30 +287,32 @@ module Groovy
225
287
  end
226
288
  end
227
289
 
228
- attr_reader :id, :attributes, :refs, :record, :changes
290
+ attr_reader :id, :attributes, :record, :changes
229
291
 
230
- def initialize(attrs = nil, record = nil)
231
- @attributes, @refs, @vectors = {}, {}, {}
292
+ def initialize(attrs = nil, record = nil, key = nil)
293
+ @attributes, @vectors, @_key = {}, {}, key # key is used on creation only
232
294
 
233
295
  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
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
239
302
 
240
- attrs ||= {}
241
- unless attrs.is_a?(Hash)
242
- 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)
243
307
  end
244
308
 
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
309
  @changes = {}
250
310
  end
251
311
 
312
+ def inspect
313
+ "#<#{self.class.name} id:#{id.inspect} attributes:[#{self.class.attribute_names.join(', ')}]>"
314
+ end
315
+
252
316
  def new_record?
253
317
  id.nil?
254
318
  # _key.nil?
@@ -259,7 +323,9 @@ module Groovy
259
323
  end
260
324
 
261
325
  def []=(key, val)
262
- return set_ref(key, val) if val.respond_to?(:record) || val.is_a?(Groonga::Record)
326
+ if self.class.schema.singular_references.include?(key.to_sym) # val.respond_to?(:record) || val.is_a?(Groonga::Record)
327
+ return set_ref(key, val)
328
+ end
263
329
 
264
330
  unless self.class.attribute_names.include?(key.to_sym)
265
331
  raise "Invalid attribute: #{key}"
@@ -300,59 +366,86 @@ module Groovy
300
366
 
301
367
  def save!(options = {})
302
368
  raise "Invalid!" unless save
369
+ self
303
370
  end
304
371
 
305
372
  def delete
306
- record.delete # doesn't work if record.id doesn't match _key
307
- # self.class.table.delete(_key)
373
+ # record.delete # doesn't work if record.id doesn't match _key
374
+ self.class.table.delete(record._id) # record.record_id
308
375
  set_record(nil)
309
376
  self
310
377
  rescue Groonga::InvalidArgument => e
311
- puts "Error: #{e.inspect}"
312
- raise RecordNotPersisted
378
+ # puts "Error: #{e.inspect}"
379
+ raise RecordNotPersisted, e.message
313
380
  end
314
381
 
315
382
  def reload
316
383
  raise RecordNotPersisted if id.nil?
317
384
  ensure_persisted!
318
- record = self.class.table[id] # _key
319
- # TODO: fix duplication
320
- set_attributes(record.attributes)
385
+ rec = self.class.table[id] # _key
386
+ # set_record(rec)
387
+ set_attributes_from_record(rec)
321
388
  @changes = {}
322
389
  self
323
390
  end
324
391
 
325
392
  def as_json(options = {})
326
- attributes
393
+ options[:only] ? attributes.slice(*options[:only]) : attributes
327
394
  end
328
395
 
329
396
  def ==(other)
330
397
  self.id == other.id
331
398
  end
332
399
 
400
+ def <=>(other)
401
+ self.id <=> other.id
402
+ end
403
+
333
404
  private
334
405
 
406
+ def get_record_attribute(key)
407
+ if val = record[key]
408
+ if self.class.schema.time_column?(key)
409
+ fix_time_value(val)
410
+ else
411
+ val
412
+ end
413
+ end
414
+ end
415
+
416
+ def fix_time_value(val)
417
+ return val.to_i == 0 ? nil : val
418
+ end
419
+
335
420
  # def _key
336
421
  # return unless record
337
422
  # record.respond_to?(:_key) ? record._key : id
338
423
  # end
339
424
 
425
+ def set_attributes_from_record(rec)
426
+ self.class.attribute_names.each do |col|
427
+ public_send("#{col}=", get_record_attribute(col))
428
+ end
429
+ end
430
+
340
431
  def set_attribute(key, val)
341
432
  changes[key.to_sym] = [self[key], val] if changes # nil when initializing
342
433
  attributes[key.to_sym] = val
343
434
  end
344
435
 
345
436
  def get_ref(name)
346
- @refs[name]
437
+ if record and obj = record[name]
438
+ Model.initialize_from_record(obj)
439
+ end
347
440
  end
348
441
 
349
442
  def set_ref(name, obj)
350
- unless obj.nil? || obj.respond_to?(:record)
351
- obj = Model.initialize_from_record(obj)
443
+ if record.nil?
444
+ set_attribute(name, obj.id) # obj should be a groovy model or groonga record
445
+ else
446
+ obj = obj.record if obj.respond_to?(:record)
447
+ record[name] = obj
352
448
  end
353
-
354
- @refs[name] = obj
355
- set_attribute(name, obj.nil? ? nil : obj.key)
356
449
  end
357
450
 
358
451
  def set_record(obj)
@@ -361,10 +454,18 @@ module Groovy
361
454
  end
362
455
 
363
456
  def create
364
- set_record(self.class.insert(attributes))
457
+ fire_callbacks(:before_create)
458
+ set_record(self.class.insert(attributes, @_key))
459
+ fire_callbacks(:after_create)
365
460
  self
366
461
  end
367
462
 
463
+ def fire_callbacks(name)
464
+ if arr = self.class.callbacks[name] and arr.any?
465
+ arr.each { |fn| send(fn) }
466
+ end
467
+ end
468
+
368
469
  def update
369
470
  ensure_persisted!
370
471
  changes.each do |key, values|
@@ -25,19 +25,28 @@ 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
40
-
41
50
  def find(id)
42
51
  find_by(_id: id)
43
52
  end
@@ -46,9 +55,9 @@ module Groovy
46
55
  where(conditions).limit(1).first
47
56
  end
48
57
 
49
- def find_each(&block)
58
+ def find_each(opts = {}, &block)
50
59
  count = 0
51
- in_batches(of: 10) do |group|
60
+ in_batches({ of: 10 }.merge(opts)) do |group|
52
61
  group.each { |item| count += 1; yield(item) }
53
62
  end
54
63
  count
@@ -76,7 +85,7 @@ module Groovy
76
85
  add_param(AND + str)
77
86
 
78
87
  else
79
- str = val.nil? || val == false || val.to_s.strip == '' ? '\\' : val.to_s
88
+ str = val.nil? || val == false || val.to_s.strip == '' ? '\\' : escape_val(val)
80
89
  add_param(AND + [key, str].join(':'))
81
90
  end
82
91
  end
@@ -110,7 +119,7 @@ module Groovy
110
119
  add_param(AND + str)
111
120
 
112
121
  else
113
- str = val.nil? || val == false || val.to_s.strip == '' ? '\\' : val.to_s
122
+ str = val.nil? || val == false || val.to_s.strip == '' ? '\\' : escape_val(val)
114
123
  add_param(AND + [key, str].join(':!')) # not
115
124
  end
116
125
  end
@@ -140,10 +149,10 @@ module Groovy
140
149
 
141
150
  # sort_by(title: :asc)
142
151
  def sort_by(hash)
143
- if hash.is_a?(String) # e.g. title.desc
144
- param, dir = hash.split('.')
152
+ if hash.is_a?(String) || hash.is_a?(Symbol) # e.g. 'title.desc' or :title (asc by default)
153
+ param, dir = hash.to_s.split('.')
145
154
  hash = {}
146
- hash[param] = dir
155
+ hash[param] = dir || 'asc'
147
156
  end
148
157
 
149
158
  sorting[:by] = hash.keys.map do |key|
@@ -183,13 +192,21 @@ module Groovy
183
192
  records.each { |r| block.call(r) }
184
193
  end
185
194
 
195
+ def update_all(attrs)
196
+ each { |r| r.update_attributes(attrs) }
197
+ end
198
+
186
199
  def total_entries
187
200
  results # ensure query has been run
188
201
  @total_entries
189
202
  end
190
203
 
191
- def last
192
- records[size-1]
204
+ def last(count = 1)
205
+ if count > 1
206
+ records[(size-count)..-1]
207
+ else
208
+ records[size-1]
209
+ end
193
210
  end
194
211
 
195
212
  def in_batches(of: 1000, from: nil, &block)
@@ -207,9 +224,7 @@ module Groovy
207
224
 
208
225
  def records
209
226
  @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)
227
+ model.new_from_record(r)
213
228
  end
214
229
  end
215
230
 
@@ -228,25 +243,26 @@ module Groovy
228
243
  def results
229
244
  @results ||= execute
230
245
  rescue Groonga::TooLargeOffset
231
- # puts "Offset is higher than table size!"
246
+ puts "Offset is higher than table size!"
232
247
  []
233
248
  end
234
249
 
235
250
  def execute
236
251
  set = if parameters.any?
237
252
  query = prepare_query
238
- puts "Finding records with query: #{query}" if ENV['DEBUG']
253
+ debug "Finding records with query: #{query}"
239
254
  table.select(query, options)
240
255
  else
256
+ debug "Finding records with options: #{options.inspect}"
241
257
  table.select(options)
242
258
  end
243
259
 
244
260
  @total_entries = set.size
245
261
 
246
- puts "Sorting with #{sort_key_and_order}, #{sorting.inspect}" if ENV['DEBUG']
262
+ debug "Sorting with #{sort_key_and_order}, #{sorting.inspect}"
247
263
  set = set.sort(sort_key_and_order, {
248
264
  limit: sorting[:limit],
249
- offset: sorting[:offset]
265
+ offset: sorting[:offset], # [sorting[:offset], @total_entries].min
250
266
  })
251
267
 
252
268
  sorting[:group_by] ? set.group(group_by) : set
@@ -266,6 +282,10 @@ module Groovy
266
282
  sorting[:by] or [{ key: @default_sort_key, order: :asc }]
267
283
  end
268
284
 
285
+ def escape_val(val)
286
+ val.to_s.gsub(':', '\:')
287
+ end
288
+
269
289
  def prepare_query
270
290
  space_regex = Regexp.new('\s([' + VALID_QUERY_CHARS + '])')
271
291
  query = parameters.join(' ').split(/ or /i).map do |part|
@@ -274,6 +294,11 @@ module Groovy
274
294
  .gsub(/(\d\d):(\d\d):(\d\d)/, '\1\:\2\:\3') # escape hh:mm:ss in timestamps
275
295
  end.join(' OR ').sub(/^-/, '_id:>0 -') #.gsub(' OR -', ' -')
276
296
  end
297
+
298
+ def debug(str)
299
+ puts str if ENV['DEBUG']
300
+ end
301
+
277
302
  end
278
303
 
279
304
  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.3.0'.freeze
2
+ VERSION = '0.4.0'.freeze
3
3
  end
@@ -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
@@ -71,6 +71,11 @@ describe Groovy::Query do
71
71
  res = TestProduct.where(tag_list: 'one, number two & three')
72
72
  expect(res.map(&:id)).to eq([@p1.id])
73
73
  end
74
+
75
+ it 'escapes required chars' do
76
+ res = TestProduct.where(name: 'Product 3: The Best')
77
+ expect(res.map(&:id)).to eq([@p3.id])
78
+ end
74
79
  end
75
80
 
76
81
  describe 'lower/greater than search (timestamps)' do
@@ -213,6 +218,10 @@ describe Groovy::Query do
213
218
  res = TestProduct.not(tag_list: 'one, number two & three')
214
219
  expect(res.map(&:id)).to eq([@p2.id, @p3.id, @p4.id, @p5.id])
215
220
  end
221
+ it 'escapes required chars' do
222
+ res = TestProduct.not(name: 'Product 3: The Best')
223
+ expect(res.map(&:id)).to eq([@p1.id, @p2.id, @p4.id, @p5.id])
224
+ end
216
225
  end
217
226
 
218
227
  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.0
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-09-09 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: []