reactive_record 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/.gitignore +20 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +124 -0
- data/Rakefile +14 -0
- data/lib/code_generator.rb +279 -0
- data/lib/generators/reactive_record/install_generator.rb +23 -0
- data/lib/lexer.rb +91 -0
- data/lib/parser.y +98 -0
- data/lib/reactive_record.rb +115 -0
- data/lib/reactive_record/version.rb +3 -0
- data/reactive_record.gemspec +26 -0
- data/spec/lexer_spec.rb +145 -0
- data/spec/parser_spec.rb +39 -0
- data/spec/reactive_record_spec.rb +119 -0
- data/spec/seed/database.sql +67 -0
- metadata +122 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
Njg2MDk2YTkyZDExNzdjODJhZWJkNTAwM2M3OWY3ZTBkNTE1YjdkMw==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
NDBiM2E0YzViNDMzNjJjNWE3NjJjMThiZmQwZjI5NmI0ZjIyYTBhMQ==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
YWRhOWFiMjE0YWI0MTM4N2E1MTAxZGY4N2MzOTMxYTIzOWEwNWJhYWFhOTMw
|
10
|
+
YThlOWNjMzc4ZTY0ODNjODA2M2MyM2Y2YjAyYTIwYjIzZTFmOTMwMGEzMTkz
|
11
|
+
MzIyZDQ4NWMxYmEwMDNkYzQzNDJkODA2NDQ3NzNmN2QzMjZkZDg=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
YzBlMGU0ZDcxNzU5ZGZjZTZiYTc0YzJmNWRkNTk4N2E3ODI1ZjI0ZTE1NGRj
|
14
|
+
OTFhZWEwNjg2Yjc0NDQ5NGQxMjZhODQ0NWNjOTQ3N2I4ZjgxMDhlYTg0NDVk
|
15
|
+
YmJlYmNjNGE4MTczZThhNThmOWQ2MDI3NGE0MTliOTIzOWNiNTM=
|
data/.gitignore
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
.ruby-version
|
19
|
+
.ruby-gemset
|
20
|
+
lib/parser.rb
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 Chris Wilson & Joe Nelson
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be included
|
14
|
+
in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
19
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
20
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
21
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
22
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
# reactive_record
|
2
|
+
|
3
|
+
Generates ActiveRecord models to fit a pre-existing Postgres database.
|
4
|
+
Now you can use Rails with the db schema you always wanted. It's
|
5
|
+
**your** convention over configuration.
|
6
|
+
|
7
|
+
## Why?
|
8
|
+
|
9
|
+
1. Your app is specific to Postgres and proud of it. You use the mature
|
10
|
+
declarative data validation that only a real database can provide.
|
11
|
+
1. You have inherited a database or are more comfortable creating one
|
12
|
+
yourself.
|
13
|
+
1. You're a grown-ass DBA who doesn't want to speak ORM baby-talk.
|
14
|
+
|
15
|
+
## Features
|
16
|
+
|
17
|
+
* Fully automatic. It just works.
|
18
|
+
* Creates a model for every table.
|
19
|
+
* Creates a comprehensive initial migration.
|
20
|
+
* Declares key-, uniqueness-, and presence-constraints.
|
21
|
+
* Creates associations.
|
22
|
+
* Adds custom validation methods for `CHECK` constraints.
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
### Already familiar with Rails?
|
27
|
+
|
28
|
+
* Set up a postgres db normally
|
29
|
+
* Set `config.active_record.schema_format = :sql` to use a SQL `schema.rb`
|
30
|
+
* After you have migrated up a table, use `rails generate reactive_record:install`
|
31
|
+
* Go examine your generated models
|
32
|
+
|
33
|
+
### Want more details?
|
34
|
+
|
35
|
+
**First** Include the `reactive_record` gem in your project's
|
36
|
+
`Gemfile`. *Oh by the way*, you'll have to use postgres in your
|
37
|
+
project. Setting up Rails for use with postgres is a bit outside
|
38
|
+
the scope of this document. Please see [Configuring a Database]
|
39
|
+
(http://guides.rubyonrails.org/configuring.html#configuring-a-database)
|
40
|
+
for what you need to do.
|
41
|
+
|
42
|
+
``` ruby
|
43
|
+
gem 'reactive_record'
|
44
|
+
```
|
45
|
+
|
46
|
+
Bundle to include the library
|
47
|
+
|
48
|
+
``` shell
|
49
|
+
$ bundle
|
50
|
+
```
|
51
|
+
|
52
|
+
**Next** Tell `ActiveRecord` to go into beast-mode. Edit your
|
53
|
+
`config/application.rb`, adding this line to use sql as the schema
|
54
|
+
format:
|
55
|
+
|
56
|
+
``` ruby
|
57
|
+
module YourApp
|
58
|
+
class Application < Rails::Application
|
59
|
+
# other configuration bric-a-brac...
|
60
|
+
config.active_record.schema_format = :sql
|
61
|
+
end
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
**Next** Create the database(s) just like you normally would:
|
66
|
+
|
67
|
+
``` shell
|
68
|
+
rake db:create
|
69
|
+
```
|
70
|
+
|
71
|
+
**Next** Generate a migration that will create the initial table:
|
72
|
+
|
73
|
+
``` shell
|
74
|
+
$ rails generate migration create_employees
|
75
|
+
```
|
76
|
+
|
77
|
+
Use your SQL powers to craft some
|
78
|
+
[DDL](http://en.wikipedia.org/wiki/Data_definition_language), perhaps
|
79
|
+
the "Hello, World!" of DB applications, `employees`?
|
80
|
+
|
81
|
+
``` ruby
|
82
|
+
class CreateEmployees < ActiveRecord::Migration
|
83
|
+
def up
|
84
|
+
execute <<-SQL
|
85
|
+
CREATE TABLE employees (
|
86
|
+
id SERIAL,
|
87
|
+
name VARCHAR(255) NOT NULL,
|
88
|
+
email VARCHAR(255) NOT NULL UNIQUE,
|
89
|
+
start_date DATE NOT NULL,
|
90
|
+
|
91
|
+
PRIMARY KEY (id),
|
92
|
+
CONSTRAINT company_email CHECK (email LIKE '%@example.com')
|
93
|
+
);
|
94
|
+
SQL
|
95
|
+
end
|
96
|
+
|
97
|
+
def down
|
98
|
+
drop_table :employees
|
99
|
+
end
|
100
|
+
end
|
101
|
+
```
|
102
|
+
|
103
|
+
**Lastly** Deploy the `reactive_record` generator:
|
104
|
+
|
105
|
+
``` shell
|
106
|
+
$ rails generate reactive_record
|
107
|
+
```
|
108
|
+
|
109
|
+
Go look at the generated file:
|
110
|
+
|
111
|
+
``` ruby
|
112
|
+
class Employees < ActiveRecord::Base
|
113
|
+
set_table_name 'employees'
|
114
|
+
set_primary_key :id
|
115
|
+
validate :id, :name, :email, :start_date, presence: true
|
116
|
+
validate :email, uniqueness: true
|
117
|
+
validate { errors.add(:email, "Expected TODO") unless email =~ /.*@example.com/ }
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
Reactive record does not currently attempt to generate any kind of
|
122
|
+
reasonable error message (I'm working on it) :)
|
123
|
+
|
124
|
+
**Enjoy**
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
|
4
|
+
RSpec::Core::RakeTask.new(:spec)
|
5
|
+
|
6
|
+
rule '.rb' => '.y' do |t|
|
7
|
+
sh "racc -l -o #{t.name} #{t.source}"
|
8
|
+
end
|
9
|
+
|
10
|
+
task :compile => 'lib/parser.rb'
|
11
|
+
|
12
|
+
task :spec => :compile
|
13
|
+
|
14
|
+
task :default => :spec
|
@@ -0,0 +1,279 @@
|
|
1
|
+
class Node
|
2
|
+
def initialize val, *args
|
3
|
+
@value = val
|
4
|
+
@children = args
|
5
|
+
end
|
6
|
+
|
7
|
+
def gen
|
8
|
+
raise "gen is not implemented: #{self}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def column
|
12
|
+
'base'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class UniqueNode < Node
|
17
|
+
def initialize val; super(val, nil); end
|
18
|
+
|
19
|
+
def gen
|
20
|
+
"validates :#{@value.gen}, uniqueness: true"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class IdentNode < Node
|
25
|
+
def initialize val; super(val, nil); end
|
26
|
+
|
27
|
+
def gen
|
28
|
+
# bottoms out: IDENT is a string
|
29
|
+
@value
|
30
|
+
end
|
31
|
+
|
32
|
+
def column
|
33
|
+
# ident is likely a column (must be?)
|
34
|
+
@value
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class ExprNode < Node
|
39
|
+
def initialize val; super(val); end
|
40
|
+
|
41
|
+
def column
|
42
|
+
@value.column
|
43
|
+
end
|
44
|
+
|
45
|
+
def gen
|
46
|
+
@value.gen
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class EmptyExprNode < Node
|
51
|
+
def gen
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def column
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class CheckNode < Node
|
61
|
+
def initialize val; super(val); end
|
62
|
+
|
63
|
+
def gen
|
64
|
+
column = @value.column
|
65
|
+
val = @value.gen
|
66
|
+
if val
|
67
|
+
"validate { errors.add(:#{column}, \"Expected TODO\") unless #{val} }"
|
68
|
+
else
|
69
|
+
"validate { true }"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class TypedExprNode < Node
|
75
|
+
def initialize val, *args
|
76
|
+
@children = args
|
77
|
+
@type = @children[0]
|
78
|
+
@value = case @type.gen
|
79
|
+
when 'text' then TextNode.new(val)
|
80
|
+
when 'date' then DateExprNode.new(val)
|
81
|
+
else
|
82
|
+
raise "Unknown type: #{@children[0]}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def column
|
87
|
+
@value.column
|
88
|
+
end
|
89
|
+
|
90
|
+
def gen
|
91
|
+
@value.gen
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class TextNode < Node
|
96
|
+
def gen
|
97
|
+
@value.gen
|
98
|
+
end
|
99
|
+
|
100
|
+
def column
|
101
|
+
@value.column
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class DateExprNode < Node
|
106
|
+
def initialize val
|
107
|
+
@value = val
|
108
|
+
end
|
109
|
+
|
110
|
+
def gen
|
111
|
+
val = @value.gen
|
112
|
+
if val == 'now'
|
113
|
+
"Time.now.to_date"
|
114
|
+
else
|
115
|
+
# YYYY-MM-DD
|
116
|
+
"Date.parse(\"#{val}\")"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class StrLitNode < Node
|
122
|
+
def initialize val; super(val); end
|
123
|
+
|
124
|
+
def gen
|
125
|
+
#FIXME HACK
|
126
|
+
if @value.respond_to? :gen
|
127
|
+
val = @value.gen
|
128
|
+
else
|
129
|
+
val = @value
|
130
|
+
end
|
131
|
+
val.gsub(/^'(.*)'$/, '\1')
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
class IntNode < Node
|
136
|
+
def initialize val; super(val); end
|
137
|
+
|
138
|
+
def gen
|
139
|
+
@value.to_s
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
class OperatorNode < Node
|
144
|
+
def initialize op, *args
|
145
|
+
@value = op
|
146
|
+
@children = args
|
147
|
+
@expr1 = @children[0]
|
148
|
+
@expr2 = @children[1]
|
149
|
+
end
|
150
|
+
|
151
|
+
def operation
|
152
|
+
case @value
|
153
|
+
when :gteq, :lteq, :neq, :eq, :gt, :lt
|
154
|
+
ComparisonExpr.new @value, @expr1, @expr2
|
155
|
+
when :plus
|
156
|
+
MathExpr.new @value, @expr1, @expr2
|
157
|
+
when :match
|
158
|
+
MatchExpr.new @expr1, @expr2
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def error_msg
|
163
|
+
case @value
|
164
|
+
when :gteq then 'to be greater than or equal to'
|
165
|
+
when :lteq then 'to be less than or equal to'
|
166
|
+
when :neq then 'to not equal'
|
167
|
+
when :eq then 'to be equal to'
|
168
|
+
when :gt then 'to be greater than'
|
169
|
+
when :lt then 'to be less than'
|
170
|
+
when :plus then 'plus'
|
171
|
+
when :match then 'to match'
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def column
|
176
|
+
c1 = @expr1.column
|
177
|
+
c2 = @expr1.column
|
178
|
+
return c1 if c1 != 'base'
|
179
|
+
return c2 if c2 != 'base'
|
180
|
+
'base'
|
181
|
+
end
|
182
|
+
|
183
|
+
def gen
|
184
|
+
operation.gen
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
class ComparisonExpr
|
189
|
+
def initialize comparison, e1, e2
|
190
|
+
@e1, @e2 = e1, e2
|
191
|
+
@comparison = {
|
192
|
+
gteq: '>=',
|
193
|
+
lteq: '<=',
|
194
|
+
neq: '!=',
|
195
|
+
eq: '==',
|
196
|
+
gt: '>',
|
197
|
+
lt: '<',
|
198
|
+
}[comparison]
|
199
|
+
end
|
200
|
+
|
201
|
+
def gen
|
202
|
+
"#{@e1.gen} #{@comparison} #{@e2.gen}"
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
class MathExpr
|
207
|
+
def initialize op, e1, e2
|
208
|
+
@e1, @e2 = e1, e2
|
209
|
+
@operation = { plus: '+', minus: '-' }[op]
|
210
|
+
end
|
211
|
+
|
212
|
+
def gen
|
213
|
+
"#{@e1.gen} #{@operation} #{@e2.gen}"
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
class MatchExpr
|
218
|
+
def initialize e1, e2
|
219
|
+
@e1 = e1
|
220
|
+
@e2 = e2
|
221
|
+
end
|
222
|
+
|
223
|
+
def convert_wildcard str
|
224
|
+
str.gsub(/%/, '.*')
|
225
|
+
end
|
226
|
+
|
227
|
+
def gen
|
228
|
+
#raise "RHS: #{@e2.class} #{@e2.gen.class}"
|
229
|
+
"#{@e1.gen} =~ /#{convert_wildcard @e2.gen}/"
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
class TableNode < Node
|
234
|
+
def initialize tab, col
|
235
|
+
@tab = tab
|
236
|
+
@col = col
|
237
|
+
end
|
238
|
+
|
239
|
+
def table_to_class
|
240
|
+
@tab.gen.capitalize
|
241
|
+
end
|
242
|
+
|
243
|
+
def key_name
|
244
|
+
@col.gen
|
245
|
+
end
|
246
|
+
|
247
|
+
def gen
|
248
|
+
@tab.gen
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
class ActionNode < Node
|
253
|
+
def initialize action, consequence
|
254
|
+
@action = action
|
255
|
+
@consequence = consequence
|
256
|
+
end
|
257
|
+
|
258
|
+
def gen
|
259
|
+
"ON #{@action} #{@consequence}"
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
class ForeignKeyNode < Node
|
264
|
+
def initialize col, table, *actions
|
265
|
+
@col = col
|
266
|
+
@table = table
|
267
|
+
if actions.count > 0
|
268
|
+
@action = actions[0]
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
def gen
|
273
|
+
col = @col.gen
|
274
|
+
table_name = @table.gen
|
275
|
+
class_name = @table.table_to_class
|
276
|
+
key = @table.key_name
|
277
|
+
"belongs_to :#{table_name}, foreign_key: '#{col}', class: '#{class_name}', primary_key: '#{key}'"
|
278
|
+
end
|
279
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require "reactive_record"
|
2
|
+
|
3
|
+
module ReactiveRecord
|
4
|
+
module Generators
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
6
|
+
include ReactiveRecord
|
7
|
+
|
8
|
+
desc "Adds models based upon your existing Postgres DB"
|
9
|
+
|
10
|
+
def create_models
|
11
|
+
db_env = Rails.configuration.database_configuration[Rails.env]
|
12
|
+
raise 'You must use the pg database adapter' unless db_env['adapter'] == 'postgresql'
|
13
|
+
db = PG.connect dbname: db_env['database']
|
14
|
+
table_names(db).each do |table|
|
15
|
+
unless table == 'schema_migrations'
|
16
|
+
create_file "app/models/#{table.underscore.pluralize}.rb", model_definition(db, table)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
data/lib/lexer.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
module ConstraintParser
|
4
|
+
class Lexer
|
5
|
+
CASCADE = /CASCADE/
|
6
|
+
CHECK = /CHECK/
|
7
|
+
COMMA = /,/
|
8
|
+
DELETE = /DELETE/
|
9
|
+
EQ = /\=/
|
10
|
+
FOREIGN_KEY = /FOREIGN KEY/
|
11
|
+
GT = /\>/
|
12
|
+
GTEQ = /\>=/
|
13
|
+
IDENT = /[a-zA-Z][a-zA-Z0-9_]*/
|
14
|
+
INT = /[0-9]+/
|
15
|
+
LPAREN = /\(/
|
16
|
+
LT = /\</
|
17
|
+
LTEQ = /\<=/
|
18
|
+
MATCH_OP = /~~/
|
19
|
+
NEQ = /\<\>/
|
20
|
+
NEWLINE = /\n/
|
21
|
+
ON = /ON/
|
22
|
+
PLUS = /\+/
|
23
|
+
PRIMARY_KEY = /PRIMARY KEY/
|
24
|
+
REFERENCES = /REFERENCES/
|
25
|
+
RESTRICT = /RESTRICT/
|
26
|
+
RPAREN = /\)/
|
27
|
+
SPACE = /[\ \t]+/
|
28
|
+
STRLIT = /\'(\\.|[^\\'])*\'/
|
29
|
+
TYPE = /::/
|
30
|
+
UNIQUE = /UNIQUE/
|
31
|
+
|
32
|
+
def initialize io
|
33
|
+
@ss = StringScanner.new io.read
|
34
|
+
end
|
35
|
+
|
36
|
+
def next_token
|
37
|
+
return if @ss.eos?
|
38
|
+
|
39
|
+
result = false
|
40
|
+
while !result
|
41
|
+
result = case
|
42
|
+
when text = @ss.scan(SPACE) then #ignore whitespace
|
43
|
+
|
44
|
+
# Operators
|
45
|
+
when text = @ss.scan(GTEQ) then [:GTEQ, text]
|
46
|
+
when text = @ss.scan(LTEQ) then [:LTEQ, text]
|
47
|
+
when text = @ss.scan(NEQ) then [:NEQ, text]
|
48
|
+
when text = @ss.scan(GT) then [:GT, text]
|
49
|
+
when text = @ss.scan(LT) then [:LT, text]
|
50
|
+
when text = @ss.scan(EQ) then [:EQ, text]
|
51
|
+
when text = @ss.scan(PLUS) then [:PLUS, text]
|
52
|
+
when text = @ss.scan(MATCH_OP) then [:MATCH_OP, text]
|
53
|
+
|
54
|
+
# SQL Keywords
|
55
|
+
when text = @ss.scan(CHECK) then [:CHECK, text]
|
56
|
+
when text = @ss.scan(UNIQUE) then [:UNIQUE, text]
|
57
|
+
when text = @ss.scan(PRIMARY_KEY) then [:PRIMARY_KEY, text]
|
58
|
+
when text = @ss.scan(FOREIGN_KEY) then [:FOREIGN_KEY, text]
|
59
|
+
when text = @ss.scan(REFERENCES) then [:REFERENCES, text]
|
60
|
+
when text = @ss.scan(DELETE) then [:DELETE, text]
|
61
|
+
when text = @ss.scan(ON) then [:ON, text]
|
62
|
+
when text = @ss.scan(RESTRICT) then [:RESTRICT, text]
|
63
|
+
when text = @ss.scan(CASCADE) then [:CASCADE, text]
|
64
|
+
|
65
|
+
# Values
|
66
|
+
when text = @ss.scan(IDENT) then [:IDENT, text]
|
67
|
+
when text = @ss.scan(STRLIT) then [:STRLIT, text]
|
68
|
+
when text = @ss.scan(INT) then [:INT, text]
|
69
|
+
|
70
|
+
# Syntax
|
71
|
+
when text = @ss.scan(LPAREN) then [:LPAREN, text]
|
72
|
+
when text = @ss.scan(RPAREN) then [:RPAREN, text]
|
73
|
+
when text = @ss.scan(TYPE) then [:TYPE, text]
|
74
|
+
when text = @ss.scan(COMMA) then [:COMMA, text]
|
75
|
+
else
|
76
|
+
x = @ss.getch
|
77
|
+
[x, x]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
result
|
81
|
+
end
|
82
|
+
|
83
|
+
def tokenize
|
84
|
+
out = []
|
85
|
+
while (tok = next_token)
|
86
|
+
out << tok
|
87
|
+
end
|
88
|
+
out
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/lib/parser.y
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
class ConstraintParser::Parser
|
2
|
+
prechigh
|
3
|
+
left PLUS MATCH_OP
|
4
|
+
left GTEQ LTEQ NEQ EQ GT LT
|
5
|
+
preclow
|
6
|
+
|
7
|
+
token CASCADE
|
8
|
+
CHECK
|
9
|
+
COMMA
|
10
|
+
DELETE
|
11
|
+
EQ
|
12
|
+
FOREIGN_KEY
|
13
|
+
GT
|
14
|
+
GTEQ
|
15
|
+
IDENT
|
16
|
+
INT
|
17
|
+
LPAREN
|
18
|
+
LT
|
19
|
+
LTEQ
|
20
|
+
MATCH_OP
|
21
|
+
NEQ
|
22
|
+
NEWLINE
|
23
|
+
ON
|
24
|
+
PLUS
|
25
|
+
PRIMARY_KEY
|
26
|
+
REFERENCES
|
27
|
+
RESTRICT
|
28
|
+
RPAREN
|
29
|
+
SPACE
|
30
|
+
STRLIT
|
31
|
+
TYPE
|
32
|
+
UNIQUE
|
33
|
+
|
34
|
+
rule
|
35
|
+
constraint : unique_column { result = val[0] }
|
36
|
+
| check_statement { result = val[0] }
|
37
|
+
| foreign_key { result = val[0] }
|
38
|
+
;
|
39
|
+
|
40
|
+
unique_column : UNIQUE LPAREN IDENT RPAREN { result = UniqueNode.new(IdentNode.new(val[2])) }
|
41
|
+
;
|
42
|
+
|
43
|
+
check_statement : CHECK expr { result = CheckNode.new(val[1]) }
|
44
|
+
|
45
|
+
expr : { result = EmptyExprNode.new :empty }
|
46
|
+
| LPAREN expr RPAREN { result = ExprNode.new(val[1]) }
|
47
|
+
| expr type_signature { result = TypedExprNode.new(val[0], val[1]) }
|
48
|
+
| expr operator expr { result = OperatorNode.new(val[1], val[0], val[2]) }
|
49
|
+
| IDENT { result = IdentNode.new(val[0]) }
|
50
|
+
| STRLIT { result = StrLitNode.new(val[0]) }
|
51
|
+
| INT { result = IntNode.new(val[0]) }
|
52
|
+
;
|
53
|
+
|
54
|
+
operator : GTEQ { result = :gteq }
|
55
|
+
| LTEQ { result = :lteq }
|
56
|
+
| NEQ { result = :neq }
|
57
|
+
| EQ { result = :eq }
|
58
|
+
| GT { result = :gt }
|
59
|
+
| LT { result = :lt }
|
60
|
+
| PLUS { result = :plus }
|
61
|
+
| MATCH_OP { result = :match }
|
62
|
+
;
|
63
|
+
|
64
|
+
type_signature : TYPE IDENT { result = IdentNode.new(val[1]) }
|
65
|
+
;
|
66
|
+
|
67
|
+
foreign_key : FOREIGN_KEY column_spec REFERENCES table_spec { result = ForeignKeyNode.new(val[1], val[3]) }
|
68
|
+
| FOREIGN_KEY column_spec REFERENCES table_spec action_spec { result = ForeignKeyNode.new(val[1], val[3], val[4]) }
|
69
|
+
;
|
70
|
+
|
71
|
+
column_spec : LPAREN IDENT RPAREN { result = IdentNode.new(val[1]) }
|
72
|
+
;
|
73
|
+
|
74
|
+
table_spec : IDENT column_spec { result = TableNode.new(IdentNode.new(val[0]), val[1]) }
|
75
|
+
;
|
76
|
+
|
77
|
+
action_spec : ON DELETE RESTRICT { result = ActionNode.new(:delete, :restrict) }
|
78
|
+
| ON DELETE CASCADE { result = ActionNode.new(:delete, :cascade) }
|
79
|
+
;
|
80
|
+
end
|
81
|
+
|
82
|
+
---- inner
|
83
|
+
|
84
|
+
require 'lexer'
|
85
|
+
require 'code_generator'
|
86
|
+
|
87
|
+
def initialize tokenizer, handler = nil
|
88
|
+
@tokenizer = tokenizer
|
89
|
+
super()
|
90
|
+
end
|
91
|
+
|
92
|
+
def next_token
|
93
|
+
@tokenizer.next_token
|
94
|
+
end
|
95
|
+
|
96
|
+
def parse
|
97
|
+
do_parse
|
98
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require_relative "./reactive_record/version"
|
2
|
+
|
3
|
+
require 'pg'
|
4
|
+
require 'active_support/inflector'
|
5
|
+
require 'parser'
|
6
|
+
|
7
|
+
module ReactiveRecord
|
8
|
+
def model_definition db, table_name
|
9
|
+
header = "class #{table_name.classify.pluralize} < ActiveRecord::Base\n"
|
10
|
+
footer = "end\n"
|
11
|
+
|
12
|
+
body = []
|
13
|
+
body << "set_table_name '#{table_name}'"
|
14
|
+
body << "set_primary_key :#{primary_key db, table_name}"
|
15
|
+
body << "#{validate_definition non_nullable_columns(db, table_name), 'presence'}"
|
16
|
+
body << "#{validate_definition unique_columns(db, table_name), 'uniqueness'}"
|
17
|
+
|
18
|
+
generate_constraints(db, table_name).each do |con|
|
19
|
+
body << con
|
20
|
+
end
|
21
|
+
|
22
|
+
indent = " "
|
23
|
+
body = indent + body.join("\n" + indent) + "\n"
|
24
|
+
header + body + footer
|
25
|
+
end
|
26
|
+
|
27
|
+
def validate_definition cols, type
|
28
|
+
return '' if cols.empty?
|
29
|
+
symbols = cols.map { |c| ":#{c}" }
|
30
|
+
"validate #{symbols.join ', '}, #{type}: true"
|
31
|
+
end
|
32
|
+
|
33
|
+
def table_names db
|
34
|
+
results = db.exec(
|
35
|
+
"select table_name from information_schema.tables where table_schema = $1",
|
36
|
+
['public']
|
37
|
+
)
|
38
|
+
results.map { |r| r['table_name'] }
|
39
|
+
end
|
40
|
+
|
41
|
+
def constraints db, table
|
42
|
+
db.exec("""
|
43
|
+
SELECT c.relname AS table_name,
|
44
|
+
con.conname AS constraint_name,
|
45
|
+
pg_get_constraintdef( con.oid, false) AS constraint_src
|
46
|
+
FROM pg_constraint con
|
47
|
+
JOIN pg_namespace n on (n.oid = con.connamespace)
|
48
|
+
JOIN pg_class c on (c.oid = con.conrelid)
|
49
|
+
WHERE con.conrelid != 0
|
50
|
+
AND c.relname = $1
|
51
|
+
ORDER BY con.conname;
|
52
|
+
""", [table])
|
53
|
+
end
|
54
|
+
|
55
|
+
def generate_constraints db, table
|
56
|
+
key_or_pkey = lambda do |row|
|
57
|
+
row['constraint_name'].end_with?('_key') || row['constraint_name'].end_with?('_pkey')
|
58
|
+
end
|
59
|
+
|
60
|
+
constraints(db, table)
|
61
|
+
.reject(&key_or_pkey)
|
62
|
+
.map(&parse_constraint)
|
63
|
+
end
|
64
|
+
|
65
|
+
def parse_constraint
|
66
|
+
lambda do |row|
|
67
|
+
src = row['constraint_src']
|
68
|
+
parser = ConstraintParser::Parser.new(ConstraintParser::Lexer.new(StringIO.new src))
|
69
|
+
parser.parse.gen
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def cols_with_contype db, table_name, type
|
74
|
+
db.exec """
|
75
|
+
SELECT column_name, conname
|
76
|
+
FROM pg_constraint, information_schema.columns
|
77
|
+
WHERE table_name = $1
|
78
|
+
AND contype = $2
|
79
|
+
AND ordinal_position = any(conkey);
|
80
|
+
""", [table_name, type]
|
81
|
+
end
|
82
|
+
|
83
|
+
def column_name
|
84
|
+
lambda {|row| row['column_name']}
|
85
|
+
end
|
86
|
+
|
87
|
+
def table_name
|
88
|
+
lambda {|row| row['table_name']}
|
89
|
+
end
|
90
|
+
|
91
|
+
def primary_key db, table_name
|
92
|
+
matching_primary_key = lambda {|row| row['conname'] == "#{table_name}_pkey"}
|
93
|
+
cols_with_contype(db, table_name, 'p')
|
94
|
+
.select(&matching_primary_key)
|
95
|
+
.map(&column_name)
|
96
|
+
.first
|
97
|
+
end
|
98
|
+
|
99
|
+
def unique_columns db, table_name
|
100
|
+
matching_unique_constraint = lambda {|row| row['conname'] == "#{table_name}_#{row['column_name']}_key"}
|
101
|
+
cols_with_contype(db, table_name, 'u')
|
102
|
+
.select(&matching_unique_constraint)
|
103
|
+
.map(&column_name)
|
104
|
+
end
|
105
|
+
|
106
|
+
def non_nullable_columns db, table_name
|
107
|
+
result = db.exec """
|
108
|
+
SELECT column_name
|
109
|
+
FROM information_schema.columns
|
110
|
+
WHERE table_name = $1
|
111
|
+
AND is_nullable = $2
|
112
|
+
""", [table_name, 'NO']
|
113
|
+
result.map { |r| r['column_name'] }
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'reactive_record/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "reactive_record"
|
8
|
+
gem.version = ReactiveRecord::VERSION
|
9
|
+
gem.authors = ["Joe Nelson", "Chris Wilson"]
|
10
|
+
gem.email = ["christopher.j.wilson@gmail.com"]
|
11
|
+
gem.description = %q{Generate ActiveRecord models from a pre-existing Postgres db}
|
12
|
+
gem.summary = %q{Use the schema you always wanted.}
|
13
|
+
gem.homepage = "https://github.com/twopoint718/reactive_record"
|
14
|
+
gem.licenses = ['MIT']
|
15
|
+
|
16
|
+
gem.files = `git ls-files`.split($/)
|
17
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
18
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
19
|
+
gem.require_paths = ["lib"]
|
20
|
+
|
21
|
+
gem.add_dependency "pg"
|
22
|
+
gem.add_dependency "activesupport"
|
23
|
+
|
24
|
+
gem.add_development_dependency 'rspec'
|
25
|
+
gem.add_development_dependency 'racc'
|
26
|
+
end
|
data/spec/lexer_spec.rb
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require_relative '../lib/lexer.rb'
|
3
|
+
|
4
|
+
describe ConstraintParser::Lexer do
|
5
|
+
let(:lex){ConstraintParser::Lexer}
|
6
|
+
|
7
|
+
it "CHECK (((email)::text ~~ '%@bendyworks.com'::text))" do
|
8
|
+
@lex = lex.new \
|
9
|
+
StringIO.new "CHECK (((email)::text ~~ '%@bendyworks.com'::text))"
|
10
|
+
@lex.tokenize.should == [
|
11
|
+
[:CHECK, 'CHECK'],
|
12
|
+
[:LPAREN, '('],
|
13
|
+
[:LPAREN, '('],
|
14
|
+
[:LPAREN, '('],
|
15
|
+
[:IDENT, 'email'],
|
16
|
+
[:RPAREN, ')'],
|
17
|
+
[:TYPE, '::'],
|
18
|
+
[:IDENT, 'text'],
|
19
|
+
[:MATCH_OP, '~~'],
|
20
|
+
[:STRLIT, "'%@bendyworks.com'"],
|
21
|
+
[:TYPE, '::'],
|
22
|
+
[:IDENT, 'text'],
|
23
|
+
[:RPAREN, ')'],
|
24
|
+
[:RPAREN, ')'],
|
25
|
+
]
|
26
|
+
end
|
27
|
+
|
28
|
+
it "UNIQUE (email)" do
|
29
|
+
@lex = lex.new StringIO.new "UNIQUE (email)"
|
30
|
+
@lex.tokenize.should == [
|
31
|
+
[:UNIQUE, 'UNIQUE'],
|
32
|
+
[:LPAREN, '('],
|
33
|
+
[:IDENT, 'email'],
|
34
|
+
[:RPAREN, ')']
|
35
|
+
]
|
36
|
+
end
|
37
|
+
|
38
|
+
it "PRIMARY KEY (employee_id)" do
|
39
|
+
@lex = lex.new StringIO.new "PRIMARY KEY (employee_id)"
|
40
|
+
@lex.tokenize.should == [
|
41
|
+
[:PRIMARY_KEY, 'PRIMARY KEY'],
|
42
|
+
[:LPAREN, '('],
|
43
|
+
[:IDENT, 'employee_id'],
|
44
|
+
[:RPAREN, ')']
|
45
|
+
]
|
46
|
+
end
|
47
|
+
|
48
|
+
it "CHECK ((start_date >= '2009-04-13'::date))" do
|
49
|
+
@lex = lex.new StringIO.new "CHECK ((start_date >= '2009-04-13'::date))"
|
50
|
+
@lex.tokenize.should == [
|
51
|
+
[:CHECK, 'CHECK'],
|
52
|
+
[:LPAREN, '('],
|
53
|
+
[:LPAREN, '('],
|
54
|
+
[:IDENT, 'start_date'],
|
55
|
+
[:GTEQ, '>='],
|
56
|
+
[:STRLIT, "'2009-04-13'"],
|
57
|
+
[:TYPE, '::'],
|
58
|
+
[:IDENT, 'date'],
|
59
|
+
[:RPAREN, ')'],
|
60
|
+
[:RPAREN, ')'],
|
61
|
+
]
|
62
|
+
end
|
63
|
+
|
64
|
+
it '"CHECK ((start_date <= ((\'now\'::text)::date + 365)))"' do
|
65
|
+
@lex = lex.new StringIO.new "CHECK ((start_date <= ((\'now\'::text)::date + 365)))"
|
66
|
+
@lex.tokenize.should == [
|
67
|
+
[:CHECK, 'CHECK'],
|
68
|
+
[:LPAREN, '('],
|
69
|
+
[:LPAREN, '('],
|
70
|
+
[:IDENT, 'start_date'],
|
71
|
+
[:LTEQ, '<='],
|
72
|
+
[:LPAREN, '('],
|
73
|
+
[:LPAREN, '('],
|
74
|
+
[:STRLIT, "'now'"],
|
75
|
+
[:TYPE, '::'],
|
76
|
+
[:IDENT, 'text'],
|
77
|
+
[:RPAREN, ')'],
|
78
|
+
[:TYPE, '::'],
|
79
|
+
[:IDENT, 'date'],
|
80
|
+
[:PLUS, '+'],
|
81
|
+
[:INT, '365'],
|
82
|
+
[:RPAREN, ')'],
|
83
|
+
[:RPAREN, ')'],
|
84
|
+
[:RPAREN, ')'],
|
85
|
+
]
|
86
|
+
end
|
87
|
+
|
88
|
+
it "FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE RESTRICT" do
|
89
|
+
@lex = lex.new StringIO.new "FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE RESTRICT"
|
90
|
+
@lex.tokenize.should == [
|
91
|
+
[:FOREIGN_KEY, 'FOREIGN KEY'],
|
92
|
+
[:LPAREN, '('],
|
93
|
+
[:IDENT, 'employee_id'],
|
94
|
+
[:RPAREN, ')'],
|
95
|
+
[:REFERENCES, 'REFERENCES'],
|
96
|
+
[:IDENT, 'employees'],
|
97
|
+
[:LPAREN, '('],
|
98
|
+
[:IDENT, 'employee_id'],
|
99
|
+
[:RPAREN, ')'],
|
100
|
+
[:ON, 'ON'],
|
101
|
+
[:DELETE, 'DELETE'],
|
102
|
+
[:RESTRICT, 'RESTRICT'],
|
103
|
+
]
|
104
|
+
end
|
105
|
+
|
106
|
+
it "PRIMARY KEY (employee_id, project_id)" do
|
107
|
+
@lex = lex.new StringIO.new "PRIMARY KEY (employee_id, project_id)"
|
108
|
+
@lex.tokenize.should == [
|
109
|
+
[:PRIMARY_KEY, 'PRIMARY KEY'],
|
110
|
+
[:LPAREN, '('],
|
111
|
+
[:IDENT, 'employee_id'],
|
112
|
+
[:COMMA, ','],
|
113
|
+
[:IDENT, 'project_id'],
|
114
|
+
[:RPAREN, ')']
|
115
|
+
]
|
116
|
+
end
|
117
|
+
|
118
|
+
it "FOREIGN KEY (project_id) REFERENCES projects(project_id) ON DELETE CASCADE" do
|
119
|
+
@lex = lex.new StringIO.new "FOREIGN KEY (project_id) REFERENCES projects(project_id) ON DELETE CASCADE"
|
120
|
+
@lex.tokenize.should == [
|
121
|
+
[:FOREIGN_KEY, 'FOREIGN KEY'],
|
122
|
+
[:LPAREN, '('],
|
123
|
+
[:IDENT, 'project_id'],
|
124
|
+
[:RPAREN, ')'],
|
125
|
+
[:REFERENCES, 'REFERENCES'],
|
126
|
+
[:IDENT, 'projects'],
|
127
|
+
[:LPAREN, '('],
|
128
|
+
[:IDENT, 'project_id'],
|
129
|
+
[:RPAREN, ')'],
|
130
|
+
[:ON, 'ON'],
|
131
|
+
[:DELETE, 'DELETE'],
|
132
|
+
[:CASCADE, 'CASCADE'],
|
133
|
+
]
|
134
|
+
end
|
135
|
+
|
136
|
+
it "PRIMARY KEY (project_id)" do
|
137
|
+
@lex = lex.new StringIO.new "PRIMARY KEY (project_id)"
|
138
|
+
@lex.tokenize.should == [
|
139
|
+
[:PRIMARY_KEY, 'PRIMARY KEY'],
|
140
|
+
[:LPAREN, '('],
|
141
|
+
[:IDENT, 'project_id'],
|
142
|
+
[:RPAREN, ')'],
|
143
|
+
]
|
144
|
+
end
|
145
|
+
end
|
data/spec/parser_spec.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require_relative '../lib/lexer.rb'
|
3
|
+
require_relative '../lib/parser.rb'
|
4
|
+
require_relative '../lib/code_generator.rb'
|
5
|
+
|
6
|
+
describe ConstraintParser::Parser do
|
7
|
+
let(:lex){ConstraintParser::Lexer}
|
8
|
+
let(:par){ConstraintParser::Parser}
|
9
|
+
|
10
|
+
it 'CHECK ()' do
|
11
|
+
@parser = par.new(lex.new StringIO.new 'CHECK ()')
|
12
|
+
@parser.parse.gen.should == 'validate { true }'
|
13
|
+
end
|
14
|
+
|
15
|
+
it "CHECK (((email)::text ~~ '%@bendyworks.com'::text))" do
|
16
|
+
@parser = par.new(lex.new StringIO.new "CHECK (((email)::text ~~ '%@bendyworks.com'::text))")
|
17
|
+
@parser.parse.gen.should == 'validate { errors.add(:email, "Expected TODO") unless email =~ /.*@bendyworks.com/ }'
|
18
|
+
end
|
19
|
+
|
20
|
+
it "UNIQUE (email)" do
|
21
|
+
@parser = par.new(lex.new StringIO.new 'UNIQUE (email)')
|
22
|
+
@parser.parse.gen.should == "validates :email, uniqueness: true"
|
23
|
+
end
|
24
|
+
|
25
|
+
it "CHECK ((start_date <= ('now'::text)::date))" do
|
26
|
+
@parser = par.new(lex.new StringIO.new "CHECK ((start_date <= ('now'::text)::date))")
|
27
|
+
@parser.parse.gen.should == "validate { errors.add(:start_date, \"Expected TODO\") unless start_date <= Time.now.to_date }"
|
28
|
+
end
|
29
|
+
|
30
|
+
it "CHECK ((start_date <= (('now'::text)::date + 365)))" do
|
31
|
+
@parser = par.new(lex.new StringIO.new "CHECK ((start_date <= (('now'::text)::date + 365)))")
|
32
|
+
@parser.parse.gen.should == 'validate { errors.add(:start_date, "Expected TODO") unless start_date <= Time.now.to_date + 365 }'
|
33
|
+
end
|
34
|
+
|
35
|
+
it "FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE RESTRICT" do
|
36
|
+
@parser = par.new(lex.new StringIO.new "FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE RESTRICT")
|
37
|
+
@parser.parse.gen.should == "belongs_to :employees, foreign_key: 'employee_id', class: 'Employees', primary_key: 'employee_id'"
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require_relative '../lib/reactive_record'
|
3
|
+
|
4
|
+
include ReactiveRecord
|
5
|
+
|
6
|
+
describe 'ReactiveRecord' do
|
7
|
+
before :all do
|
8
|
+
# db setup
|
9
|
+
@dbname = "reactive_record_test_#{Process.pid}"
|
10
|
+
|
11
|
+
# N.B. all reactive_record methods are read only and so I believe
|
12
|
+
# that it is valid to share a db connection. Nothing reactive record
|
13
|
+
# does should mutate any global state
|
14
|
+
system "createdb #{@dbname}"
|
15
|
+
@db = PG.connect dbname: @dbname
|
16
|
+
@db.exec File.read('spec/seed/database.sql')
|
17
|
+
end
|
18
|
+
|
19
|
+
after :all do
|
20
|
+
# db teardown
|
21
|
+
@db.close
|
22
|
+
system "dropdb #{@dbname}"
|
23
|
+
end
|
24
|
+
|
25
|
+
context '#model_definition' do
|
26
|
+
it 'generates an employee model def' do
|
27
|
+
model_definition(@db, 'employees').should ==
|
28
|
+
<<-EOS.gsub(/^ {10}/, '')
|
29
|
+
class Employees < ActiveRecord::Base
|
30
|
+
set_table_name 'employees'
|
31
|
+
set_primary_key :id
|
32
|
+
validate :id, :name, :email, :start_date, presence: true
|
33
|
+
validate :email, uniqueness: true
|
34
|
+
validate { errors.add(:email, "Expected TODO") unless email =~ /.*@example.com/ }
|
35
|
+
validate { errors.add(:start_date, "Expected TODO") unless start_date >= Date.parse(\"2009-04-13\") }
|
36
|
+
validate { errors.add(:start_date, "Expected TODO") unless start_date <= Time.now.to_date + 365 }
|
37
|
+
end
|
38
|
+
EOS
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'generates a project model def' do
|
42
|
+
model_definition(@db, 'projects').should ==
|
43
|
+
<<-EOS.gsub(/^ {10}/, '')
|
44
|
+
class Projects < ActiveRecord::Base
|
45
|
+
set_table_name 'projects'
|
46
|
+
set_primary_key :id
|
47
|
+
validate :id, :name, presence: true
|
48
|
+
validate :name, uniqueness: true
|
49
|
+
end
|
50
|
+
EOS
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context '#constraints' do
|
55
|
+
it 'gathers all constraints on the employees table' do
|
56
|
+
constraints(@db, 'employees').to_a.should == [
|
57
|
+
{"table_name"=>"employees", "constraint_name"=>"company_email", "constraint_src"=>"CHECK (((email)::text ~~ '%@example.com'::text))"},
|
58
|
+
{"table_name"=>"employees", "constraint_name"=>"employees_email_key", "constraint_src"=>"UNIQUE (email)"},
|
59
|
+
{"table_name"=>"employees", "constraint_name"=>"employees_pkey", "constraint_src"=>"PRIMARY KEY (id)"},
|
60
|
+
{"table_name"=>"employees", "constraint_name"=>"founding_date", "constraint_src"=>"CHECK ((start_date >= '2009-04-13'::date))"},
|
61
|
+
{"table_name"=>"employees", "constraint_name"=>"future_start_date", "constraint_src"=>"CHECK ((start_date <= (('now'::text)::date + 365)))"}
|
62
|
+
]
|
63
|
+
end
|
64
|
+
it 'gathers all constraints on the projects table' do
|
65
|
+
constraints(@db, 'projects').to_a.should == [
|
66
|
+
{"table_name"=>"projects", "constraint_name"=>"projects_name_key", "constraint_src"=>"UNIQUE (name)"},
|
67
|
+
{"table_name"=>"projects", "constraint_name"=>"projects_pkey", "constraint_src"=>"PRIMARY KEY (id)"}
|
68
|
+
]
|
69
|
+
end
|
70
|
+
it 'gathers all constraints on the employees_projects table' do
|
71
|
+
constraints(@db, 'employees_projects').to_a.should == [
|
72
|
+
{
|
73
|
+
"table_name"=>"employees_projects",
|
74
|
+
"constraint_name"=>"employees_projects_employee_id_fkey",
|
75
|
+
"constraint_src"=>"FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE RESTRICT"
|
76
|
+
},
|
77
|
+
{
|
78
|
+
"table_name"=>"employees_projects",
|
79
|
+
"constraint_name"=>"employees_projects_pkey",
|
80
|
+
"constraint_src"=>"PRIMARY KEY (employee_id, project_id)"
|
81
|
+
},
|
82
|
+
{
|
83
|
+
"table_name"=>"employees_projects",
|
84
|
+
"constraint_name"=>"employees_projects_project_id_fkey",
|
85
|
+
"constraint_src"=>"FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE"
|
86
|
+
}
|
87
|
+
]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context '#generate_constraints' do
|
92
|
+
it 'generates ruby code for employees constraints' do
|
93
|
+
generate_constraints(@db, 'employees').should == [
|
94
|
+
"validate { errors.add(:email, \"Expected TODO\") unless email =~ /.*@example.com/ }",
|
95
|
+
"validate { errors.add(:start_date, \"Expected TODO\") unless start_date >= Date.parse(\"2009-04-13\") }",
|
96
|
+
"validate { errors.add(:start_date, \"Expected TODO\") unless start_date <= Time.now.to_date + 365 }",
|
97
|
+
]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
context '#unique_columns' do
|
102
|
+
it 'returns email for employees' do
|
103
|
+
unique_columns(@db, 'employees').should == ['email']
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
context '#cols_with_contype' do
|
108
|
+
it 'identifies UNIQUE columns in database' do
|
109
|
+
cols_with_contype(@db, 'employees', 'u').to_a.should == [{"column_name"=>"email", "conname"=>"employees_email_key"},
|
110
|
+
{"column_name"=>"name", "conname"=>"projects_name_key"}]
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
context '#non_nullable_columns' do
|
115
|
+
it 'identifies NOT NULL columns in employees' do
|
116
|
+
non_nullable_columns(@db, 'employees').should == ['id', 'name', 'email', 'start_date']
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
-- create tables
|
2
|
+
|
3
|
+
CREATE TABLE employees (
|
4
|
+
id SERIAL,
|
5
|
+
name VARCHAR(255) NOT NULL,
|
6
|
+
email VARCHAR(255) NOT NULL UNIQUE,
|
7
|
+
start_date DATE NOT NULL,
|
8
|
+
|
9
|
+
PRIMARY KEY (id),
|
10
|
+
|
11
|
+
CONSTRAINT founding_date CHECK (start_date >= '2009-04-13'),
|
12
|
+
CONSTRAINT future_start_date CHECK (start_date <= CURRENT_DATE + 365),
|
13
|
+
CONSTRAINT company_email CHECK (email LIKE '%@example.com')
|
14
|
+
);
|
15
|
+
|
16
|
+
CREATE TABLE projects (
|
17
|
+
id INTEGER NOT NULL,
|
18
|
+
name VARCHAR(255) NOT NULL UNIQUE,
|
19
|
+
|
20
|
+
PRIMARY KEY (id)
|
21
|
+
);
|
22
|
+
|
23
|
+
CREATE TABLE employees_projects (
|
24
|
+
employee_id INTEGER REFERENCES employees ON DELETE RESTRICT,
|
25
|
+
project_id INTEGER REFERENCES projects ON DELETE CASCADE,
|
26
|
+
|
27
|
+
PRIMARY KEY (employee_id, project_id)
|
28
|
+
);
|
29
|
+
|
30
|
+
-- seed data
|
31
|
+
|
32
|
+
INSERT INTO employees (id, name, email, start_date)
|
33
|
+
VALUES
|
34
|
+
(1, 'Alfred Arnold', 'alfred@example.com', '2009-04-13'),
|
35
|
+
(2, 'Benedict Burton', 'benedict@example.com', '2011-09-01'),
|
36
|
+
(3, 'Cat Cams', 'cat@example.com', '2009-04-13'),
|
37
|
+
(4, 'Duane Drummond', 'duane@example.com', '2009-04-13'),
|
38
|
+
(5, 'Elizabeth Eggers', 'elizabeth@example.com', '2009-04-13'),
|
39
|
+
(6, 'Fred Fitzgerald', 'fred@example.com', '2012-01-01'),
|
40
|
+
(7, 'Greg Gruber', 'greg@example.com', '2013-01-07'),
|
41
|
+
(8, 'Horatio Helms', 'horatio@example.com', '2012-01-01'),
|
42
|
+
(9, 'Ian Ives', 'ian@example.com', '2009-04-13'),
|
43
|
+
(10, 'Jan Jarvis', 'jan@example.com', '2013-01-28'),
|
44
|
+
(11, 'Kevin Kelvin', 'kevin@example.com', '2009-04-13'),
|
45
|
+
(12, 'Lucy Lemieux', 'lucy@example.com', '2009-04-13');
|
46
|
+
|
47
|
+
INSERT INTO projects (id, name)
|
48
|
+
VALUES
|
49
|
+
(1, 'Yoyodyne Inc.'),
|
50
|
+
(2, 'Global Omni Mega Corp.'),
|
51
|
+
(3, 'Murray''s Widgets Ltd.'),
|
52
|
+
(4, 'Spatula City'),
|
53
|
+
(5, 'Aperture Laboratories');
|
54
|
+
|
55
|
+
INSERT INTO employees_projects (employee_id, project_id)
|
56
|
+
VALUES
|
57
|
+
(1,1),
|
58
|
+
(2,1),
|
59
|
+
(3,2),
|
60
|
+
(4,2),
|
61
|
+
(5,3),
|
62
|
+
(6,3),
|
63
|
+
(7,4),
|
64
|
+
(8,4),
|
65
|
+
(9,5),
|
66
|
+
(10,5),
|
67
|
+
(11,5);
|
metadata
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: reactive_record
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Joe Nelson
|
8
|
+
- Chris Wilson
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-10-02 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: pg
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ! '>='
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ! '>='
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: activesupport
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ! '>='
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ! '>='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: rspec
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ! '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: racc
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
description: Generate ActiveRecord models from a pre-existing Postgres db
|
71
|
+
email:
|
72
|
+
- christopher.j.wilson@gmail.com
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- .gitignore
|
78
|
+
- .rspec
|
79
|
+
- Gemfile
|
80
|
+
- LICENSE.txt
|
81
|
+
- README.md
|
82
|
+
- Rakefile
|
83
|
+
- lib/code_generator.rb
|
84
|
+
- lib/generators/reactive_record/install_generator.rb
|
85
|
+
- lib/lexer.rb
|
86
|
+
- lib/parser.y
|
87
|
+
- lib/reactive_record.rb
|
88
|
+
- lib/reactive_record/version.rb
|
89
|
+
- reactive_record.gemspec
|
90
|
+
- spec/lexer_spec.rb
|
91
|
+
- spec/parser_spec.rb
|
92
|
+
- spec/reactive_record_spec.rb
|
93
|
+
- spec/seed/database.sql
|
94
|
+
homepage: https://github.com/twopoint718/reactive_record
|
95
|
+
licenses:
|
96
|
+
- MIT
|
97
|
+
metadata: {}
|
98
|
+
post_install_message:
|
99
|
+
rdoc_options: []
|
100
|
+
require_paths:
|
101
|
+
- lib
|
102
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ! '>='
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ! '>='
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
requirements: []
|
113
|
+
rubyforge_project:
|
114
|
+
rubygems_version: 2.1.5
|
115
|
+
signing_key:
|
116
|
+
specification_version: 4
|
117
|
+
summary: Use the schema you always wanted.
|
118
|
+
test_files:
|
119
|
+
- spec/lexer_spec.rb
|
120
|
+
- spec/parser_spec.rb
|
121
|
+
- spec/reactive_record_spec.rb
|
122
|
+
- spec/seed/database.sql
|