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