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 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
+
@@ -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
@@ -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'
@@ -0,0 +1,9 @@
1
+ class String
2
+
3
+ def downcaseFirstLetter
4
+ duplicate = self.dup
5
+ duplicate[0, 1] = duplicate[0, 1].downcase
6
+ duplicate
7
+ end
8
+
9
+ end
@@ -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: