drysql 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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:
|