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 +4 -4
- data/example/relations.rb +19 -4
- data/lib/groovy.rb +4 -4
- data/lib/groovy/model.rb +176 -75
- data/lib/groovy/query.rb +49 -24
- data/lib/groovy/schema.rb +25 -7
- data/lib/groovy/vector.rb +26 -5
- data/lib/groovy/version.rb +1 -1
- data/spec/query_spec.rb +10 -1
- data/spec/search_spec.rb +54 -0
- data/spec/spec_helper.rb +3 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1019bd31fda4785bec67322dfb8aeddfab4d0fa12143b23a8dd75f4a3c84515e
|
4
|
+
data.tar.gz: 44ab6a5fd87d136bc3b6915ab323fac95b6a8012f5cdc4ea2fb6498d5236cb3a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2ae8f4bcfb8d6166fa4ad8814521c498a44f3e512de23c4cb986d7ce7cd4ef98722a889c3a8e69e1f55bdfc9651f19a9b03e9d31bcb2f87552c73c194fc8f099
|
7
|
+
data.tar.gz: 4a29d7007af10d67f4265ef9e431fa347ebc979745c87037fbf8957e2441fc012904cf5385d8aad8d1da6708e91ffae6b407acecf6e3c8e19f2f0bc787d90639
|
data/example/relations.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
require 'bundler/setup'
|
2
2
|
require 'groovy'
|
3
3
|
|
4
|
-
Groovy.open('./db/
|
5
|
-
# Groovy.open('./db/
|
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 ==
|
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
|
data/lib/groovy.rb
CHANGED
@@ -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
|
data/lib/groovy/model.rb
CHANGED
@@ -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.
|
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
|
-
|
72
|
-
|
73
|
-
|
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
|
77
|
-
if
|
78
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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.
|
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(
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
102
|
-
|
135
|
+
def update_all(attrs)
|
136
|
+
find_each { |child| child.update_attributes(attrs) }
|
103
137
|
end
|
104
138
|
|
105
139
|
def delete_all
|
106
|
-
|
107
|
-
|
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
|
-
|
116
|
-
|
117
|
-
|
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
|
121
|
-
|
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(
|
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
|
177
|
-
|
178
|
-
|
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]
|
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, :
|
290
|
+
attr_reader :id, :attributes, :record, :changes
|
229
291
|
|
230
|
-
def initialize(attrs = nil, record = nil)
|
231
|
-
@attributes, @
|
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
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
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
|
-
|
241
|
-
|
242
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
319
|
-
#
|
320
|
-
|
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
|
-
|
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
|
-
|
351
|
-
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
|
-
|
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|
|
data/lib/groovy/query.rb
CHANGED
@@ -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
|
-
|
32
|
-
end
|
33
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/groovy/schema.rb
CHANGED
@@ -16,7 +16,9 @@ module Groovy
|
|
16
16
|
'boolean' => 'boolean',
|
17
17
|
'integer' => 'int32',
|
18
18
|
'big_integer' => 'int64',
|
19
|
-
'
|
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
|
-
|
44
|
+
# @singular_references ||=
|
45
|
+
get_names(table.columns.select(&:reference_column?).reject(&:vector?))
|
43
46
|
end
|
44
47
|
|
45
48
|
def plural_references
|
46
|
-
|
49
|
+
# @plural_references ||=
|
50
|
+
get_names(table.columns.select(&:vector?))
|
47
51
|
end
|
48
52
|
|
49
53
|
def attribute_columns
|
50
|
-
|
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
|
-
|
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(
|
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}")
|
data/lib/groovy/vector.rb
CHANGED
@@ -9,16 +9,37 @@ module Groovy
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def size
|
12
|
-
|
13
|
-
|
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
|
-
|
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
|
data/lib/groovy/version.rb
CHANGED
data/spec/query_spec.rb
CHANGED
@@ -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
|
data/spec/search_spec.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
CHANGED
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.
|
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-
|
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: []
|