dag_me 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.
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DagMe
4
+ class Configuration
5
+ MAINTAIN_MODES = %i[postgresql_closure recursive_cte].freeze
6
+
7
+ attr_reader :model, :name, :maintain, :prefix, :edge_table, :paths_table, :scope_columns
8
+
9
+ def initialize(model:, name: nil, maintain: :postgresql_closure, scope: nil, edge_table: nil, paths_table: nil)
10
+ unless MAINTAIN_MODES.include?(maintain)
11
+ raise ArgumentError, "maintain must be one of #{MAINTAIN_MODES.inspect}, got #{maintain.inspect}"
12
+ end
13
+
14
+ @model = model
15
+ @name = name&.to_sym
16
+ @maintain = maintain
17
+ @scope_columns = Array(scope).map(&:to_sym).freeze
18
+ @prefix = [model.table_name.singularize, @name, 'dag'].compact.join('_')
19
+ @edge_table = edge_table || "#{prefix}_edges"
20
+ @paths_table = paths_table || "#{prefix}_paths"
21
+ # Ivar peek keeps class load DB-free; composite keys must be declared
22
+ # before the macro. Array === because AR seeds a BasicObject sentinel.
23
+ @composite_pk = model.instance_variable_defined?(:@primary_key) &&
24
+ Array === model.instance_variable_get(:@primary_key) # rubocop:disable Style/CaseEquality
25
+ end
26
+
27
+ def closure?
28
+ maintain == :postgresql_closure
29
+ end
30
+
31
+ # The unnamed dag from a bare `dag_me` call. Named dags come from
32
+ # `dag_me :power` and get name-prefixed tables, constants, and
33
+ # associations so several graphs can coexist on one model.
34
+ def default?
35
+ name.nil?
36
+ end
37
+
38
+ # Task::DagEdge for the default dag, Relay::PowerDagEdge for dag_me :power.
39
+ def edge_class_name
40
+ default? ? 'DagEdge' : "#{name.to_s.camelize}DagEdge"
41
+ end
42
+
43
+ def paths_class_name
44
+ default? ? 'DagPath' : "#{name.to_s.camelize}DagPath"
45
+ end
46
+
47
+ def edge_class
48
+ model.const_get(edge_class_name)
49
+ end
50
+
51
+ def paths_class
52
+ model.const_get(paths_class_name)
53
+ end
54
+
55
+ # children / power_children, dag_child_edges / power_dag_child_edges, ...
56
+ def association_name(base)
57
+ default? ? base.to_sym : :"#{name}_#{base}"
58
+ end
59
+
60
+ def composite_pk?
61
+ @composite_pk
62
+ end
63
+
64
+ def node_table
65
+ model.table_name
66
+ end
67
+
68
+ # Node identity as an ordered column list. Single-key models yield one
69
+ # column and everything downstream degenerates to the classic layout.
70
+ def node_pk_columns
71
+ @node_pk_columns ||= if composite_pk?
72
+ model.primary_key.map(&:to_s).freeze
73
+ else
74
+ pk = model.primary_key
75
+ if pk.is_a?(Array)
76
+ raise ArgumentError,
77
+ "#{model.name}: declare `self.primary_key = [...]` before calling dag_me"
78
+ end
79
+ [pk.to_s].freeze
80
+ end
81
+ end
82
+
83
+ # Graph column names, one per pk column. Single-key models keep the
84
+ # classic parent_id / child_id / ancestor_id / descendant_id regardless
85
+ # of the pk's actual name; composite keys suffix each key column.
86
+ def edge_parent_columns
87
+ @edge_parent_columns ||= role_columns('parent')
88
+ end
89
+
90
+ def edge_child_columns
91
+ @edge_child_columns ||= role_columns('child')
92
+ end
93
+
94
+ def paths_ancestor_columns
95
+ @paths_ancestor_columns ||= role_columns('ancestor')
96
+ end
97
+
98
+ def paths_descendant_columns
99
+ @paths_descendant_columns ||= role_columns('descendant')
100
+ end
101
+
102
+ def node_pk_type(column)
103
+ model.columns_hash.fetch(column.to_s).sql_type
104
+ end
105
+
106
+ def scope_column_type(column)
107
+ model.columns_hash.fetch(column.to_s).sql_type
108
+ end
109
+
110
+ def adapter
111
+ @adapter ||= if closure?
112
+ Adapters::PostgresqlClosure.new(self)
113
+ else
114
+ Adapters::RecursiveCte.new(self)
115
+ end
116
+ end
117
+
118
+ private
119
+
120
+ def role_columns(role)
121
+ return ["#{role}_id"].freeze unless composite_pk?
122
+
123
+ node_pk_columns.map { |c| "#{role}_#{c}" }.freeze
124
+ end
125
+ end
126
+ end