shoden 0.4.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 +5 -5
- data/.gitignore +1 -0
- data/.travis.yml +7 -3
- data/Gemfile +5 -0
- data/Gemfile.lock +33 -0
- data/README.md +42 -4
- data/Rakefile +1 -1
- data/lib/shoden.rb +9 -300
- data/lib/shoden/model.rb +259 -0
- data/lib/shoden/proxy.rb +46 -0
- data/shoden.gemspec +6 -4
- data/test/shoden_test.rb +46 -32
- metadata +35 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d83a4046d2ee009631a461d017eb91587d878950b5bd2ccdd71caff7d1cbff18
|
4
|
+
data.tar.gz: 16d8fd4fd98795d99727440786ef56e7afc5a25b772753831c21dade14cb28c2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1790591d549b5d230962ad6b72d718857e536a040f32c7c2f6a7ea04db6ae52dbe42575904d0ea2210e09f23d6c9076114440f2ba888ba487e0a1ba570256f63
|
7
|
+
data.tar.gz: 21bd29c0a876cbce995a9a833e03c453c179db0bae6e51a209995196dbac156633fd91120618759b25016dfa2b8c63dca68a5c9b25b93792b66edb22bcfa8cfb
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
coverage
|
data/.travis.yml
CHANGED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
shoden (1.0)
|
5
|
+
pg (~> 1.1)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
clap (1.0.0)
|
11
|
+
cutest (1.2.3)
|
12
|
+
clap
|
13
|
+
docile (1.3.1)
|
14
|
+
json (2.2.0)
|
15
|
+
pg (1.1.4)
|
16
|
+
rake (12.3.2)
|
17
|
+
simplecov (0.16.1)
|
18
|
+
docile (~> 1.1)
|
19
|
+
json (>= 1.8, < 3)
|
20
|
+
simplecov-html (~> 0.10.0)
|
21
|
+
simplecov-html (0.10.2)
|
22
|
+
|
23
|
+
PLATFORMS
|
24
|
+
ruby
|
25
|
+
|
26
|
+
DEPENDENCIES
|
27
|
+
cutest (~> 1.2)
|
28
|
+
rake
|
29
|
+
shoden!
|
30
|
+
simplecov (~> 0.16)
|
31
|
+
|
32
|
+
BUNDLED WITH
|
33
|
+
1.17.2
|
data/README.md
CHANGED
@@ -1,10 +1,9 @@
|
|
1
|
-
# Shôden - [](https://travis-ci.org/elcuervo/shoden)
|
2
2
|
|
3
|
-

|
4
4
|
|
5
5
|
Shôden is a persistance library on top of Postgres.
|
6
|
-
|
7
|
-
Postgres as a main database.
|
6
|
+
Uses JSONB as an storage abstraction so no need for migrations.
|
8
7
|
|
9
8
|
## Installation
|
10
9
|
|
@@ -12,6 +11,20 @@ Postgres as a main database.
|
|
12
11
|
gem install shoden
|
13
12
|
```
|
14
13
|
|
14
|
+
## Connect
|
15
|
+
|
16
|
+
Shoden connects by default using a `DATABASE_URL` env variable.
|
17
|
+
But you can change the connection string by calling `Shoden.url=`
|
18
|
+
|
19
|
+
## Setup
|
20
|
+
|
21
|
+
Shoden needs a setup method to create the proper tables.
|
22
|
+
You should do that after connecting
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
Shoden.setup
|
26
|
+
```
|
27
|
+
|
15
28
|
## Models
|
16
29
|
|
17
30
|
```ruby
|
@@ -47,6 +60,17 @@ class Post < Shoden::Model
|
|
47
60
|
end
|
48
61
|
```
|
49
62
|
|
63
|
+
## Attributes
|
64
|
+
|
65
|
+
Shoden attributes offer you a way to type cast the values, or to perform changes
|
66
|
+
in the data itself.
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
class Shout < Shoden::Model
|
70
|
+
attribute :what, ->(x) { x.uppcase }
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
50
74
|
## Indexing
|
51
75
|
|
52
76
|
```ruby
|
@@ -58,3 +82,17 @@ class User < Shoden::Model
|
|
58
82
|
unique :email
|
59
83
|
end
|
60
84
|
```
|
85
|
+
|
86
|
+
## Querying
|
87
|
+
|
88
|
+
You can query models or relations using the `filter` method.
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
User.filter(email: "elcuervo@elcuervo.net")
|
92
|
+
User.first
|
93
|
+
User.last
|
94
|
+
User.count
|
95
|
+
```
|
96
|
+
|
97
|
+
You can go through the entire set using: `User.all` which will give you a
|
98
|
+
`Enumerator::Lazy`
|
data/Rakefile
CHANGED
data/lib/shoden.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
|
4
|
-
Sequel.extension :pg_hstore, :pg_hstore_ops
|
1
|
+
require "pg"
|
2
|
+
require "set"
|
3
|
+
require "shoden/model"
|
5
4
|
|
6
5
|
module Shoden
|
7
6
|
Error = Class.new(StandardError)
|
@@ -9,56 +8,12 @@ module Shoden
|
|
9
8
|
NotFound = Class.new(Error)
|
10
9
|
UniqueIndexViolation = Class.new(Error)
|
11
10
|
|
12
|
-
Proxy = Struct.new(:klass, :parent) do
|
13
|
-
def create(args = {})
|
14
|
-
klass.create(args.merge(key => parent.id))
|
15
|
-
end
|
16
|
-
|
17
|
-
def all
|
18
|
-
klass.filter(parent_filter)
|
19
|
-
end
|
20
|
-
|
21
|
-
def count
|
22
|
-
klass.count
|
23
|
-
end
|
24
|
-
|
25
|
-
def any?
|
26
|
-
count > 0
|
27
|
-
end
|
28
|
-
|
29
|
-
def first
|
30
|
-
filter = { order: "id ASC LIMIT 1" }.merge!(parent_filter)
|
31
|
-
klass.filter(filter).first
|
32
|
-
end
|
33
|
-
|
34
|
-
def last
|
35
|
-
filter = { order: "id DESC LIMIT 1" }.merge!(parent_filter)
|
36
|
-
klass.filter(filter).first
|
37
|
-
end
|
38
|
-
|
39
|
-
def [](id)
|
40
|
-
filter = { id: id }.merge!(parent_filter)
|
41
|
-
|
42
|
-
klass.filter(filter).first
|
43
|
-
end
|
44
|
-
|
45
|
-
private
|
46
|
-
|
47
|
-
def parent_filter
|
48
|
-
{ key => parent.id }
|
49
|
-
end
|
50
|
-
|
51
|
-
def key
|
52
|
-
"#{parent.class.to_reference}_id".freeze
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
11
|
def self.url=(url)
|
57
12
|
@_url = url
|
58
13
|
end
|
59
14
|
|
60
15
|
def self.url
|
61
|
-
@_url ||= ENV[
|
16
|
+
@_url ||= ENV["DATABASE_URL"]
|
62
17
|
end
|
63
18
|
|
64
19
|
def self.models
|
@@ -66,263 +21,17 @@ module Shoden
|
|
66
21
|
end
|
67
22
|
|
68
23
|
def self.connection
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
require 'logger'
|
73
|
-
loggers << Logger.new($stdout)
|
24
|
+
@_connection ||= begin
|
25
|
+
uri = URI.parse(url)
|
26
|
+
PG.connect(uri.hostname, uri.port, nil, nil, uri.path[1..-1], uri.user, uri.password)
|
74
27
|
end
|
75
|
-
|
76
|
-
@_connection ||= Sequel.connect(url, loggers: loggers)
|
77
28
|
end
|
78
29
|
|
79
30
|
def self.setup
|
80
|
-
|
81
|
-
models.each { |m| m.setup }
|
31
|
+
models.each(&:setup)
|
82
32
|
end
|
83
33
|
|
84
34
|
def self.destroy_tables
|
85
|
-
models.each
|
86
|
-
end
|
87
|
-
|
88
|
-
class Model
|
89
|
-
def initialize(attrs = {})
|
90
|
-
@_id = attrs.delete(:id) if attrs[:id]
|
91
|
-
@attributes = {}
|
92
|
-
update(attrs)
|
93
|
-
end
|
94
|
-
|
95
|
-
def id
|
96
|
-
return nil if !defined?(@_id)
|
97
|
-
@_id.to_i
|
98
|
-
end
|
99
|
-
|
100
|
-
def destroy
|
101
|
-
self.class.lookup(id).delete
|
102
|
-
end
|
103
|
-
|
104
|
-
def update(attrs = {})
|
105
|
-
attrs.each { |name, value| send(:"#{name}=", value) }
|
106
|
-
end
|
107
|
-
|
108
|
-
def update_attributes(attrs = {})
|
109
|
-
update(attrs)
|
110
|
-
save
|
111
|
-
end
|
112
|
-
|
113
|
-
def save
|
114
|
-
self.class.save(self)
|
115
|
-
self
|
116
|
-
end
|
117
|
-
|
118
|
-
def load!
|
119
|
-
ret = self.class.lookup(@_id)
|
120
|
-
return nil if ret.nil?
|
121
|
-
update(ret.to_a.first[:data])
|
122
|
-
self
|
123
|
-
end
|
124
|
-
|
125
|
-
def self.inherited(model)
|
126
|
-
Shoden.models.add(model)
|
127
|
-
end
|
128
|
-
|
129
|
-
def self.save(record)
|
130
|
-
if record.id
|
131
|
-
table.where(id: record.id).update(data: record.attributes)
|
132
|
-
else
|
133
|
-
begin
|
134
|
-
id = table.insert(data: record.attributes)
|
135
|
-
record.instance_variable_set(:@_id, id)
|
136
|
-
rescue Sequel::UniqueConstraintViolation
|
137
|
-
raise UniqueIndexViolation
|
138
|
-
end
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
|
-
def self.all
|
143
|
-
collect
|
144
|
-
end
|
145
|
-
|
146
|
-
def self.count
|
147
|
-
size = 0
|
148
|
-
Shoden.connection.fetch("SELECT COUNT(*) FROM \"#{table_name}\"") do |r|
|
149
|
-
size = r[:count]
|
150
|
-
end
|
151
|
-
|
152
|
-
size
|
153
|
-
end
|
154
|
-
|
155
|
-
def self.first
|
156
|
-
collect("ORDER BY id ASC LIMIT 1").first
|
157
|
-
end
|
158
|
-
|
159
|
-
def self.last
|
160
|
-
collect("ORDER BY id DESC LIMIT 1").first
|
161
|
-
end
|
162
|
-
|
163
|
-
def self.create(attrs = {})
|
164
|
-
new(attrs).save
|
165
|
-
end
|
166
|
-
|
167
|
-
def self.attributes
|
168
|
-
@attributes ||= []
|
169
|
-
end
|
170
|
-
|
171
|
-
def self.indices
|
172
|
-
@indices ||= []
|
173
|
-
end
|
174
|
-
|
175
|
-
def self.uniques
|
176
|
-
@uniques ||= []
|
177
|
-
end
|
178
|
-
|
179
|
-
def self.[](id)
|
180
|
-
new(id: id).load!
|
181
|
-
end
|
182
|
-
|
183
|
-
def self.index(name)
|
184
|
-
indices << name if !indices.include?(name)
|
185
|
-
end
|
186
|
-
|
187
|
-
def self.unique(name)
|
188
|
-
uniques << name if !uniques.include?(name)
|
189
|
-
end
|
190
|
-
|
191
|
-
def self.attribute(name, caster = ->(x) { x })
|
192
|
-
attributes << name if !attributes.include?(name)
|
193
|
-
|
194
|
-
define_method(name) { caster[@attributes[name]] }
|
195
|
-
define_method(:"#{name}=") { |value| @attributes[name] = value }
|
196
|
-
end
|
197
|
-
|
198
|
-
def self.collection(name, model)
|
199
|
-
define_method(name) do
|
200
|
-
klass = Kernel.const_get(model)
|
201
|
-
Proxy.new(klass, self)
|
202
|
-
end
|
203
|
-
end
|
204
|
-
|
205
|
-
def self.reference(name, model)
|
206
|
-
reader = :"#{name}_id"
|
207
|
-
writer = :"#{name}_id="
|
208
|
-
|
209
|
-
attributes << name if !attributes.include?(name)
|
210
|
-
|
211
|
-
define_method(reader) { @attributes[reader] }
|
212
|
-
define_method(writer) { |value| @attributes[reader] = value }
|
213
|
-
|
214
|
-
define_method(name) do
|
215
|
-
klass = Kernel.const_get("Shoden::#{model}")
|
216
|
-
klass[send(reader)]
|
217
|
-
end
|
218
|
-
end
|
219
|
-
|
220
|
-
def self.filter(conditions = {})
|
221
|
-
query = []
|
222
|
-
id = conditions.delete(:id)
|
223
|
-
order = conditions.delete(:order)
|
224
|
-
|
225
|
-
if id && !conditions.any?
|
226
|
-
rows = table.where(id: id)
|
227
|
-
else
|
228
|
-
conditions.each { |k,v| query << "data->'#{k}' = '#{v}'" }
|
229
|
-
seek_conditions = query.join(" AND ")
|
230
|
-
|
231
|
-
where = "WHERE (#{seek_conditions})"
|
232
|
-
|
233
|
-
where += " AND id = '#{id}'" if id
|
234
|
-
order_condition = "ORDER BY #{order}" if order
|
235
|
-
|
236
|
-
sql = "#{base_query} #{where} #{order_condition}"
|
237
|
-
|
238
|
-
rows = Shoden.connection.fetch(sql) || []
|
239
|
-
end
|
240
|
-
|
241
|
-
rows.lazy.map do |row|
|
242
|
-
attrs = row[:data].merge({ id: row[:id] })
|
243
|
-
|
244
|
-
new(attrs)
|
245
|
-
end
|
246
|
-
end
|
247
|
-
|
248
|
-
def attributes
|
249
|
-
sanitized = @attributes.map do |k, _|
|
250
|
-
val = send(k)
|
251
|
-
return if val.nil?
|
252
|
-
[k, val.to_s]
|
253
|
-
end.compact
|
254
|
-
|
255
|
-
Sequel::Postgres::HStore.new(sanitized)
|
256
|
-
end
|
257
|
-
|
258
|
-
private
|
259
|
-
|
260
|
-
def self.base_query
|
261
|
-
"SELECT * FROM \"#{table_name}\""
|
262
|
-
end
|
263
|
-
|
264
|
-
def self.collect(condition = '')
|
265
|
-
records = []
|
266
|
-
Shoden.connection.fetch("SELECT * FROM \"#{table_name}\" #{condition}") do |r|
|
267
|
-
attrs = r[:data].merge(id: r[:id])
|
268
|
-
records << new(attrs)
|
269
|
-
end
|
270
|
-
records
|
271
|
-
end
|
272
|
-
|
273
|
-
def self.table_name
|
274
|
-
:"Shoden::#{self.name}"
|
275
|
-
end
|
276
|
-
|
277
|
-
def self.to_reference
|
278
|
-
name.to_s.
|
279
|
-
match(/^(?:.*::)*(.*)$/)[1].
|
280
|
-
gsub(/([a-z\d])([A-Z])/, '\1_\2').
|
281
|
-
downcase.to_sym
|
282
|
-
end
|
283
|
-
|
284
|
-
def self.create_index(name, type = '')
|
285
|
-
conn.execute <<EOS
|
286
|
-
CREATE #{type.upcase} INDEX index_#{self.name}_#{name}
|
287
|
-
ON "#{table_name}" (( data -> '#{name}'))
|
288
|
-
WHERE ( data ? '#{name}' );
|
289
|
-
EOS
|
290
|
-
rescue
|
291
|
-
end
|
292
|
-
|
293
|
-
def self.lookup(id)
|
294
|
-
row = table.where(id: id)
|
295
|
-
return nil if !row.any?
|
296
|
-
|
297
|
-
row
|
298
|
-
end
|
299
|
-
|
300
|
-
def self.setup
|
301
|
-
conn.create_table? table_name do
|
302
|
-
primary_key :id
|
303
|
-
hstore :data
|
304
|
-
end
|
305
|
-
|
306
|
-
indices.each { |i| create_index(i) }
|
307
|
-
uniques.each { |i| create_index(i, :unique) }
|
308
|
-
end
|
309
|
-
|
310
|
-
def self.destroy_all
|
311
|
-
conn.execute("DELETE FROM \"#{table_name}\"")
|
312
|
-
rescue Sequel::DatabaseError
|
313
|
-
end
|
314
|
-
|
315
|
-
def self.destroy_table
|
316
|
-
conn.drop_table(table_name)
|
317
|
-
rescue Sequel::DatabaseError
|
318
|
-
end
|
319
|
-
|
320
|
-
def self.table
|
321
|
-
conn[table_name]
|
322
|
-
end
|
323
|
-
|
324
|
-
def self.conn
|
325
|
-
Shoden.connection
|
326
|
-
end
|
35
|
+
models.each(&:destroy_table)
|
327
36
|
end
|
328
37
|
end
|
data/lib/shoden/model.rb
ADDED
@@ -0,0 +1,259 @@
|
|
1
|
+
require "json"
|
2
|
+
require "shoden/proxy"
|
3
|
+
|
4
|
+
module Shoden
|
5
|
+
class Model
|
6
|
+
attr_reader :attributes
|
7
|
+
|
8
|
+
def initialize(attrs = {})
|
9
|
+
@_id = attrs.delete(:id) if attrs[:id]
|
10
|
+
@attributes = {}
|
11
|
+
update(attrs)
|
12
|
+
end
|
13
|
+
|
14
|
+
def id
|
15
|
+
return nil unless defined?(@_id)
|
16
|
+
@_id.to_i
|
17
|
+
end
|
18
|
+
|
19
|
+
def destroy
|
20
|
+
query = "DELETE FROM \"#{self.class.table_name}\" WHERE id = $1 RETURNING id"
|
21
|
+
ret = Shoden.connection.exec_params(query, [id])
|
22
|
+
ret.first["id"]
|
23
|
+
end
|
24
|
+
|
25
|
+
def update(attrs = {})
|
26
|
+
attrs.each { |name, value| send(:"#{name}=", value) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def update_attributes(attrs = {})
|
30
|
+
update(attrs)
|
31
|
+
save
|
32
|
+
end
|
33
|
+
|
34
|
+
def save
|
35
|
+
self.class.save(self)
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
def load!
|
40
|
+
ret = self.class.lookup(@_id)
|
41
|
+
return nil if ret.nil?
|
42
|
+
data = self.class.from_json(ret.first["data"])
|
43
|
+
update(data)
|
44
|
+
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.inherited(model)
|
49
|
+
Shoden.models.add(model)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.save(record)
|
53
|
+
if record.id
|
54
|
+
query = "UPDATE \"#{table_name}\" SET data = $1 WHERE id = $2"
|
55
|
+
Shoden.connection.exec_params(query, [record.attributes, record.id])
|
56
|
+
else
|
57
|
+
begin
|
58
|
+
query = "INSERT INTO \"#{table_name}\" (data) VALUES ($1) RETURNING id"
|
59
|
+
res = Shoden.connection.exec_params(query, [record.attributes.to_json])
|
60
|
+
record.instance_variable_set(:@_id, res.first["id"])
|
61
|
+
rescue PG::UniqueViolation
|
62
|
+
raise Shoden::UniqueIndexViolation
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.all
|
68
|
+
collect
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.count
|
72
|
+
query = "SELECT COUNT(*) FROM \"#{table_name}\""
|
73
|
+
Shoden.connection.exec(query).first["count"].to_i
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.first
|
77
|
+
collect(order: "id ASC", limit: 1).first
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.last
|
81
|
+
collect(order: "id DESC", limit: 1).first
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.create(attrs = {})
|
85
|
+
new(attrs).save
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.attributes
|
89
|
+
@attributes ||= []
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.indices
|
93
|
+
@indices ||= []
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.uniques
|
97
|
+
@uniques ||= []
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.[](id)
|
101
|
+
new(id: id).load!
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.index(name)
|
105
|
+
indices << name unless indices.include?(name)
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.unique(name)
|
109
|
+
uniques << name unless uniques.include?(name)
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.attribute(name, caster = ->(x) { x })
|
113
|
+
attributes << name unless attributes.include?(name)
|
114
|
+
|
115
|
+
define_method(name) { caster[@attributes[name]] }
|
116
|
+
define_method(:"#{name}=") { |value| @attributes[name] = value }
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.collection(name, model)
|
120
|
+
define_method(name) do
|
121
|
+
klass = Kernel.const_get(model)
|
122
|
+
Shoden::Proxy.new(klass, self)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.reference(name, model)
|
127
|
+
reader = :"#{name}_id"
|
128
|
+
writer = :"#{name}_id="
|
129
|
+
|
130
|
+
attributes << name unless attributes.include?(name)
|
131
|
+
|
132
|
+
define_method(reader) { @attributes[reader] }
|
133
|
+
define_method(writer) { |value| @attributes[reader] = value }
|
134
|
+
|
135
|
+
define_method(name) do
|
136
|
+
klass = Kernel.const_get("Shoden::#{model}")
|
137
|
+
klass[send(reader)]
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def self.filter(conditions = {})
|
142
|
+
rows = query(conditions: conditions)
|
143
|
+
|
144
|
+
rows.lazy.map do |row|
|
145
|
+
data = from_json(row["data"])
|
146
|
+
data[:id] = row["id"].to_i
|
147
|
+
|
148
|
+
new(data)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def self.query(fields: "*", conditions: {})
|
153
|
+
id = conditions.delete(:id)
|
154
|
+
order = conditions.delete(:order)
|
155
|
+
|
156
|
+
if id && conditions.none?
|
157
|
+
sql = "#{base_query(fields)} WHERE id = $1"
|
158
|
+
Shoden.connection.exec_params(sql, [id]) || []
|
159
|
+
else
|
160
|
+
count = conditions.count
|
161
|
+
where = count.times.map { |i| "data->>$#{2 * i + 1} = $#{2 * i + 2}" }
|
162
|
+
params = conditions.flatten
|
163
|
+
|
164
|
+
if id
|
165
|
+
where << "id = $#{count * 2 + 1}"
|
166
|
+
params << id
|
167
|
+
end
|
168
|
+
|
169
|
+
where = where.join(" AND ")
|
170
|
+
order_condition = "ORDER BY #{order}" if order
|
171
|
+
sql = "#{base_query(fields)} WHERE #{where} #{order_condition}"
|
172
|
+
|
173
|
+
Shoden.connection.exec_params(sql, params) || []
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
private
|
178
|
+
|
179
|
+
def self.from_json(string)
|
180
|
+
JSON.parse(string, symbolize_names: true)
|
181
|
+
end
|
182
|
+
|
183
|
+
def self.base_query(fields = "*")
|
184
|
+
"SELECT #{fields} FROM \"#{table_name}\""
|
185
|
+
end
|
186
|
+
|
187
|
+
def self.collect(order: :id, limit: 0)
|
188
|
+
query = base_query("*")
|
189
|
+
|
190
|
+
params = [].tap do |item|
|
191
|
+
item << order
|
192
|
+
query << " ORDER BY $1"
|
193
|
+
|
194
|
+
if limit > 0
|
195
|
+
item << limit
|
196
|
+
query << " LIMIT $2 "
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
[].tap do |records|
|
201
|
+
Shoden.connection.exec_params(query, params).each do |row|
|
202
|
+
data = from_json(row["data"])
|
203
|
+
data[:id] = row["id"].to_i
|
204
|
+
|
205
|
+
records << new(data)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def self.table_name
|
211
|
+
:"Shoden::#{name}"
|
212
|
+
end
|
213
|
+
|
214
|
+
def self.to_reference
|
215
|
+
name.to_s
|
216
|
+
.match(/^(?:.*::)*(.*)$/)[1]
|
217
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
218
|
+
.downcase.to_sym
|
219
|
+
end
|
220
|
+
|
221
|
+
def self.create_index(name, type = "")
|
222
|
+
query = <<~EOS
|
223
|
+
CREATE #{type.upcase} INDEX index_#{self.name}_#{name}
|
224
|
+
ON "#{table_name}" (( data ->> '#{name}'))
|
225
|
+
WHERE ( data ? '#{name}' );
|
226
|
+
EOS
|
227
|
+
|
228
|
+
Shoden.connection.exec(query)
|
229
|
+
end
|
230
|
+
|
231
|
+
def self.lookup(id)
|
232
|
+
query = "SELECT * FROM \"#{table_name}\" WHERE id = $1"
|
233
|
+
row = Shoden.connection.exec_params(query, [id])
|
234
|
+
return nil if row.none?
|
235
|
+
|
236
|
+
row
|
237
|
+
end
|
238
|
+
|
239
|
+
def self.setup
|
240
|
+
Shoden.connection.exec <<~EOS
|
241
|
+
CREATE TABLE IF NOT EXISTS \"#{table_name}\" (
|
242
|
+
id SERIAL NOT NULL PRIMARY KEY,
|
243
|
+
data JSONB
|
244
|
+
)
|
245
|
+
EOS
|
246
|
+
|
247
|
+
indices.each { |i| create_index(i) }
|
248
|
+
uniques.each { |i| create_index(i, :unique) }
|
249
|
+
end
|
250
|
+
|
251
|
+
def self.destroy_all
|
252
|
+
Shoden.connection.exec("DELETE FROM \"#{table_name}\"")
|
253
|
+
end
|
254
|
+
|
255
|
+
def self.destroy_table
|
256
|
+
Shoden.connection.exec("DROP TABLE IF EXISTS \"#{table_name}\"")
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
data/lib/shoden/proxy.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
module Shoden
|
2
|
+
Proxy = Struct.new(:klass, :parent) do
|
3
|
+
def create(args = {})
|
4
|
+
klass.create(args.merge(key => parent.id))
|
5
|
+
end
|
6
|
+
|
7
|
+
def all
|
8
|
+
klass.filter(parent_filter)
|
9
|
+
end
|
10
|
+
|
11
|
+
def count
|
12
|
+
row = klass.query(fields: "COUNT(id)", conditions: parent_filter)
|
13
|
+
row.first["count"].to_i
|
14
|
+
end
|
15
|
+
|
16
|
+
def any?
|
17
|
+
count > 0
|
18
|
+
end
|
19
|
+
|
20
|
+
def first
|
21
|
+
filter = { order: "id ASC LIMIT 1" }.merge!(parent_filter)
|
22
|
+
klass.filter(filter).first
|
23
|
+
end
|
24
|
+
|
25
|
+
def last
|
26
|
+
filter = { order: "id DESC LIMIT 1" }.merge!(parent_filter)
|
27
|
+
klass.filter(filter).first
|
28
|
+
end
|
29
|
+
|
30
|
+
def [](id)
|
31
|
+
filter = { id: id }.merge!(parent_filter)
|
32
|
+
|
33
|
+
klass.filter(filter).first
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def parent_filter
|
39
|
+
{ key => parent.id }
|
40
|
+
end
|
41
|
+
|
42
|
+
def key
|
43
|
+
"#{parent.class.to_reference}_id".freeze
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/shoden.gemspec
CHANGED
@@ -1,15 +1,17 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = "shoden"
|
3
|
-
s.version = "
|
3
|
+
s.version = "1.0"
|
4
4
|
s.summary = "Object hash mapper for postgres"
|
5
5
|
s.description = "Slim postgres models"
|
6
6
|
s.authors = ["elcuervo"]
|
7
|
-
s.licenses = [
|
7
|
+
s.licenses = %w[MIT HUGWARE]
|
8
8
|
s.email = ["yo@brunoaguirre.com"]
|
9
9
|
s.homepage = "http://github.com/elcuervo/shoden"
|
10
10
|
s.files = `git ls-files`.split("\n")
|
11
11
|
s.test_files = `git ls-files test`.split("\n")
|
12
12
|
|
13
|
-
s.add_dependency("
|
14
|
-
|
13
|
+
s.add_dependency("pg", "~> 1.1")
|
14
|
+
|
15
|
+
s.add_development_dependency("cutest", "~> 1.2")
|
16
|
+
s.add_development_dependency("simplecov", "~> 0.16")
|
15
17
|
end
|
data/test/shoden_test.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "cutest"
|
2
|
+
require "simplecov"
|
3
|
+
|
4
|
+
SimpleCov.start
|
5
|
+
|
6
|
+
require "shoden"
|
3
7
|
|
4
8
|
Model = Class.new(Shoden::Model)
|
5
9
|
|
@@ -12,8 +16,8 @@ class Person < Shoden::Model
|
|
12
16
|
end
|
13
17
|
|
14
18
|
class Tree < Shoden::Model
|
15
|
-
attribute
|
16
|
-
collection
|
19
|
+
attribute :name
|
20
|
+
collection :sprouts, :Sprout
|
17
21
|
end
|
18
22
|
|
19
23
|
class Sprout < Shoden::Model
|
@@ -34,61 +38,70 @@ setup do
|
|
34
38
|
Shoden.setup
|
35
39
|
end
|
36
40
|
|
37
|
-
test
|
41
|
+
test "url setup" do
|
42
|
+
Shoden.url = ENV["DATABASE_URL"]
|
43
|
+
end
|
44
|
+
|
45
|
+
test "model" do
|
38
46
|
model = Model.create
|
39
|
-
|
47
|
+
|
48
|
+
assert Model.count
|
49
|
+
assert_equal model.id.class, Integer
|
40
50
|
end
|
41
51
|
|
42
|
-
test
|
43
|
-
user = User.create name:
|
44
|
-
assert_equal user.name,
|
52
|
+
test "attributes" do
|
53
|
+
user = User.create name: "Michel"
|
54
|
+
assert_equal user.name, "Michel"
|
45
55
|
end
|
46
56
|
|
47
|
-
test
|
48
|
-
user = User.create name:
|
57
|
+
test "update" do
|
58
|
+
user = User.create name: "Cyril"
|
49
59
|
id = user.id
|
50
60
|
|
51
|
-
assert_equal user.name,
|
61
|
+
assert_equal user.name, "Cyril"
|
52
62
|
|
53
|
-
user.name =
|
63
|
+
user.name = "cyx"
|
54
64
|
user.save
|
55
65
|
|
56
|
-
assert_equal user.name,
|
66
|
+
assert_equal user.name, "cyx"
|
57
67
|
assert_equal user.id, id
|
58
68
|
|
59
|
-
user.update_attributes(name:
|
60
|
-
assert_equal user.name,
|
69
|
+
user.update_attributes(name: "Cyril")
|
70
|
+
assert_equal user.name, "Cyril"
|
61
71
|
end
|
62
72
|
|
63
|
-
test
|
64
|
-
person = { email:
|
73
|
+
test "filtering" do
|
74
|
+
person = { email: "edgar@poe.com" }
|
65
75
|
Person.create(person)
|
66
76
|
p = Person.filter(person).first
|
67
77
|
|
68
|
-
assert p.email ==
|
78
|
+
assert p.email == "edgar@poe.com"
|
69
79
|
end
|
70
80
|
|
71
|
-
test
|
72
|
-
tree = Tree.create(name:
|
81
|
+
test "relations" do
|
82
|
+
tree = Tree.create(name: "asd")
|
83
|
+
tree2 = Tree.create(name: "qwe")
|
73
84
|
|
74
85
|
assert tree.id
|
75
|
-
assert_equal tree.name,
|
86
|
+
assert_equal tree.name, "asd"
|
76
87
|
|
77
88
|
sprout = tree.sprouts.create(leaves: 4)
|
89
|
+
tree2.sprouts.create(leaves: 5)
|
78
90
|
|
79
91
|
assert sprout.is_a?(Sprout)
|
80
92
|
assert tree.sprouts.all.each.is_a?(Enumerator)
|
81
93
|
|
82
94
|
assert tree.sprouts[sprout.id].id == sprout.id
|
83
95
|
|
96
|
+
assert tree.sprouts.any?
|
84
97
|
assert_equal tree.sprouts.count, 1
|
85
98
|
assert_equal sprout.tree.id, tree.id
|
86
|
-
assert tree.sprouts.first
|
87
|
-
assert tree.sprouts.last
|
99
|
+
assert !tree.sprouts.first.nil?
|
100
|
+
assert !tree.sprouts.last.nil?
|
88
101
|
end
|
89
102
|
|
90
|
-
test
|
91
|
-
user = User.create(name:
|
103
|
+
test "deletion" do
|
104
|
+
user = User.create(name: "Damian")
|
92
105
|
id = user.id
|
93
106
|
|
94
107
|
user.destroy
|
@@ -96,27 +109,28 @@ test 'deletion' do
|
|
96
109
|
assert_equal User[id], nil
|
97
110
|
end
|
98
111
|
|
99
|
-
test
|
112
|
+
test "casting" do
|
100
113
|
a = A.create(n: 1)
|
101
114
|
a_prime = A[a.id]
|
102
115
|
|
103
116
|
assert_equal a_prime.n, 1
|
104
117
|
end
|
105
118
|
|
106
|
-
test
|
107
|
-
person = Person.create(email:
|
119
|
+
test "indices" do
|
120
|
+
person = Person.create(email: "edgar@poe.com", origin: "The internerd")
|
108
121
|
|
109
122
|
assert person.id
|
110
123
|
|
111
124
|
assert_raise Shoden::UniqueIndexViolation do
|
112
|
-
Person.create(email:
|
125
|
+
Person.create(email: "edgar@poe.com", origin: "Montevideo City")
|
113
126
|
end
|
114
127
|
end
|
115
128
|
|
116
|
-
test
|
129
|
+
test "basic querying" do
|
117
130
|
User.destroy_all
|
118
131
|
5.times { User.create }
|
119
132
|
|
133
|
+
assert User.query(conditions: { id: 1 })
|
134
|
+
assert User.first != User.last
|
120
135
|
assert_equal User.all.size, 5
|
121
|
-
|
122
136
|
end
|
metadata
CHANGED
@@ -1,43 +1,57 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shoden
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: '1.0'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- elcuervo
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-09-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: pg
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - ~>
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: '1.1'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - ~>
|
24
|
+
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: '1.1'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: cutest
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - ~>
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 1.2
|
33
|
+
version: '1.2'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - ~>
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 1.2
|
40
|
+
version: '1.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: simplecov
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.16'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.16'
|
41
55
|
description: Slim postgres models
|
42
56
|
email:
|
43
57
|
- yo@brunoaguirre.com
|
@@ -45,13 +59,18 @@ executables: []
|
|
45
59
|
extensions: []
|
46
60
|
extra_rdoc_files: []
|
47
61
|
files:
|
48
|
-
- .gems
|
49
|
-
- .
|
62
|
+
- ".gems"
|
63
|
+
- ".gitignore"
|
64
|
+
- ".travis.yml"
|
65
|
+
- Gemfile
|
66
|
+
- Gemfile.lock
|
50
67
|
- HUGS
|
51
68
|
- LICENSE
|
52
69
|
- README.md
|
53
70
|
- Rakefile
|
54
71
|
- lib/shoden.rb
|
72
|
+
- lib/shoden/model.rb
|
73
|
+
- lib/shoden/proxy.rb
|
55
74
|
- shoden.gemspec
|
56
75
|
- test/shoden_test.rb
|
57
76
|
homepage: http://github.com/elcuervo/shoden
|
@@ -65,17 +84,16 @@ require_paths:
|
|
65
84
|
- lib
|
66
85
|
required_ruby_version: !ruby/object:Gem::Requirement
|
67
86
|
requirements:
|
68
|
-
- -
|
87
|
+
- - ">="
|
69
88
|
- !ruby/object:Gem::Version
|
70
89
|
version: '0'
|
71
90
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
91
|
requirements:
|
73
|
-
- -
|
92
|
+
- - ">="
|
74
93
|
- !ruby/object:Gem::Version
|
75
94
|
version: '0'
|
76
95
|
requirements: []
|
77
|
-
|
78
|
-
rubygems_version: 2.0.14
|
96
|
+
rubygems_version: 3.0.3
|
79
97
|
signing_key:
|
80
98
|
specification_version: 4
|
81
99
|
summary: Object hash mapper for postgres
|