Class: DagMe::DDL

Inherits:
Object
  • Object
show all
Defined in:
lib/dag_me/ddl.rb

Overview

Generates and executes the SQL objects for a dag_me model.

Everything is derived from the model's Configuration. For a tasks table with prefix task_dag, installs:

task_dag_edges              -- source of truth (parent_*, child_* [, scope cols])
task_dag_paths              -- transitive closure incl. self-rows
                             (ancestor_*, descendant_*, min_depth, path_count [, scope cols])
task_dag_lock(text)         -- isolation guard + per-scope pg_advisory_xact_lock
task_dag_edge_insert_check  -- BEFORE INSERT: scope stamp + lock + cycle rejection
task_dag_edge_insert_apply  -- AFTER INSERT: incremental closure expansion
task_dag_edge_delete_apply  -- AFTER DELETE: path_count decrement + min_depth repair
task_dag_node_insert        -- AFTER INSERT on tasks: self-row
task_dag_node_update        -- BEFORE UPDATE on tasks: scope-change guard (scoped only)
task_dag_node_delete        -- BEFORE DELETE on tasks: drop edges through triggers
task_dag_rebuild_paths()    -- full closure rebuild from edges
task_dag_validate_paths()   -- closure vs recursive-CTE truth diff

Node identity is an ordered column list (Configuration#node_pk_columns): single-key models get the classic parent_id / child_id / ancestor_id / descendant_id columns, composite keys get one column per key column (parent_org_id, parent_serial, ...). All joins and comparisons are generated as per-column AND lists, so both shapes share one code path.

Integrity violations RAISE with the DagMe::SQLSTATE_* codes, so the Ruby layer translates them without depending on message wording.

With scope columns, edges may only connect nodes whose scope values match; the trigger stamps the node's scope onto edge and closure rows, so raw SQL writers cannot cross tenants either.

The paths table and its triggers are skipped for maintain: :recursive_cte; cycle rejection then uses a recursive CTE in the BEFORE INSERT trigger.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config) ⇒ DDL

Returns a new instance of DDL.



51
52
53
# File 'lib/dag_me/ddl.rb', line 51

def initialize(config)
  @config = config
end

Instance Attribute Details

#configObject (readonly)

Returns the value of attribute config.



49
50
51
# File 'lib/dag_me/ddl.rb', line 49

def config
  @config
end

Class Method Details

.install!(model) ⇒ Object

Installs / removes the SQL objects for every dag the model declares.



40
41
42
# File 'lib/dag_me/ddl.rb', line 40

def install!(model)
  model.dag_configs.each_value { |config| new(config).install! }
end

.uninstall!(model) ⇒ Object



44
45
46
# File 'lib/dag_me/ddl.rb', line 44

def uninstall!(model)
  model.dag_configs.each_value { |config| new(config).uninstall! }
end

Instance Method Details

#backfill_self_rows_sqlObject

Graph#backfill_self_rows! reuses this to repair nodes inserted with triggers disabled (e.g. Rails fixture loading).



81
82
83
84
85
86
87
# File 'lib/dag_me/ddl.rb', line 81

def backfill_self_rows_sql
  <<~SQL
    INSERT INTO #{config.paths_table} (#{list(anc_cols)}, #{list(desc_cols)}, min_depth, path_count#{scope_column_list})
    SELECT #{list(pk_cols)}, #{list(pk_cols)}, 0, 1#{scope_column_list} FROM #{config.node_table}
    ON CONFLICT DO NOTHING;
  SQL
end

#install!Object



55
56
57
# File 'lib/dag_me/ddl.rb', line 55

def install!
  execute_all(install_sql)
end

#install_sqlObject



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/dag_me/ddl.rb', line 63

def install_sql
  statements = [edges_table_sql]
  if config.closure?
    statements << paths_table_sql
    statements.concat(closure_function_sql)
    statements.concat(closure_trigger_sql)
    statements << backfill_self_rows_sql
    statements << rebuild_function_sql
    statements << validate_function_sql
  else
    statements.concat(cte_function_sql)
    statements.concat(cte_trigger_sql)
  end
  statements
end

#uninstall!Object



59
60
61
# File 'lib/dag_me/ddl.rb', line 59

def uninstall!
  execute_all(uninstall_sql)
end

#uninstall_sqlObject



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/dag_me/ddl.rb', line 89

def uninstall_sql
  [
    "DROP TRIGGER IF EXISTS #{config.trigger_name('node_insert')} ON #{config.node_table};",
    "DROP TRIGGER IF EXISTS #{config.trigger_name('node_update')} ON #{config.node_table};",
    "DROP TRIGGER IF EXISTS #{config.trigger_name('node_delete')} ON #{config.node_table};",
    "DROP TABLE IF EXISTS #{config.paths_table};",
    "DROP TABLE IF EXISTS #{config.edge_table};",
    "DROP FUNCTION IF EXISTS #{config.function_ref('lock')}(text);",
    "DROP FUNCTION IF EXISTS #{config.function_ref('edge_insert_check')}();",
    "DROP FUNCTION IF EXISTS #{config.function_ref('edge_insert_apply')}();",
    "DROP FUNCTION IF EXISTS #{config.function_ref('edge_delete_apply')}();",
    "DROP FUNCTION IF EXISTS #{config.function_ref('node_insert')}();",
    "DROP FUNCTION IF EXISTS #{config.function_ref('node_update')}();",
    "DROP FUNCTION IF EXISTS #{config.function_ref('node_delete')}();",
    "DROP FUNCTION IF EXISTS #{config.function_ref('rebuild_paths')}();",
    "DROP FUNCTION IF EXISTS #{config.function_ref('validate_paths')}();"
  ]
end