dart 0.0.1

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.
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