groovy 0.3.0 → 0.4.0

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: 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: []