dart 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +31 -0
  6. data/Rakefile +2 -0
  7. data/dart.gemspec +28 -0
  8. data/lib/dart.rb +1 -0
  9. data/lib/dart/active_record_model_reflection.rb +7 -0
  10. data/lib/dart/core.rb +17 -0
  11. data/lib/dart/core/association.rb +29 -0
  12. data/lib/dart/core/direct_association.rb +36 -0
  13. data/lib/dart/core/foreign_key_info.rb +8 -0
  14. data/lib/dart/core/many_to_many_association.rb +55 -0
  15. data/lib/dart/core/many_to_one_association.rb +18 -0
  16. data/lib/dart/core/one_to_many_association.rb +13 -0
  17. data/lib/dart/core/one_to_one_association.rb +17 -0
  18. data/lib/dart/core/relation.rb +46 -0
  19. data/lib/dart/database.rb +8 -0
  20. data/lib/dart/database/many_to_many_association.rb +14 -0
  21. data/lib/dart/database/many_to_one_association.rb +14 -0
  22. data/lib/dart/database/one_to_many_association.rb +14 -0
  23. data/lib/dart/database/relation.rb +21 -0
  24. data/lib/dart/database/test_helpers.rb +23 -0
  25. data/lib/dart/naming_conventions.rb +18 -0
  26. data/lib/dart/naming_conventions/abstract_base.rb +71 -0
  27. data/lib/dart/naming_conventions/association_helpers.rb +16 -0
  28. data/lib/dart/naming_conventions/direct_association_helpers.rb +21 -0
  29. data/lib/dart/naming_conventions/foreign_key_finder.rb +26 -0
  30. data/lib/dart/naming_conventions/many_to_many_association_helpers.rb +52 -0
  31. data/lib/dart/naming_conventions/many_to_one_association_helpers.rb +20 -0
  32. data/lib/dart/naming_conventions/one_to_many_association_helpers.rb +26 -0
  33. data/lib/dart/naming_conventions/relation_helpers.rb +40 -0
  34. data/lib/dart/reflection/abstract_resolver.rb +7 -0
  35. data/lib/dart/reflection/active_record_model/resolver.rb +122 -0
  36. data/lib/dart/reflection/orm_model_resolver.rb +64 -0
  37. data/lib/dart/reflection/sequel/naming_conventions.rb +25 -0
  38. data/lib/dart/reflection/sequel/sequelizer.rb +14 -0
  39. data/lib/dart/reflection/sequel_model/resolver.rb +78 -0
  40. data/lib/dart/reflection/sequel_table/foreign_key_finder.rb +50 -0
  41. data/lib/dart/reflection/sequel_table/reflector.rb +151 -0
  42. data/lib/dart/reflection/sequel_table/resolver.rb +60 -0
  43. data/lib/dart/reflection/sequel_table/schema.rb +52 -0
  44. data/lib/dart/sequel_model_reflection.rb +10 -0
  45. data/lib/dart/sequel_table_reflection.rb +26 -0
  46. data/lib/dart/version.rb +3 -0
  47. data/test/dart/database/many_to_many_association_test.rb +80 -0
  48. data/test/dart/naming_conventions/abstract_base_test.rb +38 -0
  49. data/test/dart/reflection/orm_model_resolver_test.rb +66 -0
  50. data/test/test_helper.rb +6 -0
  51. metadata +182 -0
@@ -0,0 +1,21 @@
1
+ module Dart
2
+ module Database
3
+ # A Database::Relation extends Dart::Relation with naming conventions to support table reflection
4
+ class Relation < Dart::Relation
5
+ include NamingConventions::RelationHelpers
6
+
7
+ def add_many_to_one(*ass_args)
8
+ add_association ManyToOneAssociation.new(*ass_args)
9
+ end
10
+
11
+ def add_one_to_many(*ass_args)
12
+ add_association OneToManyAssociation.new(*ass_args)
13
+ end
14
+
15
+ def add_many_to_many(*ass_args)
16
+ add_association ManyToManyAssociation.new(*ass_args)
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ module Dart
2
+ module Database
3
+
4
+ # Test support class used to easily construct associations
5
+ module TestHelpers
6
+ def relation(*args)
7
+ Relation.new(*args)
8
+ end
9
+
10
+ def many_to_one_ass(*args)
11
+ ManyToOneAssociation.new(*args)
12
+ end
13
+
14
+ def one_to_many_ass(*args)
15
+ OneToManyAssociation.new(*args)
16
+ end
17
+
18
+ def many_to_many_ass(*args)
19
+ ManyToManyAssociation.new(*args)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ require_relative 'naming_conventions/foreign_key_finder'
2
+
3
+ require_relative 'naming_conventions/relation_helpers'
4
+ require_relative 'naming_conventions/association_helpers'
5
+ require_relative 'naming_conventions/direct_association_helpers'
6
+ require_relative 'naming_conventions/many_to_many_association_helpers'
7
+ require_relative 'naming_conventions/many_to_one_association_helpers'
8
+ require_relative 'naming_conventions/one_to_many_association_helpers'
9
+
10
+ module Dart
11
+ module NamingConventions
12
+
13
+ # NamingConventions.instance must be set by the user of this module. It is normally a subclass of AbstractBase
14
+ class << self
15
+ attr_accessor :instance
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,71 @@
1
+ module Dart
2
+ module NamingConventions
3
+
4
+ PUBLIC_API_METHODS = [:parent_table_for, :singular_association_name, :plural_association_name, :long_association_name, :conventional_join_table_names]
5
+
6
+ class AbstractBase
7
+ abstract_method :conventional_primary_key, :foreign_key_regex
8
+ abstract_method :pluralize, :singularize
9
+
10
+ # Returns the name of a possibly referenced table if the given possible_foreign_key follows the naming convention,
11
+ # otherwise returns nil
12
+ #
13
+ # Examples:
14
+ # parent_table_for('group_id') => 'groups'
15
+ # parent_table_for('created_by') => nil
16
+ #
17
+ # @param [String|Symbol] possible_foreign_key name of the possibly referencing column
18
+ # @return [String|NilClass] name of the possibly referenced table if found by convention
19
+ #
20
+ def parent_table_for(possible_foreign_key)
21
+ pluralize singular_association_name(possible_foreign_key) if possible_foreign_key =~ foreign_key_regex
22
+ end
23
+
24
+ # Returns a many_to_one association name based on the given foreign_key according to the naming convention
25
+ # Examples:
26
+ # to_association('group_id') => 'group'
27
+ # to_association('team_id') => 'team'
28
+ # to_association('created_by') => 'created_by'
29
+ #
30
+ def singular_association_name(foreign_key)
31
+ if foreign_key =~ foreign_key_regex
32
+ foreign_key[0...foreign_key.index(foreign_key_regex)]
33
+ else
34
+ foreign_key
35
+ end
36
+ end
37
+
38
+ # Returns a one_to_many association name based on the given foreign_key according to the naming convention
39
+ # Examples:
40
+ # to_association('group_id') => 'groups'
41
+ # to_association('team_id') => 'teams'
42
+ # to_association('created_by') => 'created_bies'
43
+ #
44
+ def plural_association_name(foreign_key)
45
+ pluralize singular_association_name(foreign_key)
46
+ end
47
+
48
+ # Returns a long many_to_many association name based on the given join table and foreign_keys according to the
49
+ # naming convention
50
+ # Examples:
51
+ # long_association_name('topic_assignments', 'group_id', 'groups', 'user_id') => 'topic_assignment_users'
52
+ # long_association_name('broadcasts', 'created_by', 'users', 'item_id') => 'broadcast_created_by_items'
53
+ #
54
+ def long_association_name(join_table, left_key, left_table, right_key)
55
+ left_key_qualifier = "_#{left_key}" if left_table != parent_table_for(left_key)
56
+ "#{singularize(join_table)}#{left_key_qualifier}_#{plural_association_name(right_key)}"
57
+ end
58
+
59
+ # Returns unique singular and plural combinations of the given table names that would be used in constructing a
60
+ # conventional join table name (assumes the given table_names are plural)
61
+ def conventional_join_table_names(left_table_name, right_table_name)
62
+ # TODO consider [left_table_name, pluralize(left_table_name), singularize(left_table_name)].uniq
63
+ left_names = [left_table_name, singularize(left_table_name)].uniq
64
+ right_names = [right_table_name, singularize(right_table_name)].uniq
65
+
66
+ (left_names.product(right_names) + right_names.product(left_names)).map { |t1, t2| "#{t1}_#{t2}" }
67
+ end
68
+
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,16 @@
1
+ module Dart
2
+ module NamingConventions
3
+ module AssociationHelpers
4
+
5
+ abstract_method :conventional_name, :name_is_conventional?
6
+
7
+ def naming_conventions
8
+ Dart::NamingConventions.instance or fail "naming conventions have not been configured"
9
+ end
10
+
11
+ def set_conventional_name!
12
+ set_name! conventional_name
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,21 @@
1
+ module Dart
2
+ module NamingConventions
3
+ module DirectAssociationHelpers
4
+
5
+ # Returns true if this association has a conventional foreign key pointing to the given table_name
6
+ def conventional_parent?(table_name)
7
+ parent_table == table_name && conventional_foreign_key?
8
+ end
9
+
10
+ # Returns true if this association has a conventional primary key on the parent table
11
+ def conventional_primary_key?
12
+ primary_key == naming_conventions.conventional_primary_key
13
+ end
14
+
15
+ # Returns true if this association has a conventionally named foreign key based on the parent table name
16
+ def conventional_foreign_key?
17
+ parent_table == naming_conventions.parent_table_for(foreign_key)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ module Dart
2
+ module NamingConventions
3
+ class ForeignKeyFinder
4
+
5
+ # Returns a set of possible foreign keys based on columns in this relation matching the naming convention and
6
+ # reference a table name that is in the given schema
7
+ #
8
+ # @param [Relation] relation the relation to search
9
+ # @param [Schema] schema defines the set of referenceable tables
10
+ #
11
+ def foreign_keys_for(relation, schema)
12
+ naming_conventions = Dart::NamingConventions.instance
13
+ relation.column_names.map do |possible_foreign_key|
14
+ if parent_table = naming_conventions.parent_table_for(possible_foreign_key)
15
+ if schema.has_table?(parent_table)
16
+ ForeignKeyInfo.new(relation.table_name,
17
+ possible_foreign_key,
18
+ parent_table,
19
+ naming_conventions.conventional_primary_key)
20
+ end
21
+ end
22
+ end.compact
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,52 @@
1
+ module Dart
2
+ module NamingConventions
3
+ module ManyToManyAssociationHelpers
4
+ include AssociationHelpers
5
+
6
+ # Returns the name of a referenced association according to the naming convention
7
+ #
8
+ # @return [String] the name of the referenced association
9
+ #
10
+ def conventional_name
11
+ # just return e.g. 'groups' if right_ass.foreign_key is group_id
12
+ naming_conventions.plural_association_name(right_ass.foreign_key)
13
+ end
14
+
15
+ def name_is_conventional?
16
+ name == associated_table
17
+ end
18
+
19
+ def name_and_right_foreign_key_are_conventional?
20
+ # name == associated_table == naming_conventions.parent_table_for(right_ass.foreign_key)
21
+ name_is_conventional? && right_foreign_key_is_conventional?
22
+ end
23
+
24
+ def left_foreign_key_is_conventional?
25
+ left_ass.conventional_foreign_key?
26
+ end
27
+
28
+ def right_foreign_key_is_conventional?
29
+ right_ass.conventional_foreign_key?
30
+ end
31
+
32
+ # forces long-form name to disambiguate from other joins to same association, where short-form name is the same
33
+ def disambiguate_name!
34
+ unless is_semi_conventional_join_table?
35
+ # add a foreign_key disambiguator when the key referencing me is unconventional
36
+ set_name! naming_conventions.long_association_name(join_table, left_ass.foreign_key, left_ass.parent_table, right_ass.foreign_key)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ # Returns true if the table joining t1 and t2 is semi-conventional, i.e. some variation of t1_t2 or t2_t1, for
43
+ # plural or singular forms of t1 and t2
44
+ def is_semi_conventional_join_table?
45
+ @semi_conventional_join_table ||= begin
46
+ naming_conventions.conventional_join_table_names(left_ass.parent_table, right_ass.parent_table).include?(join_table)
47
+ end
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,20 @@
1
+ module Dart
2
+ module NamingConventions
3
+ module ManyToOneAssociationHelpers
4
+ include AssociationHelpers
5
+ include DirectAssociationHelpers
6
+
7
+ # Returns the name of a referenced association according to the naming convention
8
+ #
9
+ # @return [String] the name of the referenced association
10
+ #
11
+ def conventional_name
12
+ naming_conventions.singular_association_name(foreign_key)
13
+ end
14
+
15
+ def name_is_conventional?
16
+ naming_conventions.plural_association_name(foreign_key) == associated_table
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,26 @@
1
+ module Dart
2
+ module NamingConventions
3
+ module OneToManyAssociationHelpers
4
+ include AssociationHelpers
5
+ include DirectAssociationHelpers
6
+
7
+ # Returns the name of a referenced association according to the naming convention
8
+ #
9
+ # @return [String] the name of the referenced association
10
+ #
11
+ def conventional_name
12
+ if conventional_foreign_key?
13
+ associated_table
14
+ else
15
+ "#{naming_conventions.singular_association_name(foreign_key)}_#{child_table}"
16
+ end
17
+ end
18
+
19
+ def name_is_conventional?
20
+ name == associated_table
21
+ end
22
+
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,40 @@
1
+ module Dart
2
+ module NamingConventions
3
+ module RelationHelpers
4
+
5
+ # Returns true if this relation has a many_to_one association with the given table_name
6
+ # @param [String] table_name
7
+ def has_direct_conventional_parent?(table_name)
8
+ parent_associations.any? { |a| a.conventional_parent?(table_name) }
9
+ end
10
+
11
+ # Finds joins with the same name and marks all but the conventional one as requiring a long name.
12
+ #
13
+ def disambiguate_conflicting_join_names!
14
+ # Corner case problem: if the schema changes and a conventional join table is added, then what was formerly
15
+ # foo many_to_many: :bars join_table: poop_stinks, will need to be (manually) changed to something like
16
+ # foo many_to_many: :poop_stink_bars, class: :Bar, join_table: poop_stinks
17
+ # and a request for foo.bars will now return bars from the foo_bars join table instead of the poop_stinks join
18
+ # table. Making all non-conventional joins have long names is worse since in most cases (80/20) the "real"
19
+ # join is just the existing unconventionally named table.
20
+ # many_to_many :bars seems better for a vast majority of joins that are not conventional.
21
+
22
+ duplicate_join_association_names.each do |_, join_associations|
23
+ join_associations.each { |join| join.disambiguate_name! }
24
+ end
25
+ end
26
+
27
+ # Returns a Hash of ass_name => number of copies for all duplicated join associations
28
+ def duplicate_join_association_names
29
+ join_associations.group_by(&:name).select { |name, join_associations| join_associations.count > 1 }
30
+ end
31
+
32
+ # Returns pairs of many_to_one associations that could make up a join through this relation
33
+ # @return [Array<[String,String]]
34
+ def possible_join_pairs
35
+ parent_associations.combination(2).to_a
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,7 @@
1
+ module Dart
2
+ module Reflection
3
+ class AbstractResolver
4
+ abstract_method :build_from_association, :association_for, :column_for, :table_name
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,122 @@
1
+ module Dart
2
+ module Reflection
3
+ module ActiveRecordModel
4
+ class Resolver < OrmModelResolver
5
+
6
+ private
7
+
8
+ def reflection_from(ass_name)
9
+ this_model_class.reflect_on_association(active_recordize(ass_name))
10
+ end
11
+
12
+ def has_column?(col_name)
13
+ this_model_class.column_names.include?(col_name)
14
+ end
15
+
16
+ # = Active Record Associations
17
+ #
18
+ # This is the root class of all associations ('+ Foo' signifies an included module Foo):
19
+ #
20
+ # Association
21
+ # SingularAssociation
22
+ # HasOneAssociation + ForeignAssociation
23
+ # HasOneThroughAssociation + ThroughAssociation
24
+ # BelongsToAssociation
25
+ # BelongsToPolymorphicAssociation
26
+ # CollectionAssociation
27
+ # HasManyAssociation + ForeignAssociation
28
+ # HasManyThroughAssociation + ThroughAssociation
29
+
30
+ def build_association(ass_reflection)
31
+ # use class name because association_class is the actual class, not an instance of it, so === won't match it
32
+ ass = case ass_reflection.association_class.name.demodulize # is it possible to have activerecord without activesupport?
33
+ when 'HasOneAssociation'
34
+ OneToOneAssociation.new(child_table: ass_reflection.table_name,
35
+ foreign_key: ass_reflection.foreign_key,
36
+ parent_table: ass_reflection.active_record.table_name,
37
+ primary_key: ass_reflection.active_record_primary_key)
38
+
39
+ # TODO HasOneThroughAssociation
40
+ # when 'HasOneThroughAssociation'
41
+
42
+ when 'HasManyAssociation'
43
+ OneToManyAssociation.new(child_table: ass_reflection.table_name,
44
+ foreign_key: ass_reflection.foreign_key,
45
+ parent_table: ass_reflection.active_record.table_name,
46
+ primary_key: ass_reflection.active_record_primary_key)
47
+
48
+ when 'BelongsToAssociation'
49
+ # pk = ass_reflection.primary_key_column.name
50
+ ManyToOneAssociation.new(child_table: ass_reflection.active_record.table_name,
51
+ foreign_key: ass_reflection.foreign_key,
52
+ parent_table: ass_reflection.table_name,
53
+ primary_key: ass_reflection.association_primary_key)
54
+
55
+ when 'HasManyThroughAssociation'
56
+ join_ass = ass_reflection.through_reflection # has_many through:
57
+ left_ass = ManyToOneAssociation.new(child_table: join_ass.table_name,
58
+ foreign_key: join_ass.foreign_key,
59
+ parent_table: ass_reflection.active_record.table_name,
60
+ primary_key: ass_reflection.active_record_primary_key)
61
+ right_ass = ManyToOneAssociation.new(child_table: join_ass.table_name,
62
+ foreign_key: ass_reflection.association_foreign_key,
63
+ parent_table: ass_reflection.table_name,
64
+ primary_key: ass_reflection.association_primary_key)
65
+ ManyToManyAssociation.new(left_ass, right_ass)
66
+
67
+ # TODO BelongsToPolymorphicAssociation
68
+ # when 'BelongsToPolymorphicAssociation'
69
+ else
70
+ fail "don't yet know how to resolve associations of type '#{ass_reflection.association_class}' model=#{ass_reflection.klass} association=#{ass_reflection.name}"
71
+ end
72
+
73
+ ass.model_class = ass_reflection.klass
74
+
75
+ ass.scope = scope_for_association(ass_reflection)
76
+
77
+ ass.set_name!(ass_reflection.name)
78
+ ass
79
+ end
80
+
81
+ # TODO just put the association_reflection in the association and get all the SQL including the JOINs from
82
+ # the ORM
83
+ def scope_for_association(ass_reflection)
84
+ case ActiveRecord::VERSION::MAJOR
85
+ when 3
86
+ # in rails 3 ass_reflection.options contains everything except the where
87
+ non_where_scope = ass_reflection.options.slice(*QUERY_OPTIONS)
88
+
89
+ # in rails 3 where is in ass_reflection.options[:conditions]
90
+ conds = ass_reflection.options[:conditions]
91
+ conds = conds.call if conds.is_a?(Proc) # options[:conditions] could be a proc that needs to be evaluated
92
+ scope_hash_from(ass_reflection.klass.where(conds).to_sql).merge(non_where_scope)
93
+
94
+ # This one-liner almost works but adds an "AND fk IS NULL" that would need to be extracted out of the where
95
+ # sql_string = ass_reflection.klass.new.association(ass_name).send(:scoped).to_sql
96
+
97
+ when 4
98
+ # TODO use scope chain for through associations e.g. Broadcast.tracked_conversations
99
+
100
+ # With rails 4, it seems there is either a scope on the association reflection (which includes the target
101
+ # scope) or just a target scope on the association itself
102
+ sql_string = if scope = ass_reflection.scope
103
+ ass_reflection.klass.instance_exec(&scope).to_sql
104
+ else # just use the target scope
105
+ model_class.new.association(ass_name).send(:target_scope).to_sql
106
+ end
107
+
108
+ scope_hash_from(sql_string)
109
+ else
110
+ fail "ActiveRecord version #{ActiveRecord::VERSION::MAJOR}.x is not supported"
111
+ end
112
+ end
113
+
114
+ # Converts the given identifier to the format needed by ActiveRecord
115
+ def active_recordize(id)
116
+ id.to_sym
117
+ end
118
+
119
+ end
120
+ end
121
+ end
122
+ end