Empact-sexy_pg_constraints 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ gem "activerecord", ">= 3.0.0"
5
+ gem "pg"
6
+
7
+ # Add dependencies to develop your gem here.
8
+ # Include everything needed to run rake, tests, features, etc.
9
+ group :development do
10
+ gem "shoulda", ">= 0"
11
+ gem "bundler", "~> 1.0.0"
12
+ gem "jeweler", "~> 1.5.2"
13
+ gem "rcov", ">= 0"
14
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,37 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activemodel (3.0.7)
5
+ activesupport (= 3.0.7)
6
+ builder (~> 2.1.2)
7
+ i18n (~> 0.5.0)
8
+ activerecord (3.0.7)
9
+ activemodel (= 3.0.7)
10
+ activesupport (= 3.0.7)
11
+ arel (~> 2.0.2)
12
+ tzinfo (~> 0.3.23)
13
+ activesupport (3.0.7)
14
+ arel (2.0.9)
15
+ builder (2.1.2)
16
+ git (1.2.5)
17
+ i18n (0.5.0)
18
+ jeweler (1.5.2)
19
+ bundler (~> 1.0.0)
20
+ git (>= 1.2.5)
21
+ rake
22
+ pg (0.11.0)
23
+ rake (0.8.7)
24
+ rcov (0.9.9)
25
+ shoulda (2.11.3)
26
+ tzinfo (0.3.26)
27
+
28
+ PLATFORMS
29
+ ruby
30
+
31
+ DEPENDENCIES
32
+ activerecord (>= 3.0.0)
33
+ bundler (~> 1.0.0)
34
+ jeweler (~> 1.5.2)
35
+ pg
36
+ rcov
37
+ shoulda
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Maxim Chernyak
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,123 @@
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
+ * lowercase
110
+ * xor
111
+
112
+ == Extensibility
113
+
114
+ 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};."
115
+
116
+ == TODO
117
+
118
+ * Add support for Rails schema.rb
119
+ * Create better API for adding constraints
120
+
121
+ == Contributors
122
+ * Empact[http://github.com/Empact] (Big thanks for lots of work. Better flexibility, more tests, organizing code, bug fixes.)
123
+ * look[http://github.com/look] (Extra constraints: lowercase and xor.)
data/Rakefile ADDED
@@ -0,0 +1,51 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = 'Empact-sexy_pg_constraints'
16
+ gem.homepage = "http://github.com/maxim/sexy_pg_constraints"
17
+ gem.description = "Use migrations and simple syntax to manage constraints in PostgreSQL DB."
18
+ gem.email = "ben.woosley@gmail.com"
19
+ gem.authors = ["Maxim Chernyak", "Ben Woosley"]
20
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
21
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
22
+ # gem.add_runtime_dependency 'jabber4r', '> 0.1'
23
+ # gem.add_development_dependency 'rspec', '> 1.2.3'
24
+ end
25
+ Jeweler::RubygemsDotOrgTasks.new
26
+
27
+ require 'rake/testtask'
28
+ Rake::TestTask.new(:test) do |test|
29
+ test.libs << 'lib' << 'test'
30
+ test.pattern = 'test/*_test.rb'
31
+ test.verbose = true
32
+ end
33
+
34
+ require 'rcov/rcovtask'
35
+ Rcov::RcovTask.new do |test|
36
+ test.libs << 'test'
37
+ test.pattern = 'test/**/test_*.rb'
38
+ test.verbose = true
39
+ end
40
+
41
+ task :default => :test
42
+
43
+ require 'rake/rdoctask'
44
+ Rake::RDocTask.new do |rdoc|
45
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
46
+
47
+ rdoc.rdoc_dir = 'rdoc'
48
+ rdoc.title = "test #{version}"
49
+ rdoc.rdoc_files.include('README*')
50
+ rdoc.rdoc_files.include('lib/**/*.rb')
51
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'sexy_pg_constraints'
@@ -0,0 +1,23 @@
1
+ require 'sexy_pg_constraints/initializer'
2
+ require "sexy_pg_constraints/helpers"
3
+ require "sexy_pg_constraints/constrainer"
4
+ require "sexy_pg_constraints/deconstrainer"
5
+ require "sexy_pg_constraints/constraints"
6
+
7
+ module SexyPgConstraints
8
+ def constrain(*args)
9
+ if block_given?
10
+ yield SexyPgConstraints::Constrainer.new(args[0].to_s)
11
+ else
12
+ SexyPgConstraints::Constrainer::add_constraints(*args)
13
+ end
14
+ end
15
+
16
+ def deconstrain(*args)
17
+ if block_given?
18
+ yield SexyPgConstraints::DeConstrainer.new(args[0])
19
+ else
20
+ SexyPgConstraints::DeConstrainer::drop_constraints(*args)
21
+ end
22
+ end
23
+ end
@@ -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, table, column, options) + ';'
28
+
29
+ execute sql
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,179 @@
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(table, column, options)
12
+ "check (#{table}.#{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(table, column, options)
22
+ "check (#{table}.#{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(table, column, options)
32
+ "check ( length(trim(both from #{table}.#{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(table, column, options)
44
+ column_ref = column.to_s.include?('.') ? column : "#{table}.#{column}"
45
+ "check (#{column_ref} >= #{options.begin} and #{column_ref} #{options.exclude_end? ? ' < ' : ' <= '} #{options.end})"
46
+ end
47
+
48
+ ##
49
+ # Check the length of strings/text to be within the range.
50
+ #
51
+ # Example:
52
+ # constrain :books, :author, :length_within => 4..50
53
+ #
54
+ def length_within(table, column, options)
55
+ within(table, "length(#{table}.#{column})", options)
56
+ end
57
+
58
+ ##
59
+ # Allow only valid email format.
60
+ #
61
+ # Example:
62
+ # constrain :books, :author, :email => true
63
+ #
64
+ def email(table, column, options)
65
+ "check (((#{table}.#{column})::text ~ E'^([-a-z0-9]+)@([-a-z0-9]+[.]+[a-z]{2,4})$'::text))"
66
+ end
67
+
68
+ ##
69
+ # Allow only alphanumeric values.
70
+ #
71
+ # Example:
72
+ # constrain :books, :author, :alphanumeric => true
73
+ #
74
+ def alphanumeric(table, column, options)
75
+ "check (((#{table}.#{column})::text ~* '^[a-z0-9]+$'::text))"
76
+ end
77
+
78
+ ##
79
+ # Allow only lower case values.
80
+ #
81
+ # Example:
82
+ # constrain :books, :author, :lowercase => true
83
+ #
84
+ def lowercase(table, column, options)
85
+ "check (#{table}.#{column} = lower(#{table}.#{column}))"
86
+ end
87
+
88
+ ##
89
+ # Allow only positive values.
90
+ #
91
+ # Example:
92
+ # constrain :books, :quantity, :positive => true
93
+ #
94
+ def positive(table, column, options)
95
+ "check (#{table}.#{column} >= 0)"
96
+ end
97
+
98
+ ##
99
+ # Allow only odd values.
100
+ #
101
+ # Example:
102
+ # constrain :books, :quantity, :odd => true
103
+ #
104
+ def odd(table, column, options)
105
+ "check (mod(#{table}.#{column}, 2) != 0)"
106
+ end
107
+
108
+ ##
109
+ # Allow only even values.
110
+ #
111
+ # Example:
112
+ # constrain :books, :quantity, :even => true
113
+ #
114
+ def even(table, column, options)
115
+ "check (mod(#{table}.#{column}, 2) = 0)"
116
+ end
117
+
118
+ ##
119
+ # Make sure every entry in the column is unique.
120
+ #
121
+ # Example:
122
+ # constrain :books, :isbn, :unique => true
123
+ #
124
+ def unique(table, column, options)
125
+ column = Array(column).map {|c| %{"#{c}"} }.join(', ')
126
+ "unique (#{column})"
127
+ end
128
+
129
+ ##
130
+ # Allow only one of the values in the given columns to be true.
131
+ # Only reasonable with more than one column.
132
+ # See Enterprise Rails, Chapter 10 for details.
133
+ #
134
+ # Example:
135
+ # constrain :books, [], :xor => true
136
+ #
137
+ def xor(table, column, options)
138
+ addition = Array(column).map {|c| %{("#{c}" is not null)::integer} }.join(' + ')
139
+
140
+ "check (#{addition} = 1)"
141
+ end
142
+
143
+ ##
144
+ # Allow only text/strings of the exact length specified, no more, no less.
145
+ #
146
+ # Example:
147
+ # constrain :books, :hash, :exact_length => 32
148
+ #
149
+ def exact_length(table, column, options)
150
+ "check ( length(trim(both from #{table}.#{column})) = #{options} )"
151
+ end
152
+
153
+ ##
154
+ # Allow only values that match the regular expression.
155
+ #
156
+ # Example:
157
+ # constrain :orders, :visa, :format => /^([4]{1})([0-9]{12,15})$/
158
+ #
159
+ def format(table, column, options)
160
+ "check (((#{table}.#{column})::text #{options.casefold? ? '~*' : '~'} E'#{options.source}'::text ))"
161
+ end
162
+
163
+ ##
164
+ # Add foreign key constraint.
165
+ #
166
+ # Example:
167
+ # constrain :books, :author_id, :reference => {:authors => :id, :on_delete => :cascade}
168
+ #
169
+ def reference(table, column, options)
170
+ on_delete = options.delete(:on_delete)
171
+ fk_table = options.keys.first
172
+ fk_column = options[fk_table]
173
+
174
+ on_delete = "on delete #{on_delete}" if on_delete
175
+
176
+ %{foreign key ("#{column}") references #{fk_table} (#{fk_column}) #{on_delete}}
177
+ end
178
+ end
179
+ end