model_schema 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/.gitignore +11 -0
- data/.travis.yml +11 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +21 -0
- data/README.md +251 -0
- data/Rakefile +10 -0
- data/bin/dump_model_schema +121 -0
- data/bin/repl +6 -0
- data/lib/model_schema.rb +6 -0
- data/lib/model_schema/constants.rb +42 -0
- data/lib/model_schema/plugin.rb +216 -0
- data/lib/model_schema/schema_error.rb +112 -0
- data/lib/model_schema/version.rb +3 -0
- data/model_schema.gemspec +33 -0
- metadata +186 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1f90013060ee4c6589d5da0f52a6d43d122f1098
|
4
|
+
data.tar.gz: e6a196f114bfdfb96f360404cdb7e3b318f43545
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 40b269d838239123dad028eb77512fac7299ed248b249daa567243a38c99e64e13d9b465b5eaf97ea8b7f02d8820be1b52b388b48ea0e5de2834ebac6d9d9148
|
7
|
+
data.tar.gz: e2d6ffe9929ea0940a7c2e94a29fa47747feb8c03749c8ac49c1385d6f66ffd00044f510d2e98fda5072e5d3d4758c42a8ebfb93abd019f9b4bfaf5e54e7891a
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Karthik Viswanathan
|
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,251 @@
|
|
1
|
+
# ModelSchema [](https://travis-ci.org/karthikv/model_schema)
|
2
|
+
ModelSchema lets you annotate a [Sequel](https://github.com/jeremyevans/sequel/)
|
3
|
+
Model with its expected schema (columns and indexes). Instead of seeing
|
4
|
+
a Sequel Model file that looks like this:
|
5
|
+
|
6
|
+
```rb
|
7
|
+
class User < Sequel::Model(:users)
|
8
|
+
end
|
9
|
+
```
|
10
|
+
|
11
|
+
You'll see one that looks like this:
|
12
|
+
|
13
|
+
```rb
|
14
|
+
class User < Sequel::Model(:users)
|
15
|
+
model_schema do
|
16
|
+
primary_key :id
|
17
|
+
|
18
|
+
String :email, :null => false
|
19
|
+
String :password, :null => false
|
20
|
+
|
21
|
+
TrueClass :is_admin, :default => false
|
22
|
+
|
23
|
+
DateTime :created_at, :null => false
|
24
|
+
DateTime :updated_at
|
25
|
+
|
26
|
+
index :email
|
27
|
+
end
|
28
|
+
end
|
29
|
+
```
|
30
|
+
|
31
|
+
Unlike other similar gems, ModelSchema provides *enforcement*; if the schema you
|
32
|
+
specify doesn't match the table schema, ModelSchema will raise an error and
|
33
|
+
tell you exactly what's wrong, like so:
|
34
|
+
|
35
|
+
```
|
36
|
+
ModelSchema::SchemaError: Table users does not match the expected schema.
|
37
|
+
|
38
|
+
Table users has extra columns:
|
39
|
+
|
40
|
+
Integer :age
|
41
|
+
|
42
|
+
Table users is missing columns:
|
43
|
+
|
44
|
+
TrueClass :is_admin, :default => false
|
45
|
+
|
46
|
+
Table users has mismatched indexes:
|
47
|
+
|
48
|
+
actual: index [:email], :unique => true
|
49
|
+
expected: index [:email]
|
50
|
+
|
51
|
+
You may disable schema checks by passing :disable => true to model_schema or by
|
52
|
+
setting the ENV variable DISABLE_MODEL_SCHEMA=1.
|
53
|
+
```
|
54
|
+
|
55
|
+
When developing on a team, local databases of team members can easily get out
|
56
|
+
of sync due to differing migrations. ModelSchema immediately lets you know if
|
57
|
+
the schema you expect differs from the actual schema. This ensures you identify
|
58
|
+
database inconsistencies before they cause problems. As a nice added benefit,
|
59
|
+
ModelSchema lets you see a list of columns for a model directly within the
|
60
|
+
class itself.
|
61
|
+
|
62
|
+
## Support for ActiveRecord
|
63
|
+
Currently, ModelSchema only works with [Sequel](https://github.com/jeremyevans/sequel/).
|
64
|
+
If you'd like something similar for other Ruby ORMs, like ActiveRecord, please
|
65
|
+
express your interest in [this issue](https://github.com/karthikv/model_schema/issues/1).
|
66
|
+
|
67
|
+
## Installation
|
68
|
+
Add `model_schema` to your Gemfile:
|
69
|
+
|
70
|
+
```rb
|
71
|
+
gem 'model_schema'
|
72
|
+
```
|
73
|
+
|
74
|
+
And then execute `bundle` in your terminal. You can also install `model_schema`
|
75
|
+
with `gem` directly by running `gem install model_schema`.
|
76
|
+
|
77
|
+
## Usage
|
78
|
+
Require `model_schema` and register the plugin with Sequel:
|
79
|
+
|
80
|
+
```rb
|
81
|
+
require 'model_schema'
|
82
|
+
Sequel.plugin(ModelSchema::Plugin)
|
83
|
+
```
|
84
|
+
|
85
|
+
Then, in each model where you'd like to use ModelSchema, introduce a call to
|
86
|
+
`model_schema`, passing in a block that defines the schema. The block operates
|
87
|
+
exactly as a [Sequel `create_table`
|
88
|
+
block](http://sequel.jeremyevans.net/rdoc/files/doc/schema_modification_rdoc.html).
|
89
|
+
See the documentation on that page for further details.
|
90
|
+
|
91
|
+
```rb
|
92
|
+
class Post < Sequel::Model(:posts)
|
93
|
+
model_schema do
|
94
|
+
primary_key :id
|
95
|
+
|
96
|
+
String :title, :null => false
|
97
|
+
String :description, :text => true, :null => false
|
98
|
+
DateTime :date_posted, :null => false
|
99
|
+
end
|
100
|
+
end
|
101
|
+
```
|
102
|
+
|
103
|
+
When the class is loaded, ModelSchema will ensure the table schema matches the
|
104
|
+
given schema. If there are any errors, it will raise
|
105
|
+
a `ModelSchema::SchemaError`, notifying you of any inconsistencies.
|
106
|
+
|
107
|
+
You may pass an optional hash to `model_schema` with the following options:
|
108
|
+
|
109
|
+
`disable`: `true` to disable all schema checks, `false` otherwise
|
110
|
+
`no_indexes`: `true` to disable schema checks for indexes (columns will still
|
111
|
+
be checked), `false` otherwise
|
112
|
+
|
113
|
+
For instance, to disable index checking:
|
114
|
+
|
115
|
+
```rb
|
116
|
+
class Item < Sequel::Model(:items)
|
117
|
+
model_schema(:no_indexes => true) do
|
118
|
+
...
|
119
|
+
end
|
120
|
+
end
|
121
|
+
```
|
122
|
+
|
123
|
+
Note that you can disable ModelSchema in two ways: either pass `:disable =>
|
124
|
+
true` to the `model_schema` method, or set the environment variable
|
125
|
+
`DISABLE_MODEL_SCHEMA=1` .
|
126
|
+
|
127
|
+
## Bootstrap Existing Project
|
128
|
+
To help bootstrap existing projects that don't yet use ModelSchema, you can use
|
129
|
+
the `dump_model_schema` executable. It will automatically dump an up-to-date
|
130
|
+
`model_schema` block in each Sequel Model class. Use it like so:
|
131
|
+
|
132
|
+
```sh
|
133
|
+
$ dump_model_schema -m [model_file] -c [connection_string]
|
134
|
+
```
|
135
|
+
|
136
|
+
where `model_file` is a path to a ruby file that contains a single Sequel
|
137
|
+
Model, and `connection_string` is the database connection string to pass to
|
138
|
+
`Sequel.connect()`.
|
139
|
+
|
140
|
+
`dump_model_schema` will insert a `model_schema` block right after the
|
141
|
+
definition of the Sequel Model class. Specifically, it looks for a line of the
|
142
|
+
form `class SomeClassName < Sequel::Model(:table_name)`, and inserts a valid
|
143
|
+
schema for table `table_name` directly after that line. Note that
|
144
|
+
`dump_model_schema` overwrites the model file.
|
145
|
+
|
146
|
+
For instance, say you had a file `items.rb` that looks like this:
|
147
|
+
|
148
|
+
```rb
|
149
|
+
module SomeModule
|
150
|
+
class Item < Sequel::Model(:items)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
```
|
154
|
+
|
155
|
+
If you run:
|
156
|
+
|
157
|
+
```sh
|
158
|
+
$ dump_model_schema -m items.rb -c [connection_string]
|
159
|
+
```
|
160
|
+
|
161
|
+
`items.rb` might now look like:
|
162
|
+
|
163
|
+
```rb
|
164
|
+
module SomeModule
|
165
|
+
class Item < Sequel::Model(:items)
|
166
|
+
model_schema do
|
167
|
+
primary_key :id,
|
168
|
+
|
169
|
+
String :name, :null => false
|
170
|
+
Integer :quantity
|
171
|
+
|
172
|
+
index [:name], :name => :items_name_key
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
```
|
177
|
+
|
178
|
+
By default, `dump_model_schema` assumes a tab size of 2 spaces, but you can
|
179
|
+
change this with the `-t` option. Pass an integer representing the number of
|
180
|
+
spaces, or 0 if you want to use hard tabs.
|
181
|
+
|
182
|
+
You may see help text with `dump_model_schema -h` and view the version of
|
183
|
+
ModelSchema with `dump_model_schema -v`.
|
184
|
+
|
185
|
+
## Limitations
|
186
|
+
ModelSchema has a few notable limitations:
|
187
|
+
|
188
|
+
- It checks columns independently from indexes. Say you create a table like so:
|
189
|
+
|
190
|
+
```rb
|
191
|
+
DB.create_table(:items) do
|
192
|
+
String :name, :unique => true
|
193
|
+
Integer :value, :index => true
|
194
|
+
end
|
195
|
+
```
|
196
|
+
|
197
|
+
The corresponding `model_schema` block would be:
|
198
|
+
|
199
|
+
```rb
|
200
|
+
class Item < Sequel::Model(:items)
|
201
|
+
model_schema do
|
202
|
+
String :name
|
203
|
+
Integer :value
|
204
|
+
|
205
|
+
index :name, :unique => true
|
206
|
+
index :value
|
207
|
+
end
|
208
|
+
end
|
209
|
+
```
|
210
|
+
|
211
|
+
You have to separate the columns from the indexes, since the schema dumper
|
212
|
+
reads them independently of one another.
|
213
|
+
|
214
|
+
- It relies on Sequel's [schema dumper extension](http://sequel.jeremyevans.net/rdoc/files/doc/migration_rdoc.html#label-Dumping+the+current+schema+as+a+migration)
|
215
|
+
to read your table's schema. The schema dumper doesn't read constraints,
|
216
|
+
triggers, special index types (e.g. gin, gist) or partial indexes; you'll
|
217
|
+
have to omit these from your `model_schema` block.
|
218
|
+
|
219
|
+
- It doesn't handle all type aliasing. For instance, the Postgres types
|
220
|
+
`character varying(255)[]` and `varchar(255)[]` are equivalent, but
|
221
|
+
ModelSchema is unaware of this. In turn, you might see this error message:
|
222
|
+
|
223
|
+
```
|
224
|
+
Table complex has mismatched columns:
|
225
|
+
|
226
|
+
actual: column :advisors, "character varying(255)[]", :null=>false
|
227
|
+
expected: column :advisors, "varchar(255)[]", :null=>false
|
228
|
+
```
|
229
|
+
|
230
|
+
In the above case, you'll need to change `varchar(255)[]` to `character
|
231
|
+
varying(255)[]` in your `model_schema` block to fix the issue.
|
232
|
+
|
233
|
+
A similar problem occurs with `numeric(x, 0)` and `numeric(x)`, where x is an
|
234
|
+
integer; they are equivalent in Postgres, but ModelSchema doesn't know this.
|
235
|
+
|
236
|
+
## Development and Contributing
|
237
|
+
After cloning this repository, execute `bundle` to install dependencies. You
|
238
|
+
may run tests with `rake test`, and open up a REPL using `bin/repl`.
|
239
|
+
|
240
|
+
Note that tests require access to a Postgres database. Set the environment
|
241
|
+
variable `DB_URL` to a Postgres connection string (e.g.
|
242
|
+
`postgres://localhost:5432/model_schema`) prior to running tests. See
|
243
|
+
[connecting to a database](http://sequel.jeremyevans.net/rdoc/files/doc/opening_databases_rdoc.html)
|
244
|
+
for information about connection strings.
|
245
|
+
|
246
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
247
|
+
|
248
|
+
Any bug reports and pull requests are welcome.
|
249
|
+
|
250
|
+
## License
|
251
|
+
See the [LICENSE.txt](LICENSE.txt) file.
|
data/Rakefile
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'model_schema'
|
4
|
+
require 'slop'
|
5
|
+
|
6
|
+
SEQUEL_MODEL_REGEX = /class\s+.*?<\s+Sequel::Model\((.*?)\)/
|
7
|
+
LEADING_WHITESPACE_REGEX = /^\s*/
|
8
|
+
|
9
|
+
# Parses options and then dumps the model schema.
|
10
|
+
def main(args)
|
11
|
+
opts = {}
|
12
|
+
opts[:tabbing] = 2
|
13
|
+
|
14
|
+
parser = OptionParser.new do |p|
|
15
|
+
p.banner = "Usage: dump_model_schema [options]"
|
16
|
+
|
17
|
+
p.on('-m', '--model MODEL', 'Model file to dump schema in') do |model|
|
18
|
+
opts[:model] = model
|
19
|
+
end
|
20
|
+
|
21
|
+
p.on('-c', '--connection CONNECTION',
|
22
|
+
'Connection string for database') do |connection|
|
23
|
+
opts[:connection] = connection
|
24
|
+
end
|
25
|
+
|
26
|
+
p.on('-t', '--tabbing TABBING', Integer,
|
27
|
+
'Number of spaces for tabbing, or 0 for hard tabs') do |tabbing|
|
28
|
+
opts[:tabbing] = tabbing
|
29
|
+
end
|
30
|
+
|
31
|
+
p.on('-v', '--version', 'Print version') do
|
32
|
+
puts ModelSchema::VERSION
|
33
|
+
exit
|
34
|
+
end
|
35
|
+
|
36
|
+
p.on('-h', '--help', 'Print help') do
|
37
|
+
puts parser
|
38
|
+
exit
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
parser.parse(args)
|
43
|
+
|
44
|
+
# model and connection are required
|
45
|
+
abort 'Must provide a model file with -m or --model.' if !opts[:model]
|
46
|
+
abort 'Must provide a connection string with -c or --connection.' if !opts[:connection]
|
47
|
+
|
48
|
+
dump_model_schema(opts)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Dumps the model schema based on the given options (see option parsing above).
|
52
|
+
def dump_model_schema(opts)
|
53
|
+
model_info = parse_model_file(opts[:model])
|
54
|
+
abort "Couldn't find class that extends Sequel::Model" if !model_info
|
55
|
+
|
56
|
+
db = Sequel.connect(opts[:connection])
|
57
|
+
db.extension(:schema_dumper)
|
58
|
+
|
59
|
+
klass = Class.new(Sequel::Model(model_info[:table_name]))
|
60
|
+
klass.db = db
|
61
|
+
|
62
|
+
# dump table generator given by model_schema
|
63
|
+
generator = klass.send(:table_generator)
|
64
|
+
commands = [generator.dump_columns, generator.dump_constraints,
|
65
|
+
generator.dump_indexes].reject{|s| s == ''}.join("\n\n")
|
66
|
+
|
67
|
+
# account for indentation
|
68
|
+
tab = opts[:tabbing] == 0 ? "\t" : ' ' * opts[:tabbing]
|
69
|
+
schema_indentation = model_info[:indentation] + tab
|
70
|
+
command_indentation = schema_indentation + tab
|
71
|
+
|
72
|
+
commands = commands.lines.map {|l| l == "\n" ? l : command_indentation + l}.join
|
73
|
+
commands = commands.gsub('=>', ' => ')
|
74
|
+
|
75
|
+
dump_lines = ["#{schema_indentation}model_schema do\n",
|
76
|
+
"#{commands}\n",
|
77
|
+
"#{schema_indentation}end\n"]
|
78
|
+
|
79
|
+
lines = model_info[:lines_before] + dump_lines + model_info[:lines_after]
|
80
|
+
File.write(opts[:model], lines.join)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Parses the model file at the given path, returning a hash of the form:
|
84
|
+
#
|
85
|
+
# :table_name => the model table name
|
86
|
+
# :lines_before => an array of lines before the expected model schema dump
|
87
|
+
# :lines_after => an array of lines after the expected model schema dump
|
88
|
+
# :indentation => the indentation (leading whitespace) of the model class
|
89
|
+
#
|
90
|
+
# Returns nil if the file couldn't be parsed.
|
91
|
+
def parse_model_file(path)
|
92
|
+
lines = File.read(path).lines
|
93
|
+
|
94
|
+
lines.each_with_index do |line, index|
|
95
|
+
match = SEQUEL_MODEL_REGEX.match(line)
|
96
|
+
|
97
|
+
if match
|
98
|
+
# extract table name as symbol
|
99
|
+
table_name = match[1]
|
100
|
+
if table_name[0] == ':'
|
101
|
+
table_name = table_name[1..-1].to_sym
|
102
|
+
else
|
103
|
+
abort "Can't find a symbol table name on line: #{line}"
|
104
|
+
end
|
105
|
+
|
106
|
+
# indentation for model_schema block
|
107
|
+
indentation = LEADING_WHITESPACE_REGEX.match(line)[0]
|
108
|
+
|
109
|
+
return {
|
110
|
+
:table_name => table_name.to_sym,
|
111
|
+
:lines_before => lines[0..index],
|
112
|
+
:lines_after => lines[(index + 1)..-1],
|
113
|
+
:indentation => indentation,
|
114
|
+
}
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
nil
|
119
|
+
end
|
120
|
+
|
121
|
+
main(ARGV) if $PROGRAM_NAME == __FILE__
|
data/bin/repl
ADDED
data/lib/model_schema.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
module ModelSchema
|
2
|
+
# ENV variable name to disable schema checks.
|
3
|
+
DISABLE_MODEL_SCHEMA_KEY = 'DISABLE_MODEL_SCHEMA'
|
4
|
+
|
5
|
+
# field types representing table columns and table indexes
|
6
|
+
FIELD_COLUMNS = :columns
|
7
|
+
FIELD_INDEXES = :indexes
|
8
|
+
FIELDS = [FIELD_COLUMNS, FIELD_INDEXES]
|
9
|
+
|
10
|
+
# default column parameters
|
11
|
+
DEFAULT_COL = {
|
12
|
+
:name => nil,
|
13
|
+
:type => nil,
|
14
|
+
:collate => nil,
|
15
|
+
:default => nil,
|
16
|
+
:deferrable => nil,
|
17
|
+
:index => nil,
|
18
|
+
:key => [:id],
|
19
|
+
:null => nil,
|
20
|
+
:on_delete => :no_action,
|
21
|
+
:on_update => :no_action,
|
22
|
+
:primary_key => nil,
|
23
|
+
:primary_key_constraint_name => nil,
|
24
|
+
:unique => nil,
|
25
|
+
:unique_constraint_name => nil,
|
26
|
+
:serial => nil,
|
27
|
+
:table => nil,
|
28
|
+
:text => nil,
|
29
|
+
:fixed => nil,
|
30
|
+
:size => nil,
|
31
|
+
:only_time => nil,
|
32
|
+
}
|
33
|
+
|
34
|
+
# default index parameters
|
35
|
+
DEFAULT_INDEX = {
|
36
|
+
:columns => nil,
|
37
|
+
:name => nil,
|
38
|
+
:type => nil,
|
39
|
+
:unique => nil,
|
40
|
+
:where => nil,
|
41
|
+
}
|
42
|
+
end
|
@@ -0,0 +1,216 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
|
3
|
+
module ModelSchema
|
4
|
+
# Allows you to define an expected schema for a Sequel::Model class and fail
|
5
|
+
# if that schema is not met.
|
6
|
+
module Plugin
|
7
|
+
module ClassMethods
|
8
|
+
# Checks if the model's table schema matches the schema specified by the
|
9
|
+
# given block. Raises a SchemaError if this isn't the case.
|
10
|
+
#
|
11
|
+
# options:
|
12
|
+
# :disable => true to disable schema checks;
|
13
|
+
# you may also set the ENV variable DISABLE_MODEL_SCHEMA=1
|
14
|
+
# :no_indexes => true to disable index checks
|
15
|
+
def model_schema(options={}, &block)
|
16
|
+
return if ENV[DISABLE_MODEL_SCHEMA_KEY] == '1' || options[:disable]
|
17
|
+
db.extension(:schema_dumper)
|
18
|
+
|
19
|
+
# table generators are Sequel's way of representing schemas
|
20
|
+
db_generator = table_generator
|
21
|
+
exp_generator = db.create_table_generator(&block)
|
22
|
+
|
23
|
+
schema_errors = check_all(FIELD_COLUMNS, db_generator, exp_generator)
|
24
|
+
if !options[:no_indexes]
|
25
|
+
schema_errors += check_all(FIELD_INDEXES, db_generator, exp_generator)
|
26
|
+
end
|
27
|
+
|
28
|
+
raise SchemaError.new(table_name, schema_errors) if schema_errors.length > 0
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# Returns the table generator representing this table.
|
34
|
+
def table_generator
|
35
|
+
begin
|
36
|
+
db_generator_explicit = db.send(:dump_table_generator, table_name,
|
37
|
+
:same_db => true)
|
38
|
+
db_generator_generic = db.send(:dump_table_generator, table_name)
|
39
|
+
rescue Sequel::DatabaseError => error
|
40
|
+
if error.message.include?('PG::UndefinedTable:')
|
41
|
+
fail NameError, "Table #{table_name} doesn't exist."
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# db_generator_explicit contains explicit string types for each field,
|
46
|
+
# specific to the current database; db_generator_generic contains ruby
|
47
|
+
# types for each field. When there's no corresponding ruby type,
|
48
|
+
# db_generator_generic defaults to the String type. We'd like to
|
49
|
+
# combine db_generator_explicit and db_generator_generic into one
|
50
|
+
# generator, where ruby types are used if they are accurate. If there
|
51
|
+
# is no accurate ruby type, we use the explicit database type. This
|
52
|
+
# gives us cleaner column dumps, as ruby types have a better, more
|
53
|
+
# generic interface (e.g. `String :col_name` as opposed to
|
54
|
+
# `column :col_name, 'varchar(255)`).
|
55
|
+
|
56
|
+
# start with db_generator_generic, and correct as need be
|
57
|
+
db_generator = db_generator_generic.dup
|
58
|
+
|
59
|
+
# avoid using Sequel::Model.db_schema because it has odd caching
|
60
|
+
# behavior across classes that breaks tests
|
61
|
+
db.schema(table_name).each do |name, col_schema|
|
62
|
+
type_hash = db.column_schema_to_ruby_type(col_schema)
|
63
|
+
|
64
|
+
if type_hash == {:type => String}
|
65
|
+
# There's no corresponding ruby type, as per:
|
66
|
+
# <https://github.com/jeremyevans/sequel/blob/a2cfbb9/lib/sequel/
|
67
|
+
# extensions/schema_dumper.rb#L59-L61>
|
68
|
+
# Copy over the column from db_generator_explicit.
|
69
|
+
index = db_generator.columns.find_index {|c| c[:name] == name}
|
70
|
+
col = db_generator_explicit.columns.find {|c| c[:name] == name}
|
71
|
+
db_generator.columns[index] = col
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
db_generator
|
76
|
+
end
|
77
|
+
|
78
|
+
# Check if db_generator and exp_generator match for the given field
|
79
|
+
# (FIELD_COLUMNS for columns or FIELD_INDEXES for indexes).
|
80
|
+
def check_all(field, db_generator, exp_generator)
|
81
|
+
# To find an accurate diff, we perform two passes on exp_array. In the
|
82
|
+
# first pass, we find perfect matches between exp_array and db_array,
|
83
|
+
# deleting the corresponding elements. In the second pass, for each
|
84
|
+
# exp_elem in exp_array, we find the closest db_elem in db_array that
|
85
|
+
# matches it. We then add a mismatch diff between db_elem and exp_elem
|
86
|
+
# and remove db_elem from db_array. If no db_elem is deemed close
|
87
|
+
# enough, we add a missing diff for exp_elem. Finally, we add an extra
|
88
|
+
# diff for each remaining db_elem in db_array.
|
89
|
+
|
90
|
+
# don't modify original arrays
|
91
|
+
db_array = db_generator.send(field).dup
|
92
|
+
exp_array = exp_generator.send(field).dup
|
93
|
+
|
94
|
+
# first pass: remove perfect matches
|
95
|
+
exp_array.select! do |exp_elem|
|
96
|
+
diffs = db_array.map do |db_elem|
|
97
|
+
check_single(field, :db_generator => db_generator,
|
98
|
+
:exp_generator => exp_generator,
|
99
|
+
:db_elem => db_elem,
|
100
|
+
:exp_elem => exp_elem)
|
101
|
+
end
|
102
|
+
|
103
|
+
index = diffs.find_index(nil)
|
104
|
+
if index
|
105
|
+
# found perfect match; delete elem so it won't be matched again
|
106
|
+
db_array.delete_at(index)
|
107
|
+
false # we've accounted for this element
|
108
|
+
else
|
109
|
+
true # we still need to account for this element
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
schema_diffs = []
|
114
|
+
|
115
|
+
# second pass: find diffs
|
116
|
+
exp_array.each do |exp_elem|
|
117
|
+
index = find_close_match(field, exp_elem, db_array)
|
118
|
+
|
119
|
+
if index
|
120
|
+
# add mismatch diff between exp_elem and db_array[index]
|
121
|
+
schema_diffs << check_single(field, :db_generator => db_generator,
|
122
|
+
:exp_generator => exp_generator,
|
123
|
+
:db_elem => db_array[index],
|
124
|
+
:exp_elem => exp_elem)
|
125
|
+
db_array.delete_at(index)
|
126
|
+
else
|
127
|
+
# add missing diff, since no db_elem is deemed close enough
|
128
|
+
schema_diffs << {:field => field,
|
129
|
+
:type => SchemaError::TYPE_MISSING,
|
130
|
+
:generator => exp_generator,
|
131
|
+
:elem => exp_elem}
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# because we deleted as we went on, db_array holds extra elements
|
136
|
+
db_array.each do |db_elem|
|
137
|
+
schema_diffs << {:field => field,
|
138
|
+
:type => SchemaError::TYPE_EXTRA,
|
139
|
+
:generator => db_generator,
|
140
|
+
:elem => db_elem}
|
141
|
+
end
|
142
|
+
|
143
|
+
schema_diffs
|
144
|
+
end
|
145
|
+
|
146
|
+
# Returns the index of an element in db_array that closely matches exp_elem,
|
147
|
+
# or nil if no such element exists.
|
148
|
+
def find_close_match(field, exp_elem, db_array)
|
149
|
+
case field
|
150
|
+
when FIELD_COLUMNS
|
151
|
+
db_array.find_index {|e| e[:name] == exp_elem[:name]}
|
152
|
+
when FIELD_INDEXES
|
153
|
+
db_array.find_index do |e|
|
154
|
+
e[:name] == exp_elem[:name] || e[:columns] == exp_elem[:columns]
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Check if the given database element matches the expected element.
|
160
|
+
#
|
161
|
+
# field: FIELD_COLUMNS for columns or FIELD_INDEXES for indexes
|
162
|
+
# opts:
|
163
|
+
# :db_generator => db table generator
|
164
|
+
# :exp_generator => expected table generator
|
165
|
+
# :db_elem => column, constraint, or index from db_generator
|
166
|
+
# :exp_elem => column, constraint, or index from exp_generator
|
167
|
+
def check_single(field, opts)
|
168
|
+
db_generator, exp_generator = opts.values_at(:db_generator, :exp_generator)
|
169
|
+
db_elem, exp_elem = opts.values_at(:db_elem, :exp_elem)
|
170
|
+
|
171
|
+
error = {:field => field,
|
172
|
+
:type => SchemaError::TYPE_MISMATCH,
|
173
|
+
:db_generator => db_generator,
|
174
|
+
:exp_generator => exp_generator,
|
175
|
+
:db_elem => db_elem,
|
176
|
+
:exp_elem => exp_elem}
|
177
|
+
|
178
|
+
# db_elem and exp_elem now have the same keys; compare then
|
179
|
+
case field
|
180
|
+
when FIELD_COLUMNS
|
181
|
+
db_elem_defaults = DEFAULT_COL.merge(db_elem)
|
182
|
+
exp_elem_defaults = DEFAULT_COL.merge(exp_elem)
|
183
|
+
return error if db_elem_defaults.length != exp_elem_defaults.length
|
184
|
+
|
185
|
+
type_literal = db.method(:type_literal)
|
186
|
+
# already accounted for in type check
|
187
|
+
keys_accounted_for = [:text, :fixed, :size, :serial]
|
188
|
+
|
189
|
+
match = db_elem_defaults.all? do |key, value|
|
190
|
+
if key == :type
|
191
|
+
# types could either be strings or ruby types; normalize them
|
192
|
+
db_type = type_literal.call(db_elem_defaults).to_s
|
193
|
+
exp_type = type_literal.call(exp_elem_defaults).to_s
|
194
|
+
db_type == exp_type
|
195
|
+
elsif keys_accounted_for.include?(key)
|
196
|
+
true
|
197
|
+
else
|
198
|
+
value == exp_elem_defaults[key]
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
when FIELD_INDEXES
|
203
|
+
db_elem_defaults = DEFAULT_INDEX.merge(db_elem)
|
204
|
+
exp_elem_defaults = DEFAULT_INDEX.merge(exp_elem)
|
205
|
+
return error if db_elem_defaults.length != exp_elem_defaults.length
|
206
|
+
|
207
|
+
# if no index name is specified, accept any name
|
208
|
+
db_elem_defaults.delete(:name) if !exp_elem_defaults[:name]
|
209
|
+
match = db_elem_defaults.all? {|key, value| value == exp_elem_defaults[key]}
|
210
|
+
end
|
211
|
+
|
212
|
+
match ? nil : error
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module ModelSchema
|
2
|
+
# Tracks differences between the expected schema and database table schema.
|
3
|
+
class SchemaError < StandardError
|
4
|
+
TYPE_EXTRA = :extra
|
5
|
+
TYPE_MISSING = :missing
|
6
|
+
TYPE_MISMATCH = :mismatch
|
7
|
+
|
8
|
+
attr_reader :schema_diffs
|
9
|
+
|
10
|
+
# Creates a SchemaError for the given table with an array of schema
|
11
|
+
# differences. Each element of schema_diffs should be a hash of the
|
12
|
+
# following form:
|
13
|
+
#
|
14
|
+
# :field => if a column is different, use FIELD_COLUMNS;
|
15
|
+
# if an index is different, use FIELD_INDEXES
|
16
|
+
# :type => if there's an extra column/index, use TYPE_EXTRA;
|
17
|
+
# if there's a missing column/index, use TYPE_MISSING;
|
18
|
+
# if there's a mismatched column/index, use TYPE_MISMATCH
|
19
|
+
#
|
20
|
+
# For TYPE_EXTRA and TYPE_MISSING:
|
21
|
+
# :generator => the table generator that contains the extra/missing index/column
|
22
|
+
# :elem => the missing index/column, as a hash from the table generator
|
23
|
+
#
|
24
|
+
# For TYPE_MISMATCH:
|
25
|
+
# :db_generator => the db table generator
|
26
|
+
# :exp_generator => the expected table generator
|
27
|
+
# :db_elem => the index/column in the db table generator as a hash
|
28
|
+
# :exp_elem => the index/column in the exp table generator as a hash
|
29
|
+
def initialize(table_name, schema_diffs)
|
30
|
+
@table_name = table_name
|
31
|
+
@schema_diffs = schema_diffs
|
32
|
+
end
|
33
|
+
|
34
|
+
# Dumps a single column/index from the generator to its string representation.
|
35
|
+
#
|
36
|
+
# field: FIELD_COLUMNS for a column or FIELD_INDEXES for an index
|
37
|
+
# generator: the table generator
|
38
|
+
# elem: the index/column in the generator as a hash
|
39
|
+
def dump_single(field, generator, elem)
|
40
|
+
array = generator.send(field)
|
41
|
+
index = array.find_index(elem)
|
42
|
+
fail ArgumentError, "#{elem.inspect} not part of #{array.inspect}" if !index
|
43
|
+
|
44
|
+
lines = generator.send(:"dump_#{field}").lines.map(&:strip)
|
45
|
+
lines[index]
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns the diffs in schema_diffs that have the given field and type.
|
49
|
+
def diffs_by_field_type(field, type)
|
50
|
+
@schema_diffs.select {|diff| diff[:field] == field && diff[:type] == type}
|
51
|
+
end
|
52
|
+
|
53
|
+
# Dumps all diffs that have the given field and are of TYPE_EXTRA.
|
54
|
+
def dump_extra_diffs(field)
|
55
|
+
extra_diffs = diffs_by_field_type(field, TYPE_EXTRA)
|
56
|
+
|
57
|
+
if extra_diffs.length > 0
|
58
|
+
header = "Table #{@table_name} has extra #{field}:\n"
|
59
|
+
diff_str = extra_diffs.map do |diff|
|
60
|
+
dump_single(field, diff[:generator], diff[:elem])
|
61
|
+
end.join("\n\t")
|
62
|
+
|
63
|
+
"#{header}\n\t#{diff_str}\n"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Dumps all diffs that have the given field and are of TYPE_MISSING.
|
68
|
+
def dump_missing_diffs(field)
|
69
|
+
missing_diffs = diffs_by_field_type(field, TYPE_MISSING)
|
70
|
+
|
71
|
+
if missing_diffs.length > 0
|
72
|
+
header = "Table #{@table_name} is missing #{field}:\n"
|
73
|
+
diff_str = missing_diffs.map do |diff|
|
74
|
+
dump_single(field, diff[:generator], diff[:elem])
|
75
|
+
end.join("\n\t")
|
76
|
+
|
77
|
+
"#{header}\n\t#{diff_str}\n"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Dumps all diffs that have the given field and are of TYPE_MISMATCH.
|
82
|
+
def dump_mismatch_diffs(field)
|
83
|
+
mismatch_diffs = diffs_by_field_type(field, TYPE_MISMATCH)
|
84
|
+
|
85
|
+
if mismatch_diffs.length > 0
|
86
|
+
header = "Table #{@table_name} has mismatched #{field}:\n"
|
87
|
+
diff_str = mismatch_diffs.map do |diff|
|
88
|
+
"actual: #{dump_single(field, diff[:db_generator], diff[:db_elem])}\n\t" +
|
89
|
+
"expected: #{dump_single(field, diff[:exp_generator], diff[:exp_elem])}"
|
90
|
+
end.join("\n\n\t")
|
91
|
+
|
92
|
+
"#{header}\n\t#{diff_str}\n"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Combines all dumps into one cohesive error message.
|
97
|
+
def to_s
|
98
|
+
parts = FIELDS.flat_map do |field|
|
99
|
+
[dump_extra_diffs(field),
|
100
|
+
dump_missing_diffs(field),
|
101
|
+
dump_mismatch_diffs(field)]
|
102
|
+
end
|
103
|
+
|
104
|
+
[
|
105
|
+
"Table #{@table_name} does not match the expected schema.\n\n",
|
106
|
+
parts.compact.join("\n"),
|
107
|
+
"\nYou may disable schema checks by passing :disable => true to model_",
|
108
|
+
"schema or by setting the ENV variable #{DISABLE_MODEL_SCHEMA_KEY}=1.\n"
|
109
|
+
].join
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'model_schema/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'model_schema'
|
8
|
+
spec.version = ModelSchema::VERSION
|
9
|
+
spec.authors = ['Karthik Viswanathan']
|
10
|
+
spec.email = ['karthik.ksv@gmail.com']
|
11
|
+
|
12
|
+
spec.summary = %(Enforced, Annotated Schema for Ruby Sequel Models)
|
13
|
+
spec.description = %(Annotate a Sequel Model with its expected schema
|
14
|
+
and immediately identify inconsistencies.).gsub(/\n\s+/, '')
|
15
|
+
spec.homepage = 'https://github.com/karthikv/model_schema'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0").reject {|f| f.match(%r{^(test|spec|features)/})}
|
19
|
+
spec.bindir = 'bin'
|
20
|
+
spec.executables << 'dump_model_schema'
|
21
|
+
spec.require_paths = ['lib']
|
22
|
+
|
23
|
+
spec.add_development_dependency('bundler', '~> 1.10')
|
24
|
+
spec.add_development_dependency('rake', '~> 10.0')
|
25
|
+
spec.add_development_dependency('minitest')
|
26
|
+
spec.add_development_dependency('minitest-hooks')
|
27
|
+
spec.add_development_dependency('mocha')
|
28
|
+
spec.add_development_dependency('pg')
|
29
|
+
spec.add_development_dependency('pry')
|
30
|
+
spec.add_development_dependency('awesome_print')
|
31
|
+
|
32
|
+
spec.add_runtime_dependency('sequel')
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: model_schema
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Karthik Viswanathan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-11-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.10'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.10'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest-hooks
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: mocha
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pg
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: pry
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: awesome_print
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: sequel
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
description: Annotate a Sequel Model with its expected schema and immediately identify
|
140
|
+
inconsistencies.
|
141
|
+
email:
|
142
|
+
- karthik.ksv@gmail.com
|
143
|
+
executables:
|
144
|
+
- dump_model_schema
|
145
|
+
extensions: []
|
146
|
+
extra_rdoc_files: []
|
147
|
+
files:
|
148
|
+
- ".gitignore"
|
149
|
+
- ".travis.yml"
|
150
|
+
- Gemfile
|
151
|
+
- LICENSE.txt
|
152
|
+
- README.md
|
153
|
+
- Rakefile
|
154
|
+
- bin/dump_model_schema
|
155
|
+
- bin/repl
|
156
|
+
- lib/model_schema.rb
|
157
|
+
- lib/model_schema/constants.rb
|
158
|
+
- lib/model_schema/plugin.rb
|
159
|
+
- lib/model_schema/schema_error.rb
|
160
|
+
- lib/model_schema/version.rb
|
161
|
+
- model_schema.gemspec
|
162
|
+
homepage: https://github.com/karthikv/model_schema
|
163
|
+
licenses:
|
164
|
+
- MIT
|
165
|
+
metadata: {}
|
166
|
+
post_install_message:
|
167
|
+
rdoc_options: []
|
168
|
+
require_paths:
|
169
|
+
- lib
|
170
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
171
|
+
requirements:
|
172
|
+
- - ">="
|
173
|
+
- !ruby/object:Gem::Version
|
174
|
+
version: '0'
|
175
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '0'
|
180
|
+
requirements: []
|
181
|
+
rubyforge_project:
|
182
|
+
rubygems_version: 2.2.2
|
183
|
+
signing_key:
|
184
|
+
specification_version: 4
|
185
|
+
summary: Enforced, Annotated Schema for Ruby Sequel Models
|
186
|
+
test_files: []
|