groovy 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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: []
|