groovy 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Rakefile +2 -0
- data/example/basic.rb +25 -0
- data/example/config.ru +4 -13
- data/example/relations.rb +47 -0
- data/lib/groovy.rb +53 -12
- data/lib/groovy/middleware.rb +14 -0
- data/lib/groovy/model.rb +111 -97
- data/lib/groovy/query.rb +0 -11
- data/lib/groovy/schema.rb +125 -86
- data/lib/groovy/vector.rb +52 -0
- data/spec/groovy_spec.rb +58 -0
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b2968c89f9c0019f1984b53b34a1d5c4520e82bede4f56020a7ef490c76897fe
|
4
|
+
data.tar.gz: 6de69f1590be6a8bddd69f60bed379e8e59e75dcd2a9a00c5696ef5160b211a5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1b7be573999c72a9a5bb1a5b140388644702430c51a07a315d163c8a6f7db89058416ebcc252351498fd5d3b5e86d2cafeea06653e396bc6ddcf3d4bfaf6e6d3
|
7
|
+
data.tar.gz: 8693eb5c4472d634b62ad3be13e57e1c54172374aef7f8792807b107de068457bef293106437521b9200977db95122b524e2182438441fee780e72c15f129f1f
|
data/Rakefile
ADDED
data/example/basic.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'groovy'
|
3
|
+
|
4
|
+
Groovy.open('./db/products')
|
5
|
+
|
6
|
+
class Product
|
7
|
+
include Groovy::Model
|
8
|
+
|
9
|
+
schema do |t|
|
10
|
+
t.column :name, String
|
11
|
+
t.column :price, Integer
|
12
|
+
t.timestamps
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
50_000.times do |i|
|
17
|
+
puts "Creating product #{i}" if i % 1000 == 0
|
18
|
+
Product.create!(name: "A product with index #{i}", price: 10000 + i)
|
19
|
+
end
|
20
|
+
|
21
|
+
puts Product.count
|
22
|
+
|
23
|
+
# 50_000 products: 50M
|
24
|
+
# 100_000 products: 50M
|
25
|
+
# 500_000 products: 62M
|
data/example/config.ru
CHANGED
@@ -1,15 +1,5 @@
|
|
1
|
-
require '
|
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
|
1
|
+
require './models'
|
2
|
+
require 'groovy/middleware'
|
13
3
|
|
14
4
|
Place.delete_all
|
15
5
|
|
@@ -20,8 +10,9 @@ end
|
|
20
10
|
Place.last.delete
|
21
11
|
|
22
12
|
app = Proc.new do |env|
|
23
|
-
body = Place.all.collect(&:as_json)
|
13
|
+
body = Place.all.collect(&:as_json).inspect
|
24
14
|
[200, { 'Content-Type' => 'text/plain' }, [body]]
|
25
15
|
end
|
26
16
|
|
17
|
+
use Groovy::MemoryPoolMiddleware
|
27
18
|
run app
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'groovy'
|
3
|
+
|
4
|
+
Groovy.open('./db/2019', :current)
|
5
|
+
Groovy.open('./db/2020', :next)
|
6
|
+
|
7
|
+
module Groovy::Model::ClassMethods
|
8
|
+
def context_name
|
9
|
+
Time.now.year == 2019 ? :current : :next
|
10
|
+
end
|
11
|
+
|
12
|
+
def table
|
13
|
+
obj = db_context[table_name]
|
14
|
+
schema.sync(db_context) if obj.nil?
|
15
|
+
db_context[table_name]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Category
|
20
|
+
include Groovy::Model
|
21
|
+
|
22
|
+
schema do |t|
|
23
|
+
t.column :name, String
|
24
|
+
t.timestamps
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Place
|
29
|
+
include Groovy::Model
|
30
|
+
|
31
|
+
schema do |t|
|
32
|
+
t.reference :categories, "Categories", type: :vector
|
33
|
+
t.column :name, String
|
34
|
+
t.column :description, String, index: true
|
35
|
+
t.timestamps
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class Location
|
40
|
+
include Groovy::Model
|
41
|
+
|
42
|
+
schema do |t|
|
43
|
+
t.reference :place, "Places"
|
44
|
+
t.column :coords, String
|
45
|
+
t.timestamps
|
46
|
+
end
|
47
|
+
end
|
data/lib/groovy.rb
CHANGED
@@ -2,18 +2,59 @@ require 'groonga'
|
|
2
2
|
require File.expand_path(File.dirname(__FILE__)) + '/groovy/model'
|
3
3
|
|
4
4
|
module Groovy
|
5
|
-
VERSION = '0.1.
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
5
|
+
VERSION = '0.1.1'.freeze
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
def contexts
|
10
|
+
@contexts ||= {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def [](key)
|
14
|
+
contexts[key.to_sym]
|
15
|
+
end
|
16
|
+
|
17
|
+
def first_context_name
|
18
|
+
contexts.keys.first
|
19
|
+
end
|
20
|
+
|
21
|
+
def open(db_path, name = :default, opts = {})
|
22
|
+
raise "Context already defined: #{name}" if contexts[name.to_sym]
|
23
|
+
contexts[name.to_sym] = if name == :default
|
24
|
+
Groonga::Context.default.tap { |ctx| open_or_create_db(ctx, db_path) }
|
25
|
+
else
|
26
|
+
init_context(db_path, opts)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def logger=(obj)
|
31
|
+
@logger = obj
|
16
32
|
end
|
33
|
+
|
34
|
+
def logger
|
35
|
+
@logger ||= Logger.new(STDOUT)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def init_context(db_path, opts)
|
41
|
+
Groonga::Context.new(opts).tap do |ctx|
|
42
|
+
open_or_create_db(ctx, db_path)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def open_or_create_db(ctx, path)
|
47
|
+
if File.exist?(path)
|
48
|
+
logger.info "Opening DB at #{path}"
|
49
|
+
ctx.open_database(path)
|
50
|
+
else
|
51
|
+
dir = File.dirname(path)
|
52
|
+
logger.info "Creating DB in #{dir}"
|
53
|
+
FileUtils.mkdir_p(dir)
|
54
|
+
ctx.create_database(path)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
17
58
|
end
|
18
59
|
|
19
|
-
end
|
60
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Groovy
|
2
|
+
class MemoryPoolMiddleware
|
3
|
+
def initialize(app, options = {})
|
4
|
+
@app = app
|
5
|
+
end
|
6
|
+
|
7
|
+
def call(env)
|
8
|
+
Groovy.contexts.each { |name, ctx| ctx.push_memory_pool }
|
9
|
+
code, headers, body = @app.call(env)
|
10
|
+
Groovy.contexts.each { |name, ctx| ctx.pop_memory_pool }
|
11
|
+
[code, headers, body]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/groovy/model.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
require File.expand_path(File.dirname(__FILE__)) + '/query'
|
2
|
-
require File.expand_path(File.dirname(__FILE__)) + '/types'
|
3
1
|
require File.expand_path(File.dirname(__FILE__)) + '/schema'
|
2
|
+
require File.expand_path(File.dirname(__FILE__)) + '/vector'
|
3
|
+
require File.expand_path(File.dirname(__FILE__)) + '/query'
|
4
4
|
|
5
5
|
class Hash
|
6
6
|
def symbolize_keys
|
@@ -11,69 +11,52 @@ end unless {}.respond_to?(:symbolize_keys)
|
|
11
11
|
module Groovy
|
12
12
|
module Model
|
13
13
|
|
14
|
-
|
14
|
+
def self.initialize_from_record(obj)
|
15
|
+
model = model_from_table(obj.table.name)
|
16
|
+
model.new_from_record(obj)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.model_from_table(table_name)
|
20
|
+
Kernel.const_get(table_name.sub(/ies$/, 'y').sub(/s$/, ''))
|
21
|
+
end
|
15
22
|
|
16
23
|
def self.included(base)
|
17
24
|
base.extend(ClassMethods)
|
18
25
|
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
|
26
|
+
base.table_name = base.name.sub(/y$/, 'ie') + 's'
|
31
27
|
end
|
32
28
|
|
33
29
|
module ClassMethods
|
34
30
|
extend Forwardable
|
35
|
-
attr_accessor :table_name
|
31
|
+
attr_accessor :context_name, :table_name
|
36
32
|
|
37
33
|
def validatable!
|
38
34
|
include(ActiveModel::Validations)
|
39
35
|
end
|
40
36
|
|
41
37
|
def table
|
42
|
-
|
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)
|
38
|
+
db_context[table_name]
|
53
39
|
end
|
54
40
|
|
55
|
-
def
|
56
|
-
|
57
|
-
schema.column(name, column_type, options)
|
58
|
-
add_accessor(name)
|
41
|
+
def attribute_names
|
42
|
+
schema.attribute_columns
|
59
43
|
end
|
60
44
|
|
61
|
-
def
|
62
|
-
|
63
|
-
|
64
|
-
|
45
|
+
def schema(options = {}, &block)
|
46
|
+
@schema ||= begin
|
47
|
+
self.context_name = options[:context] || Groovy.first_context_name
|
48
|
+
self.table_name = options[:table_name] if options[:table_name]
|
65
49
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
def add_accessor(col)
|
71
|
-
define_method(col) { self[col] }
|
72
|
-
define_method("#{col}=") { |val| self[col] = val }
|
73
|
-
end
|
50
|
+
s = Schema.new(db_context, table_name, {})
|
51
|
+
yield s if block
|
52
|
+
s.sync
|
74
53
|
|
75
|
-
|
76
|
-
|
54
|
+
extend(PatriciaTrieMethods) if table.is_a?(Groonga::PatriciaTrie)
|
55
|
+
s.attribute_columns.each { |col| add_attr_accessors(col) }
|
56
|
+
s.singular_references.each { |col| add_ref_accessors(col) }
|
57
|
+
s.plural_references.each { |col| add_vector_accessors(col) }
|
58
|
+
s
|
59
|
+
end
|
77
60
|
end
|
78
61
|
|
79
62
|
def scope(name, obj)
|
@@ -82,25 +65,20 @@ module Groovy
|
|
82
65
|
end
|
83
66
|
end
|
84
67
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
end
|
68
|
+
# called from Query, so needs to be public
|
69
|
+
def new_from_record(record)
|
70
|
+
new(record.attributes, record)
|
89
71
|
end
|
90
72
|
|
91
|
-
def
|
92
|
-
if table.
|
93
|
-
|
94
|
-
else # key is attributes
|
95
|
-
set_timestamp(key, :created_at)
|
96
|
-
set_timestamp(key, :updated_at)
|
97
|
-
table.add(key)
|
73
|
+
def find(id)
|
74
|
+
if record = table[id] and record.id
|
75
|
+
new_from_record(record)
|
98
76
|
end
|
99
77
|
end
|
100
78
|
|
101
79
|
def create(key, attributes = nil)
|
102
80
|
if record = insert(key, attributes)
|
103
|
-
|
81
|
+
new_from_record(record)
|
104
82
|
end
|
105
83
|
end
|
106
84
|
|
@@ -112,36 +90,15 @@ module Groovy
|
|
112
90
|
schema.rebuild!
|
113
91
|
end
|
114
92
|
|
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
93
|
# def column(name)
|
141
94
|
# Groonga["#{table_name}.#{name}"] # .search, .similar_search, etc
|
142
95
|
# end
|
143
96
|
|
144
97
|
def similar_search(col, q)
|
98
|
+
unless schema.index_columns.include?(col.to_sym)
|
99
|
+
raise "Column '#{col}' doesn't have an index set!"
|
100
|
+
end
|
101
|
+
|
145
102
|
table.select { |r| r[col].similar_search(q) }
|
146
103
|
# table.select("#{col}:#{q}", operator: Groonga::Operation::SIMILAR)
|
147
104
|
end
|
@@ -153,7 +110,7 @@ module Groovy
|
|
153
110
|
def_instance_delegators :all, :first, :last
|
154
111
|
|
155
112
|
def find_by(params)
|
156
|
-
where(params).first
|
113
|
+
where(params).limit(1).first
|
157
114
|
end
|
158
115
|
|
159
116
|
[:search, :where, :not, :sort_by, :limit, :offset].each do |scope_method|
|
@@ -172,17 +129,45 @@ module Groovy
|
|
172
129
|
|
173
130
|
def_instance_delegators :table, :count, :size
|
174
131
|
|
132
|
+
private
|
133
|
+
|
134
|
+
def db_context
|
135
|
+
Groovy.contexts[context_name] or raise "Context not defined: #{context_name}"
|
136
|
+
end
|
137
|
+
|
138
|
+
def insert(key, attributes = nil)
|
139
|
+
if table.support_key?
|
140
|
+
table.add(key, attributes)
|
141
|
+
else # key is attributes
|
142
|
+
set_timestamp(key, :created_at)
|
143
|
+
set_timestamp(key, :updated_at)
|
144
|
+
table.add(key)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
175
148
|
def set_timestamp(obj, key_name)
|
176
149
|
obj[key_name] = Time.now if attribute_names.include?(key_name.to_sym)
|
177
150
|
end
|
178
151
|
|
152
|
+
def add_attr_accessors(col)
|
153
|
+
define_method(col) { self[col] }
|
154
|
+
define_method("#{col}=") { |val| self[col] = val }
|
155
|
+
end
|
156
|
+
|
157
|
+
def add_ref_accessors(key)
|
158
|
+
define_method(key) { get_ref(key) }
|
159
|
+
define_method("#{key}=") { |val| set_ref(key, val) }
|
160
|
+
end
|
161
|
+
|
162
|
+
def add_vector_accessors(key)
|
163
|
+
define_method(key) { @vectors[key] ||= Vector.new(self, key) }
|
164
|
+
define_method("#{key}=") { |items| self.public_send(key).set(items) }
|
165
|
+
end
|
179
166
|
end
|
180
167
|
|
181
168
|
module PatriciaTrieMethods
|
182
169
|
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
|
170
|
+
def_instance_delegators :table, :scan, :prefix_search, :open_prefix_cursor, :tag_keys
|
186
171
|
|
187
172
|
def each_with_prefix(prefix, options = {}, &block)
|
188
173
|
table.open_prefix_cursor(prefix, options) do |cursor|
|
@@ -191,15 +176,22 @@ module Groovy
|
|
191
176
|
end
|
192
177
|
end
|
193
178
|
|
194
|
-
attr_reader :attributes, :record, :changes
|
179
|
+
attr_reader :attributes, :refs, :record, :changes
|
180
|
+
|
181
|
+
def initialize(attributes = nil, record = nil)
|
182
|
+
@attributes = (attributes || {}).symbolize_keys.slice(*self.class.attribute_names)
|
183
|
+
@refs, @vectors = {}, {}
|
184
|
+
|
185
|
+
if @record = record
|
186
|
+
# TODO: lazy load this
|
187
|
+
self.class.schema.singular_references.each { |col| set_ref(col, record.public_send(col)) }
|
188
|
+
end
|
195
189
|
|
196
|
-
def initialize(attributes, record = nil)
|
197
|
-
@attributes = attributes.symbolize_keys.slice(*self.class.attribute_names)
|
198
|
-
@record = record
|
199
190
|
@changes = {}
|
200
191
|
end
|
201
192
|
|
202
193
|
def key
|
194
|
+
return unless record
|
203
195
|
record.respond_to?(:_key) ? record._key : record.id
|
204
196
|
end
|
205
197
|
|
@@ -208,9 +200,13 @@ module Groovy
|
|
208
200
|
end
|
209
201
|
|
210
202
|
def []=(key, val)
|
211
|
-
|
212
|
-
|
213
|
-
|
203
|
+
return set_ref(key, val) if val.respond_to?(:record) || val.is_a?(Groonga::Record)
|
204
|
+
|
205
|
+
unless self.class.attribute_names.include?(key.to_sym)
|
206
|
+
raise "Invalid attribute: #{key}"
|
207
|
+
end
|
208
|
+
|
209
|
+
set_attribute(key, val)
|
214
210
|
end
|
215
211
|
|
216
212
|
def increment(values, do_save = true)
|
@@ -244,8 +240,8 @@ module Groovy
|
|
244
240
|
def reload
|
245
241
|
raise "Not persisted" if key.nil?
|
246
242
|
record = self.class.table[key]
|
247
|
-
|
248
|
-
|
243
|
+
attributes = record.attributes.symbolize_keys
|
244
|
+
changes = {}
|
249
245
|
self
|
250
246
|
end
|
251
247
|
|
@@ -255,6 +251,24 @@ module Groovy
|
|
255
251
|
|
256
252
|
private
|
257
253
|
|
254
|
+
def set_attribute(key, val)
|
255
|
+
changes[key.to_sym] = [self[key], val] if changes # nil when initializing
|
256
|
+
attributes[key.to_sym] = val
|
257
|
+
end
|
258
|
+
|
259
|
+
def get_ref(name)
|
260
|
+
@refs[name]
|
261
|
+
end
|
262
|
+
|
263
|
+
def set_ref(name, obj)
|
264
|
+
unless obj.nil? || obj.respond_to?(:record)
|
265
|
+
obj = Model.initialize_from_record(obj)
|
266
|
+
end
|
267
|
+
|
268
|
+
@refs[name] = obj
|
269
|
+
set_attribute(name, obj.nil? ? nil : obj.key)
|
270
|
+
end
|
271
|
+
|
258
272
|
def create
|
259
273
|
@record = self.class.insert(attributes)
|
260
274
|
self
|
@@ -262,12 +276,12 @@ module Groovy
|
|
262
276
|
|
263
277
|
def update
|
264
278
|
raise "Not persisted" unless key
|
265
|
-
changes.
|
279
|
+
changes.each do |key, values|
|
266
280
|
# puts "Updating #{key} from #{values[0]} to #{values[1]}"
|
267
281
|
record[key] = values.last
|
268
282
|
end
|
269
283
|
self.class.set_timestamp(record, :updated_at)
|
270
|
-
|
284
|
+
changes = {}
|
271
285
|
self
|
272
286
|
end
|
273
287
|
|
data/lib/groovy/query.rb
CHANGED
@@ -7,17 +7,6 @@ module Groovy
|
|
7
7
|
|
8
8
|
attr_reader :parameters, :sorting
|
9
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
10
|
def initialize(model, table, options = {})
|
22
11
|
@model, @table, @options = model, table, options
|
23
12
|
@parameters = options.delete(:parameters) || []
|
data/lib/groovy/schema.rb
CHANGED
@@ -1,122 +1,161 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__)) + '/types'
|
2
|
+
|
1
3
|
module Groovy
|
2
4
|
class Schema
|
3
|
-
attr_reader :table, :table_name
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
|
6
|
+
SEARCH_TABLE_NAME = 'Terms'.freeze
|
7
|
+
COLUMN_DEFAULTS = {
|
8
|
+
compress: :zstandard
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
attr_reader :index_columns
|
12
|
+
|
13
|
+
def initialize(context, table_name, opts = {})
|
14
|
+
@context, @table_name, @opts = context, table_name, opts || {}
|
15
|
+
@spec, @index_columns = {}, []
|
8
16
|
end
|
9
17
|
|
10
18
|
def table
|
11
|
-
@table ||=
|
19
|
+
@table ||= context[table_name]
|
12
20
|
end
|
13
21
|
|
14
|
-
def
|
15
|
-
|
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"
|
22
|
+
def search_table
|
23
|
+
@search_table ||= context[SEARCH_TABLE_NAME]
|
21
24
|
end
|
22
25
|
|
23
|
-
def
|
24
|
-
|
26
|
+
def column_names
|
27
|
+
get_names table.columns
|
28
|
+
end
|
29
|
+
|
30
|
+
def singular_references
|
31
|
+
get_names table.columns.select(&:reference_column?).reject(&:vector?)
|
32
|
+
end
|
33
|
+
|
34
|
+
def plural_references
|
35
|
+
get_names table.columns.select(&:vector?)
|
36
|
+
end
|
37
|
+
|
38
|
+
def attribute_columns
|
39
|
+
get_names table.columns.select(&:column?)
|
40
|
+
end
|
41
|
+
|
42
|
+
def rebuild!
|
43
|
+
log("Rebuilding!")
|
44
|
+
# remove_table! if table
|
45
|
+
remove_columns_not_in([])
|
46
|
+
sync
|
25
47
|
end
|
26
48
|
|
27
49
|
def column(name, type, options = {})
|
50
|
+
groonga_type = Types.map(type)
|
51
|
+
@index_columns.push(name) if options.delete(:index)
|
52
|
+
@spec[name.to_sym] = { type: groonga_type, args: [COLUMN_DEFAULTS.merge(options)] }
|
53
|
+
end
|
54
|
+
|
55
|
+
def reference(name, table_name = nil, options = {})
|
56
|
+
table_name = "#{name}s" if table_name.nil?
|
57
|
+
@spec[name.to_sym] = { type: :reference, args: [table_name, options] }
|
58
|
+
end
|
59
|
+
|
60
|
+
def timestamps(opts = {})
|
61
|
+
column(:created_at, 'Time')
|
62
|
+
column(:updated_at, 'Time')
|
63
|
+
end
|
64
|
+
|
65
|
+
def sync(db_context = nil)
|
66
|
+
switch_to_context(db_context) if db_context
|
67
|
+
|
28
68
|
ensure_created!
|
29
|
-
@
|
30
|
-
|
69
|
+
remove_columns_not_in(@spec.keys)
|
70
|
+
@spec.each do |col, spec|
|
71
|
+
check_and_add_column(col, spec[:type], spec[:args])
|
72
|
+
end
|
73
|
+
@index_columns.each do |col|
|
74
|
+
add_index_on(col)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
attr_reader :context, :table_name
|
80
|
+
|
81
|
+
def switch_to_context(db_context)
|
82
|
+
log("Switching context to #{db_context}")
|
83
|
+
@context = db_context
|
84
|
+
@table = @search_table = nil # clear cached vars
|
85
|
+
end
|
86
|
+
|
87
|
+
def add_index_on(col)
|
88
|
+
ensure_search_table!
|
89
|
+
return false if search_table.have_column?([table_name, col].join('_'))
|
90
|
+
|
91
|
+
log "Adding index on #{col}"
|
92
|
+
Groonga::Schema.change_table(SEARCH_TABLE_NAME, context: context) do |table|
|
93
|
+
table.index([table_name, col].join('.'))
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def ensure_search_table!
|
98
|
+
return if search_table
|
99
|
+
opts = (@opts[:search_table] || {}).merge({
|
100
|
+
type: :patricia_trie,
|
101
|
+
normalizer: :NormalizerAuto,
|
102
|
+
default_tokenizer: "TokenBigram"
|
103
|
+
})
|
104
|
+
log("Creating search table with options: #{opts.inspect}")
|
105
|
+
Groonga::Schema.create_table(SEARCH_TABLE_NAME, opts.merge(context: context))
|
106
|
+
end
|
107
|
+
|
108
|
+
def remove_columns_not_in(list)
|
109
|
+
column_names.each do |col|
|
110
|
+
remove_column(col) unless list.include?(col)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def ensure_created!
|
115
|
+
create_table! if table.nil?
|
31
116
|
end
|
32
117
|
|
33
118
|
def check_and_add_column(name, type, options)
|
34
119
|
add_column(name, type, options) unless has_column?(name, type)
|
35
120
|
end
|
36
121
|
|
37
|
-
def has_column?(name, type)
|
122
|
+
def has_column?(name, type = nil)
|
38
123
|
table.columns.any? do |col|
|
39
|
-
col.name == "#{table_name}.#{name}"
|
124
|
+
col.name == "#{table_name}.#{name}" # && (type.nil? or col.type == type)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def add_column(name, type, args = [])
|
129
|
+
log "Adding column #{name} with type #{type}, args: #{args.inspect}"
|
130
|
+
Groonga::Schema.change_table(table_name, context: context) do |table|
|
131
|
+
table.public_send(type, name, *args)
|
40
132
|
end
|
41
133
|
end
|
42
134
|
|
43
|
-
def
|
44
|
-
|
45
|
-
Groonga::Schema.change_table(table_name) do |table|
|
46
|
-
table.
|
135
|
+
def remove_column(name)
|
136
|
+
log "Removing column #{name}"
|
137
|
+
Groonga::Schema.change_table(table_name, context: context) do |table|
|
138
|
+
table.remove_column(name)
|
47
139
|
end
|
48
140
|
end
|
49
141
|
|
50
142
|
def create_table!
|
51
|
-
|
52
|
-
Groonga::Schema.create_table(table_name,
|
143
|
+
log "Creating table!"
|
144
|
+
Groonga::Schema.create_table(table_name, context: context)
|
53
145
|
end
|
54
146
|
|
55
147
|
def remove_table!
|
56
|
-
|
57
|
-
Groonga::Schema.remove_table(table_name)
|
148
|
+
log "Removing table!"
|
149
|
+
Groonga::Schema.remove_table(table_name, context: context)
|
58
150
|
@table = nil
|
59
151
|
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
152
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
table.reference("tweeter", "Tweeters")
|
109
|
-
table.uint32("tweet_id")
|
110
|
-
table.short_text("content")
|
111
|
-
table.time("created_at")
|
112
|
-
end
|
153
|
+
def get_names(columns)
|
154
|
+
columns.map { |col| col.name.split('.').last.to_sym }
|
155
|
+
end
|
113
156
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
table.short_text("timezone")
|
118
|
-
table.short_text("token")
|
119
|
-
table.uint32("received_count")
|
120
|
-
table.time("last_received_at")
|
157
|
+
def log(str)
|
158
|
+
puts "[#{table_name}] #{str}" if ENV['DEBUG']
|
159
|
+
end
|
121
160
|
end
|
122
|
-
end
|
161
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Groovy
|
2
|
+
class Vector
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
def initialize(obj, key)
|
6
|
+
@obj, @key = obj, key
|
7
|
+
end
|
8
|
+
|
9
|
+
def count
|
10
|
+
records.count
|
11
|
+
end
|
12
|
+
|
13
|
+
def each(&block)
|
14
|
+
items.each { |r| block.call(r) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def clear
|
18
|
+
set([])
|
19
|
+
end
|
20
|
+
|
21
|
+
def set(items)
|
22
|
+
raise "Please save parent record first" unless obj.record
|
23
|
+
obj.record[key] = items
|
24
|
+
end
|
25
|
+
|
26
|
+
def push(item)
|
27
|
+
raise "Please save parent record first" unless obj.record
|
28
|
+
obj.record[key] = obj.record[key].concat([item.record])
|
29
|
+
end
|
30
|
+
|
31
|
+
def remove(item)
|
32
|
+
raise "Please save parent record first" unless obj.record
|
33
|
+
recs = obj.record[key].delete_if { |r| r == item.record }
|
34
|
+
obj.record[key] = recs
|
35
|
+
end
|
36
|
+
|
37
|
+
alias_method :<<, :push
|
38
|
+
|
39
|
+
private
|
40
|
+
attr_reader :obj, :key
|
41
|
+
|
42
|
+
def items
|
43
|
+
return [] unless obj.record
|
44
|
+
records.map { |r| Model.initialize_from_record(r) }
|
45
|
+
end
|
46
|
+
|
47
|
+
def records
|
48
|
+
obj.record[key]
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
data/spec/groovy_spec.rb
CHANGED
@@ -1,3 +1,61 @@
|
|
1
1
|
require './lib/groovy'
|
2
2
|
require 'rspec/mocks'
|
3
3
|
|
4
|
+
describe Groovy::Model do
|
5
|
+
|
6
|
+
describe '.schema' do
|
7
|
+
end
|
8
|
+
|
9
|
+
describe '.scope' do
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '.create' do
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '.find' do
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '.find_by' do
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '.delete_all' do
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#[]' do
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#[]=' do
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#increment' do
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#dirty' do
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#update_attributes' do
|
37
|
+
end
|
38
|
+
|
39
|
+
describe '#save' do
|
40
|
+
end
|
41
|
+
|
42
|
+
describe '#delete' do
|
43
|
+
end
|
44
|
+
|
45
|
+
describe '#reload' do
|
46
|
+
end
|
47
|
+
|
48
|
+
describe 'attributes accessors' do
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
describe 'singular refs' do
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
describe 'plural refs' do
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: groovy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.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-04-
|
11
|
+
date: 2019-04-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -47,15 +47,20 @@ extra_rdoc_files: []
|
|
47
47
|
files:
|
48
48
|
- ".gitignore"
|
49
49
|
- Gemfile
|
50
|
+
- Rakefile
|
50
51
|
- example/Gemfile
|
51
52
|
- example/Gemfile.lock
|
53
|
+
- example/basic.rb
|
52
54
|
- example/config.ru
|
55
|
+
- example/relations.rb
|
53
56
|
- groovy.gemspec
|
54
57
|
- lib/groovy.rb
|
58
|
+
- lib/groovy/middleware.rb
|
55
59
|
- lib/groovy/model.rb
|
56
60
|
- lib/groovy/query.rb
|
57
61
|
- lib/groovy/schema.rb
|
58
62
|
- lib/groovy/types.rb
|
63
|
+
- lib/groovy/vector.rb
|
59
64
|
- spec/groovy_spec.rb
|
60
65
|
homepage: https://github.com/tomas/groovy
|
61
66
|
licenses: []
|