├── .gitignore
├── .rspec
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── img
├── bendyworks_github_footer.png
└── reactive_record.png
├── lib
├── code_generator.rb
├── generators
│ └── reactive_record
│ │ └── install_generator.rb
├── lexer.rb
├── parser.rb
├── parser.y
├── reactive_record.rb
└── reactive_record
│ └── version.rb
├── reactive_record.gemspec
└── spec
├── lexer_spec.rb
├── parser_spec.rb
├── reactive_record_spec.rb
└── seed
└── database.sql
/.gitignore:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Specify your gem's dependencies in reactive_record.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
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.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 | Logo design: Kelly Rauwerdink,
6 | @missingdink,
7 | Dribbble.
8 |
9 |
10 |
11 | [](http://badge.fury.io/rb/reactive_record)
12 |
13 | Generates ActiveRecord models to fit a pre-existing Postgres database.
14 | Now you can use Rails with the db schema you always wanted. It's
15 | **your** convention over configuration.
16 |
17 | ## Why?
18 |
19 | 1. Your app is specific to Postgres and proud of it. You use the mature
20 | declarative data validation that only a real database can provide.
21 | 1. You have inherited a database or are more comfortable creating one
22 | yourself.
23 | 1. You're a grown-ass DBA who doesn't want to speak ORM baby-talk.
24 |
25 | ## Features
26 |
27 | * Fully automatic. It just works.
28 | * Creates a model for every table.
29 | * Creates a comprehensive initial migration.
30 | * Declares key-, uniqueness-, and presence-constraints.
31 | * Creates associations.
32 | * Adds custom validation methods for `CHECK` constraints.
33 |
34 | ## Usage
35 |
36 | ### Already familiar with Rails?
37 |
38 | * Set up a postgres db normally
39 | * Set `config.active_record.schema_format = :sql` to use a SQL `schema.rb`
40 | * After you have migrated up a table, use `rails generate reactive_record:install`
41 | * Go examine your generated models
42 |
43 | ### Want more details?
44 |
45 | **First** Include the `reactive_record` gem in your project's
46 | `Gemfile`. *Oh by the way*, you'll have to use postgres in your
47 | project. Setting up Rails for use with postgres is a bit outside
48 | the scope of this document. Please see [Configuring a Database]
49 | (http://guides.rubyonrails.org/configuring.html#configuring-a-database)
50 | for what you need to do.
51 |
52 | ``` ruby
53 | gem 'reactive_record'
54 | ```
55 |
56 | Bundle to include the library
57 |
58 | ``` shell
59 | $ bundle
60 | ```
61 |
62 | **Next** Tell `ActiveRecord` to go into beast-mode. Edit your
63 | `config/application.rb`, adding this line to use sql as the schema
64 | format:
65 |
66 | ``` ruby
67 | module YourApp
68 | class Application < Rails::Application
69 | # other configuration bric-a-brac...
70 | config.active_record.schema_format = :sql
71 | end
72 | end
73 | ```
74 |
75 | **Next** Create the database(s) just like you normally would:
76 |
77 | ``` shell
78 | rake db:create
79 | ```
80 |
81 | **Next** Generate a migration that will create the initial table:
82 |
83 | ``` shell
84 | $ rails generate migration create_employees
85 | ```
86 |
87 | Use your SQL powers to craft some
88 | [DDL](http://en.wikipedia.org/wiki/Data_definition_language), perhaps
89 | the "Hello, World!" of DB applications, `employees`?
90 |
91 | ``` ruby
92 | class CreateEmployees < ActiveRecord::Migration
93 | def up
94 | execute <<-SQL
95 | CREATE TABLE employees (
96 | id SERIAL,
97 | name VARCHAR(255) NOT NULL,
98 | email VARCHAR(255) NOT NULL UNIQUE,
99 | start_date DATE NOT NULL,
100 |
101 | PRIMARY KEY (id),
102 | CONSTRAINT company_email CHECK (email LIKE '%@example.com')
103 | );
104 | SQL
105 | end
106 |
107 | def down
108 | drop_table :employees
109 | end
110 | end
111 | ```
112 |
113 | **Lastly** Deploy the `reactive_record` generator:
114 |
115 | ``` shell
116 | $ rails generate reactive_record:install
117 | ```
118 |
119 | Go look at the generated file:
120 |
121 | ``` ruby
122 | class Employees < ActiveRecord::Base
123 | set_table_name 'employees'
124 | set_primary_key :id
125 | validate :id, :name, :email, :start_date, presence: true
126 | validate :email, uniqueness: true
127 | validate { errors.add(:email, "Expected TODO") unless email =~ /.*@example.com/ }
128 | end
129 | ```
130 |
131 | Reactive record does not currently attempt to generate any kind of
132 | reasonable error message (I'm working on it) :)
133 |
134 | **Enjoy**
135 |
136 | ## Credits
137 |
138 | Firstly, thank you,
139 | [contributors](https://github.com/twopoint718/reactive_record/graphs/contributors)!
140 |
141 | Also a special thanks to Joe Nelson,
142 | [@begriffs](https://github.com/begriffs), for contributions and
143 | inspiration; Reactive Record would not exist without his efforts. Thanks to [Bendyworks](http://bendyworks.com/) for the 20%
144 | time to work on this project!
145 |
146 | And, of *course*, a huge thank you to Kelly Rauwerdink for her amazing ability to make "an art" even when all I can do is sorta half-articulate what I'm talking about. Thanks!
147 |
148 | 
149 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
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
15 |
--------------------------------------------------------------------------------
/img/bendyworks_github_footer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twopoint718/reactive_record/0d688eeeb508933d239e00ed3740dc5b93160720/img/bendyworks_github_footer.png
--------------------------------------------------------------------------------
/img/reactive_record.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twopoint718/reactive_record/0d688eeeb508933d239e00ed3740dc5b93160720/img/reactive_record.png
--------------------------------------------------------------------------------
/lib/code_generator.rb:
--------------------------------------------------------------------------------
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 | when :and
160 | AndExpr.new @expr1, @expr2
161 | end
162 | end
163 |
164 | def error_msg
165 | case @value
166 | when :gteq then 'to be greater than or equal to'
167 | when :lteq then 'to be less than or equal to'
168 | when :neq then 'to not equal'
169 | when :eq then 'to be equal to'
170 | when :gt then 'to be greater than'
171 | when :lt then 'to be less than'
172 | when :plus then 'plus'
173 | when :match then 'to match'
174 | end
175 | end
176 |
177 | def column
178 | c1 = @expr1.column
179 | c2 = @expr1.column
180 | return c1 if c1 != 'base'
181 | return c2 if c2 != 'base'
182 | 'base'
183 | end
184 |
185 | def gen
186 | operation.gen
187 | end
188 | end
189 |
190 | class ComparisonExpr
191 | def initialize comparison, e1, e2
192 | @e1, @e2 = e1, e2
193 | @comparison = {
194 | gteq: '>=',
195 | lteq: '<=',
196 | neq: '!=',
197 | eq: '==',
198 | gt: '>',
199 | lt: '<',
200 | }[comparison]
201 | end
202 |
203 | def gen
204 | "#{@e1.gen} #{@comparison} #{@e2.gen}"
205 | end
206 | end
207 |
208 | class AndExpr
209 | def initialize e1, e2
210 | @e1, @e2 = e1, e2
211 | end
212 |
213 | def gen
214 | "#{@e1.gen} && #{@e2.gen}"
215 | end
216 | end
217 |
218 | class MathExpr
219 | def initialize op, e1, e2
220 | @e1, @e2 = e1, e2
221 | @operation = { plus: '+', minus: '-' }[op]
222 | end
223 |
224 | def gen
225 | "#{@e1.gen} #{@operation} #{@e2.gen}"
226 | end
227 | end
228 |
229 | class MatchExpr
230 | def initialize e1, e2
231 | @e1 = e1
232 | @e2 = e2
233 | end
234 |
235 | def convert_wildcard str
236 | str.gsub(/%/, '.*')
237 | end
238 |
239 | def gen
240 | #raise "RHS: #{@e2.class} #{@e2.gen.class}"
241 | "#{@e1.gen} =~ /#{convert_wildcard @e2.gen}/"
242 | end
243 | end
244 |
245 | class TableNode < Node
246 | def initialize tab, col
247 | @tab = tab
248 | @col = col
249 | end
250 |
251 | def table_to_class
252 | @tab.gen.capitalize
253 | end
254 |
255 | def key_name
256 | @col.gen
257 | end
258 |
259 | def gen
260 | @tab.gen
261 | end
262 | end
263 |
264 | class ActionNode < Node
265 | def initialize action, consequence
266 | @action = action
267 | @consequence = consequence
268 | end
269 |
270 | def gen
271 | "ON #{@action} #{@consequence}"
272 | end
273 | end
274 |
275 | class ForeignKeyNode < Node
276 | def initialize col, table, *actions
277 | @col = col
278 | @table = table
279 | if actions.count > 0
280 | @action = actions[0]
281 | end
282 | end
283 |
284 | def gen
285 | col = @col.gen
286 | table_name = @table.gen
287 | class_name = @table.table_to_class
288 | key = @table.key_name
289 | "belongs_to :#{table_name}, foreign_key: '#{col}', class: '#{class_name}', primary_key: '#{key}'"
290 | end
291 | end
292 |
--------------------------------------------------------------------------------
/lib/generators/reactive_record/install_generator.rb:
--------------------------------------------------------------------------------
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 |
24 |
--------------------------------------------------------------------------------
/lib/lexer.rb:
--------------------------------------------------------------------------------
1 | require 'strscan'
2 |
3 | module ConstraintParser
4 | class Lexer
5 | AND = /AND/
6 | CASCADE = /CASCADE/
7 | CHECK = /CHECK/
8 | COMMA = /,/
9 | DELETE = /DELETE/
10 | EQ = /\=/
11 | FOREIGN_KEY = /FOREIGN KEY/
12 | GT = /\>/
13 | GTEQ = /\>=/
14 | IDENT = /[a-zA-Z][a-zA-Z0-9_]*/
15 | INT = /[0-9]+/
16 | LPAREN = /\(/
17 | LT = /\
18 | LTEQ = /\<=/
19 | MATCH_OP = /~~/
20 | NEQ = /\<\>/
21 | NEWLINE = /\n/
22 | ON = /ON/
23 | PLUS = /\+/
24 | PRIMARY_KEY = /PRIMARY KEY/
25 | REFERENCES = /REFERENCES/
26 | RESTRICT = /RESTRICT/
27 | RPAREN = /\)/
28 | SPACE = /[\ \t]+/
29 | STRLIT = /\'(\\.|[^\\'])*\'/
30 | TYPE = /::/
31 | UNIQUE = /UNIQUE/
32 |
33 | def initialize io
34 | @ss = StringScanner.new io.read
35 | end
36 |
37 | def next_token
38 | return if @ss.eos?
39 |
40 | result = false
41 | while !result
42 | result = case
43 | when text = @ss.scan(SPACE) then #ignore whitespace
44 |
45 | # Operators
46 | when text = @ss.scan(GTEQ) then [:GTEQ, text]
47 | when text = @ss.scan(LTEQ) then [:LTEQ, text]
48 | when text = @ss.scan(NEQ) then [:NEQ, text]
49 | when text = @ss.scan(GT) then [:GT, text]
50 | when text = @ss.scan(LT) then [:LT, text]
51 | when text = @ss.scan(EQ) then [:EQ, text]
52 | when text = @ss.scan(PLUS) then [:PLUS, text]
53 | when text = @ss.scan(MATCH_OP) then [:MATCH_OP, text]
54 | when text = @ss.scan(AND) then [:AND, text]
55 |
56 | # SQL Keywords
57 | when text = @ss.scan(CHECK) then [:CHECK, text]
58 | when text = @ss.scan(UNIQUE) then [:UNIQUE, text]
59 | when text = @ss.scan(PRIMARY_KEY) then [:PRIMARY_KEY, text]
60 | when text = @ss.scan(FOREIGN_KEY) then [:FOREIGN_KEY, text]
61 | when text = @ss.scan(REFERENCES) then [:REFERENCES, text]
62 | when text = @ss.scan(DELETE) then [:DELETE, text]
63 | when text = @ss.scan(ON) then [:ON, text]
64 | when text = @ss.scan(RESTRICT) then [:RESTRICT, text]
65 | when text = @ss.scan(CASCADE) then [:CASCADE, text]
66 |
67 | # Values
68 | when text = @ss.scan(IDENT) then [:IDENT, text]
69 | when text = @ss.scan(STRLIT) then [:STRLIT, text]
70 | when text = @ss.scan(INT) then [:INT, text]
71 |
72 | # Syntax
73 | when text = @ss.scan(LPAREN) then [:LPAREN, text]
74 | when text = @ss.scan(RPAREN) then [:RPAREN, text]
75 | when text = @ss.scan(TYPE) then [:TYPE, text]
76 | when text = @ss.scan(COMMA) then [:COMMA, text]
77 | else
78 | x = @ss.getch
79 | [x, x]
80 | end
81 | end
82 | result
83 | end
84 |
85 | def tokenize
86 | out = []
87 | while (tok = next_token)
88 | out << tok
89 | end
90 | out
91 | end
92 | end
93 | end
94 |
--------------------------------------------------------------------------------
/lib/parser.rb:
--------------------------------------------------------------------------------
1 | #
2 | # DO NOT MODIFY!!!!
3 | # This file is automatically generated by Racc 1.4.9
4 | # from Racc grammer file "".
5 | #
6 |
7 | require 'racc/parser.rb'
8 | module ConstraintParser
9 | class Parser < Racc::Parser
10 |
11 |
12 | require 'lexer'
13 | require 'code_generator'
14 |
15 | def initialize tokenizer, handler = nil
16 | @tokenizer = tokenizer
17 | super()
18 | end
19 |
20 | def next_token
21 | @tokenizer.next_token
22 | end
23 |
24 | def parse
25 | do_parse
26 | end
27 | ##### State transition tables begin ###
28 |
29 | racc_action_table = [
30 | 27, 28, 21, 22, 23, 24, 25, 26, 29, 27,
31 | 28, 21, 22, 23, 24, 25, 26, 29, 46, 12,
32 | 14, 11, 12, 14, 11, 30, 36, 32, 6, 13,
33 | 45, 7, 13, 33, 30, 27, 28, 21, 22, 23,
34 | 24, 25, 26, 29, 5, 12, 14, 11, 16, 18,
35 | 17, 9, 39, 40, 8, 13, 42, 37, 16, 44,
36 | 30, 34 ]
37 |
38 | racc_action_check = [
39 | 10, 10, 10, 10, 10, 10, 10, 10, 10, 35,
40 | 35, 35, 35, 35, 35, 35, 35, 35, 44, 11,
41 | 11, 11, 20, 20, 20, 10, 30, 15, 0, 11,
42 | 44, 0, 20, 16, 35, 31, 31, 31, 31, 31,
43 | 31, 31, 31, 31, 0, 6, 6, 6, 7, 9,
44 | 8, 5, 32, 33, 1, 6, 38, 31, 39, 42,
45 | 31, 18 ]
46 |
47 | racc_action_pointer = [
48 | 16, 54, nil, nil, nil, 33, 29, 30, 50, 33,
49 | -2, 3, nil, nil, nil, 5, 17, nil, 37, nil,
50 | 6, nil, nil, nil, nil, nil, nil, nil, nil, nil,
51 | 10, 33, 36, 29, nil, 7, nil, nil, 36, 40,
52 | nil, nil, 45, nil, 7, nil, nil ]
53 |
54 | racc_action_default = [
55 | -29, -29, -1, -2, -3, -29, -6, -29, -29, -29,
56 | -5, -6, -10, -11, -12, -29, -29, 47, -29, -8,
57 | -6, -13, -14, -15, -16, -17, -18, -19, -20, -21,
58 | -29, -29, -29, -29, -4, -9, -22, -7, -23, -29,
59 | -25, -24, -29, -26, -29, -27, -28 ]
60 |
61 | racc_goto_table = [
62 | 15, 10, 1, 4, 3, 2, 31, 38, 41, nil,
63 | nil, nil, nil, nil, nil, 35, nil, nil, nil, nil,
64 | nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
65 | nil, nil, 43 ]
66 |
67 | racc_goto_check = [
68 | 8, 5, 1, 4, 3, 2, 5, 9, 10, nil,
69 | nil, nil, nil, nil, nil, 5, nil, nil, nil, nil,
70 | nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
71 | nil, nil, 8 ]
72 |
73 | racc_goto_pointer = [
74 | nil, 2, 5, 4, 3, -5, nil, nil, -7, -25,
75 | -30 ]
76 |
77 | racc_goto_default = [
78 | nil, nil, nil, nil, nil, nil, 19, 20, nil, nil,
79 | nil ]
80 |
81 | racc_reduce_table = [
82 | 0, 0, :racc_error,
83 | 1, 30, :_reduce_1,
84 | 1, 30, :_reduce_2,
85 | 1, 30, :_reduce_3,
86 | 4, 31, :_reduce_4,
87 | 2, 32, :_reduce_5,
88 | 0, 34, :_reduce_6,
89 | 3, 34, :_reduce_7,
90 | 2, 34, :_reduce_8,
91 | 3, 34, :_reduce_9,
92 | 1, 34, :_reduce_10,
93 | 1, 34, :_reduce_11,
94 | 1, 34, :_reduce_12,
95 | 1, 36, :_reduce_13,
96 | 1, 36, :_reduce_14,
97 | 1, 36, :_reduce_15,
98 | 1, 36, :_reduce_16,
99 | 1, 36, :_reduce_17,
100 | 1, 36, :_reduce_18,
101 | 1, 36, :_reduce_19,
102 | 1, 36, :_reduce_20,
103 | 1, 36, :_reduce_21,
104 | 2, 35, :_reduce_22,
105 | 4, 33, :_reduce_23,
106 | 5, 33, :_reduce_24,
107 | 3, 37, :_reduce_25,
108 | 2, 38, :_reduce_26,
109 | 3, 39, :_reduce_27,
110 | 3, 39, :_reduce_28 ]
111 |
112 | racc_reduce_n = 29
113 |
114 | racc_shift_n = 47
115 |
116 | racc_token_table = {
117 | false => 0,
118 | :error => 1,
119 | :PLUS => 2,
120 | :MATCH_OP => 3,
121 | :GTEQ => 4,
122 | :LTEQ => 5,
123 | :NEQ => 6,
124 | :EQ => 7,
125 | :GT => 8,
126 | :LT => 9,
127 | :AND => 10,
128 | :CASCADE => 11,
129 | :CHECK => 12,
130 | :COMMA => 13,
131 | :DELETE => 14,
132 | :FOREIGN_KEY => 15,
133 | :IDENT => 16,
134 | :INT => 17,
135 | :LPAREN => 18,
136 | :NEWLINE => 19,
137 | :ON => 20,
138 | :PRIMARY_KEY => 21,
139 | :REFERENCES => 22,
140 | :RESTRICT => 23,
141 | :RPAREN => 24,
142 | :SPACE => 25,
143 | :STRLIT => 26,
144 | :TYPE => 27,
145 | :UNIQUE => 28 }
146 |
147 | racc_nt_base = 29
148 |
149 | racc_use_result_var = true
150 |
151 | Racc_arg = [
152 | racc_action_table,
153 | racc_action_check,
154 | racc_action_default,
155 | racc_action_pointer,
156 | racc_goto_table,
157 | racc_goto_check,
158 | racc_goto_default,
159 | racc_goto_pointer,
160 | racc_nt_base,
161 | racc_reduce_table,
162 | racc_token_table,
163 | racc_shift_n,
164 | racc_reduce_n,
165 | racc_use_result_var ]
166 |
167 | Racc_token_to_s_table = [
168 | "$end",
169 | "error",
170 | "PLUS",
171 | "MATCH_OP",
172 | "GTEQ",
173 | "LTEQ",
174 | "NEQ",
175 | "EQ",
176 | "GT",
177 | "LT",
178 | "AND",
179 | "CASCADE",
180 | "CHECK",
181 | "COMMA",
182 | "DELETE",
183 | "FOREIGN_KEY",
184 | "IDENT",
185 | "INT",
186 | "LPAREN",
187 | "NEWLINE",
188 | "ON",
189 | "PRIMARY_KEY",
190 | "REFERENCES",
191 | "RESTRICT",
192 | "RPAREN",
193 | "SPACE",
194 | "STRLIT",
195 | "TYPE",
196 | "UNIQUE",
197 | "$start",
198 | "constraint",
199 | "unique_column",
200 | "check_statement",
201 | "foreign_key",
202 | "expr",
203 | "type_signature",
204 | "operator",
205 | "column_spec",
206 | "table_spec",
207 | "action_spec" ]
208 |
209 | Racc_debug_parser = false
210 |
211 | ##### State transition tables end #####
212 |
213 | # reduce 0 omitted
214 |
215 | def _reduce_1(val, _values, result)
216 | result = val[0]
217 | result
218 | end
219 |
220 | def _reduce_2(val, _values, result)
221 | result = val[0]
222 | result
223 | end
224 |
225 | def _reduce_3(val, _values, result)
226 | result = val[0]
227 | result
228 | end
229 |
230 | def _reduce_4(val, _values, result)
231 | result = UniqueNode.new(IdentNode.new(val[2]))
232 | result
233 | end
234 |
235 | def _reduce_5(val, _values, result)
236 | result = CheckNode.new(val[1])
237 | result
238 | end
239 |
240 | def _reduce_6(val, _values, result)
241 | result = EmptyExprNode.new :empty
242 | result
243 | end
244 |
245 | def _reduce_7(val, _values, result)
246 | result = ExprNode.new(val[1])
247 | result
248 | end
249 |
250 | def _reduce_8(val, _values, result)
251 | result = TypedExprNode.new(val[0], val[1])
252 | result
253 | end
254 |
255 | def _reduce_9(val, _values, result)
256 | result = OperatorNode.new(val[1], val[0], val[2])
257 | result
258 | end
259 |
260 | def _reduce_10(val, _values, result)
261 | result = IdentNode.new(val[0])
262 | result
263 | end
264 |
265 | def _reduce_11(val, _values, result)
266 | result = StrLitNode.new(val[0])
267 | result
268 | end
269 |
270 | def _reduce_12(val, _values, result)
271 | result = IntNode.new(val[0])
272 | result
273 | end
274 |
275 | def _reduce_13(val, _values, result)
276 | result = :gteq
277 | result
278 | end
279 |
280 | def _reduce_14(val, _values, result)
281 | result = :lteq
282 | result
283 | end
284 |
285 | def _reduce_15(val, _values, result)
286 | result = :neq
287 | result
288 | end
289 |
290 | def _reduce_16(val, _values, result)
291 | result = :eq
292 | result
293 | end
294 |
295 | def _reduce_17(val, _values, result)
296 | result = :gt
297 | result
298 | end
299 |
300 | def _reduce_18(val, _values, result)
301 | result = :lt
302 | result
303 | end
304 |
305 | def _reduce_19(val, _values, result)
306 | result = :plus
307 | result
308 | end
309 |
310 | def _reduce_20(val, _values, result)
311 | result = :match
312 | result
313 | end
314 |
315 | def _reduce_21(val, _values, result)
316 | result = :and
317 | result
318 | end
319 |
320 | def _reduce_22(val, _values, result)
321 | result = IdentNode.new(val[1])
322 | result
323 | end
324 |
325 | def _reduce_23(val, _values, result)
326 | result = ForeignKeyNode.new(val[1], val[3])
327 | result
328 | end
329 |
330 | def _reduce_24(val, _values, result)
331 | result = ForeignKeyNode.new(val[1], val[3], val[4])
332 | result
333 | end
334 |
335 | def _reduce_25(val, _values, result)
336 | result = IdentNode.new(val[1])
337 | result
338 | end
339 |
340 | def _reduce_26(val, _values, result)
341 | result = TableNode.new(IdentNode.new(val[0]), val[1])
342 | result
343 | end
344 |
345 | def _reduce_27(val, _values, result)
346 | result = ActionNode.new(:delete, :restrict)
347 | result
348 | end
349 |
350 | def _reduce_28(val, _values, result)
351 | result = ActionNode.new(:delete, :cascade)
352 | result
353 | end
354 |
355 | def _reduce_none(val, _values, result)
356 | val[0]
357 | end
358 |
359 | end # class Parser
360 | end # module ConstraintParser
361 |
--------------------------------------------------------------------------------
/lib/parser.y:
--------------------------------------------------------------------------------
1 | class ConstraintParser::Parser
2 | prechigh
3 | left PLUS MATCH_OP
4 | left GTEQ LTEQ NEQ EQ GT LT
5 | preclow
6 |
7 | token AND
8 | CASCADE
9 | CHECK
10 | COMMA
11 | DELETE
12 | EQ
13 | FOREIGN_KEY
14 | GT
15 | GTEQ
16 | IDENT
17 | INT
18 | LPAREN
19 | LT
20 | LTEQ
21 | MATCH_OP
22 | NEQ
23 | NEWLINE
24 | ON
25 | PLUS
26 | PRIMARY_KEY
27 | REFERENCES
28 | RESTRICT
29 | RPAREN
30 | SPACE
31 | STRLIT
32 | TYPE
33 | UNIQUE
34 |
35 | rule
36 | constraint : unique_column { result = val[0] }
37 | | check_statement { result = val[0] }
38 | | foreign_key { result = val[0] }
39 | ;
40 |
41 | unique_column : UNIQUE LPAREN IDENT RPAREN { result = UniqueNode.new(IdentNode.new(val[2])) }
42 | ;
43 |
44 | check_statement : CHECK expr { result = CheckNode.new(val[1]) }
45 |
46 | expr : { result = EmptyExprNode.new :empty }
47 | | LPAREN expr RPAREN { result = ExprNode.new(val[1]) }
48 | | expr type_signature { result = TypedExprNode.new(val[0], val[1]) }
49 | | expr operator expr { result = OperatorNode.new(val[1], val[0], val[2]) }
50 | | IDENT { result = IdentNode.new(val[0]) }
51 | | STRLIT { result = StrLitNode.new(val[0]) }
52 | | INT { result = IntNode.new(val[0]) }
53 | ;
54 |
55 | operator : GTEQ { result = :gteq }
56 | | LTEQ { result = :lteq }
57 | | NEQ { result = :neq }
58 | | EQ { result = :eq }
59 | | GT { result = :gt }
60 | | LT { result = :lt }
61 | | PLUS { result = :plus }
62 | | MATCH_OP { result = :match }
63 | | AND { result = :and }
64 | ;
65 |
66 | type_signature : TYPE IDENT { result = IdentNode.new(val[1]) }
67 | ;
68 |
69 | foreign_key : FOREIGN_KEY column_spec REFERENCES table_spec { result = ForeignKeyNode.new(val[1], val[3]) }
70 | | FOREIGN_KEY column_spec REFERENCES table_spec action_spec { result = ForeignKeyNode.new(val[1], val[3], val[4]) }
71 | ;
72 |
73 | column_spec : LPAREN IDENT RPAREN { result = IdentNode.new(val[1]) }
74 | ;
75 |
76 | table_spec : IDENT column_spec { result = TableNode.new(IdentNode.new(val[0]), val[1]) }
77 | ;
78 |
79 | action_spec : ON DELETE RESTRICT { result = ActionNode.new(:delete, :restrict) }
80 | | ON DELETE CASCADE { result = ActionNode.new(:delete, :cascade) }
81 | ;
82 | end
83 |
84 | ---- inner
85 |
86 | require 'lexer'
87 | require 'code_generator'
88 |
89 | def initialize tokenizer, handler = nil
90 | @tokenizer = tokenizer
91 | super()
92 | end
93 |
94 | def next_token
95 | @tokenizer.next_token
96 | end
97 |
98 | def parse
99 | do_parse
100 | end
101 |
--------------------------------------------------------------------------------
/lib/reactive_record.rb:
--------------------------------------------------------------------------------
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
116 |
--------------------------------------------------------------------------------
/lib/reactive_record/version.rb:
--------------------------------------------------------------------------------
1 | module ReactiveRecord
2 | VERSION = "0.0.4"
3 | end
4 |
--------------------------------------------------------------------------------
/reactive_record.gemspec:
--------------------------------------------------------------------------------
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("\n")
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
27 |
--------------------------------------------------------------------------------
/spec/lexer_spec.rb:
--------------------------------------------------------------------------------
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 | it "CHECK (((port >= 1024) AND (port <= 65634)))" do
146 | @lex = lex.new StringIO.new "CHECK (((port >= 1024) AND (port <= 65634)))"
147 | @lex.tokenize.should == [
148 | [:CHECK, 'CHECK'],
149 | [:LPAREN, '('],
150 | [:LPAREN, '('],
151 | [:LPAREN, '('],
152 | [:IDENT, 'port'],
153 | [:GTEQ, '>='],
154 | [:INT, "1024"],
155 | [:RPAREN, ')'],
156 | [:AND, 'AND'],
157 | [:LPAREN, '('],
158 | [:IDENT, 'port'],
159 | [:LTEQ, '<='],
160 | [:INT, "65634"],
161 | [:RPAREN, ')'],
162 | [:RPAREN, ')'],
163 | [:RPAREN, ')'],
164 | ]
165 | end
166 |
167 | end
168 |
--------------------------------------------------------------------------------
/spec/parser_spec.rb:
--------------------------------------------------------------------------------
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 "CHECK (((port >= 1024) AND (port <= 65634)))" do
36 | @parser = par.new(lex.new StringIO.new "CHECK (((port >= 1024) AND (port <= 65634)))")
37 | @parser.parse.gen.should == 'validate { errors.add(:port, "Expected TODO") unless port >= 1024 && port <= 65634 }'
38 | end
39 |
40 | it "FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE RESTRICT" do
41 | @parser = par.new(lex.new StringIO.new "FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ON DELETE RESTRICT")
42 | @parser.parse.gen.should == "belongs_to :employees, foreign_key: 'employee_id', class: 'Employees', primary_key: 'employee_id'"
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/spec/reactive_record_spec.rb:
--------------------------------------------------------------------------------
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
120 |
--------------------------------------------------------------------------------
/spec/seed/database.sql:
--------------------------------------------------------------------------------
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);
68 |
--------------------------------------------------------------------------------