sack 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +21 -0
- data/README.md +291 -0
- data/Rakefile +9 -0
- data/lib/sack.rb +14 -0
- data/lib/sack/connectors.rb +14 -0
- data/lib/sack/connectors/sqlite3.rb +46 -0
- data/lib/sack/database.rb +96 -0
- data/lib/sack/database/data.rb +141 -0
- data/lib/sack/database/ftypes.rb +31 -0
- data/lib/sack/database/generator.rb +54 -0
- data/lib/sack/database/model.rb +108 -0
- data/lib/sack/database/model/data.rb +66 -0
- data/lib/sack/database/model/relationships.rb +35 -0
- data/lib/sack/database/model/relationships/belongs_to.rb +62 -0
- data/lib/sack/database/model/relationships/has_many.rb +56 -0
- data/lib/sack/database/model/validation.rb +138 -0
- data/lib/sack/database/sanitizer.rb +78 -0
- data/lib/sack/database/schema.rb +32 -0
- data/lib/sack/database/statement.rb +27 -0
- data/lib/sack/version.rb +9 -0
- data/sack.gemspec +25 -0
- metadata +137 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: efb08ab6965016ac346ea8e63141972a58ef0228
|
4
|
+
data.tar.gz: 37f648bcc771fc4ec30a09f2fb04b370a88b0a6d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c8e4f163b354e31e8fd9d2cf537a0e9211ed545caf64887f4fd68dc24b79e3859d9226bb80dec0862ffdf0aed07d718a6c52b8fd18ecdc6f2551294abed7de00
|
7
|
+
data.tar.gz: a2279b88ad6eb8083f53ce9ced7abe3c17ea4d113716daae0ff6966459f9975b1bee78eba99f03be0c5800c58cb2fa7e8336d1bd688825a196e58b51ed51d0ff
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Paul Duncan
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,291 @@
|
|
1
|
+
# Sack
|
2
|
+
|
3
|
+
Minimalistic Database Layer based on SQLite3
|
4
|
+
|
5
|
+
## Presentation
|
6
|
+
|
7
|
+
This library provides a lightweight an easy-to-use database interface.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
### Gemfile
|
12
|
+
```ruby
|
13
|
+
gem 'sack'
|
14
|
+
```
|
15
|
+
|
16
|
+
### Terminal
|
17
|
+
```bash
|
18
|
+
gem install -V sack
|
19
|
+
```
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
Sack may be used at different levels to achieve various levels of complexity.
|
24
|
+
Let's explore the major uses.
|
25
|
+
|
26
|
+
### Most basic
|
27
|
+
|
28
|
+
In its simplest form, Sack provides a direct CRUD to any database, as long as you provide it with *schema* information, a connector and a connection string.
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
# Define the Schema
|
32
|
+
SCHEMA = {
|
33
|
+
|
34
|
+
# 'user' Table
|
35
|
+
user: {
|
36
|
+
|
37
|
+
# ID - Auto-Increment (:ai) Primary-Key (:pk) Integer (:int)
|
38
|
+
id: [:int, :pk, :ai],
|
39
|
+
|
40
|
+
# Name - String
|
41
|
+
name: [:str]
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
# Create a Database object using the SQLite3 Connector
|
46
|
+
db = Sack::Database.new Sack::Connectors::SQLite3Connector, 'example.db', SCHEMA
|
47
|
+
|
48
|
+
# Create
|
49
|
+
db.create :user, name: 'foobar'
|
50
|
+
|
51
|
+
# Fetch by Field
|
52
|
+
u = db.fetch_by(:user, :name, 'foobar').first
|
53
|
+
|
54
|
+
# Fetch by ID
|
55
|
+
u = db.fetch(:user, u[:id]).first
|
56
|
+
|
57
|
+
# Fetch All
|
58
|
+
users = db.fetch_all :user
|
59
|
+
|
60
|
+
# Find (fetch by ID - first result)
|
61
|
+
u = db.find :user, u[:id]
|
62
|
+
|
63
|
+
# Count
|
64
|
+
user_count = db.count :user
|
65
|
+
|
66
|
+
# Update
|
67
|
+
db.update :user, u[:id], name: 'John Doe'
|
68
|
+
|
69
|
+
# Save (Combined Create / Update)
|
70
|
+
u[:name] = 'Jane Doe'
|
71
|
+
db.save :user, u
|
72
|
+
|
73
|
+
# Delete
|
74
|
+
db.delete :user, u[:id]
|
75
|
+
```
|
76
|
+
|
77
|
+
### Model-based Schema
|
78
|
+
|
79
|
+
Defining a Sack Schema by hand can be painful.
|
80
|
+
Also, the concept of *relationships* between entities is not available.
|
81
|
+
Finally, all validation has to be performed by the application.
|
82
|
+
|
83
|
+
Another option is to use Sack's *Model* abstraction, providing schema generation, validation and relationships.
|
84
|
+
|
85
|
+
Each model is defined using a module which includes *Sack::Database::Model*, placed inside a _root module_.
|
86
|
+
|
87
|
+
The schema for the whole entity model can then be derived from this root module using *Sack::Schema.from_module*.
|
88
|
+
|
89
|
+
The module presented above can be re-written as such:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
# Model Root
|
93
|
+
module DataModel
|
94
|
+
|
95
|
+
# Users
|
96
|
+
module User
|
97
|
+
include Sack::Database::Model
|
98
|
+
|
99
|
+
# Fields
|
100
|
+
field id: [:int, :pk, :ai]
|
101
|
+
field name: [:str]
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Create a Database object using the SQLite3 Connector and a Schema derived from our DataModel
|
106
|
+
db = Sack::Database.new Sack::Connectors::SQLite3Connector, 'example.db', Sack::Database::Schema.from_module(DataModel)
|
107
|
+
```
|
108
|
+
|
109
|
+
### Validation
|
110
|
+
|
111
|
+
The Model abstraction provided by Sack offers validation, which can be specified directly on the fields defined in each model.
|
112
|
+
Let's add some validation on our user model:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
# Users
|
116
|
+
module User
|
117
|
+
include Sack::Database::Model
|
118
|
+
|
119
|
+
# Fields
|
120
|
+
field id: [:int, :pk, :ai]
|
121
|
+
field name: [:str],
|
122
|
+
required: true,
|
123
|
+
unique: true,
|
124
|
+
min_length: 3,
|
125
|
+
max_length: 24,
|
126
|
+
regex: /^[a-zA-Z][a-zA-Z0-9_-]+$/,
|
127
|
+
validate: :no_prefix
|
128
|
+
|
129
|
+
# Custom Validation Method - No Prefix
|
130
|
+
# @param [Database] db Database instance
|
131
|
+
# @param [Hash] data The entity being validated
|
132
|
+
# @param [Symbol] name The field being validated
|
133
|
+
# @param [Object] val The field's value
|
134
|
+
# @param [Hash] rules The field's validation rules hash (possibly containing any custom validation params)
|
135
|
+
# @param [Array] errors The list of errors for the entity being validated (if no error is added but the method returns false, a generic error message will be added by Sack)
|
136
|
+
# @return [Object] true / false (true = valid)
|
137
|
+
def self.no_prefix db, data, name, val, rules, errors
|
138
|
+
valid = !(/^mr|ms|dr|mrs /i =~ val)
|
139
|
+
errors << "Field [#{name}] should not include prefixes such as mr. ms. dr. etc..."
|
140
|
+
valid
|
141
|
+
end
|
142
|
+
end
|
143
|
+
```
|
144
|
+
|
145
|
+
*Note*: The *unique* validator can be either set to _true_ to validate global unicity, or it can be set to an array of other fields to scope unicity.
|
146
|
+
|
147
|
+
Validity can then be checked by the application at any time using the *is_valid?* method injected into every model.
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
User.is_valid? db, name: 'foobar'
|
151
|
+
# => true
|
152
|
+
|
153
|
+
User.is_valid? db, name: nil
|
154
|
+
# => false
|
155
|
+
|
156
|
+
errors = []
|
157
|
+
User.is_valid?(db, { name: '000' }, errors)
|
158
|
+
# => false
|
159
|
+
|
160
|
+
errors.each { |e| puts e }
|
161
|
+
# Field [name] doesn't match allowed pattern (/^[a-zA-Z][a-zA-Z0-9_-]+$/)
|
162
|
+
```
|
163
|
+
|
164
|
+
### Relationships
|
165
|
+
|
166
|
+
Using the Model abstraction, we can define relationships between our entities.
|
167
|
+
Let's add an _article_ model and define a one-to-many relationship from users to articles.
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
# User Model
|
171
|
+
module User
|
172
|
+
include Sack::Database::Model
|
173
|
+
|
174
|
+
# Fields
|
175
|
+
field id: [:int, :pk, :ai]
|
176
|
+
field name: [:str]
|
177
|
+
|
178
|
+
# Relationships
|
179
|
+
has_many articles: :article, fk: :author
|
180
|
+
end
|
181
|
+
|
182
|
+
# Article Model
|
183
|
+
module Article
|
184
|
+
include Sack::Database::Model
|
185
|
+
|
186
|
+
# Fields
|
187
|
+
field id: [:int, :pk, :ai]
|
188
|
+
field author: [:int]
|
189
|
+
field title: [:str]
|
190
|
+
field body: [:txt]
|
191
|
+
|
192
|
+
# Relationships
|
193
|
+
belongs_to author: :user
|
194
|
+
end
|
195
|
+
```
|
196
|
+
|
197
|
+
This then allows us to use the relationships to simplify our lives a little.
|
198
|
+
|
199
|
+
```ruby
|
200
|
+
# Fetching association with direct parameters
|
201
|
+
articles = User.articles db, id: 0
|
202
|
+
|
203
|
+
# Fetching association with entity
|
204
|
+
u = User.find db, id
|
205
|
+
articles = User.articles db, u
|
206
|
+
|
207
|
+
a = articles.first
|
208
|
+
u = Article.author db, a
|
209
|
+
```
|
210
|
+
|
211
|
+
This is nice, but we can do better.
|
212
|
+
Actually, although every entity handled by Sack is really just a *Hash* (to keep things simple), the models also inject a small router into any entity it loads.
|
213
|
+
This allows to access an entity's associations directly, and through multiple levels.
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
u = User.find db, id
|
217
|
+
|
218
|
+
articles = u.articles(db)
|
219
|
+
u = articles.first.author(db)
|
220
|
+
|
221
|
+
# we can recurse as many levels as we want
|
222
|
+
articles = u.articles(db).first.author(db).articles(db).first.author(db).articles(db)
|
223
|
+
```
|
224
|
+
|
225
|
+
#### Belongs To
|
226
|
+
|
227
|
+
The belongs-to relationship injects a method in the model, with the name of the association. This method allows fetching the associated entity.
|
228
|
+
The name of the association _MUST MATCH_ the name of the field storing the key to the associated entity.
|
229
|
+
|
230
|
+
If given ONLY a name, belongs_to will auto-guess the name of the associated model (CamelCased version of the association's name).
|
231
|
+
To alter this behavior (as in the example above - the association is called 'author' but the target model is actually 'user'), simply provide a model name:
|
232
|
+
|
233
|
+
```ruby
|
234
|
+
module Foo
|
235
|
+
include Sack::Database::Model
|
236
|
+
field id: [:int, :pk, :ai]
|
237
|
+
end
|
238
|
+
|
239
|
+
module Bar
|
240
|
+
include Sack::Database::Model
|
241
|
+
field id: [:int, :pk, :ai]
|
242
|
+
|
243
|
+
# A 'Bar' belongs to two 'Foo's
|
244
|
+
field foo: [:int]
|
245
|
+
field other_foo: [:int]
|
246
|
+
|
247
|
+
# Target model is Foo - no need to specify it
|
248
|
+
belongs_to :foo
|
249
|
+
|
250
|
+
# Explicitly use Foo as the target model
|
251
|
+
belongs_to other_foo: :foo
|
252
|
+
end
|
253
|
+
```
|
254
|
+
|
255
|
+
#### Has Many
|
256
|
+
|
257
|
+
The has-many relationship injects a method in the model, with the name of the association. This method allows fetching the associated entities.
|
258
|
+
The name of the association can be anything. The target model NEEDS to be specified.
|
259
|
+
|
260
|
+
Unless explicitly specified, Sack will use the name of the current model to determine the foreign key (the field within the target model which holds the ID of the current model).
|
261
|
+
|
262
|
+
```ruby
|
263
|
+
module Foo
|
264
|
+
include Sack::Database::Model
|
265
|
+
field id: [:int, :pk, :ai]
|
266
|
+
|
267
|
+
# Foreign Key in Bar is 'foo'
|
268
|
+
has_many bars: :bar
|
269
|
+
|
270
|
+
# Foreign Key in Bork is 'parent'
|
271
|
+
has_many borks: :bork, fk: :parent
|
272
|
+
end
|
273
|
+
|
274
|
+
module Bar
|
275
|
+
include Sack::Database::Model
|
276
|
+
field id: [:int, :pk, :ai]
|
277
|
+
field foo: [:int]
|
278
|
+
belongs_to :foo
|
279
|
+
end
|
280
|
+
|
281
|
+
module Bork
|
282
|
+
include Sack::Database::Model
|
283
|
+
field id: [:int, :pk, :ai]
|
284
|
+
field parent: [:int]
|
285
|
+
belongs_to parent: :foo
|
286
|
+
end
|
287
|
+
```
|
288
|
+
|
289
|
+
## License
|
290
|
+
|
291
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/lib/sack.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# Sack
|
2
|
+
# by Eresse <eresse@eresse.net>
|
3
|
+
|
4
|
+
# Internal Includes
|
5
|
+
require 'sack/version'
|
6
|
+
require 'sack/database'
|
7
|
+
require 'sack/database/model'
|
8
|
+
require 'sack/database/schema'
|
9
|
+
require 'sack/connectors'
|
10
|
+
|
11
|
+
# Sack Module:
|
12
|
+
# Root Module for Sack.
|
13
|
+
module Sack
|
14
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# Sack
|
2
|
+
# by Eresse <eresse@eresse.net>
|
3
|
+
|
4
|
+
# External Includes
|
5
|
+
require 'sqlite3'
|
6
|
+
|
7
|
+
# Internal Includes
|
8
|
+
require 'sack/database/sanitizer'
|
9
|
+
|
10
|
+
# Sack Module
|
11
|
+
module Sack
|
12
|
+
|
13
|
+
# Connectors Module
|
14
|
+
module Connectors
|
15
|
+
|
16
|
+
# SQLite3 Connector Module:
|
17
|
+
# Provides SQLite3 connectivity for Sack Database.
|
18
|
+
module SQLite3Connector
|
19
|
+
|
20
|
+
# Open:
|
21
|
+
# Opens a connection to an SQLite3 database.
|
22
|
+
# @param [String] conn_string The connection string (path to a db file)
|
23
|
+
# @return [Object] Database connection
|
24
|
+
def self.open conn_string
|
25
|
+
SQLite3::Database.open conn_string
|
26
|
+
end
|
27
|
+
|
28
|
+
# Close:
|
29
|
+
# Closes a previously-opened database connection.
|
30
|
+
# @param [Object] dbc Database connection
|
31
|
+
def self.close dbc
|
32
|
+
dbc.close
|
33
|
+
end
|
34
|
+
|
35
|
+
# Execute
|
36
|
+
# Executes an SQL statement with parameters
|
37
|
+
# @param [Object] dbc Database connection
|
38
|
+
# @param [String] q Statement
|
39
|
+
# @param [Array] params Statement parameters
|
40
|
+
# @return [Array] Statement results
|
41
|
+
def self.exec dbc, q
|
42
|
+
dbc.exec q
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# Sack
|
2
|
+
# by Eresse <eresse@eresse.net>
|
3
|
+
|
4
|
+
# External Includes
|
5
|
+
require 'thread'
|
6
|
+
require 'sqlite3'
|
7
|
+
|
8
|
+
# Internal Includes
|
9
|
+
require 'sack/database/data'
|
10
|
+
|
11
|
+
# Sack Module
|
12
|
+
module Sack
|
13
|
+
|
14
|
+
# Database Class:
|
15
|
+
# Main Database Access Class - provides CRUD methods (and a little more) against a given Schema on a provided Database Connector and Connection String.
|
16
|
+
class Database
|
17
|
+
|
18
|
+
# Actions:
|
19
|
+
# Allowed data methods
|
20
|
+
ACTIONS = [
|
21
|
+
:create_table,
|
22
|
+
:count,
|
23
|
+
:find,
|
24
|
+
:fetch,
|
25
|
+
:fetch_by,
|
26
|
+
:fetch_all,
|
27
|
+
:create,
|
28
|
+
:update,
|
29
|
+
:save,
|
30
|
+
:delete
|
31
|
+
]
|
32
|
+
|
33
|
+
# Construct:
|
34
|
+
# Builds a *Database* on top of a backend _connector_ and _connstring_, set to operate on _schema_.
|
35
|
+
# @param [Object] connector Generic database backend connector
|
36
|
+
# @param [String] connstring Connector-specific connection string
|
37
|
+
# @param [Hash] schema Schema definition - see README
|
38
|
+
def initialize connector, connstring, schema
|
39
|
+
|
40
|
+
# Set Connector & Connstring
|
41
|
+
@connector = connector
|
42
|
+
@connstring = connstring
|
43
|
+
|
44
|
+
# Set Schema
|
45
|
+
@schema = schema
|
46
|
+
|
47
|
+
# Create Lock
|
48
|
+
@lock = Mutex.new
|
49
|
+
|
50
|
+
# Verify Schema
|
51
|
+
verify_schema
|
52
|
+
end
|
53
|
+
|
54
|
+
# Open:
|
55
|
+
# Opens a session on the database for yielding.
|
56
|
+
def open
|
57
|
+
|
58
|
+
# Lock DB
|
59
|
+
@lock.synchronize do
|
60
|
+
|
61
|
+
# Open Database
|
62
|
+
db = @connector.open @connstring
|
63
|
+
|
64
|
+
# Yield Block
|
65
|
+
yield Data.new(db, @schema) if block_given?
|
66
|
+
|
67
|
+
# Close Database
|
68
|
+
db.close
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Verify Schema:
|
73
|
+
# Verifies the supplied schema against the actual database & re-creates it if necessary.
|
74
|
+
def verify_schema
|
75
|
+
open { |data| rebuild_schema data unless @schema.keys.inject(true) { |a, table| a && (data.exec "select count(*) from #{table};" rescue nil) } }
|
76
|
+
end
|
77
|
+
|
78
|
+
# Rebuild Schema:
|
79
|
+
# Re-creates the database according to the supplied schema.
|
80
|
+
# @param [Data] data Data access interface
|
81
|
+
def rebuild_schema data
|
82
|
+
@schema.each { |table, fields| data.create_table table, fields }
|
83
|
+
end
|
84
|
+
|
85
|
+
# Method Missing:
|
86
|
+
# Catches and routes database actions through data access interface.
|
87
|
+
def method_missing name, *args
|
88
|
+
|
89
|
+
# Check Action
|
90
|
+
raise "Unknown action [#{name}]" unless ACTIONS.include? name
|
91
|
+
|
92
|
+
# Open Database { Perform Action }
|
93
|
+
open { |db| return db.send name, *args }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|