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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +19 -0
- data/README.md +346 -0
- data/lib/dag_me/adapters/base.rb +90 -0
- data/lib/dag_me/adapters/postgresql_closure.rb +59 -0
- data/lib/dag_me/adapters/recursive_cte.rb +68 -0
- data/lib/dag_me/configuration.rb +126 -0
- data/lib/dag_me/ddl.rb +602 -0
- data/lib/dag_me/errors.rb +60 -0
- data/lib/dag_me/graph.rb +84 -0
- data/lib/dag_me/macro.rb +31 -0
- data/lib/dag_me/model.rb +171 -0
- data/lib/dag_me/railtie.rb +15 -0
- data/lib/dag_me/railties/tasks.rake +27 -0
- data/lib/dag_me/task_helpers.rb +120 -0
- data/lib/dag_me/test_helper.rb +72 -0
- data/lib/dag_me/version.rb +5 -0
- data/lib/dag_me.rb +32 -0
- data/lib/generators/dag_me/migration_generator.rb +30 -0
- data/lib/generators/dag_me/templates/install_dag.rb.erb +11 -0
- metadata +93 -0
|
@@ -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
|