sexy_pg_constraints 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +12 -0
- data/Manifest +14 -0
- data/README.rdoc +139 -0
- data/Rakefile +15 -0
- data/init.rb +1 -0
- data/lib/constrainer.rb +34 -0
- data/lib/constraints.rb +154 -0
- data/lib/deconstrainer.rb +31 -0
- data/lib/helpers.rb +19 -0
- data/lib/sexy_pg_constraints.rb +24 -0
- data/sexy_pg_constraints.gemspec +31 -0
- data/test/postgresql_adapter.rb +1065 -0
- data/test/sexy_pg_constraints_test.rb +572 -0
- data/test/test_helper.rb +5 -0
- metadata +80 -0
data/CHANGELOG.rdoc
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
=== 0.1.3 - 01.19.2009
|
2
|
+
|
3
|
+
* Bugfix: positive constraint fixed from >0 to >=0. Deconstrain and constrain to reapply.
|
4
|
+
|
5
|
+
=== 0.1.2 - 11.30.2008
|
6
|
+
|
7
|
+
* New constraints: even, odd, format
|
8
|
+
|
9
|
+
=== 0.1.1 - 11.30.2008
|
10
|
+
|
11
|
+
* Added foreign key constraints support.
|
12
|
+
* Added multi-column constraints support.
|
data/Manifest
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
CHANGELOG.rdoc
|
2
|
+
Manifest
|
3
|
+
README.rdoc
|
4
|
+
Rakefile
|
5
|
+
init.rb
|
6
|
+
lib/constrainer.rb
|
7
|
+
lib/constraints.rb
|
8
|
+
lib/deconstrainer.rb
|
9
|
+
lib/helpers.rb
|
10
|
+
lib/sexy_pg_constraints.rb
|
11
|
+
sexy_pg_constraints.gemspec
|
12
|
+
test/postgresql_adapter.rb
|
13
|
+
test/sexy_pg_constraints_test.rb
|
14
|
+
test/test_helper.rb
|
data/README.rdoc
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
= Sexy PG Constraints
|
2
|
+
|
3
|
+
If you're on PostgreSQL and see the importance of data-layer constraints - this gem/plugin is for you. It integrates constraints into PostgreSQL adapter so you can add/remove them in your migrations. You get two simple methods for adding/removing constraints, as well as a pack of pre-made constraints.
|
4
|
+
|
5
|
+
== Install
|
6
|
+
As a gem
|
7
|
+
gem install maxim-sexy_pg_constraints --source http://gems.github.com
|
8
|
+
or as a plugin
|
9
|
+
script/plugin install git://github.com/maxim/sexy_pg_constraints.git
|
10
|
+
|
11
|
+
|
12
|
+
One more thing. Make sure that in your environment.rb file you have the following line uncommented.
|
13
|
+
|
14
|
+
config.active_record.schema_format = :sql
|
15
|
+
|
16
|
+
Otherwise your test database will not have these constraints replicated.
|
17
|
+
|
18
|
+
== Usage
|
19
|
+
|
20
|
+
=== Single-column constraints
|
21
|
+
Say you have a table "books" and you want your Postgres DB to ensure that their title is not-blank, alphanumeric, and its length is between 3 and 50 chars. You also want to make sure that their isbn is unique. In addition you want to blacklist a few isbn numbers from ever being in your database. You can tell all that to your Postgres in no time. Generate a migration and write the following.
|
22
|
+
|
23
|
+
class AddConstraintsToBooks < ActiveRecord::Migration
|
24
|
+
def self.up
|
25
|
+
constrain :books do |t|
|
26
|
+
t.title :not_blank => true, :alphanumeric => true, :length_within => 3..50
|
27
|
+
t.isbn :unique => true, :blacklist => %w(badbook1 badbook2)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.down
|
32
|
+
deconstrain :books do |t|
|
33
|
+
t.title :not_blank, :alphanumeric, :length_within
|
34
|
+
t.isbn :unique, :blacklist
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
This will add all the necessary constraints to the database on the next migration, and remove them on rollback.
|
40
|
+
|
41
|
+
There's also a syntax for when you don't need to work with multiple columns at once.
|
42
|
+
|
43
|
+
constrain :books, :title, :not_blank => true, :length_within => 3..50
|
44
|
+
|
45
|
+
The above line works exactly the same as this block
|
46
|
+
|
47
|
+
constrain :books do |t|
|
48
|
+
t.title :not_blank => true, :length_within => 3..50
|
49
|
+
end
|
50
|
+
|
51
|
+
Same applies to deconstrain.
|
52
|
+
|
53
|
+
=== Multi-column constraints
|
54
|
+
Say you have the same table "books" only now you want to tell your Postgres to make sure that you should never have the same title + author_id combination. It means that you want to apply uniqueness to two columns, not just one. There is a special syntax for working with multicolumn constraints.
|
55
|
+
|
56
|
+
class AddConstraintsToBooks < ActiveRecord::Migration
|
57
|
+
def self.up
|
58
|
+
constrain :books do |t|
|
59
|
+
t[:title, :author_id].all :unique => true # Notice how multiple columns are listed in brackets.
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.down
|
64
|
+
deconstrain :books do |t|
|
65
|
+
t[:title, :author_id].all :unique
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
It's important to note that you shouldn't mix multicolumn constraints with regular ones in one line. This may cause unexpected behavior.
|
71
|
+
|
72
|
+
=== Foreign key constrants
|
73
|
+
In our table "books" we have column "author_id" which should reference the "id" column in the "authors" table. Here's the very simple syntax for setting up foreign key constraint that will tell Postgres to enforce this relationship.
|
74
|
+
|
75
|
+
class AddConstraintsToBooks < ActiveRecord::Migration
|
76
|
+
def self.up
|
77
|
+
constrain :books do |t|
|
78
|
+
t.author_id :reference => {:authors => :id, :on_delete => :cascade} # :on_delete is optional
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.down
|
83
|
+
deconstrain :books do |t|
|
84
|
+
t.author_id :reference
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
In this example we're telling Postgres to enforce the connection of author_id to the column "id" in table "authors". However, we're also telling it to cascade on delete. This means that when an author is deleted - every book that referred to that author will be deleted as well.
|
90
|
+
|
91
|
+
== Available constraints
|
92
|
+
|
93
|
+
Below is the list of constraints available and tested so far.
|
94
|
+
|
95
|
+
* whitelist
|
96
|
+
* blacklist
|
97
|
+
* not_blank
|
98
|
+
* within
|
99
|
+
* length_within
|
100
|
+
* email
|
101
|
+
* alphanumeric
|
102
|
+
* positive
|
103
|
+
* unique
|
104
|
+
* exact_length
|
105
|
+
* reference
|
106
|
+
* even
|
107
|
+
* odd
|
108
|
+
* format
|
109
|
+
|
110
|
+
== Extensibility
|
111
|
+
|
112
|
+
All constraints are located in the lib/constraints.rb. Extending this module with more methods will automatically make constraints available in migrations. All methods in the Constraints module are under module_function directive. Each method is supposed to return a piece of SQL that is inserted "alter table foo add constraint bar #{RIGHT HERE};."
|
113
|
+
|
114
|
+
== TODO
|
115
|
+
|
116
|
+
* Create better API for adding constraints
|
117
|
+
|
118
|
+
== License
|
119
|
+
|
120
|
+
Copyright (c) 2008 Maxim Chernyak
|
121
|
+
|
122
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
123
|
+
a copy of this software and associated documentation files (the
|
124
|
+
"Software"), to deal in the Software without restriction, including
|
125
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
126
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
127
|
+
permit persons to whom the Software is furnished to do so, subject to
|
128
|
+
the following conditions:
|
129
|
+
|
130
|
+
The above copyright notice and this permission notice shall be
|
131
|
+
included in all copies or substantial portions of the Software.
|
132
|
+
|
133
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
134
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
135
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
136
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
137
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
138
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
139
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'echoe'
|
4
|
+
|
5
|
+
Echoe.new('sexy_pg_constraints', '0.1.2') do |p|
|
6
|
+
p.description = "Use migrations and simple syntax to manage constraints in PostgreSQL DB."
|
7
|
+
p.summary = "If you're on PostgreSQL and see the importance of data-layer constraints - this gem/plugin is for you. It integrates constraints into PostgreSQL adapter so you can add/remove them in your migrations. You get two simple methods for adding/removing constraints, as well as a pack of pre-made constraints."
|
8
|
+
p.url = "http://github.com/maxim/sexy_pg_constraints"
|
9
|
+
p.author = "Maxim Chernyak"
|
10
|
+
p.email = "max@bitsonnet.com"
|
11
|
+
p.ignore_pattern = ["tmp/*", "script/*"]
|
12
|
+
p.development_dependencies = []
|
13
|
+
end
|
14
|
+
|
15
|
+
Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'sexy_pg_constraints'
|
data/lib/constrainer.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
module SexyPgConstraints
|
2
|
+
class Constrainer
|
3
|
+
include SexyPgConstraints::Helpers
|
4
|
+
|
5
|
+
def initialize(table, columns = [])
|
6
|
+
@table = table.to_s
|
7
|
+
@columns = columns
|
8
|
+
end
|
9
|
+
|
10
|
+
def method_missing(column, constraints)
|
11
|
+
self.class.add_constraints(@table, column.to_s, constraints)
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](*columns)
|
15
|
+
@columns = columns.map{|c| c.to_s}
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def all(constraints)
|
20
|
+
self.class.add_constraints(@table, @columns, constraints)
|
21
|
+
end
|
22
|
+
|
23
|
+
class << self
|
24
|
+
def add_constraints(table, column, constraints)
|
25
|
+
constraints.each_pair do |type, options|
|
26
|
+
sql = "alter table #{table} add constraint #{make_title(table, column, type)} " +
|
27
|
+
SexyPgConstraints::Constraints.send(type, column, options) + ';'
|
28
|
+
|
29
|
+
execute sql
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/constraints.rb
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
module SexyPgConstraints
|
2
|
+
module Constraints
|
3
|
+
module_function
|
4
|
+
|
5
|
+
##
|
6
|
+
# Only allow listed values.
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
# constrain :books, :variation, :whitelist => %w(hardcover softcover)
|
10
|
+
#
|
11
|
+
def whitelist(column, options)
|
12
|
+
"check (#{column} in (#{ options.collect{|v| "'#{v}'"}.join(',') }))"
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# Prohibit listed values.
|
17
|
+
#
|
18
|
+
# Example:
|
19
|
+
# constrain :books, :isbn, :blacklist => %w(invalid_isbn1 invalid_isbn2)
|
20
|
+
#
|
21
|
+
def blacklist(column, options)
|
22
|
+
"check (#{column} not in (#{ options.collect{|v| "'#{v}'"}.join(',') }))"
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# The value must have at least 1 non-space character.
|
27
|
+
#
|
28
|
+
# Example:
|
29
|
+
# constrain :books, :title, :not_blank => true
|
30
|
+
#
|
31
|
+
def not_blank(column, options)
|
32
|
+
"check ( length(trim(both from #{column})) > 0 )"
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# The numeric value must be within given range.
|
37
|
+
#
|
38
|
+
# Example:
|
39
|
+
# constrain :books, :year, :within => 1980..2008
|
40
|
+
# constrain :books, :year, :within => 1980...2009
|
41
|
+
# (the two lines above do the same thing)
|
42
|
+
#
|
43
|
+
def within(column, options)
|
44
|
+
"check (#{column} >= #{options.begin} and #{column} #{options.exclude_end? ? ' < ' : ' <= '} #{options.end})"
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# Check the length of strings/text to be within the range.
|
49
|
+
#
|
50
|
+
# Example:
|
51
|
+
# constrain :books, :author, :length_within => 4..50
|
52
|
+
#
|
53
|
+
def length_within(column, options)
|
54
|
+
within("length(#{column})", options)
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# Allow only valid email format.
|
59
|
+
#
|
60
|
+
# Example:
|
61
|
+
# constrain :books, :author, :email => true
|
62
|
+
#
|
63
|
+
def email(column, options)
|
64
|
+
"check (((#{column})::text ~ E'^([-a-z0-9]+)@([-a-z0-9]+[.]+[a-z]{2,4})$'::text))"
|
65
|
+
end
|
66
|
+
|
67
|
+
##
|
68
|
+
# Allow only alphanumeric values.
|
69
|
+
#
|
70
|
+
# Example:
|
71
|
+
# constrain :books, :author, :alphanumeric => true
|
72
|
+
#
|
73
|
+
def alphanumeric(column, options)
|
74
|
+
"check (((#{column})::text ~* '^[a-z0-9]+$'::text))"
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# Allow only positive values.
|
79
|
+
#
|
80
|
+
# Example:
|
81
|
+
# constrain :books, :quantity, :positive => true
|
82
|
+
#
|
83
|
+
def positive(column, options)
|
84
|
+
"check (#{column} >= 0)"
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Allow only odd values.
|
89
|
+
#
|
90
|
+
# Example:
|
91
|
+
# constrain :books, :quantity, :odd => true
|
92
|
+
#
|
93
|
+
def odd(column, options)
|
94
|
+
"check (mod(#{column}, 2) != 0)"
|
95
|
+
end
|
96
|
+
|
97
|
+
##
|
98
|
+
# Allow only even values.
|
99
|
+
#
|
100
|
+
# Example:
|
101
|
+
# constrain :books, :quantity, :even => true
|
102
|
+
#
|
103
|
+
def even(column, options)
|
104
|
+
"check (mod(#{column}, 2) = 0)"
|
105
|
+
end
|
106
|
+
|
107
|
+
##
|
108
|
+
# Make sure every entry in the column is unique.
|
109
|
+
#
|
110
|
+
# Example:
|
111
|
+
# constrain :books, :isbn, :unique => true
|
112
|
+
#
|
113
|
+
def unique(column, options)
|
114
|
+
column = column.join(', ') if column.respond_to?(:join)
|
115
|
+
"unique (#{column})"
|
116
|
+
end
|
117
|
+
|
118
|
+
##
|
119
|
+
# Allow only text/strings of the exact length specified, no more, no less.
|
120
|
+
#
|
121
|
+
# Example:
|
122
|
+
# constrain :books, :hash, :exact_length => 32
|
123
|
+
#
|
124
|
+
def exact_length(column, options)
|
125
|
+
"check ( length(trim(both from #{column})) = #{options} )"
|
126
|
+
end
|
127
|
+
|
128
|
+
##
|
129
|
+
# Allow only values that match the regular expression.
|
130
|
+
#
|
131
|
+
# Example:
|
132
|
+
# constrain :orders, :visa, :format => /^([4]{1})([0-9]{12,15})$/
|
133
|
+
#
|
134
|
+
def format(column, options)
|
135
|
+
"check (((#{column})::text #{options.casefold? ? '~*' : '~'} E'#{options.source}'::text ))"
|
136
|
+
end
|
137
|
+
|
138
|
+
##
|
139
|
+
# Add foreign key constraint.
|
140
|
+
#
|
141
|
+
# Example:
|
142
|
+
# constrain :books, :author_id, :reference => {:authors => :id, :on_delete => :cascade}
|
143
|
+
#
|
144
|
+
def reference(column, options)
|
145
|
+
on_delete = options.delete(:on_delete)
|
146
|
+
fk_table = options.keys.first
|
147
|
+
fk_column = options[fk_table]
|
148
|
+
|
149
|
+
on_delete = "on delete #{on_delete}" if on_delete
|
150
|
+
|
151
|
+
"foreign key (#{column}) references #{fk_table} (#{fk_column}) #{on_delete}"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module SexyPgConstraints
|
2
|
+
class DeConstrainer
|
3
|
+
include SexyPgConstraints::Helpers
|
4
|
+
|
5
|
+
def initialize(table, columns = [])
|
6
|
+
@table = table.to_s
|
7
|
+
@columns = columns
|
8
|
+
end
|
9
|
+
|
10
|
+
def method_missing(column, *constraints)
|
11
|
+
self.class.drop_constraints(@table, column.to_s, *constraints)
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](*columns)
|
15
|
+
@columns = columns.map{|c| c.to_s}
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def all(*constraints)
|
20
|
+
self.class.drop_constraints(@table, @columns, *constraints)
|
21
|
+
end
|
22
|
+
|
23
|
+
class << self
|
24
|
+
def drop_constraints(table, column, *constraints)
|
25
|
+
constraints.each do |type|
|
26
|
+
execute "alter table #{table} drop constraint #{make_title(table, column, type)};"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/helpers.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module SexyPgConstraints
|
2
|
+
module Helpers
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def make_title(table, column, type)
|
9
|
+
column = column.join('_') if column.respond_to?(:join)
|
10
|
+
|
11
|
+
"#{table}_#{column}_#{type}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(*args)
|
15
|
+
ActiveRecord::Base.connection.execute(*args)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require "helpers"
|
2
|
+
require "constrainer"
|
3
|
+
require "deconstrainer"
|
4
|
+
require "constraints"
|
5
|
+
|
6
|
+
module SexyPgConstraints
|
7
|
+
def constrain(*args)
|
8
|
+
if block_given?
|
9
|
+
yield SexyPgConstraints::Constrainer.new(args[0].to_s)
|
10
|
+
else
|
11
|
+
SexyPgConstraints::Constrainer::add_constraints(*args)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def deconstrain(*args)
|
16
|
+
if block_given?
|
17
|
+
yield SexyPgConstraints::DeConstrainer.new(args[0])
|
18
|
+
else
|
19
|
+
SexyPgConstraints::DeConstrainer::drop_constraints(*args)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:include, SexyPgConstraints)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{sexy_pg_constraints}
|
5
|
+
s.version = "0.1.2"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Maxim Chernyak"]
|
9
|
+
s.date = %q{2009-10-04}
|
10
|
+
s.description = %q{Use migrations and simple syntax to manage constraints in PostgreSQL DB.}
|
11
|
+
s.email = %q{max@bitsonnet.com}
|
12
|
+
s.extra_rdoc_files = ["CHANGELOG.rdoc", "README.rdoc", "lib/constrainer.rb", "lib/constraints.rb", "lib/deconstrainer.rb", "lib/helpers.rb", "lib/sexy_pg_constraints.rb"]
|
13
|
+
s.files = ["CHANGELOG.rdoc", "Manifest", "README.rdoc", "Rakefile", "init.rb", "lib/constrainer.rb", "lib/constraints.rb", "lib/deconstrainer.rb", "lib/helpers.rb", "lib/sexy_pg_constraints.rb", "sexy_pg_constraints.gemspec", "test/postgresql_adapter.rb", "test/sexy_pg_constraints_test.rb", "test/test_helper.rb"]
|
14
|
+
s.homepage = %q{http://github.com/maxim/sexy_pg_constraints}
|
15
|
+
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Sexy_pg_constraints", "--main", "README.rdoc"]
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
s.rubyforge_project = %q{sexy_pg_constraints}
|
18
|
+
s.rubygems_version = %q{1.3.5}
|
19
|
+
s.summary = %q{If you're on PostgreSQL and see the importance of data-layer constraints - this gem/plugin is for you. It integrates constraints into PostgreSQL adapter so you can add/remove them in your migrations. You get two simple methods for adding/removing constraints, as well as a pack of pre-made constraints.}
|
20
|
+
s.test_files = ["test/sexy_pg_constraints_test.rb", "test/test_helper.rb"]
|
21
|
+
|
22
|
+
if s.respond_to? :specification_version then
|
23
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
24
|
+
s.specification_version = 3
|
25
|
+
|
26
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
27
|
+
else
|
28
|
+
end
|
29
|
+
else
|
30
|
+
end
|
31
|
+
end
|