groovy 0.2.7 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d680ff00fc84414047dbb1006f81ea3b1e1238c3561da3dc0928eb585f1d1740
4
- data.tar.gz: bb19e5187dfb71dca014474a80e91c3f6e1cfb9a188b71c8b94156120369e048
3
+ metadata.gz: b64b5151ebc9ad89b92fac064613d778e067449e11eb0c87cbf8ce40ca5e2630
4
+ data.tar.gz: 3e95ca4bac3946bcf79805c174216be0cb2870c06e47369056054bfb4d61a637
5
5
  SHA512:
6
- metadata.gz: 7ad510d03e1b28baafd7709056ce075f0216459169a85e98b764cf14112afa63d9c5744f4037049cd38516c0cbf16e6d2273dacfe3f9dbc4f2bf6d14f9c8441e
7
- data.tar.gz: 9906f89b42ec4a592dc3910ead17b602ba81ae28d21c806c96abf26fb71ce82c7da69cb6943a9e9c2df0d8e51ff2da830d234d3a03b724da2218b1766edb9e5b
6
+ metadata.gz: 3320e1c8e3d31b9d48c2fbf409f2688b3be16734eebd631b9ee44f9509934968079b9ed1ab14a02422a559865e2619da1954640feed825bc17b8865ab96ac4d1
7
+ data.tar.gz: 1d1ebb476d22fa504a2b1f7ef7585f298fb5fbc84dbdc22cb34997c1a8e57032622e0c12867aab730bf75f08293d7f093a49059efb68d11c150be5684e4c9c34
@@ -9,6 +9,7 @@ class Product
9
9
  schema do |t|
10
10
  t.string :name
11
11
  t.integer :price
12
+ t.boolean :published
12
13
  t.timestamps
13
14
  end
14
15
 
@@ -19,11 +20,11 @@ end
19
20
  def populate
20
21
  5_000.times do |i|
21
22
  puts "Creating product #{i}" if i % 1000 == 0
22
- Product.create!(name: "A product with index #{i}", price: 10000 + i % 10)
23
+ Product.create!(name: "A product with index #{i}", published: true, price: 10000 + i % 10)
23
24
  end
24
25
  end
25
26
 
26
- populate if Product.count == 0
27
+ populate if Product.count == 0
27
28
 
28
29
  # 50_000 products: 50M
29
30
  # 100_000 products: 50M
@@ -1,12 +1,12 @@
1
1
  require 'bundler/setup'
2
2
  require 'groovy'
3
3
 
4
- Groovy.open('./db/2019', :current)
5
- # Groovy.open('./db/2020', :next)
4
+ Groovy.open('./db/2020', :current)
5
+ # Groovy.open('./db/2021', :next)
6
6
 
7
7
  module Groovy::Model::ClassMethods
8
8
  def context_name
9
- Time.now.year == 2019 ? :current : :next
9
+ Time.now.year == 2020 ? :current : :next
10
10
  end
11
11
 
12
12
  def table
@@ -34,6 +34,7 @@ class Place
34
34
  t.reference :categories, "Categories", type: :vector
35
35
  t.string :name
36
36
  t.string :description, index: true
37
+ t.boolean :visible
37
38
  t.timestamps
38
39
  end
39
40
 
@@ -50,6 +51,21 @@ class Location
50
51
  end
51
52
  end
52
53
 
54
+ # from groonga tests
55
+ #
56
+ # schema.create_table("Users", :type => :hash, :key_type => "UInt32") { |table| }
57
+ #
58
+ # schema.create_table("Communities", :type => :hash, :key_type => "ShortText") do |table|
59
+ # table.reference("users", "Users", :type => :vector)
60
+ # end
61
+ #
62
+ # groonga = @communities.add("groonga")
63
+ # morita = @users.add(29)
64
+ # groonga["users"] = [morita]
65
+ #
66
+ # assert_equal([29], @users.collect {|record| record.key})
67
+ # assert_equal([29], groonga["users"].collect {|record| record.key})
68
+
53
69
  def insert_places(count = 1000)
54
70
  puts "Inserting #{count} places!"
55
71
  count.times do |i|
@@ -57,4 +73,4 @@ def insert_places(count = 1000)
57
73
  end
58
74
  end
59
75
 
60
- insert_places if Place.count == 0
76
+ insert_places if Place.count == 0
@@ -14,7 +14,7 @@ Gem::Specification.new do |s|
14
14
  # s.required_rubygems_version = ">= 1.3.6"
15
15
  # s.rubyforge_project = "groovy"
16
16
 
17
- s.add_runtime_dependency "rroonga", "~> 9.0"
17
+ s.add_runtime_dependency "rroonga", "= 9.0.3"
18
18
 
19
19
  s.add_development_dependency "bundler", ">= 1.0.0"
20
20
  s.add_development_dependency "rspec", '~> 3.0', '>= 3.0.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,11 +20,16 @@ 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
- def self.included(base)
32
+ def self.included(base)
28
33
  base.extend(ClassMethods)
29
34
  base.include(Forwardable)
30
35
  base.table_name = base.name.sub(/y$/, 'ie') + 's'
@@ -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,63 +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
99
129
  end
100
130
 
101
- def create!(key, attributes = nil)
102
- create(key, attributes) or raise "Invalid!"
131
+ def create!(attributes, key = nil)
132
+ create(attributes, key) or raise Error, "Invalid"
133
+ end
134
+
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.each { |child| child.delete }.count
107
- # schema.rebuild!
140
+ # find_each { |child| child.delete }
141
+ table.delete { |record| record._id > -1 }
108
142
  end
109
143
 
144
+ # def dump_table!
145
+ # Groonga::TableDumper.new(table).dump
146
+ # # schema.rebuild!
147
+ # end
148
+
110
149
  # def column(name)
111
150
  # Groonga["#{table_name}.#{name}"] # .search, .similar_search, etc
112
151
  # end
113
152
 
114
- def similar_search(col, q)
115
- unless schema.index_columns.include?(col.to_sym)
116
- 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!"
117
162
  end
118
163
 
119
- table.select { |r| r[col].similar_search(q) }
120
- # 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
121
175
  end
122
176
 
123
177
  def unique_values_for(column, limit: -1, cache: false)
@@ -144,7 +198,7 @@ module Groovy
144
198
  end
145
199
 
146
200
  def query
147
- query_class.new(self, table)
201
+ query_class.new(self, table)
148
202
  end
149
203
 
150
204
  def scope(name, obj)
@@ -154,28 +208,25 @@ module Groovy
154
208
  end
155
209
  end
156
210
 
157
- [: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|
158
212
  define_method scope_method do |*args, &block|
159
213
  query.public_send(scope_method, *args, &block)
160
214
  end
161
215
  end
162
-
163
- # this seems to be the same as: `table[id]`
164
- # def search(key, options = nil)
165
- # raise "Not supported!" unless table.respond_to?(:search)
166
- # table.search(key, options)
167
- # end
168
216
 
169
217
  def_instance_delegators :table, :count, :size
170
218
 
171
219
  # called from instance too, so must by public
172
- def insert(key, attributes = nil)
220
+ def insert(attributes, key = nil)
221
+ set_timestamp(attributes, :created_at)
222
+ set_timestamp(attributes, :updated_at)
223
+
173
224
  if table.support_key?
225
+ raise "Key required" if key.nil?
174
226
  table.add(key, attributes)
175
- else # key is attributes
176
- set_timestamp(key, :created_at)
177
- set_timestamp(key, :updated_at)
178
- table.add(key)
227
+ else
228
+ raise "Key present, but unsupported" if key
229
+ table.add(attributes)
179
230
  end
180
231
  end
181
232
 
@@ -183,6 +234,17 @@ module Groovy
183
234
  obj[key_name] = Time.now if attribute_names.include?(key_name.to_sym)
184
235
  end
185
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
+
186
248
  private
187
249
 
188
250
  def query_class
@@ -194,7 +256,8 @@ module Groovy
194
256
  end
195
257
 
196
258
  def db_context
197
- 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."
198
261
  end
199
262
 
200
263
  def add_attr_accessors(col)
@@ -224,30 +287,32 @@ module Groovy
224
287
  end
225
288
  end
226
289
 
227
- attr_reader :id, :attributes, :refs, :record, :changes
290
+ attr_reader :id, :attributes, :record, :changes
228
291
 
229
- def initialize(attrs = nil, record = nil)
230
- @attributes, @refs, @vectors = {}, {}, {}
292
+ def initialize(attrs = nil, record = nil, key = nil)
293
+ @attributes, @vectors, @_key = {}, {}, key # key is used on creation only
231
294
 
232
295
  if set_record(record)
233
- # TODO: lazy load this
234
- # self.class.schema.singular_references.each do |col|
235
- # set_ref(col, record.public_send(col))
236
- # end
237
- 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
238
302
 
239
- attrs ||= {}
240
- unless attrs.is_a?(Hash)
241
- 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)
242
307
  end
243
308
 
244
- # don't call set_attributes since we don't want to call
245
- # setters, that might be overriden with custom logic.
246
- # attrs.each { |k,v| self[k] = v }
247
- set_attributes(attrs)
248
309
  @changes = {}
249
310
  end
250
311
 
312
+ def inspect
313
+ "#<#{self.class.name} id:#{id.inspect} attributes:[#{self.class.attribute_names.join(', ')}]>"
314
+ end
315
+
251
316
  def new_record?
252
317
  id.nil?
253
318
  # _key.nil?
@@ -258,10 +323,12 @@ module Groovy
258
323
  end
259
324
 
260
325
  def []=(key, val)
261
- 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
262
329
 
263
330
  unless self.class.attribute_names.include?(key.to_sym)
264
- raise "Invalid attribute: #{key}"
331
+ raise "Invalid attribute: #{key}"
265
332
  end
266
333
 
267
334
  set_attribute(key, val)
@@ -299,58 +366,85 @@ module Groovy
299
366
 
300
367
  def save!(options = {})
301
368
  raise "Invalid!" unless save
369
+ self
302
370
  end
303
371
 
304
372
  def delete
305
- record.delete # doesn't work if record.id doesn't match _key
306
- # 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
307
375
  set_record(nil)
308
376
  self
309
377
  rescue Groonga::InvalidArgument => e
310
- puts "Error: #{e.inspect}"
311
- raise RecordNotPersisted
378
+ # puts "Error: #{e.inspect}"
379
+ raise RecordNotPersisted, e.message
312
380
  end
313
381
 
314
382
  def reload
383
+ raise RecordNotPersisted if id.nil?
315
384
  ensure_persisted!
316
- record = self.class.table[id] # _key
317
- # TODO: fix duplication
318
- set_attributes(record.attributes)
385
+ rec = self.class.table[id] # _key
386
+ # set_record(rec)
387
+ set_attributes_from_record(rec)
319
388
  @changes = {}
320
389
  self
321
390
  end
322
391
 
323
392
  def as_json(options = {})
324
- attributes
393
+ options[:only] ? attributes.slice(*options[:only]) : attributes
325
394
  end
326
395
 
327
396
  def ==(other)
328
397
  self.id == other.id
329
398
  end
330
399
 
400
+ def <=>(other)
401
+ self.id <=> other.id
402
+ end
403
+
331
404
  private
332
405
 
406
+ def get_record_attribute(key)
407
+ 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
+
415
+ def fix_time_value(val)
416
+ return val.to_i == 0 ? nil : val
417
+ end
418
+
333
419
  # def _key
334
420
  # return unless record
335
421
  # record.respond_to?(:_key) ? record._key : id
336
422
  # end
337
423
 
424
+ def set_attributes_from_record(rec)
425
+ self.class.attribute_names.each do |col|
426
+ public_send("#{col}=", get_record_attribute(col))
427
+ end
428
+ end
429
+
338
430
  def set_attribute(key, val)
339
431
  changes[key.to_sym] = [self[key], val] if changes # nil when initializing
340
432
  attributes[key.to_sym] = val
341
433
  end
342
434
 
343
435
  def get_ref(name)
344
- @refs[name]
436
+ if record and obj = record[name]
437
+ Model.initialize_from_record(obj)
438
+ end
345
439
  end
346
440
 
347
441
  def set_ref(name, obj)
348
- unless obj.nil? || obj.respond_to?(:record)
349
- obj = Model.initialize_from_record(obj)
442
+ if record.nil?
443
+ set_attribute(name, obj.id) # obj should be a groovy model or groonga record
444
+ else
445
+ obj = obj.record if obj.respond_to?(:record)
446
+ record[name] = obj
350
447
  end
351
-
352
- @refs[name] = obj
353
- set_attribute(name, obj.nil? ? nil : obj.key)
354
448
  end
355
449
 
356
450
  def set_record(obj)
@@ -359,10 +453,18 @@ module Groovy
359
453
  end
360
454
 
361
455
  def create
362
- set_record(self.class.insert(attributes))
456
+ fire_callbacks(:before_create)
457
+ set_record(self.class.insert(attributes, @_key))
458
+ fire_callbacks(:after_create)
363
459
  self
364
460
  end
365
461
 
462
+ def fire_callbacks(name)
463
+ if arr = self.class.callbacks[name] and arr.any?
464
+ arr.each { |fn| send(fn) }
465
+ end
466
+ end
467
+
366
468
  def update
367
469
  ensure_persisted!
368
470
  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,10 +55,12 @@ module Groovy
46
55
  where(conditions).limit(1).first
47
56
  end
48
57
 
49
- def find_each(&block)
50
- in_batches(of: 10) do |group|
51
- group.each { |item| yield(item) }
58
+ def find_each(opts = {}, &block)
59
+ count = 0
60
+ in_batches({ of: 10 }.merge(opts)) do |group|
61
+ group.each { |item| count += 1; yield(item) }
52
62
  end
63
+ count
53
64
  end
54
65
 
55
66
  # http://groonga.org/docs/reference/grn_expr/query_syntax.html
@@ -74,7 +85,7 @@ module Groovy
74
85
  add_param(AND + str)
75
86
 
76
87
  else
77
- str = val.nil? || val.to_s.strip == '' ? '\\' : val.to_s
88
+ str = val.nil? || val === false || val.to_s.strip == '' ? "\"\"" : escape_val(val)
78
89
  add_param(AND + [key, str].join(':'))
79
90
  end
80
91
  end
@@ -108,7 +119,7 @@ module Groovy
108
119
  add_param(AND + str)
109
120
 
110
121
  else
111
- str = val.nil? || val.to_s.strip == '' ? '\\' : val.to_s
122
+ str = val.nil? || val === false || val.to_s.strip == '' ? "\"\"" : escape_val(val)
112
123
  add_param(AND + [key, str].join(':!')) # not
113
124
  end
114
125
  end
@@ -138,10 +149,10 @@ module Groovy
138
149
 
139
150
  # sort_by(title: :asc)
140
151
  def sort_by(hash)
141
- if hash.is_a?(String) # e.g. title.desc
142
- 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('.')
143
154
  hash = {}
144
- hash[param] = dir
155
+ hash[param] = dir || 'asc'
145
156
  end
146
157
 
147
158
  sorting[:by] = hash.keys.map do |key|
@@ -181,13 +192,21 @@ module Groovy
181
192
  records.each { |r| block.call(r) }
182
193
  end
183
194
 
195
+ def update_all(attrs)
196
+ each { |r| r.update_attributes(attrs) }
197
+ end
198
+
184
199
  def total_entries
185
200
  results # ensure query has been run
186
201
  @total_entries
187
202
  end
188
203
 
189
- def last
190
- 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
191
210
  end
192
211
 
193
212
  def in_batches(of: 1000, from: nil, &block)
@@ -205,9 +224,7 @@ module Groovy
205
224
 
206
225
  def records
207
226
  @records ||= results.map do |r|
208
- # FIXME: figure out the right way to do this.
209
- id = r.attributes['_value']['_key']['_id']
210
- model.find_and_init_record(id)
227
+ model.new_from_record(r)
211
228
  end
212
229
  end
213
230
 
@@ -215,36 +232,33 @@ module Groovy
215
232
  attr_reader :model, :table, :options
216
233
 
217
234
  def add_param(param)
218
- if parameters.include?(param)
219
- raise "Duplicate param: #{param}"
220
- end
221
-
222
- # if param matches blank/nil, put at the end of the stack
223
- param[/:\!?\\/] ? parameters.push(param) : parameters.unshift(param)
235
+ raise "Duplicate param: #{param}" if parameters.include?(param)
236
+ parameters.push(param)
224
237
  end
225
238
 
226
239
  def results
227
240
  @results ||= execute
228
241
  rescue Groonga::TooLargeOffset
229
- # puts "Offset is higher than table size!"
242
+ puts "Offset is higher than table size!"
230
243
  []
231
244
  end
232
245
 
233
246
  def execute
234
247
  set = if parameters.any?
235
248
  query = prepare_query
236
- puts "Finding records with query: #{query}" if ENV['DEBUG']
249
+ debug "Finding records with query: #{query}"
237
250
  table.select(query, options)
238
251
  else
252
+ debug "Finding records with options: #{options.inspect}"
239
253
  table.select(options)
240
254
  end
241
255
 
242
256
  @total_entries = set.size
243
257
 
244
- puts "Sorting with #{sort_key_and_order}, #{sorting.inspect}" if ENV['DEBUG']
258
+ debug "Sorting with #{sort_key_and_order}, #{sorting.inspect}"
245
259
  set = set.sort(sort_key_and_order, {
246
260
  limit: sorting[:limit],
247
- offset: sorting[:offset]
261
+ offset: sorting[:offset], # [sorting[:offset], @total_entries].min
248
262
  })
249
263
 
250
264
  sorting[:group_by] ? set.group(group_by) : set
@@ -264,6 +278,10 @@ module Groovy
264
278
  sorting[:by] or [{ key: @default_sort_key, order: :asc }]
265
279
  end
266
280
 
281
+ def escape_val(val)
282
+ val.to_s.gsub(':', '\:')
283
+ end
284
+
267
285
  def prepare_query
268
286
  space_regex = Regexp.new('\s([' + VALID_QUERY_CHARS + '])')
269
287
  query = parameters.join(' ').split(/ or /i).map do |part|
@@ -272,6 +290,11 @@ module Groovy
272
290
  .gsub(/(\d\d):(\d\d):(\d\d)/, '\1\:\2\:\3') # escape hh:mm:ss in timestamps
273
291
  end.join(' OR ').sub(/^-/, '_id:>0 -') #.gsub(' OR -', ' -')
274
292
  end
293
+
294
+ def debug(str)
295
+ puts str if ENV['DEBUG']
296
+ end
297
+
275
298
  end
276
299
 
277
300
  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.2.7'.freeze
2
+ VERSION = '0.4.1'.freeze
3
3
  end
@@ -58,6 +58,23 @@ describe Groovy::Model do
58
58
  end
59
59
 
60
60
  describe '.delete_all' do
61
+
62
+ before do
63
+ @first = TestProduct.create!(name: 'A product', price: 100)
64
+ @second = TestProduct.create!(name: 'Another product', price: 200)
65
+ expect(TestProduct.count).to eq(2)
66
+ end
67
+
68
+ it 'deletes all' do
69
+ TestProduct.delete_all
70
+ expect(TestProduct.count).to eq(0)
71
+
72
+ # expect { @first.reload }.to raise_error
73
+ # expect { @second.reload }.to raise_error
74
+ # expect { TestProduct.find(@first.id) }.to raise_error
75
+ # expect { TestProduct.find(@second.id) }.to raise_error
76
+ end
77
+
61
78
  end
62
79
 
63
80
  describe '#[]' do
@@ -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
@@ -17,11 +17,36 @@ describe Groovy::Query do
17
17
  end
18
18
 
19
19
  describe '#where' do
20
+ describe 'boolean value' do
21
+ it 'finds expected records' do
22
+ res = TestProduct.where(visible: true)
23
+ expect(res.map(&:id)).to eq([@p1.id, @p3.id, @p5.id])
24
+
25
+ res = TestProduct.where(visible: false)
26
+ expect(res.map(&:id)).to eq([@p2.id, @p4.id])
27
+ end
28
+
29
+ it 'works with other args too' do
30
+ res = TestProduct.where(visible: true).where(name: 'Product 5')
31
+ expect(res.map(&:id)).to eq([@p5.id])
32
+
33
+ res = TestProduct.where(visible: false).where(name: 'Product 2')
34
+ expect(res.map(&:id)).to eq([@p2.id])
35
+ end
36
+ end
37
+
20
38
  describe 'nil value' do
21
39
  it 'finds expected records' do
22
40
  res = TestProduct.where(tag_list: nil)
23
41
  expect(res.map(&:id)).to eq([@p3.id, @p5.id])
24
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
25
50
  it 'works with other args too' do
26
51
  res = TestProduct.where(name: 'Product 5').where(tag_list: nil)
27
52
  expect(res.map(&:id)).to eq([@p5.id])
@@ -53,6 +78,11 @@ describe Groovy::Query do
53
78
  res = TestProduct.where(tag_list: 'one, number two & three')
54
79
  expect(res.map(&:id)).to eq([@p1.id])
55
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
56
86
  end
57
87
 
58
88
  describe 'lower/greater than search (timestamps)' do
@@ -141,11 +171,36 @@ describe Groovy::Query do
141
171
  end
142
172
 
143
173
  describe '#not' do
174
+ describe 'boolean value' do
175
+ it 'finds expected records' do
176
+ res = TestProduct.where.not(visible: true)
177
+ expect(res.map(&:id)).to eq([@p2.id, @p4.id])
178
+
179
+ res = TestProduct.where.not(visible: false)
180
+ expect(res.map(&:id)).to eq([@p1.id, @p3.id, @p5.id])
181
+ end
182
+
183
+ it 'works with other args too' do
184
+ res = TestProduct.where.not(visible: true).where(name: 'Product 2')
185
+ expect(res.map(&:id)).to eq([@p2.id])
186
+
187
+ res = TestProduct.where.not(visible: false).where(name: 'Product 5')
188
+ expect(res.map(&:id)).to eq([@p5.id])
189
+ end
190
+ end
191
+
144
192
  describe 'nil value' do
145
193
  it 'finds expected records' do
146
194
  res = TestProduct.where.not(tag_list: nil)
147
195
  expect(res.map(&:id)).to eq([@p1.id, @p2.id, @p4.id])
148
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
149
204
  it 'works with other args too' do
150
205
  res = TestProduct.where.not(name: 'Product 2').where.not(tag_list: nil)
151
206
  expect(res.map(&:id)).to eq([@p1.id, @p4.id])
@@ -177,6 +232,10 @@ describe Groovy::Query do
177
232
  res = TestProduct.not(tag_list: 'one, number two & three')
178
233
  expect(res.map(&:id)).to eq([@p2.id, @p3.id, @p4.id, @p5.id])
179
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
180
239
  end
181
240
 
182
241
  context 'basic regex' do
@@ -0,0 +1,54 @@
1
+ require_relative './spec_helper'
2
+
3
+ describe Groovy::Model, 'searching' do
4
+
5
+ before :all do
6
+ Groovy.open('tmp/search', 'search_spec')
7
+ load_schema! 'search_spec'
8
+
9
+ TestProduct.add_column :description, :string, index: true
10
+
11
+ TestProduct.create!(name: 'First product', description: 'Lorem ipsum dolor sit amet')
12
+ TestProduct.create!(name: 'Second product', description: 'Lorea el ipsum poh loco')
13
+ end
14
+
15
+ after :all do
16
+ Groovy.close('search_spec')
17
+ end
18
+
19
+ describe 'single word, exact match' do
20
+ it 'returns results' do
21
+ # res = TestProduct.search(description: 'sit')
22
+ res = TestProduct.search(description: 'sit')
23
+ expect(res.first.name).to eq('First product')
24
+
25
+ # res = TestProduct.search(description: 'loco')
26
+ res = TestProduct.search(description: 'loco')
27
+ expect(res.first.name).to eq('Second product')
28
+ end
29
+ end
30
+
31
+ describe 'two consecutive words, exact match' do
32
+ it 'returns results' do
33
+ # res = TestProduct.search(description: 'sit amet')
34
+ res = TestProduct.search(description: 'sit amet')
35
+ expect(res.first.name).to eq('First product')
36
+
37
+ # res = TestProduct.search(description: 'lorea el')
38
+ res = TestProduct.search(description: 'lorea el')
39
+ expect(res.first.name).to eq('Second product')
40
+ end
41
+ end
42
+
43
+ describe 'two random words, both match' do
44
+ it 'returns results' do
45
+ # res = TestProduct.search(description: 'amet ipsum')
46
+ res = TestProduct.search(description: 'amet ipsum')
47
+ expect(res.first.name).to eq('First product')
48
+
49
+ # res = TestProduct.search(description: 'poh el')
50
+ res = TestProduct.search(description: 'poh el')
51
+ expect(res.first.name).to eq('Second product')
52
+ end
53
+ end
54
+ end
@@ -4,6 +4,9 @@ Bundler.require(:default, :test)
4
4
  require 'rspec/core'
5
5
  require './lib/groovy'
6
6
 
7
+ # Groonga::Logger.path = "/tmp/groonga.log"
8
+ # Groonga::Logger.max_level = :debug
9
+
7
10
  RSpec.configure do |config|
8
11
  config.before(:each) do
9
12
  # Groonga::Context.default = nil
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: groovy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.7
4
+ version: 0.4.1
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: 2019-09-27 00:00:00.000000000 Z
11
+ date: 2020-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rroonga
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: '9.0'
19
+ version: 9.0.3
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: '9.0'
26
+ version: 9.0.3
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -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: []