db-struct 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +125 -0
- data/Rakefile +35 -0
- data/db-struct.gemspec +37 -0
- data/doc/DBStruct/BogoHash.html +1384 -0
- data/doc/DBStruct/ErrorHelpers.html +288 -0
- data/doc/DBStruct/Failure.html +139 -0
- data/doc/DBStruct/FieldError.html +143 -0
- data/doc/DBStruct/MissingRowError.html +143 -0
- data/doc/DBStruct/TypeError.html +143 -0
- data/doc/DBStruct.html +1652 -0
- data/doc/_index.html +189 -0
- data/doc/class_list.html +54 -0
- data/doc/css/common.css +1 -0
- data/doc/css/full_list.css +58 -0
- data/doc/css/style.css +503 -0
- data/doc/file.README.html +187 -0
- data/doc/file_list.html +59 -0
- data/doc/frames.html +22 -0
- data/doc/index.html +187 -0
- data/doc/js/app.js +344 -0
- data/doc/js/full_list.js +242 -0
- data/doc/js/jquery.js +4 -0
- data/doc/method_list.html +342 -0
- data/doc/top-level-namespace.html +110 -0
- data/gem.deps.rb +7 -0
- data/lib/dbstruct.rb +29 -0
- data/lib/internal/bogohash.rb +226 -0
- data/lib/internal/dbstruct_base.rb +144 -0
- data/lib/internal/dbstruct_class.rb +301 -0
- data/lib/internal/error.rb +44 -0
- data/lib/internal/util.rb +11 -0
- data/spec/dbstruct_spec.rb +452 -0
- data/spec/spec_helper.rb +48 -0
- metadata +138 -0
@@ -0,0 +1,110 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
+
<title>
|
7
|
+
Top Level Namespace
|
8
|
+
|
9
|
+
— Documentation by YARD 0.9.37
|
10
|
+
|
11
|
+
</title>
|
12
|
+
|
13
|
+
<link rel="stylesheet" href="css/style.css" type="text/css" />
|
14
|
+
|
15
|
+
<link rel="stylesheet" href="css/common.css" type="text/css" />
|
16
|
+
|
17
|
+
<script type="text/javascript">
|
18
|
+
pathId = "";
|
19
|
+
relpath = '';
|
20
|
+
</script>
|
21
|
+
|
22
|
+
|
23
|
+
<script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
|
24
|
+
|
25
|
+
<script type="text/javascript" charset="utf-8" src="js/app.js"></script>
|
26
|
+
|
27
|
+
|
28
|
+
</head>
|
29
|
+
<body>
|
30
|
+
<div class="nav_wrap">
|
31
|
+
<iframe id="nav" src="class_list.html?1"></iframe>
|
32
|
+
<div id="resizer"></div>
|
33
|
+
</div>
|
34
|
+
|
35
|
+
<div id="main" tabindex="-1">
|
36
|
+
<div id="header">
|
37
|
+
<div id="menu">
|
38
|
+
|
39
|
+
<a href="_index.html">Index</a> »
|
40
|
+
|
41
|
+
|
42
|
+
<span class="title">Top Level Namespace</span>
|
43
|
+
|
44
|
+
</div>
|
45
|
+
|
46
|
+
<div id="search">
|
47
|
+
|
48
|
+
<a class="full_list_link" id="class_list_link"
|
49
|
+
href="class_list.html">
|
50
|
+
|
51
|
+
<svg width="24" height="24">
|
52
|
+
<rect x="0" y="4" width="24" height="4" rx="1" ry="1"></rect>
|
53
|
+
<rect x="0" y="12" width="24" height="4" rx="1" ry="1"></rect>
|
54
|
+
<rect x="0" y="20" width="24" height="4" rx="1" ry="1"></rect>
|
55
|
+
</svg>
|
56
|
+
</a>
|
57
|
+
|
58
|
+
</div>
|
59
|
+
<div class="clear"></div>
|
60
|
+
</div>
|
61
|
+
|
62
|
+
<div id="content"><h1>Top Level Namespace
|
63
|
+
|
64
|
+
|
65
|
+
|
66
|
+
</h1>
|
67
|
+
<div class="box_info">
|
68
|
+
|
69
|
+
|
70
|
+
|
71
|
+
|
72
|
+
|
73
|
+
|
74
|
+
|
75
|
+
|
76
|
+
|
77
|
+
|
78
|
+
|
79
|
+
</div>
|
80
|
+
|
81
|
+
<h2>Defined Under Namespace</h2>
|
82
|
+
<p class="children">
|
83
|
+
|
84
|
+
|
85
|
+
|
86
|
+
|
87
|
+
<strong class="classes">Classes:</strong> <span class='object_link'><a href="DBStruct.html" title="DBStruct (class)">DBStruct</a></span>
|
88
|
+
|
89
|
+
|
90
|
+
</p>
|
91
|
+
|
92
|
+
|
93
|
+
|
94
|
+
|
95
|
+
|
96
|
+
|
97
|
+
|
98
|
+
|
99
|
+
|
100
|
+
</div>
|
101
|
+
|
102
|
+
<div id="footer">
|
103
|
+
Generated on Wed Sep 11 13:00:26 2024 by
|
104
|
+
<a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
|
105
|
+
0.9.37 (ruby-3.1.4).
|
106
|
+
</div>
|
107
|
+
|
108
|
+
</div>
|
109
|
+
</body>
|
110
|
+
</html>
|
data/gem.deps.rb
ADDED
data/lib/dbstruct.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
require 'sequel'
|
5
|
+
|
6
|
+
# `DBStruct` presents a (SQLite3) database table in a way that
|
7
|
+
# closely mimics a Ruby `Hash` of `Struct` objects.
|
8
|
+
#
|
9
|
+
# This is an abstract base class. To use it, you create a subclass
|
10
|
+
# using the `with` method and define the fields to be used. This also
|
11
|
+
# associates the new class with a `Sequel` dataset.
|
12
|
+
#
|
13
|
+
# Subinstances behave similarly to Ruby's `Struct` class. However,
|
14
|
+
# setting or reading a value accesses the corresponding database row.
|
15
|
+
#
|
16
|
+
# Each object also knows its database row ID, an integer that can be
|
17
|
+
# used to look up this object. If a subinstance outlives its database
|
18
|
+
# row, attempting to access the fields will raise a
|
19
|
+
# {DBStruct::MissingRowError}. You can use methods {.present?} or
|
20
|
+
# {.deleted?} to test if this has happened.
|
21
|
+
class DBStruct
|
22
|
+
class BogoHash; end
|
23
|
+
end
|
24
|
+
|
25
|
+
require 'internal/error'
|
26
|
+
require 'internal/util'
|
27
|
+
require 'internal/dbstruct_base'
|
28
|
+
require 'internal/dbstruct_class'
|
29
|
+
require 'internal/bogohash'
|
@@ -0,0 +1,226 @@
|
|
1
|
+
|
2
|
+
# `BogoHash` provides a `Hash`-like interface to the database table
|
3
|
+
# corresponding to a {DBStruct} subclass.
|
4
|
+
#
|
5
|
+
# You can think if it as a `Hash` that holds all instances of a
|
6
|
+
# particular {DBStruct} subclass keyed on the objects' `rowid`
|
7
|
+
# attributes. Most of the `Hash` interface is available (including
|
8
|
+
# `Enumerable`) with the exception of `[]=`. `delete` and `delete_if`
|
9
|
+
# work as expected, however.
|
10
|
+
#
|
11
|
+
# You should never explicitly create a `BogoHash`. Instead, use the
|
12
|
+
# class's {DBStruct.items} or {DBStruct.where} methods.
|
13
|
+
#
|
14
|
+
# Since these methods can be used to narrow the range of items, a
|
15
|
+
# `BogoHash` does not necessarily encompass all instances of its
|
16
|
+
# associated `DBStruct` subclass. Rows that do not match the
|
17
|
+
# selection criteria are not available even when explicitly addressed
|
18
|
+
# by row ID.
|
19
|
+
#
|
20
|
+
# Under the hood, a `BogoHash` is a wrapper around a
|
21
|
+
# `Sequel::Dataset`.
|
22
|
+
class DBStruct::BogoHash
|
23
|
+
include Enumerable
|
24
|
+
|
25
|
+
# @!visibility private
|
26
|
+
# Internal use only!
|
27
|
+
def initialize(dbs_klass, dataset, query)
|
28
|
+
@dbs_klass = dbs_klass
|
29
|
+
@dataset = dataset
|
30
|
+
@query = query.dup.freeze # used for printing only
|
31
|
+
end
|
32
|
+
|
33
|
+
# Return a human-friendly string describing this object.
|
34
|
+
#
|
35
|
+
# @return [String]
|
36
|
+
def inspect
|
37
|
+
params = ""
|
38
|
+
params = "(#{@query.map(&:inspect).join(", ")})" unless @query.empty?
|
39
|
+
return "#{@dbs_klass}.items#{params}"
|
40
|
+
end
|
41
|
+
alias to_s inspect
|
42
|
+
|
43
|
+
# Retrieve the item at `rowid` or nil if its not present.
|
44
|
+
def [](rowid)
|
45
|
+
row = @dataset.first(_id: rowid)
|
46
|
+
return nil unless row
|
47
|
+
return @dbs_klass.new(_id: rowid)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Test if `rowid` is one of the keys in `self`.
|
51
|
+
def has_key?(rowid) = !! self[rowid]
|
52
|
+
alias include? has_key?
|
53
|
+
alias member? has_key?
|
54
|
+
alias key? has_key?
|
55
|
+
|
56
|
+
# Return the number of items in `self`.
|
57
|
+
def size = @dataset.count
|
58
|
+
alias length size
|
59
|
+
|
60
|
+
# Test if self has no items
|
61
|
+
def empty? = size() == 0
|
62
|
+
|
63
|
+
# Return all keys.
|
64
|
+
# @return [Array[Integer]] - the row IDs
|
65
|
+
def keys = map{|k,v| k}
|
66
|
+
|
67
|
+
# Return all values.
|
68
|
+
# @return [Array[DBStruct.with(...)]]
|
69
|
+
def values = map{|k,v| v}
|
70
|
+
|
71
|
+
# Return all keys and values as an array of two-element arrays.
|
72
|
+
# @return [Array[Array[Integer, DBStruct.with(...)]]]
|
73
|
+
def to_a = map{|k,v| [k,v]}
|
74
|
+
|
75
|
+
#
|
76
|
+
# Deletion
|
77
|
+
#
|
78
|
+
|
79
|
+
|
80
|
+
# Delete `rowid` and its associated value from the underlying table
|
81
|
+
# (and `self` by extension). If `rowid` is not present, does
|
82
|
+
# nothing.
|
83
|
+
#
|
84
|
+
# Note that the deleted row object will be returned but can no
|
85
|
+
# longer be used. In particular, it does not hold the contents of
|
86
|
+
# the deleted row.
|
87
|
+
#
|
88
|
+
# @return [DBStruct.with(...)] The deleted value or nil
|
89
|
+
def delete(rowid, &block)
|
90
|
+
transaction {
|
91
|
+
unless has_key?(rowid)
|
92
|
+
return block.value if block
|
93
|
+
return nil
|
94
|
+
end
|
95
|
+
|
96
|
+
val = self[rowid]
|
97
|
+
@dataset.where(_id: rowid).delete
|
98
|
+
return val
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
# Evaluate the block on each key-value pair in `self` end delete
|
103
|
+
# each entry for which the block returns true.
|
104
|
+
#
|
105
|
+
# Like all enumeration operations, the tests and deletions occur in
|
106
|
+
# a single transaction.
|
107
|
+
#
|
108
|
+
# @yield [value] The block to evaluate
|
109
|
+
def delete_if(&block)
|
110
|
+
transaction {
|
111
|
+
self.each{ |k, v| block.call(k,v) and delete(k) }
|
112
|
+
}
|
113
|
+
return self
|
114
|
+
end
|
115
|
+
alias reject! delete_if
|
116
|
+
|
117
|
+
# Delete all entries
|
118
|
+
def clear
|
119
|
+
@dataset.delete
|
120
|
+
return self
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
#
|
125
|
+
# Enumeration
|
126
|
+
#
|
127
|
+
|
128
|
+
|
129
|
+
# Evaluate `block` over each key-and-value pair (i.e. row and
|
130
|
+
# row-id). If no block is given, returns an Enumerator instead.
|
131
|
+
#
|
132
|
+
# It is safe to perform other database operations inside the block
|
133
|
+
# or enumeration loop. This includes modifications to the database
|
134
|
+
# being enumerated, although whether or not added or removed items
|
135
|
+
# are still visited is undefined.
|
136
|
+
#
|
137
|
+
# If called with a block, the entire operation happens within a
|
138
|
+
# single transaction.
|
139
|
+
#
|
140
|
+
# Since this class imports Enumerable, all of its functionality is
|
141
|
+
# also available.
|
142
|
+
def each(&block) = each_backend(:basic_each, block)
|
143
|
+
alias each_pair each
|
144
|
+
|
145
|
+
# Like {.each} but only yields the key (i.e. row ID).
|
146
|
+
def each_key(&block) = each_backend(:basic_each_key, block)
|
147
|
+
|
148
|
+
# Like {.each} but only yields the value.
|
149
|
+
def each_value(&block) = each_backend(:basic_each_value, block)
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
def row_id_after(prev_rowid)
|
154
|
+
row = @dataset
|
155
|
+
.where{_id > prev_rowid}
|
156
|
+
.limit(1)
|
157
|
+
.select(:_id)
|
158
|
+
.first
|
159
|
+
return row[:_id] if row
|
160
|
+
return nil
|
161
|
+
end
|
162
|
+
|
163
|
+
# This is slow, but will usually work even if there are database
|
164
|
+
# operations in the block.
|
165
|
+
def basic_each(&block)
|
166
|
+
return if size == 0
|
167
|
+
|
168
|
+
curr = -1
|
169
|
+
while true
|
170
|
+
rowid = row_id_after(curr)
|
171
|
+
return unless rowid
|
172
|
+
|
173
|
+
block.call(rowid, self[rowid])
|
174
|
+
curr = rowid
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def basic_each_key(&block) = basic_each{|k,v| block.call(k)}
|
179
|
+
def basic_each_value(&block) = basic_each{|k,v| block.call(v)}
|
180
|
+
|
181
|
+
# Handle the enumeration methods (each, each_key, each_value) as
|
182
|
+
# expected: if given, call with the block inside a transaction;
|
183
|
+
# otherwise, return an Enumerator for it.
|
184
|
+
def each_backend(each_method, block)
|
185
|
+
return self.to_enum(each_method) unless block
|
186
|
+
transaction { self.send(each_method, &block) }
|
187
|
+
return self
|
188
|
+
end
|
189
|
+
|
190
|
+
|
191
|
+
# TODO: unsafe_each via Dataset.each; faster but troublesome if you
|
192
|
+
# modify things within the block.
|
193
|
+
|
194
|
+
public
|
195
|
+
|
196
|
+
#
|
197
|
+
# Advanced Querying
|
198
|
+
#
|
199
|
+
|
200
|
+
# Expose `Sequel::Dataset#where` for more advanced querying.
|
201
|
+
#
|
202
|
+
# The underlying `Sequel::Dataset`'s `where` method is called with
|
203
|
+
# all of the arguments and the result is a new BogoHash containing
|
204
|
+
# only the items that match the query.
|
205
|
+
#
|
206
|
+
# This is the Bigger Hammer you can use to narrow a down to a subset
|
207
|
+
# of your data when groups are not enough.
|
208
|
+
#
|
209
|
+
# @return [BogoHash]
|
210
|
+
def where(*cond, **kwcond, &block)
|
211
|
+
new_dataset = @dataset.where(*cond, **kwcond, &block)
|
212
|
+
desc = ["SQL:#{new_dataset.sql}"]
|
213
|
+
return self.class.new(@dbs_klass, new_dataset, desc)
|
214
|
+
end
|
215
|
+
|
216
|
+
#
|
217
|
+
# Helpers
|
218
|
+
#
|
219
|
+
|
220
|
+
# Calls the underling `Sequel::Database#transaction` methods with
|
221
|
+
# all arguments.
|
222
|
+
#
|
223
|
+
# Convenience method.
|
224
|
+
def transaction(*args, **kwargs, &block) =
|
225
|
+
@dbs_klass.transaction(*args, **kwargs, &block)
|
226
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class DBStruct
|
4
|
+
# Returns the numeric Row ID that is used as the primary key for
|
5
|
+
# this row in both the database and any {DBStruct::BogoHash}
|
6
|
+
# instances that may contain it.
|
7
|
+
attr_reader :rowid
|
8
|
+
|
9
|
+
# Creates a new instance of class, creates the corresponding
|
10
|
+
# database row and initializes the fields. This only works if the
|
11
|
+
# class is a subclass of `DBStruct`; `DBStruct` itself cannot be
|
12
|
+
# instantiated.
|
13
|
+
#
|
14
|
+
# If keyword arguments are given, they must correspond to the names
|
15
|
+
# of the fields and have values of the correct type or nil. Omitted
|
16
|
+
# arguments are equivalent to nil.
|
17
|
+
#
|
18
|
+
# The keyword argument `_id:` is intended for internal use only and
|
19
|
+
# should not be used.
|
20
|
+
def initialize(_id: nil, # Internal use only!
|
21
|
+
**kwargs)
|
22
|
+
|
23
|
+
check("Attempted to instantiate abstract class DBStruct.") {
|
24
|
+
self.class < DBStruct
|
25
|
+
}
|
26
|
+
|
27
|
+
@rowid = nil
|
28
|
+
|
29
|
+
# Case 1: existing row
|
30
|
+
if _id
|
31
|
+
check("Using '_id' with other arguments!") { kwargs.empty? }
|
32
|
+
|
33
|
+
@rowid = _id
|
34
|
+
check("Row '#{_id}' does not exist!", MissingRowError) { present? }
|
35
|
+
|
36
|
+
return
|
37
|
+
end
|
38
|
+
|
39
|
+
@rowid = dataset().insert({})
|
40
|
+
set_fields(**kwargs)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def dataset = self.class.dataset
|
46
|
+
def columns = self.class.columns
|
47
|
+
|
48
|
+
def check_field(name, value)
|
49
|
+
fld = self.class.fields_dict[name]
|
50
|
+
check("Unknown field name '#{name}'", FieldError) {
|
51
|
+
fld
|
52
|
+
}
|
53
|
+
|
54
|
+
check("Invalid field type: expecting #{fld.type}, got #{value.class}",
|
55
|
+
TypeError
|
56
|
+
) {
|
57
|
+
value == nil || value.class == fld.type ||
|
58
|
+
(value == false && fld.type == TrueClass)
|
59
|
+
}
|
60
|
+
|
61
|
+
check("Underlying row-id (#{@rowid}) is not in the database.",
|
62
|
+
MissingRowError) { !deleted? }
|
63
|
+
end
|
64
|
+
|
65
|
+
public
|
66
|
+
|
67
|
+
# Test if the database row associated with `self` still exists.
|
68
|
+
# Inverse of `deleted?`.
|
69
|
+
def present? = !deleted?
|
70
|
+
|
71
|
+
# Test if the database row associated with `self` no longer exists.
|
72
|
+
# Inverse of `present?`
|
73
|
+
def deleted? = dataset().where(_id:@rowid).empty?
|
74
|
+
|
75
|
+
# Sets the value of the field named by `field` to the given value.
|
76
|
+
# `field` must be one of the declared fields and value must have the
|
77
|
+
# declared class.
|
78
|
+
def get_field(name)
|
79
|
+
check_field(name, nil)
|
80
|
+
return dataset[_id: @rowid][name]
|
81
|
+
end
|
82
|
+
|
83
|
+
# Retrieve the value of the field named by symbol `field`. Raises
|
84
|
+
# an exception if `field` is not one of the declared fields.
|
85
|
+
def set_field(name, value)
|
86
|
+
check_field(name, value)
|
87
|
+
|
88
|
+
dataset()
|
89
|
+
.where(_id: @rowid)
|
90
|
+
.update({name => value})
|
91
|
+
|
92
|
+
return value
|
93
|
+
end
|
94
|
+
|
95
|
+
# Set zero or more fields at once. Keywords must be the names of
|
96
|
+
# defined fields.
|
97
|
+
#
|
98
|
+
# The update is enclosed in an transaction.
|
99
|
+
def set_fields(**kwargs)
|
100
|
+
self.class.transaction {
|
101
|
+
kwargs.each{|k, v| set_field(k, v)}
|
102
|
+
}
|
103
|
+
return self
|
104
|
+
end
|
105
|
+
|
106
|
+
# Struct-like and hash-like
|
107
|
+
|
108
|
+
# Return the contents as an array of pairs of key and value
|
109
|
+
def to_a = self.columns.map{|key| [key, get_field(key)] }
|
110
|
+
|
111
|
+
# Return the contents as a hash
|
112
|
+
def to_h = to_a.to_h
|
113
|
+
|
114
|
+
# Equivalent to `get_field`
|
115
|
+
def [](field) = get_field(field)
|
116
|
+
|
117
|
+
# Equivalent to `set_field`
|
118
|
+
def []=(field, value)
|
119
|
+
return set_field(field, value)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Equality
|
123
|
+
|
124
|
+
# Test for equality. Instances are equal if and only if they refer
|
125
|
+
# to the same database row and (as implied) are of the same class.
|
126
|
+
def eql?(other) = self.class == other.class && @rowid == other.rowid
|
127
|
+
alias == eql?
|
128
|
+
|
129
|
+
# Return a hash; here because we've also overridden {.eql?}
|
130
|
+
def hash = [self.class, @rowid].hash
|
131
|
+
|
132
|
+
# Etc
|
133
|
+
|
134
|
+
# Return a human-friendly description.
|
135
|
+
def inspect
|
136
|
+
max_line = 60
|
137
|
+
|
138
|
+
values = to_h.map{|k,v| "#{k}: #{v.inspect}"}.join(", ")
|
139
|
+
values = values[0..max_line] + "..." if values.size > max_line
|
140
|
+
return "#{self.class}.new(#{values})"
|
141
|
+
end
|
142
|
+
alias to_s inspect
|
143
|
+
|
144
|
+
end
|