groovy 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/Gemfile +4 -0
- data/example/Gemfile +3 -0
- data/example/Gemfile.lock +43 -0
- data/example/config.ru +27 -0
- data/groovy.gemspec +23 -0
- data/lib/groovy.rb +19 -0
- data/lib/groovy/model.rb +275 -0
- data/lib/groovy/query.rb +182 -0
- data/lib/groovy/schema.rb +122 -0
- data/lib/groovy/types.rb +44 -0
- data/spec/groovy_spec.rb +3 -0
- metadata +83 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 36ab3d8425ae05fd7b069e75b1cb3f168a8a31605e2bd54d3b44285f715dbc92
|
4
|
+
data.tar.gz: 7d48ca3d630488d52c0fd60e73ac753d5b1658601f6803566838c563231cce9a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2dfafb96a0e3636a7e49c9319a40d7e7217d6c21ed48bc0be6e8a4a61aee9a7ce41f9d3499987a03d16e0dc15868988d6a03049a8aec401b70f96b4c405e47b4
|
7
|
+
data.tar.gz: 90e22b09f631228277f297c552f42bbdf0bfc9faaccbea13c769b2f68b91cc297c157a1557a101790d0497d2390ba466f4f44f97bf5ac1a3d4f267666b36465e
|
data/Gemfile
ADDED
data/example/Gemfile
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ..
|
3
|
+
specs:
|
4
|
+
groovy (0.1.0)
|
5
|
+
rroonga (~> 7.1)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
archive-zip (0.12.0)
|
11
|
+
io-like (~> 0.3.0)
|
12
|
+
gqtp (1.0.6)
|
13
|
+
groonga-client (0.6.0)
|
14
|
+
gqtp (>= 1.0.4)
|
15
|
+
groonga-command (>= 1.2.8)
|
16
|
+
groonga-command-parser (>= 1.1.0)
|
17
|
+
hashie
|
18
|
+
groonga-command (1.4.1)
|
19
|
+
json
|
20
|
+
groonga-command-parser (1.1.4)
|
21
|
+
groonga-command (>= 1.4.0)
|
22
|
+
json-stream
|
23
|
+
hashie (3.6.0)
|
24
|
+
io-like (0.3.0)
|
25
|
+
json (2.2.0)
|
26
|
+
json-stream (0.2.1)
|
27
|
+
pkg-config (1.3.7)
|
28
|
+
rack (2.0.6)
|
29
|
+
rroonga (7.1.1)
|
30
|
+
archive-zip
|
31
|
+
groonga-client (>= 0.0.3)
|
32
|
+
json
|
33
|
+
pkg-config
|
34
|
+
|
35
|
+
PLATFORMS
|
36
|
+
ruby
|
37
|
+
|
38
|
+
DEPENDENCIES
|
39
|
+
groovy!
|
40
|
+
rack
|
41
|
+
|
42
|
+
BUNDLED WITH
|
43
|
+
1.16.1
|
data/example/config.ru
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'groovy'
|
3
|
+
Groovy.init('./db/test')
|
4
|
+
|
5
|
+
class Place
|
6
|
+
include Groovy::Model
|
7
|
+
|
8
|
+
# table_options type: :hash
|
9
|
+
column :name, String
|
10
|
+
column :description, String, searchable: true
|
11
|
+
timestamps!
|
12
|
+
end
|
13
|
+
|
14
|
+
Place.delete_all
|
15
|
+
|
16
|
+
10.times do |i|
|
17
|
+
Place.create!(name: "Place #{i}", description: "A nice place")
|
18
|
+
end
|
19
|
+
|
20
|
+
Place.last.delete
|
21
|
+
|
22
|
+
app = Proc.new do |env|
|
23
|
+
body = Place.all.collect(&:as_json)
|
24
|
+
[200, { 'Content-Type' => 'text/plain' }, [body]]
|
25
|
+
end
|
26
|
+
|
27
|
+
run app
|
data/groovy.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path("../lib/groovy", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "groovy"
|
6
|
+
s.version = Groovy::VERSION
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.authors = ['Tomás Pollak']
|
9
|
+
s.email = ['tomas@forkhq.com']
|
10
|
+
s.homepage = "https://github.com/tomas/groovy"
|
11
|
+
s.summary = "A wrapper around Groonga/Rroonga"
|
12
|
+
s.description = "Allows using Groonga in your models a-la ActiveRecord."
|
13
|
+
|
14
|
+
# s.required_rubygems_version = ">= 1.3.6"
|
15
|
+
# s.rubyforge_project = "groovy"
|
16
|
+
|
17
|
+
s.add_development_dependency "bundler", ">= 1.0.0"
|
18
|
+
s.add_runtime_dependency "rroonga", "~> 7.1"
|
19
|
+
|
20
|
+
s.files = `git ls-files`.split("\n")
|
21
|
+
s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
|
22
|
+
s.require_path = 'lib'
|
23
|
+
end
|
data/lib/groovy.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'groonga'
|
2
|
+
require File.expand_path(File.dirname(__FILE__)) + '/groovy/model'
|
3
|
+
|
4
|
+
module Groovy
|
5
|
+
VERSION = '0.1.0'.freeze
|
6
|
+
|
7
|
+
def self.init(db_path, opts = {})
|
8
|
+
if File.exist?(db_path)
|
9
|
+
puts "Opening DB"
|
10
|
+
Groonga::Database.open(db_path)
|
11
|
+
else
|
12
|
+
dir = File.dirname(db_path)
|
13
|
+
puts "Creating DB in #{dir}"
|
14
|
+
FileUtils.mkdir_p(dir)
|
15
|
+
Groonga::Database.create(path: db_path)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
data/lib/groovy/model.rb
ADDED
@@ -0,0 +1,275 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__)) + '/query'
|
2
|
+
require File.expand_path(File.dirname(__FILE__)) + '/types'
|
3
|
+
require File.expand_path(File.dirname(__FILE__)) + '/schema'
|
4
|
+
|
5
|
+
class Hash
|
6
|
+
def symbolize_keys
|
7
|
+
self.inject({}) { |memo, (k,v)| memo[k.to_sym] = v; memo }
|
8
|
+
end
|
9
|
+
end unless {}.respond_to?(:symbolize_keys)
|
10
|
+
|
11
|
+
module Groovy
|
12
|
+
module Model
|
13
|
+
|
14
|
+
SEARCH_TABLE_NAME = 'Terms'
|
15
|
+
|
16
|
+
def self.included(base)
|
17
|
+
base.extend(ClassMethods)
|
18
|
+
base.include(Forwardable)
|
19
|
+
base.table_name = base.name + 's'
|
20
|
+
|
21
|
+
if base.table.nil?
|
22
|
+
# abort "Please set up your database before declaring your models."
|
23
|
+
else
|
24
|
+
base.extend(PatriciaTrieMethods) if base.table.is_a?(Groonga::PatriciaTrie)
|
25
|
+
base.class_eval do
|
26
|
+
attribute_names.each do |col|
|
27
|
+
add_accessor(col)
|
28
|
+
end
|
29
|
+
end # if base.table
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module ClassMethods
|
34
|
+
extend Forwardable
|
35
|
+
attr_accessor :table_name
|
36
|
+
|
37
|
+
def validatable!
|
38
|
+
include(ActiveModel::Validations)
|
39
|
+
end
|
40
|
+
|
41
|
+
def table
|
42
|
+
# raise "Table name not set!" if table_name.nil?
|
43
|
+
# schema.table
|
44
|
+
Groonga[table_name]
|
45
|
+
end
|
46
|
+
|
47
|
+
def table_options(opts)
|
48
|
+
@table_options = opts
|
49
|
+
end
|
50
|
+
|
51
|
+
def schema
|
52
|
+
@schema ||= Schema.new(table_name, @table_options)
|
53
|
+
end
|
54
|
+
|
55
|
+
def column(name, type, options = {})
|
56
|
+
column_type = Types.map(type)
|
57
|
+
schema.column(name, column_type, options)
|
58
|
+
add_accessor(name)
|
59
|
+
end
|
60
|
+
|
61
|
+
def timestamps!(opts = {})
|
62
|
+
column(:created_at, 'Time')
|
63
|
+
column(:updated_at, 'Time')
|
64
|
+
end
|
65
|
+
|
66
|
+
# def columns
|
67
|
+
# table.columns # .collect { |x| x.name.split('.').last.to_sym }
|
68
|
+
# end
|
69
|
+
|
70
|
+
def add_accessor(col)
|
71
|
+
define_method(col) { self[col] }
|
72
|
+
define_method("#{col}=") { |val| self[col] = val }
|
73
|
+
end
|
74
|
+
|
75
|
+
def attribute_names
|
76
|
+
table.columns.select { |col| !col.reference_column? }.map { |x| x.name.split('.').last.to_sym }
|
77
|
+
end
|
78
|
+
|
79
|
+
def scope(name, obj)
|
80
|
+
define_singleton_method(name) do |*args|
|
81
|
+
obj.respond_to?(:call) ? obj.call(*args) : obj
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def find(id)
|
86
|
+
if record = table[id] and record.id
|
87
|
+
new(record.attributes, record)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def insert(key, attributes = nil)
|
92
|
+
if table.support_key?
|
93
|
+
table.add(key, attributes)
|
94
|
+
else # key is attributes
|
95
|
+
set_timestamp(key, :created_at)
|
96
|
+
set_timestamp(key, :updated_at)
|
97
|
+
table.add(key)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def create(key, attributes = nil)
|
102
|
+
if record = insert(key, attributes)
|
103
|
+
new(record.attributes, record)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def create!(key, attributes = nil)
|
108
|
+
create(key, attributes) or raise "Invalid!"
|
109
|
+
end
|
110
|
+
|
111
|
+
def delete_all
|
112
|
+
schema.rebuild!
|
113
|
+
end
|
114
|
+
|
115
|
+
def search_columns
|
116
|
+
@search_columns ||= []
|
117
|
+
end
|
118
|
+
|
119
|
+
def searchable_on(*fields)
|
120
|
+
fields.each { |f| add_index_on(f) }
|
121
|
+
end
|
122
|
+
|
123
|
+
def add_index_on(field)
|
124
|
+
ensure_search_table
|
125
|
+
search_columns.push(field)
|
126
|
+
Groonga::Schema.change_table(SEARCH_TABLE_NAME) do |table|
|
127
|
+
table.index([table_name, field].join('.'))
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def ensure_search_table
|
132
|
+
return if Groonga[SEARCH_TABLE_NAME]
|
133
|
+
Groonga::Schema.create_table(SEARCH_TABLE_NAME, {
|
134
|
+
type: :patricia_trie,
|
135
|
+
normalizer: :NormalizerAuto,
|
136
|
+
default_tokenizer: "TokenBigram"
|
137
|
+
})
|
138
|
+
end
|
139
|
+
|
140
|
+
# def column(name)
|
141
|
+
# Groonga["#{table_name}.#{name}"] # .search, .similar_search, etc
|
142
|
+
# end
|
143
|
+
|
144
|
+
def similar_search(col, q)
|
145
|
+
table.select { |r| r[col].similar_search(q) }
|
146
|
+
# table.select("#{col}:#{q}", operator: Groonga::Operation::SIMILAR)
|
147
|
+
end
|
148
|
+
|
149
|
+
def all
|
150
|
+
Query.new(self, table)
|
151
|
+
end
|
152
|
+
|
153
|
+
def_instance_delegators :all, :first, :last
|
154
|
+
|
155
|
+
def find_by(params)
|
156
|
+
where(params).first
|
157
|
+
end
|
158
|
+
|
159
|
+
[:search, :where, :not, :sort_by, :limit, :offset].each do |scope_method|
|
160
|
+
define_method scope_method do |*args|
|
161
|
+
Query.new(self, table).tap do |q|
|
162
|
+
q.public_send(scope_method, *args)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# this seems to be the same as: `table[id]`
|
168
|
+
# def search(key, options = nil)
|
169
|
+
# raise "Not supported!" unless table.respond_to?(:search)
|
170
|
+
# table.search(key, options)
|
171
|
+
# end
|
172
|
+
|
173
|
+
def_instance_delegators :table, :count, :size
|
174
|
+
|
175
|
+
def set_timestamp(obj, key_name)
|
176
|
+
obj[key_name] = Time.now if attribute_names.include?(key_name.to_sym)
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
|
181
|
+
module PatriciaTrieMethods
|
182
|
+
extend Forwardable
|
183
|
+
|
184
|
+
# def_instance_delegators :@table, :scan, :search, :prefix_search, :open_prefix_cursor, :tag_keys
|
185
|
+
def_instance_delegators :table, :scan, :search, :prefix_search, :open_prefix_cursor, :tag_keys
|
186
|
+
|
187
|
+
def each_with_prefix(prefix, options = {}, &block)
|
188
|
+
table.open_prefix_cursor(prefix, options) do |cursor|
|
189
|
+
cursor.each { |r| yield r }
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
attr_reader :attributes, :record, :changes
|
195
|
+
|
196
|
+
def initialize(attributes, record = nil)
|
197
|
+
@attributes = attributes.symbolize_keys.slice(*self.class.attribute_names)
|
198
|
+
@record = record
|
199
|
+
@changes = {}
|
200
|
+
end
|
201
|
+
|
202
|
+
def key
|
203
|
+
record.respond_to?(:_key) ? record._key : record.id
|
204
|
+
end
|
205
|
+
|
206
|
+
def [](key)
|
207
|
+
attributes[key.to_sym]
|
208
|
+
end
|
209
|
+
|
210
|
+
def []=(key, val)
|
211
|
+
raise "Invalid attribute: #{key}" unless attributes.has_key?(key.to_sym)
|
212
|
+
changes[key.to_sym] = [self[key], val]
|
213
|
+
attributes[key.to_sym] = val
|
214
|
+
end
|
215
|
+
|
216
|
+
def increment(values, do_save = true)
|
217
|
+
values.each { |key, num| self[key] += num }
|
218
|
+
save if do_save
|
219
|
+
end
|
220
|
+
|
221
|
+
def dirty?
|
222
|
+
changes.any?
|
223
|
+
end
|
224
|
+
|
225
|
+
def update_attributes(obj)
|
226
|
+
obj.each { |k,v| self[k] = v }
|
227
|
+
update
|
228
|
+
end
|
229
|
+
|
230
|
+
def save(options = {})
|
231
|
+
return false if respond_to?(:invalid) and invalid?
|
232
|
+
@record ? update : create
|
233
|
+
end
|
234
|
+
|
235
|
+
def save!(options = {})
|
236
|
+
raise "Invalid!" unless save
|
237
|
+
end
|
238
|
+
|
239
|
+
def delete
|
240
|
+
record.delete
|
241
|
+
self
|
242
|
+
end
|
243
|
+
|
244
|
+
def reload
|
245
|
+
raise "Not persisted" if key.nil?
|
246
|
+
record = self.class.table[key]
|
247
|
+
@attributes = record.attributes.symbolize_keys
|
248
|
+
@changes = {}
|
249
|
+
self
|
250
|
+
end
|
251
|
+
|
252
|
+
def as_json(options = {})
|
253
|
+
attributes
|
254
|
+
end
|
255
|
+
|
256
|
+
private
|
257
|
+
|
258
|
+
def create
|
259
|
+
@record = self.class.insert(attributes)
|
260
|
+
self
|
261
|
+
end
|
262
|
+
|
263
|
+
def update
|
264
|
+
raise "Not persisted" unless key
|
265
|
+
changes.merge.each do |key, values|
|
266
|
+
# puts "Updating #{key} from #{values[0]} to #{values[1]}"
|
267
|
+
record[key] = values.last
|
268
|
+
end
|
269
|
+
self.class.set_timestamp(record, :updated_at)
|
270
|
+
@changes = {}
|
271
|
+
self
|
272
|
+
end
|
273
|
+
|
274
|
+
end
|
275
|
+
end
|
data/lib/groovy/query.rb
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
module Groovy
|
2
|
+
|
3
|
+
class Query
|
4
|
+
include Enumerable
|
5
|
+
AND = '+'.freeze
|
6
|
+
NOT = '-'.freeze
|
7
|
+
|
8
|
+
attr_reader :parameters, :sorting
|
9
|
+
|
10
|
+
# options:
|
11
|
+
# - "operator"
|
12
|
+
# - "result"
|
13
|
+
# - "name"
|
14
|
+
# - "syntax"
|
15
|
+
# - "allow_pragma"
|
16
|
+
# - "allow_column"
|
17
|
+
# - "allow_update"
|
18
|
+
# - "allow_leading_not"
|
19
|
+
# - "default_column"
|
20
|
+
|
21
|
+
def initialize(model, table, options = {})
|
22
|
+
@model, @table, @options = model, table, options
|
23
|
+
@parameters = options.delete(:parameters) || []
|
24
|
+
@sorting = { limit: -1, offset: 0 }
|
25
|
+
@default_sort_key = table.is_a?(Groonga::Hash) ? '_key' : '_id'
|
26
|
+
end
|
27
|
+
|
28
|
+
def merge_with!(another)
|
29
|
+
# parameters.merge!(another.parameters)
|
30
|
+
parameters.concat(another.parameters)
|
31
|
+
sorting.merge!(another.sorting)
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def search(obj)
|
36
|
+
obj.each do |col, q|
|
37
|
+
raise "Not a full-text search column: #{col}" unless model.search_columns.include?(col)
|
38
|
+
parameters.push(AND + "(#{col}:@#{q})")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# http://groonga.org/docs/reference/grn_expr/query_syntax.html
|
43
|
+
# TODO: support match_columns (search value in two or more columns)
|
44
|
+
def where(conditions = nil)
|
45
|
+
case conditions
|
46
|
+
when String # "foo:bar"
|
47
|
+
parameters.push(AND + "(#{map_operator(conditions)})")
|
48
|
+
when Hash # { foo: 'bar' } or { views: 1..100 }
|
49
|
+
conditions.each do |key, val|
|
50
|
+
if val.is_a?(Range)
|
51
|
+
parameters.push(AND + [key, val.min].join(':>=')) if val.min # lte
|
52
|
+
parameters.push(AND + [key, val.max].join(':<=')) if val.max # gte
|
53
|
+
elsif val.is_a?(Regexp)
|
54
|
+
str = val.source.gsub(/[^a-z]/, '')
|
55
|
+
param = val.source[0] == '^' ? ':^' : ':~' # starts with or regexp
|
56
|
+
parameters.push(AND + [key, str].join(param))
|
57
|
+
else
|
58
|
+
parameters.push(AND + [key, val.to_s].join(':'))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
# when Array # ["foo:?", val]
|
62
|
+
# parameters.push(conditions.first.sub('?', conditions.last))
|
63
|
+
when NilClass
|
64
|
+
# doing where.not probably
|
65
|
+
else
|
66
|
+
raise 'not supported'
|
67
|
+
end
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
def not(conditions = {})
|
72
|
+
case conditions
|
73
|
+
when String # "foo:bar"
|
74
|
+
parameters.push(NOT + "(#{map_operator(conditions)})")
|
75
|
+
when Hash # { foo: 'bar' }
|
76
|
+
conditions.each do |key, val|
|
77
|
+
if val.is_a?(Range)
|
78
|
+
parameters.push(AND + [key, val.min].join(':<=')) if val.min # gte
|
79
|
+
parameters.push(AND + [key, val.max].join(':>=')) if val.max # lte
|
80
|
+
else
|
81
|
+
parameters.push(AND + [key, val.to_s].join(':!')) # not
|
82
|
+
end
|
83
|
+
end
|
84
|
+
# when Array # ["foo:?", val]
|
85
|
+
# parameters.push(conditions.first.sub('?', conditions.last))
|
86
|
+
else
|
87
|
+
raise 'not supported'
|
88
|
+
end
|
89
|
+
self
|
90
|
+
end
|
91
|
+
|
92
|
+
def limit(num)
|
93
|
+
@limit = num
|
94
|
+
self
|
95
|
+
end
|
96
|
+
|
97
|
+
def offset(num)
|
98
|
+
@offset = num
|
99
|
+
self
|
100
|
+
end
|
101
|
+
|
102
|
+
# sort_by(title: :asc)
|
103
|
+
def sort_by(hash)
|
104
|
+
sorting[:by] = hash.keys.map do |key|
|
105
|
+
{ key: key.to_s, order: hash[key] }
|
106
|
+
end
|
107
|
+
self
|
108
|
+
end
|
109
|
+
|
110
|
+
def all
|
111
|
+
@all ||= results.map { |r| model.new(r.attributes['_value']['_key'], r) }
|
112
|
+
end
|
113
|
+
|
114
|
+
def size
|
115
|
+
results.size
|
116
|
+
end
|
117
|
+
|
118
|
+
alias_method :count, :size
|
119
|
+
|
120
|
+
def to_a
|
121
|
+
all
|
122
|
+
end
|
123
|
+
|
124
|
+
def [](index)
|
125
|
+
all[index]
|
126
|
+
end
|
127
|
+
|
128
|
+
def each(&block)
|
129
|
+
all.each { |r| block.call(r) }
|
130
|
+
end
|
131
|
+
|
132
|
+
def last
|
133
|
+
all[size-1]
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
attr_reader :model, :table, :options
|
138
|
+
|
139
|
+
def results
|
140
|
+
@results ||= execute
|
141
|
+
end
|
142
|
+
|
143
|
+
def execute
|
144
|
+
set = if parameters.any?
|
145
|
+
query = parameters.join(" ").gsub(/\s(\w)/, '\ \1')
|
146
|
+
puts query if ENV['DEBUG']
|
147
|
+
table.select(query, options)
|
148
|
+
else
|
149
|
+
table.select(options)
|
150
|
+
end
|
151
|
+
|
152
|
+
set.sort(sort_key_and_order, {
|
153
|
+
limit: sorting[:limit],
|
154
|
+
offset: sorting[:offset]
|
155
|
+
})
|
156
|
+
end
|
157
|
+
|
158
|
+
def map_operator(str)
|
159
|
+
str.sub(' = ', ':')
|
160
|
+
.sub(' != ', ':!')
|
161
|
+
.sub(' ~ ', ':~')
|
162
|
+
.sub(' < ', ':<')
|
163
|
+
.sub(' > ', ':>')
|
164
|
+
.sub(' <= ', ':<=')
|
165
|
+
.sub(' >= ', ':>=')
|
166
|
+
end
|
167
|
+
|
168
|
+
def sort_key_and_order
|
169
|
+
sorting[:by] or [{ key: @default_sort_key, order: :asc }]
|
170
|
+
end
|
171
|
+
|
172
|
+
def method_missing(name, *args)
|
173
|
+
if model.respond_to?(name)
|
174
|
+
other = model.public_send(name, *args)
|
175
|
+
merge_with!(other)
|
176
|
+
else
|
177
|
+
super
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Groovy
|
2
|
+
class Schema
|
3
|
+
attr_reader :table, :table_name
|
4
|
+
|
5
|
+
def initialize(table_name, opts = {})
|
6
|
+
@table_name, @opts = table_name, opts
|
7
|
+
@columns = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def table
|
11
|
+
@table ||= Groonga[table_name]
|
12
|
+
end
|
13
|
+
|
14
|
+
def rebuild!
|
15
|
+
remove_table! if table
|
16
|
+
create_table!
|
17
|
+
@columns.each do |name, spec|
|
18
|
+
check_and_add_column(name, spec[:type], spec[:options])
|
19
|
+
end
|
20
|
+
puts "done rebuilding"
|
21
|
+
end
|
22
|
+
|
23
|
+
def ensure_created!
|
24
|
+
create_table! if table.nil?
|
25
|
+
end
|
26
|
+
|
27
|
+
def column(name, type, options = {})
|
28
|
+
ensure_created!
|
29
|
+
@columns[name] = { type: type, options: options }
|
30
|
+
check_and_add_column(name, type, options)
|
31
|
+
end
|
32
|
+
|
33
|
+
def check_and_add_column(name, type, options)
|
34
|
+
add_column(name, type, options) unless has_column?(name, type)
|
35
|
+
end
|
36
|
+
|
37
|
+
def has_column?(name, type)
|
38
|
+
table.columns.any? do |col|
|
39
|
+
col.name == "#{table_name}.#{name}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_column(name, type, options = {})
|
44
|
+
# puts "Adding column #{name}"
|
45
|
+
Groonga::Schema.change_table(table_name) do |table|
|
46
|
+
table.public_send(type, name, options)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def create_table!
|
51
|
+
# puts "creating table!"
|
52
|
+
Groonga::Schema.create_table(table_name, @opts)
|
53
|
+
end
|
54
|
+
|
55
|
+
def remove_table!
|
56
|
+
# puts "removing table!"
|
57
|
+
Groonga::Schema.remove_table(table_name)
|
58
|
+
@table = nil
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def load_schema!
|
64
|
+
Groonga::Schema.create_table("Sources")
|
65
|
+
Groonga::Schema.create_table("Topics")
|
66
|
+
Groonga::Schema.create_table("Tags", type: :patricia_trie)
|
67
|
+
Groonga::Schema.create_table("Links", type: :hash)
|
68
|
+
Groonga::Schema.create_table("Tweeters", type: :hash)
|
69
|
+
Groonga::Schema.create_table("Mentions")
|
70
|
+
Groonga::Schema.create_table("Subscribers")
|
71
|
+
|
72
|
+
Groonga::Schema.change_table("Sources") do |table|
|
73
|
+
table.short_text("name")
|
74
|
+
table.short_text("twitter_user_name")
|
75
|
+
end
|
76
|
+
|
77
|
+
Groonga::Schema.change_table("Topics") do |table|
|
78
|
+
table.reference("most_mentioned", "Links")
|
79
|
+
table.uint32("mentions_count")
|
80
|
+
table.uint32("links_count")
|
81
|
+
end
|
82
|
+
|
83
|
+
Groonga::Schema.change_table("Links") do |table|
|
84
|
+
table.reference("source", "Sources")
|
85
|
+
table.reference("topic", "Topics")
|
86
|
+
table.reference("tags", "Tags", type: :vector)
|
87
|
+
# table.short_text("url")
|
88
|
+
table.short_text("title")
|
89
|
+
table.short_text("image")
|
90
|
+
table.short_text("description") # less than 4K bytes
|
91
|
+
table.uint32("mentions_count")
|
92
|
+
table.uint32("hits_count")
|
93
|
+
table.boolean("posted")
|
94
|
+
table.boolean("removed")
|
95
|
+
table.short_text("snapshot")
|
96
|
+
table.uint32("posted_tweet_id")
|
97
|
+
end
|
98
|
+
|
99
|
+
Groonga::Schema.change_table("Tweeters") do |table|
|
100
|
+
table.short_text("name")
|
101
|
+
table.short_text("twitter_name")
|
102
|
+
table.uint32("mentions_count")
|
103
|
+
end
|
104
|
+
|
105
|
+
Groonga::Schema.change_table("Mentions") do |table|
|
106
|
+
table.reference("source", "Sources")
|
107
|
+
table.reference("link", "Links")
|
108
|
+
table.reference("tweeter", "Tweeters")
|
109
|
+
table.uint32("tweet_id")
|
110
|
+
table.short_text("content")
|
111
|
+
table.time("created_at")
|
112
|
+
end
|
113
|
+
|
114
|
+
Groonga::Schema.change_table("Subscribers") do |table|
|
115
|
+
table.short_text("name")
|
116
|
+
table.short_text("email")
|
117
|
+
table.short_text("timezone")
|
118
|
+
table.short_text("token")
|
119
|
+
table.uint32("received_count")
|
120
|
+
table.time("last_received_at")
|
121
|
+
end
|
122
|
+
end
|
data/lib/groovy/types.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
module Groovy
|
2
|
+
|
3
|
+
module Types
|
4
|
+
|
5
|
+
# BIGRAM
|
6
|
+
# BOOL
|
7
|
+
# BOOLEAN
|
8
|
+
# DELIMIT
|
9
|
+
# FLOAT
|
10
|
+
# INT16
|
11
|
+
# INT32
|
12
|
+
# INT64
|
13
|
+
# INT8
|
14
|
+
# LONG_TEXT
|
15
|
+
# MECAB
|
16
|
+
# OBJECT
|
17
|
+
# SHORT_TEXT
|
18
|
+
# TEXT
|
19
|
+
# TIME
|
20
|
+
# TRIGRAM
|
21
|
+
# UINT16
|
22
|
+
# UINT32
|
23
|
+
# UINT64
|
24
|
+
# UINT8
|
25
|
+
# UNIGRAM
|
26
|
+
|
27
|
+
MAPPINGS = {
|
28
|
+
'String' => 'short_text',
|
29
|
+
'Text' => 'text',
|
30
|
+
'Float' => 'float',
|
31
|
+
'Bool' => 'boolean',
|
32
|
+
'Boolean' => 'boolean',
|
33
|
+
'Integer' => 'int32',
|
34
|
+
'BigDecimal' => 'int64',
|
35
|
+
'Time' => 'time'
|
36
|
+
}
|
37
|
+
|
38
|
+
def self.map(type)
|
39
|
+
MAPPINGS[type.to_s] or raise "Invalid type: #{type}"
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
data/spec/groovy_spec.rb
ADDED
metadata
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: groovy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tomás Pollak
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-04-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.0.0
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rroonga
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '7.1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '7.1'
|
41
|
+
description: Allows using Groonga in your models a-la ActiveRecord.
|
42
|
+
email:
|
43
|
+
- tomas@forkhq.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- ".gitignore"
|
49
|
+
- Gemfile
|
50
|
+
- example/Gemfile
|
51
|
+
- example/Gemfile.lock
|
52
|
+
- example/config.ru
|
53
|
+
- groovy.gemspec
|
54
|
+
- lib/groovy.rb
|
55
|
+
- lib/groovy/model.rb
|
56
|
+
- lib/groovy/query.rb
|
57
|
+
- lib/groovy/schema.rb
|
58
|
+
- lib/groovy/types.rb
|
59
|
+
- spec/groovy_spec.rb
|
60
|
+
homepage: https://github.com/tomas/groovy
|
61
|
+
licenses: []
|
62
|
+
metadata: {}
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options: []
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
requirements: []
|
78
|
+
rubyforge_project:
|
79
|
+
rubygems_version: 2.7.3
|
80
|
+
signing_key:
|
81
|
+
specification_version: 4
|
82
|
+
summary: A wrapper around Groonga/Rroonga
|
83
|
+
test_files: []
|