groovy 0.2.9 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/example/basic.rb +3 -2
- data/example/relations.rb +20 -4
- data/lib/groovy.rb +12 -4
- data/lib/groovy/model.rb +187 -75
- data/lib/groovy/query.rb +64 -34
- data/lib/groovy/schema.rb +36 -7
- data/lib/groovy/vector.rb +26 -5
- data/lib/groovy/version.rb +1 -1
- data/spec/model_spec.rb +9 -1
- data/spec/query_spec.rb +24 -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: 5ebda26142ef67b09916291cafb3941fd34db0310b259aaf5c2ecf1b5fee3596
|
4
|
+
data.tar.gz: 5ee7f4f6c39b0237b281c1d04f27c3cd77ce1d9bb840a34fe8de88aa5114d28b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4a10d18486ab2f434fd83b46c2d74da3851a80a227af4a42caff4a070d3d7701716a202e52b26591041cd7c57cdb144b9c2abee06186415aeebbdb0b276df55d
|
7
|
+
data.tar.gz: af55ec1bb42eaf1fec2de9431fe9b16369348f000a3d8740abb6ec1b02297297826b97cec34c1955615901366427d87bf1b02192320faf92c8d1e1a255c44477
|
data/example/basic.rb
CHANGED
@@ -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
|
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
|
@@ -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
|
data/lib/groovy.rb
CHANGED
@@ -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
|
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,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(
|
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
|
177
|
-
|
178
|
-
|
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]
|
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, :
|
296
|
+
attr_reader :id, :attributes, :record, :changes
|
229
297
|
|
230
|
-
def initialize(attrs = nil, record = nil)
|
231
|
-
@attributes, @
|
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
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
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
|
-
|
241
|
-
|
242
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
319
|
-
#
|
320
|
-
|
397
|
+
rec = self.class.table[id] # _key
|
398
|
+
# set_record(rec)
|
399
|
+
set_attributes_from_record(rec)
|
321
400
|
@changes = {}
|
322
401
|
self
|
323
402
|
end
|
324
403
|
|
325
404
|
def as_json(options = {})
|
326
|
-
attributes
|
405
|
+
options[:only] ? attributes.slice(*options[:only]) : attributes
|
327
406
|
end
|
328
407
|
|
329
408
|
def ==(other)
|
330
409
|
self.id == other.id
|
331
410
|
end
|
332
411
|
|
412
|
+
def <=>(other)
|
413
|
+
self.id <=> other.id
|
414
|
+
end
|
415
|
+
|
333
416
|
private
|
334
417
|
|
418
|
+
def get_record_attribute(key)
|
419
|
+
val = record[key]
|
420
|
+
if self.class.schema.time_columns.include?(key)
|
421
|
+
fix_time_value(val)
|
422
|
+
else
|
423
|
+
val
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
def fix_time_value(val)
|
428
|
+
return val.to_i == 0 ? nil : val
|
429
|
+
end
|
430
|
+
|
335
431
|
# def _key
|
336
432
|
# return unless record
|
337
433
|
# record.respond_to?(:_key) ? record._key : id
|
338
434
|
# end
|
339
435
|
|
436
|
+
def set_attributes_from_record(rec)
|
437
|
+
self.class.attribute_names.each do |col|
|
438
|
+
public_send("#{col}=", get_record_attribute(col))
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
340
442
|
def set_attribute(key, val)
|
341
443
|
changes[key.to_sym] = [self[key], val] if changes # nil when initializing
|
342
444
|
attributes[key.to_sym] = val
|
343
445
|
end
|
344
446
|
|
345
447
|
def get_ref(name)
|
346
|
-
|
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
|
-
|
351
|
-
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
|
-
|
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|
|
data/lib/groovy/query.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
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
|
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
|
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) *
|
138
|
-
offset(offset).limit(
|
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
|
-
|
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
|
-
|
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
|
221
|
-
|
222
|
-
|
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
|
-
|
248
|
+
puts "Offset is higher than table size!"
|
232
249
|
[]
|
233
250
|
end
|
234
251
|
|
235
252
|
def execute
|
236
|
-
set = if
|
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
|
-
|
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
|
-
|
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
|
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,17 +41,40 @@ 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? })
|
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
|
-
|
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(
|
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}")
|
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/model_spec.rb
CHANGED
@@ -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)
|
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
|
@@ -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
|
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.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tomás Pollak
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
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: []
|