drysql 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +119 -0
- data/lib/associations.rb +90 -0
- data/lib/base.rb +136 -0
- data/lib/connection_adapters/abstract/schema_definitions.rb +21 -0
- data/lib/connection_adapters/abstract_adapter.rb +39 -0
- data/lib/connection_adapters/mysql_adapter.rb +139 -0
- data/lib/connection_adapters/postgresql_adapter.rb +116 -0
- data/lib/dependencies.rb +24 -0
- data/lib/drysql.rb +14 -0
- data/lib/helpers/string.rb +9 -0
- data/lib/validations.rb +107 -0
- metadata +63 -0
data/README
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
====================================================================
|
2
|
+
NOTE: Dave Thomas' book Programming Ruby (Pragmatic Programmers' Guide) was my
|
3
|
+
bible during the development of this plug-in so I may have inadvertently borrowed some
|
4
|
+
code from the book, and I most certainly borrowed ideas and coding conventions from the book.
|
5
|
+
Thanks for writing such a great book Dave!
|
6
|
+
====================================================================
|
7
|
+
|
8
|
+
DrySQL v.0.1.0
|
9
|
+
==========
|
10
|
+
|
11
|
+
The idea behind DrySQL is that if you have defined a schema on your database complete with keys, constraints,
|
12
|
+
relationships, and behaviour, then you shouldn't need to re-define any of this in your application code.
|
13
|
+
|
14
|
+
DrySQL retrieves metadata from your DB's information schema, and uses this to identify keys, and generate
|
15
|
+
associations and validations for your ActiveRecord::Base subclasses
|
16
|
+
|
17
|
+
For an overview of the features and some use cases, please visit the DrySQL project website:
|
18
|
+
http://drysql.rubyforge.org
|
19
|
+
|
20
|
+
How Do I Install It?
|
21
|
+
============
|
22
|
+
|
23
|
+
Assuming that you have installed RubyGems, on the command line type the following:
|
24
|
+
|
25
|
+
gem install drysql
|
26
|
+
|
27
|
+
That's it!
|
28
|
+
|
29
|
+
How Does It Work?
|
30
|
+
============
|
31
|
+
|
32
|
+
1. Loading the DrySQL gem
|
33
|
+
-------------------------------
|
34
|
+
DrySQL simply extends ActiveRecord, so all you need to do is insert the following statement in your code
|
35
|
+
somewhere where it will be processed before your application needs to use DrySQL's behaviour:
|
36
|
+
|
37
|
+
require_gem 'drysql'
|
38
|
+
|
39
|
+
Note that DrySQL depends on the ActiveRecord gem, which in turn depends on the ActiveSupport gem, so you will
|
40
|
+
need to have these installed as well.
|
41
|
+
|
42
|
+
2. Mapping classes to tables
|
43
|
+
--------------------------------
|
44
|
+
|
45
|
+
DrySQL makes the assumption that your table name and class name will be compatible with Rails naming
|
46
|
+
conventions, or you will specify the table name explicitly using set_table_name inside your class definition.
|
47
|
+
|
48
|
+
If your DB tables follow the Rails naming conventions, then you don't need to define classes for them at all.
|
49
|
+
The first time you reference a non-existant class, DrySQL will generate the class for you.
|
50
|
+
|
51
|
+
eg.
|
52
|
+
The first time you reference the non-existant class Employee in your Ruby code, DrySQL will generate the class
|
53
|
+
ActiveRecord::Base::Employee in memory. If your DB table is called Employees, then Rails will map the Employee
|
54
|
+
class to the Employees table once you instantiate an Employee object.
|
55
|
+
|
56
|
+
If you feel more comfortable defining your class explicitly, it can be as simple as this:
|
57
|
+
|
58
|
+
class Employee < ActiveRecord::Base
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
If your Employee table is not called Employees, then you will need to define your class in your Ruby application,
|
63
|
+
and call set_table_name inside your class definition:
|
64
|
+
|
65
|
+
class Employee < ActiveRecord::Base
|
66
|
+
set_table_name "XYZ123"
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
3. Generating Keys, Associations, and Validations
|
71
|
+
--------------------------------------------------------
|
72
|
+
|
73
|
+
The first time you instantiate an ActiveRecord::Base subclass (we'll continue to use Employee for these examples),
|
74
|
+
DrySQL will intercept the instantiation, query the DB's information schema for the constraints and column metadata
|
75
|
+
associated with your table, and use this information to identify the keys on the table, and generate associations and
|
76
|
+
validations for the class.
|
77
|
+
|
78
|
+
Rather than assume that your primary key is called table_name_id, DrySQL queries the DB to find out exactly what your
|
79
|
+
primary key column is called. The same is true of foreign keys. Thus, DrySQL fully supports your legacy DB tables.
|
80
|
+
|
81
|
+
DrySQL can currently generate the following associations:
|
82
|
+
belongs_to
|
83
|
+
has_one
|
84
|
+
has_many
|
85
|
+
has_many :through
|
86
|
+
|
87
|
+
Supposing that I find a bulletproof way to identify has_and_belongs_to_many associations from the information schema,
|
88
|
+
I will add functionality for auto-generating these associations.
|
89
|
+
|
90
|
+
DrySQL can currently generate the following validations:
|
91
|
+
validates_length_of
|
92
|
+
validates_numericality_of
|
93
|
+
validates_inclusion_of (for boolean columns only, CHECK constraints can be used to enhance this)
|
94
|
+
validates_nullability_of
|
95
|
+
|
96
|
+
validates_nullability_of is a new validation introduced in DrySQL. Common convention is to use validates_presence_of
|
97
|
+
on any column with a NOT NULL constraint. There are 2 holes in this approach:
|
98
|
+
1) For character columns this validation will throw an error if the value of the column is the empty string, even though the DB considers this a perfectly valid value
|
99
|
+
2) For columns that either have a default value specified or are auto-generated by the DB, we do not want to throw an error if the value is null
|
100
|
+
|
101
|
+
validates_nullability_of throws an error only if the value is nil, no default value is specified for the column, and the column value is not auto-generated by the DB
|
102
|
+
|
103
|
+
DrySQL, by design, does not auto-generate validates_uniqueness_of, because I am not convinced about its usefulness.
|
104
|
+
Validating records before querying the DB is useful because we can identify invalid data and avoid the cost of I/O by attempting to
|
105
|
+
insert/update invalid data into the DB. validates_uniquess_of needs to query the DB in order to perform the validation, so I fail to see the advantage
|
106
|
+
of using this application-side validation over just letting the DB perform the validation and having your app handle the duplicate key error thrown
|
107
|
+
by the DB.
|
108
|
+
|
109
|
+
|
110
|
+
4. A Note About the Unit Tests
|
111
|
+
-----------------------------------
|
112
|
+
|
113
|
+
I did not publish the tests with the first release of DrySQL because they are built on a clone of a corporate database, and I need to obfuscate them first.
|
114
|
+
I will publish tests with the next release
|
115
|
+
|
116
|
+
Enjoy!
|
117
|
+
|
118
|
+
|
119
|
+
|
data/lib/associations.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
|
3
|
+
class Base
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
# The @associations instvar is used as an indicator as to whether the associations have already been generated or not.
|
8
|
+
# The first time an instance of a DrySQL-enabled subclass of ActiveRecord::Base is created, the associations are generated
|
9
|
+
# for the class.
|
10
|
+
def generate_associations
|
11
|
+
unless @associations
|
12
|
+
generate_belongs_to_associations
|
13
|
+
generate_has_many_associations
|
14
|
+
generate_through_associations
|
15
|
+
@associations = reflections
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def is_associated_with(class_name_symbol)
|
20
|
+
!(reflections.detect {|key, reflection| key.to_s.singularize == class_name_symbol.to_s.singularize}).nil?
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# A belongs_to association exists from class A to class B if A contains a foreign key into B
|
26
|
+
def generate_belongs_to_associations
|
27
|
+
foreign_keys = table_constraints.select {|constraint| constraint.foreign_key?}
|
28
|
+
foreign_keys.each do |foreign_key|
|
29
|
+
belongs_to_class_name = (class_name(foreign_key.referenced_table_name.upcase)).downcaseFirstLetter
|
30
|
+
self.send(:belongs_to, :"#{belongs_to_class_name}", :foreign_key=>foreign_key.column_name)
|
31
|
+
logger.info("GENERATED ASSOCIATION: #{self.name} belongs_to :#{belongs_to_class_name}, :foreign_key=>#{foreign_key.column_name}")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# A has_many association exists from A to B if B contains a foreign key into A.
|
36
|
+
# If B's foreign key into A is a unique key within B, then the association is A has_one B
|
37
|
+
def generate_has_many_associations
|
38
|
+
foreign_constraints.each do |foreign_constraint|
|
39
|
+
if foreign_constraint_column_is_unique?(foreign_constraint)
|
40
|
+
has_one_class_name = (class_name(foreign_constraint.table_name.upcase)).downcaseFirstLetter
|
41
|
+
self.send(:has_one, :"#{has_one_class_name}", :foreign_key=>foreign_constraint.referenced_column_name)
|
42
|
+
logger.info("GENERATED ASSOCIATION: #{self.name} has_one :#{has_one_class_name}, :foreign_key=>#{foreign_constraint.referenced_column_name}")
|
43
|
+
else
|
44
|
+
has_many_class_name = (class_name(foreign_constraint.table_name.upcase)).downcaseFirstLetter.pluralize
|
45
|
+
self.send(:has_many, :"#{has_many_class_name}", :foreign_key=>foreign_constraint.referenced_column_name)
|
46
|
+
logger.info("GENERATED ASSOCIATION: #{self.name} has_many :#{has_many_class_name}, :foreign_key=>#{foreign_constraint.referenced_column_name}")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def foreign_constraint_column_is_unique?(constraint)
|
52
|
+
class_name = class_name(constraint.table_name.upcase)
|
53
|
+
klass = instance_eval(class_name)
|
54
|
+
constraints_on_given_column = klass.table_constraints.select {|current| current.column_name == constraint.column_name}
|
55
|
+
constraints_on_given_column.any? {|current| current.unique_key?}
|
56
|
+
end
|
57
|
+
|
58
|
+
# Identifying has_many :through associations:
|
59
|
+
# 1) Collect all has_many associations for this class
|
60
|
+
#
|
61
|
+
# 2) Collect all FKs from each has_many class that do not reference this class's table
|
62
|
+
#
|
63
|
+
# 3) Create a has_many :through association for each class referenced by the FKs from step 2,
|
64
|
+
# unless this class already has an association to the class referenced by the FK
|
65
|
+
#
|
66
|
+
# i.e. if this class already has a has_many association with B, don't create a has_many B :through C
|
67
|
+
# because this will overwrite our direct association
|
68
|
+
#
|
69
|
+
# FIXME: this logic doesn't stand up when A has_many :B, :through=>C *and* A has_many :B, :through=>D
|
70
|
+
# Each association will atttempt to create an accessor for B on A, the next overwriting the previous.
|
71
|
+
# --> I'm not sure that there is any way to fix this programmatically
|
72
|
+
def generate_through_associations
|
73
|
+
has_many_associations.each do |association|
|
74
|
+
# FIXME the singularization is a hack. We should be able to figure the exact class name without relying
|
75
|
+
# on naming conventions
|
76
|
+
foreign_class = instance_eval(association[0].to_s.camelize.singularize)
|
77
|
+
eligible_foreign_keys = foreign_class.table_constraints.select {|c| c.foreign_key? && c.referenced_table_name.upcase != table_name.upcase}
|
78
|
+
eligible_foreign_keys.select {|key| !self.is_associated_with(symbolized_class_name_from_table_name(key.referenced_table_name))}.each do |fk|
|
79
|
+
has_many_class_name = pluralized_symbolized_class_name_from_table_name(fk.referenced_table_name)
|
80
|
+
self.send(:has_many, has_many_class_name, :through=>association[0])
|
81
|
+
logger.info("GENERATED ASSOCIATION: #{self.name} has_many #{has_many_class_name}, :through=>#{association[0]}")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end # end of class methods
|
87
|
+
|
88
|
+
end # end of class Base
|
89
|
+
|
90
|
+
end # end of module ActiveRecord
|
data/lib/base.rb
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
|
3
|
+
class Base
|
4
|
+
|
5
|
+
# Stores a dictionary of table_name => class_name mappings to facilitate reverse lookup
|
6
|
+
# with acceptable performance
|
7
|
+
@@class_name_hash = {}
|
8
|
+
|
9
|
+
@@logger = Logger.new(STDERR)
|
10
|
+
@@logger.level = Logger::INFO
|
11
|
+
|
12
|
+
|
13
|
+
# ------------------------------------------------------------------------------------------------------------------------
|
14
|
+
# Class Methods
|
15
|
+
#
|
16
|
+
# This extension to ActiveRecord::Base creates associations and validations for data objects
|
17
|
+
# based on the actual constraints defined on the DB that are retrieved through extension APIs on the
|
18
|
+
# database adapters.
|
19
|
+
#
|
20
|
+
# To accomplish this, it subverts the 2 methods of creating ActiveRecord objects:
|
21
|
+
#
|
22
|
+
# *1 - ActiveRecord::Base.initialize - this is called when an ActiveRecord object is created with ActiveRecord::Base.new
|
23
|
+
#
|
24
|
+
# *2 - ActiveRecord::Base.instantiate - this is called when an ActiveRecord object is created through one of the find methods
|
25
|
+
#
|
26
|
+
# When an ActiveRecord object is created through these methods the class-level associations and validations
|
27
|
+
# are generated unless they already exist
|
28
|
+
class << self
|
29
|
+
|
30
|
+
def logger
|
31
|
+
@@logger
|
32
|
+
end
|
33
|
+
|
34
|
+
# Save table name keys as all UPPERCASE to simplify lookup
|
35
|
+
alias :base_set_table_name :set_table_name
|
36
|
+
def set_table_name(value = nil, &block)
|
37
|
+
base_set_table_name(value)
|
38
|
+
@@class_name_hash["#{table_name.upcase}"] = self.name
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
# Overrides primary_key in ActiveRecord::Base
|
43
|
+
#
|
44
|
+
# This implementation will need to be adjusted should composite primary keys be supported in the future
|
45
|
+
#
|
46
|
+
# set_primary_key generates an accessor method on the caller that returns the string value
|
47
|
+
# --> this generated accessor will be invoked on future calls to primary_key
|
48
|
+
# ---------------------------------------------------------------------------------------------------
|
49
|
+
def primary_key
|
50
|
+
primary = table_constraints.detect {|constraint| constraint.primary_key?}
|
51
|
+
primary_name = primary.column_name
|
52
|
+
set_primary_key(primary_name)
|
53
|
+
primary_name
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
alias :base_class_name :class_name
|
58
|
+
def class_name(table_name = table_name) # :nodoc:
|
59
|
+
#Attempt to retrieve class name from cache before attempting to generate it
|
60
|
+
name = @@class_name_hash[table_name.upcase]
|
61
|
+
if !name
|
62
|
+
return base_class_name(table_name)
|
63
|
+
end
|
64
|
+
name
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
alias :base_instantiate :instantiate
|
69
|
+
def instantiate(record)
|
70
|
+
generate_associations
|
71
|
+
generate_validations
|
72
|
+
base_instantiate(record)
|
73
|
+
end
|
74
|
+
|
75
|
+
# table_constraints are those constraints defined on this table
|
76
|
+
def table_constraints
|
77
|
+
constraints.select {|constraint| constraint.referenced_table_name.nil? || constraint.table_name.upcase == table_name.upcase}
|
78
|
+
end
|
79
|
+
|
80
|
+
# foreign_constraints are those constraints that are defined on other tables, but
|
81
|
+
# reference this table (i.e. FKs into this table)
|
82
|
+
def foreign_constraints
|
83
|
+
constraints.select {|constraint|
|
84
|
+
!constraint.referenced_table_name.nil? &&
|
85
|
+
constraint.referenced_table_name.upcase == table_name.upcase &&
|
86
|
+
constraint.table_name.upcase != table_name.upcase}
|
87
|
+
end
|
88
|
+
|
89
|
+
def associations
|
90
|
+
@associations ? @associations : {}
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def constraints
|
97
|
+
unless @constraints
|
98
|
+
@constraints = connection.constraints(table_name, "#{name} Constraints")
|
99
|
+
end
|
100
|
+
@constraints
|
101
|
+
end
|
102
|
+
|
103
|
+
# FIXME MacroReflection needs an instance method has_many?
|
104
|
+
# This logic does not belong in Base
|
105
|
+
def has_many_associations
|
106
|
+
reflections.select {|key, reflection| reflection.macro == :has_many}
|
107
|
+
end
|
108
|
+
|
109
|
+
def symbolized_class_name_from_table_name(table_name)
|
110
|
+
(class_name(table_name)).downcaseFirstLetter.to_sym
|
111
|
+
end
|
112
|
+
|
113
|
+
def pluralized_symbolized_class_name_from_table_name(table_name)
|
114
|
+
(class_name(table_name)).downcaseFirstLetter.pluralize.to_sym
|
115
|
+
end
|
116
|
+
|
117
|
+
end #end class << self (Class Methods)
|
118
|
+
|
119
|
+
# ---------------------------------------------------------------------------------------------------
|
120
|
+
|
121
|
+
public
|
122
|
+
alias :base_initialize :initialize
|
123
|
+
def initialize(attributes = nil)
|
124
|
+
base_initialize
|
125
|
+
self.class.generate_associations
|
126
|
+
self.class.generate_validations
|
127
|
+
end
|
128
|
+
|
129
|
+
def constraints
|
130
|
+
self.class.table_constraints
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
end # end class Base
|
135
|
+
|
136
|
+
end #end module ActiveRecord
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters #:nodoc:
|
3
|
+
|
4
|
+
class Column
|
5
|
+
|
6
|
+
def generated?
|
7
|
+
raise NotImplementedError, "Column subclass did not implement this method"
|
8
|
+
end
|
9
|
+
|
10
|
+
# MySQL sinks the boat on this one.
|
11
|
+
# If no default is specified for a non-nullable column, then MySQL assigns an implicit default value.
|
12
|
+
# This makes it impossible to determine whether or not a default value has been specified for any
|
13
|
+
# non-nullable column without parsing the SHOW CREATE TABLE output, which is non-standard.
|
14
|
+
def default_specified?
|
15
|
+
raise NotImplementedError, "Column subclass did not implement this method"
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
|
3
|
+
module ConnectionAdapters
|
4
|
+
|
5
|
+
# This is abstract class serves as the template for table constraints from all
|
6
|
+
# databases compatible with the ISO SQL:2003 information schema standard
|
7
|
+
class AbstractTableConstraint
|
8
|
+
|
9
|
+
def raise_subclass_responsibility_error
|
10
|
+
raise NotImplementedError, "AbstractTableConstraint subclass #{self.class} did not implement this method"
|
11
|
+
end
|
12
|
+
|
13
|
+
alias :primary_key? :raise_subclass_responsibility_error
|
14
|
+
alias :foreign_key? :raise_subclass_responsibility_error
|
15
|
+
alias :unique_key? :raise_subclass_responsibility_error
|
16
|
+
alias :component_of_unique_key? :raise_subclass_responsibility_error
|
17
|
+
alias :is_member_of_composite? :raise_subclass_responsibility_error
|
18
|
+
alias :constraint_schema :raise_subclass_responsibility_error
|
19
|
+
alias :constraint_name :raise_subclass_responsibility_error
|
20
|
+
alias :constraint_type :raise_subclass_responsibility_error
|
21
|
+
alias :table_schema :raise_subclass_responsibility_error
|
22
|
+
alias :table_name :raise_subclass_responsibility_error
|
23
|
+
alias :column_name :raise_subclass_responsibility_error
|
24
|
+
alias :referenced_table_schema :raise_subclass_responsibility_error
|
25
|
+
alias :referenced_table_name :raise_subclass_responsibility_error
|
26
|
+
alias :referenced_column_name :raise_subclass_responsibility_error
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
class AbstractAdapter
|
32
|
+
|
33
|
+
def constraints(table_name, name=nil)
|
34
|
+
raise NotImplementedError, "AbstractAdapter subclass #{self.class} did not implement this method"
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
|
3
|
+
module ConnectionAdapters
|
4
|
+
|
5
|
+
class MysqlColumn < Column #:nodoc:
|
6
|
+
attr_reader :extra
|
7
|
+
|
8
|
+
@@auto_increment = "auto_increment"
|
9
|
+
|
10
|
+
# Capture the value of the "EXTRA" column in the column metadata table.
|
11
|
+
# This can be used to generate validations
|
12
|
+
alias :base_initialize :initialize
|
13
|
+
def initialize(name, default, sql_type = nil, null = true, extra = nil)
|
14
|
+
base_initialize(name, default, sql_type, null)
|
15
|
+
@extra = extra
|
16
|
+
end
|
17
|
+
|
18
|
+
def generated?
|
19
|
+
@extra == @@auto_increment
|
20
|
+
end
|
21
|
+
|
22
|
+
# Until MySQL properly handles default values, I assume that a default value of NULL or empty
|
23
|
+
# string means that a default value has not been specified for the column.
|
24
|
+
#
|
25
|
+
# We really only care about this when the column is NOT NULL, in which case it should be safe
|
26
|
+
# to assume that no one would define the column as NOT NULL and then explicitly set the empty string
|
27
|
+
# as the default value. ...Right?
|
28
|
+
def default_specified?
|
29
|
+
!(default.nil? || default.blank?)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
# A representation of a referential constraint or key in MySQL
|
36
|
+
# Specifically: Primary, Foreign, Unique Key definitions
|
37
|
+
class MysqlConstraint < AbstractTableConstraint
|
38
|
+
@@PRIMARY_KEY_TYPE='PRIMARY KEY'
|
39
|
+
@@FOREIGN_KEY_TYPE='FOREIGN KEY'
|
40
|
+
@@UNIQUE_KEY_TYPE='UNIQUE'
|
41
|
+
|
42
|
+
attr_reader :constraint_schema, :constraint_name, :constraint_type, :table_schema, :table_name, :column_name,
|
43
|
+
:referenced_table_schema, :referenced_table_name, :referenced_column_name
|
44
|
+
|
45
|
+
attr_writer :member_of_composite
|
46
|
+
|
47
|
+
def initialize(constraint_catalog, constraint_schema, constraint_name, table_schema, table_name, constraint_type,
|
48
|
+
column_name, referenced_table_schema, referenced_table_name, referenced_column_name)
|
49
|
+
@constraint_catalog = constraint_catalog
|
50
|
+
@constraint_schema = constraint_schema
|
51
|
+
@constraint_name = constraint_name
|
52
|
+
@table_schema = table_schema
|
53
|
+
@table_name = table_name
|
54
|
+
@constraint_type = constraint_type
|
55
|
+
@column_name = column_name
|
56
|
+
@referenced_table_schema = referenced_table_schema
|
57
|
+
@referenced_table_name = referenced_table_name
|
58
|
+
@referenced_column_name = referenced_column_name
|
59
|
+
end
|
60
|
+
|
61
|
+
def primary_key?
|
62
|
+
constraint_type == @@PRIMARY_KEY_TYPE
|
63
|
+
end
|
64
|
+
|
65
|
+
def foreign_key?
|
66
|
+
constraint_type == @@FOREIGN_KEY_TYPE
|
67
|
+
end
|
68
|
+
|
69
|
+
def component_of_unique_key?
|
70
|
+
constraint_type == @@UNIQUE_KEY_TYPE
|
71
|
+
end
|
72
|
+
|
73
|
+
def unique_key?
|
74
|
+
component_of_unique_key? and !is_member_of_composite?
|
75
|
+
end
|
76
|
+
|
77
|
+
def is_member_of_composite?
|
78
|
+
@member_of_composite ? @member_of_composite : false
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
# The purpose of this extension to MysqlAdapter is to exploit the referential constraints offered in MySQL >=5.0, and
|
85
|
+
# to make the table and column metadata stored in MySQL available to ActiveRecord objects
|
86
|
+
#
|
87
|
+
# Currently, CHECK constraints are not supported in MySQL 5.x. Although the syntax for creating CHECK constraints is
|
88
|
+
# supported, they are ignored by MySQL
|
89
|
+
class MysqlAdapter < AbstractAdapter
|
90
|
+
|
91
|
+
# FIXME filter SQL queries on information_schema by schema name in case a table
|
92
|
+
# with the same name exists in > 1 schema (i.e. test vs. dev schema)
|
93
|
+
def schema
|
94
|
+
@connection_options[3]
|
95
|
+
end
|
96
|
+
|
97
|
+
# The "EXTRA" column is discarded in the standard Rails implementation of MysqlAdapter.columns.
|
98
|
+
# ActiveRecord can use this column to generate validations
|
99
|
+
def columns(table_name, name = nil)#:nodoc:
|
100
|
+
sql = "SHOW FIELDS FROM #{table_name}"
|
101
|
+
columns = []
|
102
|
+
execute(sql, name).each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES", field[5])}
|
103
|
+
columns
|
104
|
+
end
|
105
|
+
|
106
|
+
# Retrieve each DB constraint from the information_schema database that is either a constraint on
|
107
|
+
# table_name or references table_name (eg. FK into table_name)
|
108
|
+
#
|
109
|
+
# Save a boolean value on each constraint indicating whether it is part of a composite constraint or not.
|
110
|
+
# This allows us to encapsulate is_composite? logic on the constraint object itself, rather than
|
111
|
+
# depending on access to the complete set of constraints for the table at a later time
|
112
|
+
def constraints(table_name, name = nil)#:nodoc:
|
113
|
+
constraints = []
|
114
|
+
|
115
|
+
sql = "select t2.*, t1.column_name, t1.referenced_table_schema, t1.referenced_table_name, t1.referenced_column_name \
|
116
|
+
from information_schema.key_column_usage as t1 inner join information_schema.table_constraints as t2 \
|
117
|
+
using (constraint_name, table_schema, table_name) where table_name='#{table_name}' or t1.referenced_table_name='#{table_name}'"
|
118
|
+
results = execute(sql, name)
|
119
|
+
constraint_name_hash = {}
|
120
|
+
results.each do |row|
|
121
|
+
constraints << MysqlConstraint.new(row[0], row[1], row[2], row[3], row[4], row[5], row[6], row[7], row[8], row[9])
|
122
|
+
comparable_constraint_name = row[2].upcase
|
123
|
+
constraint_name_count = constraint_name_hash[comparable_constraint_name]
|
124
|
+
constraint_name_count ?
|
125
|
+
constraint_name_hash[comparable_constraint_name] = constraint_name_count + 1 :
|
126
|
+
constraint_name_hash[comparable_constraint_name] = 1
|
127
|
+
end
|
128
|
+
|
129
|
+
constraints.each do | constraint|
|
130
|
+
constraint.member_of_composite=(constraint_name_hash[constraint.constraint_name.upcase] > 1)
|
131
|
+
end
|
132
|
+
constraints
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
|
3
|
+
module ConnectionAdapters
|
4
|
+
|
5
|
+
class PostgreSQLColumn < Column #:nodoc:
|
6
|
+
attr_accessor :generated
|
7
|
+
|
8
|
+
alias :generated? :generated
|
9
|
+
|
10
|
+
def default_specified?
|
11
|
+
!default.nil?
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
class PostgreSQLConstraint < AbstractTableConstraint
|
18
|
+
@@PRIMARY_KEY_TYPE = "PRIMARY KEY"
|
19
|
+
@@FOREIGN_KEY_TYPE = "FOREIGN KEY"
|
20
|
+
@@UNIQUE_KEY_TYPE = "UNIQUE"
|
21
|
+
@@CHECK_CONSTRAINT_TYPE = "CHECK"
|
22
|
+
|
23
|
+
attr_reader :constraint_catalog, :constraint_schema, :constraint_name, :constraint_type,
|
24
|
+
:table_catalog, :table_schema, :table_name, :column_name,
|
25
|
+
:referenced_table_catalog, :referenced_table_schema, :referenced_table_name, :referenced_column_name
|
26
|
+
|
27
|
+
attr_writer :member_of_composite
|
28
|
+
|
29
|
+
def initialize(table_catalog, table_schema, table_name, column_name, constraint_catalog, constraint_schema,
|
30
|
+
constraint_name, referenced_table_catalog, referenced_table_schema, referenced_table_name, constraint_type,
|
31
|
+
referenced_column_name)
|
32
|
+
@table_catalog = table_catalog
|
33
|
+
@table_schema = table_schema
|
34
|
+
@table_name = table_name
|
35
|
+
@column_name = column_name
|
36
|
+
@constraint_catalog = constraint_catalog
|
37
|
+
@constraint_schema = constraint_schema
|
38
|
+
@constraint_name = constraint_name
|
39
|
+
@constraint_type = constraint_type
|
40
|
+
@referenced_table_catalog = referenced_table_catalog
|
41
|
+
@referenced_table_schema = referenced_table_schema
|
42
|
+
@referenced_table_name = referenced_table_name
|
43
|
+
@referenced_column_name = referenced_column_name
|
44
|
+
end
|
45
|
+
|
46
|
+
def primary_key?
|
47
|
+
constraint_type == @@PRIMARY_KEY_TYPE
|
48
|
+
end
|
49
|
+
|
50
|
+
def foreign_key?
|
51
|
+
constraint_type == @@FOREIGN_KEY_TYPE
|
52
|
+
end
|
53
|
+
|
54
|
+
def check_constraint?
|
55
|
+
constraint_type == @@CHECK_CONSTRAINT_TYPE
|
56
|
+
end
|
57
|
+
|
58
|
+
def component_of_unique_key?
|
59
|
+
constraint_type == @@UNIQUE_KEY_TYPE
|
60
|
+
end
|
61
|
+
|
62
|
+
def unique_key?
|
63
|
+
component_of_unique_key? and !is_member_of_composite?
|
64
|
+
end
|
65
|
+
|
66
|
+
def is_member_of_composite?
|
67
|
+
@member_of_composite ? @member_of_composite : false
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
class PostgreSQLAdapter < AbstractAdapter
|
74
|
+
|
75
|
+
def columns(table_name, name = nil) #:nodoc:
|
76
|
+
columns = []
|
77
|
+
column_definitions(table_name).each do |name, type, default, notnull, typmod|
|
78
|
+
# typmod now unused as limit, precision, scale all handled by superclass
|
79
|
+
current = PostgreSQLColumn.new(name, default_value(default), translate_field_type(type), notnull == "f")
|
80
|
+
current.generated= (!default.nil? && default.index('nextval') == 0)
|
81
|
+
columns << current
|
82
|
+
end
|
83
|
+
columns
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
def constraints(table_name, name = nil)#:nodoc:
|
88
|
+
constraints = []
|
89
|
+
sql = "select t3.* , t4.* from information_schema.constraint_column_usage as t3 right join \
|
90
|
+
(select t2.*, t1.column_name from information_schema.key_column_usage as t1 inner join \
|
91
|
+
information_schema.table_constraints as t2 using (constraint_name, table_catalog, table_schema, table_name)) \
|
92
|
+
as t4 using (constraint_catalog, constraint_schema, constraint_name) \
|
93
|
+
where t4.table_name='#{table_name.downcase}' or t3.table_name='#{table_name.downcase}'"
|
94
|
+
results = query(sql, name)
|
95
|
+
constraint_name_hash = {}
|
96
|
+
results.each do |row|
|
97
|
+
constraints << PostgreSQLConstraint.new(row[10], row[11], row[12], row[16], row[4], row[5], row[6], row[0], row[1], row[2], row[13], row[3])
|
98
|
+
comparable_constraint_name = row[6].upcase
|
99
|
+
constraint_name_count = constraint_name_hash[comparable_constraint_name]
|
100
|
+
constraint_name_count ?
|
101
|
+
constraint_name_hash[comparable_constraint_name] = constraint_name_count + 1 :
|
102
|
+
constraint_name_hash[comparable_constraint_name] = 1
|
103
|
+
end
|
104
|
+
|
105
|
+
constraints.each do |constraint|
|
106
|
+
constraint.member_of_composite=(constraint_name_hash[constraint.constraint_name.upcase] > 1)
|
107
|
+
end
|
108
|
+
constraints
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
data/lib/dependencies.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
class Module #:nodoc:
|
2
|
+
|
3
|
+
alias :base_const_missing :const_missing
|
4
|
+
|
5
|
+
# If a non-existant class is specified, define it as a subclass of ActiveRecord::Base here
|
6
|
+
#
|
7
|
+
# This idea was inspired by the Magic Models project: http://magicmodels.rubyforge.org/
|
8
|
+
#
|
9
|
+
# I believe the on-demand generation and simple ActiveRecord::Base subclassing
|
10
|
+
# to be a better implementation for my needs
|
11
|
+
def const_missing(class_id)
|
12
|
+
begin
|
13
|
+
base_const_missing(class_id)
|
14
|
+
rescue NameError
|
15
|
+
class_def = <<-end_class_def
|
16
|
+
class #{class_id} < ActiveRecord::Base
|
17
|
+
end
|
18
|
+
end_class_def
|
19
|
+
eval(class_def, TOPLEVEL_BINDING)
|
20
|
+
end
|
21
|
+
const_get(class_id)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
data/lib/drysql.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
unless defined?(ActiveRecord)
|
2
|
+
require 'rubygems'
|
3
|
+
require_gem 'activerecord'
|
4
|
+
end
|
5
|
+
|
6
|
+
require 'connection_adapters/abstract_adapter'
|
7
|
+
require 'connection_adapters/mysql_adapter'
|
8
|
+
require 'connection_adapters/postgresql_adapter'
|
9
|
+
require 'connection_adapters/abstract/schema_definitions'
|
10
|
+
require 'associations'
|
11
|
+
require 'validations'
|
12
|
+
require 'helpers/string'
|
13
|
+
require 'base'
|
14
|
+
require 'dependencies'
|
data/lib/validations.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
|
3
|
+
class Errors
|
4
|
+
|
5
|
+
@@default_error_messages[:null] = "can't be null"
|
6
|
+
|
7
|
+
# Add an error message to each of the attributes in +attributes+ that is null
|
8
|
+
def add_on_null(attributes, msg = @@default_error_messages[:null])
|
9
|
+
for attr in [attributes].flatten
|
10
|
+
value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
|
11
|
+
add(attr, msg) if value.nil?
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
module Validations
|
19
|
+
module ClassMethods
|
20
|
+
|
21
|
+
# NOTE: template for this method borrowed from validates_presence_of method in Rails
|
22
|
+
def validates_nullability_of(*attr_names)
|
23
|
+
configuration = { :message => ActiveRecord::Errors.default_error_messages[:null], :on => :save }
|
24
|
+
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
|
25
|
+
|
26
|
+
# can't use validates_each here, because it cannot cope with nonexistent attributes,
|
27
|
+
# while errors.add_on_empty can
|
28
|
+
attr_names.each do |attr_name|
|
29
|
+
send(validation_method(configuration[:on])) do |record|
|
30
|
+
unless configuration[:if] and not evaluate_condition(configuration[:if], record)
|
31
|
+
record.errors.add_on_null(attr_name,configuration[:message])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
class Base
|
42
|
+
|
43
|
+
class << self
|
44
|
+
|
45
|
+
# The @validations instvar is used as an indicator as to whether the validations have already been generated or not.
|
46
|
+
# The first time an instance of a DrySQL-enabled subclass of ActiveRecord::Base is created, the validations are generated
|
47
|
+
# for the class.
|
48
|
+
#
|
49
|
+
# **NOTE: Some of this code and/or the ideas behind some of this code was borrowed from Simon Harris
|
50
|
+
# and Red Hill Consulting's Schema Validations plug-in. Thanks Simon!
|
51
|
+
# See http://www.redhillconsulting.com.au/rails_plugins.html#schema_validations
|
52
|
+
# for info about this plug-in
|
53
|
+
#
|
54
|
+
# Additional features added to Schema_Validations:
|
55
|
+
# 1) proper error message generated for boolean inclusion failure (:inclusion)
|
56
|
+
#
|
57
|
+
# 2) all validations are generated with allow_nil=>true because...
|
58
|
+
#
|
59
|
+
# 3) all validations are superceded with the _new_ validates_nullability_of,
|
60
|
+
# which generates an error for any field whose associated column has a NOT NULL constraint upon it,
|
61
|
+
# for whom no default value is specified, and for whom the value is not generated by the DB
|
62
|
+
#
|
63
|
+
# 4) do not generate validates_uniqueness_of by default for unique fields. This is a performance hit, and
|
64
|
+
# in my opinion completely defeats the purpose of an application-side data validation because it
|
65
|
+
# requires a query of the DB. Why not just let the DB do the work and handle the duplicate key exception?
|
66
|
+
#
|
67
|
+
# 5) Do not generate validates_presence_of for non-nullable fields. This will throw an exception for fields that
|
68
|
+
# contain an empty string, even though that may in fact be a valid value according to the table constraints.
|
69
|
+
# This approach also fails to consider that a default value might be specified for a non-nullable field in which case
|
70
|
+
# we do not need to block the null field from being saved to the DB
|
71
|
+
#
|
72
|
+
# 6) Perform validation auto-generation on all columns rather than just those returned by Base.content_columns
|
73
|
+
# I believe there is value in validating our PK, FK, and counter columns
|
74
|
+
def generate_validations
|
75
|
+
unless @validations
|
76
|
+
columns.each do |column|
|
77
|
+
|
78
|
+
if !(column.null || column.default_specified? || column.generated?)
|
79
|
+
self.validates_nullability_of column.name
|
80
|
+
logger.info("GENERATED VALIDATION: #{self.name} validates_nullability_of #{column.name}")
|
81
|
+
end
|
82
|
+
|
83
|
+
if column.type == :integer
|
84
|
+
self.validates_numericality_of column.name, :allow_nil => true, :only_integer => true
|
85
|
+
logger.info("GENERATED VALIDATION: #{self.name} validates_numericality_of #{column.name}, :allow_nil=>true, :only_integer=>true")
|
86
|
+
elsif column.number?
|
87
|
+
self.validates_numericality_of column.name, :allow_nil => true
|
88
|
+
logger.info("GENERATED VALIDATION: #{self.name} validates_numericality_of #{column.name}, :allow_nil=>true")
|
89
|
+
elsif column.text? && column.limit
|
90
|
+
self.validates_length_of column.name, :allow_nil => true, :maximum => column.limit
|
91
|
+
logger.info("GENERATED VALIDATION: #{self.name} validates_length_of #{column.name}, :allow_nil=>true, :maximum=>#{column.limit}")
|
92
|
+
elsif column.type == :boolean
|
93
|
+
self.validates_inclusion_of column.name, :in => [true, false], :allow_nil =>true, :message => ActiveRecord::Errors.default_error_messages[:inclusion]
|
94
|
+
logger.info("GENERATED VALIDATION: #{self.name} validates_inclusion_of #{column.name}, :in=>[true, false], \
|
95
|
+
:allow_nil=>true, :message=>ActiveRecord::Errors.default_error_messages[:inclusion]")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
@validations=true
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
end # end of class methods
|
104
|
+
|
105
|
+
end # end of class Base
|
106
|
+
|
107
|
+
end # end of module ActiveRecord
|
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.8.11
|
3
|
+
specification_version: 1
|
4
|
+
name: drysql
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.1.0
|
7
|
+
date: 2006-10-30 00:00:00 -05:00
|
8
|
+
summary: Extends ActiveRecord to provide automatic identification of keys, and automatic generation of associations and validations
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: bsevans@rubyforge.org
|
12
|
+
homepage: http://drysql.rubyforge.org
|
13
|
+
rubyforge_project: drysql
|
14
|
+
description: DrySQL uses your DB's information schema to identify keys, and generate associations and validations for ActiveRecord objects. You defined the schema on your DB, don't re-define it in your application code. Legacy DB Tables/Column Names, Ruby Desktop apps, Rails apps...DrySQL's got all your (data)bases covered
|
15
|
+
autorequire: drysql
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: false
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
authors:
|
29
|
+
- Bryan Evans
|
30
|
+
files:
|
31
|
+
- lib/validations.rb
|
32
|
+
- lib/dependencies.rb
|
33
|
+
- lib/associations.rb
|
34
|
+
- lib/drysql.rb
|
35
|
+
- lib/base.rb
|
36
|
+
- lib/helpers/string.rb
|
37
|
+
- lib/connection_adapters/postgresql_adapter.rb
|
38
|
+
- lib/connection_adapters/mysql_adapter.rb
|
39
|
+
- lib/connection_adapters/abstract_adapter.rb
|
40
|
+
- lib/connection_adapters/abstract/schema_definitions.rb
|
41
|
+
- README
|
42
|
+
test_files: []
|
43
|
+
|
44
|
+
rdoc_options: []
|
45
|
+
|
46
|
+
extra_rdoc_files: []
|
47
|
+
|
48
|
+
executables: []
|
49
|
+
|
50
|
+
extensions: []
|
51
|
+
|
52
|
+
requirements: []
|
53
|
+
|
54
|
+
dependencies:
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: activerecord
|
57
|
+
version_requirement:
|
58
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 1.14.4
|
63
|
+
version:
|