groovy 0.2.7 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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: []