dag_me 0.1.0 → 0.3.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 +4 -4
- data/README.md +14 -0
- data/lib/dag_me/configuration.rb +30 -4
- data/lib/dag_me/ddl.rb +51 -43
- data/lib/dag_me/graph.rb +2 -2
- data/lib/dag_me/task_helpers.rb +10 -2
- data/lib/dag_me/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f9afbe234f1c36638c4a965eae2c76bfd93ffbc259b6f57fcf9c7ff57688176d
|
|
4
|
+
data.tar.gz: e50691a084e46d40e800f51635d7e1ab07e98fb865324a784a6ad88d51ccb93d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b6a321babe86ba1529efb4e30bfa742f2149e40d42cf9971c78b15810783eb425f7ba9d45b31d4def9a4a39e4f024f566f12d49dd52a8f30c2a3968c96134e16
|
|
7
|
+
data.tar.gz: 0b43392262483e5691e449c6eba57b7517e4649db776a5b6e29b50a1ca9868be83849f8297e69e47c564dd4988af127feead3c70978ffe7c3c92e3ea8949e25d
|
data/README.md
CHANGED
|
@@ -136,6 +136,20 @@ end
|
|
|
136
136
|
`:recursive_cte` skips the closure table entirely - good for small graphs, high mutation
|
|
137
137
|
rates, and as the truth oracle. Cycle rejection stays in-database either way.
|
|
138
138
|
|
|
139
|
+
## Schema-qualified tables
|
|
140
|
+
|
|
141
|
+
Node tables living in a named PostgreSQL schema work out of the box:
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
class Station < ApplicationRecord
|
|
145
|
+
self.table_name = 'orbital.stations'
|
|
146
|
+
dag_me
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Generated tables (`orbital.station_dag_edges`, `orbital.station_dag_paths`) and
|
|
151
|
+
functions land in the node table's schema; trigger names stay plain identifiers.
|
|
152
|
+
|
|
139
153
|
## Multi-tenancy
|
|
140
154
|
|
|
141
155
|
```ruby
|
data/lib/dag_me/configuration.rb
CHANGED
|
@@ -4,7 +4,7 @@ module DagMe
|
|
|
4
4
|
class Configuration
|
|
5
5
|
MAINTAIN_MODES = %i[postgresql_closure recursive_cte].freeze
|
|
6
6
|
|
|
7
|
-
attr_reader :model, :name, :maintain, :prefix, :edge_table, :paths_table, :scope_columns
|
|
7
|
+
attr_reader :model, :name, :maintain, :prefix, :schema, :edge_table, :paths_table, :scope_columns
|
|
8
8
|
|
|
9
9
|
def initialize(model:, name: nil, maintain: :postgresql_closure, scope: nil, edge_table: nil, paths_table: nil)
|
|
10
10
|
unless MAINTAIN_MODES.include?(maintain)
|
|
@@ -15,9 +15,13 @@ module DagMe
|
|
|
15
15
|
@name = name&.to_sym
|
|
16
16
|
@maintain = maintain
|
|
17
17
|
@scope_columns = Array(scope).map(&:to_sym).freeze
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
# Schema-qualified node tables ("ai.skills") keep the prefix a plain
|
|
19
|
+
# identifier - trigger and temp-table names cannot carry a schema - and
|
|
20
|
+
# place generated tables and functions in the node table's schema.
|
|
21
|
+
@schema, base_table = split_schema(model.table_name)
|
|
22
|
+
@prefix = [base_table.singularize, @name, 'dag'].compact.join('_')
|
|
23
|
+
@edge_table = edge_table || qualify("#{prefix}_edges")
|
|
24
|
+
@paths_table = paths_table || qualify("#{prefix}_paths")
|
|
21
25
|
# Ivar peek keeps class load DB-free; composite keys must be declared
|
|
22
26
|
# before the macro. Array === because AR seeds a BasicObject sentinel.
|
|
23
27
|
@composite_pk = model.instance_variable_defined?(:@primary_key) &&
|
|
@@ -28,6 +32,22 @@ module DagMe
|
|
|
28
32
|
maintain == :postgresql_closure
|
|
29
33
|
end
|
|
30
34
|
|
|
35
|
+
# "skill_dag_edges" -> "ai.skill_dag_edges" when the node table lives in a
|
|
36
|
+
# named schema; identity otherwise.
|
|
37
|
+
def qualify(identifier)
|
|
38
|
+
schema ? "#{schema}.#{identifier}" : identifier
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Schema-qualified reference for a generated function ("ai.skill_dag_lock").
|
|
42
|
+
def function_ref(suffix)
|
|
43
|
+
qualify("#{prefix}_#{suffix}")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Trigger names are plain identifiers; placement comes from ON <table>.
|
|
47
|
+
def trigger_name(suffix)
|
|
48
|
+
"#{prefix}_#{suffix}"
|
|
49
|
+
end
|
|
50
|
+
|
|
31
51
|
# The unnamed dag from a bare `dag_me` call. Named dags come from
|
|
32
52
|
# `dag_me :power` and get name-prefixed tables, constants, and
|
|
33
53
|
# associations so several graphs can coexist on one model.
|
|
@@ -117,6 +137,12 @@ module DagMe
|
|
|
117
137
|
|
|
118
138
|
private
|
|
119
139
|
|
|
140
|
+
def split_schema(table_name)
|
|
141
|
+
return table_name.split('.', 2) if table_name.include?('.')
|
|
142
|
+
|
|
143
|
+
[nil, table_name]
|
|
144
|
+
end
|
|
145
|
+
|
|
120
146
|
def role_columns(role)
|
|
121
147
|
return ["#{role}_id"].freeze unless composite_pk?
|
|
122
148
|
|
data/lib/dag_me/ddl.rb
CHANGED
|
@@ -77,22 +77,21 @@ module DagMe
|
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
def uninstall_sql
|
|
80
|
-
p = config.prefix
|
|
81
80
|
[
|
|
82
|
-
"DROP TRIGGER IF EXISTS #{
|
|
83
|
-
"DROP TRIGGER IF EXISTS #{
|
|
84
|
-
"DROP TRIGGER IF EXISTS #{
|
|
81
|
+
"DROP TRIGGER IF EXISTS #{config.trigger_name('node_insert')} ON #{config.node_table};",
|
|
82
|
+
"DROP TRIGGER IF EXISTS #{config.trigger_name('node_update')} ON #{config.node_table};",
|
|
83
|
+
"DROP TRIGGER IF EXISTS #{config.trigger_name('node_delete')} ON #{config.node_table};",
|
|
85
84
|
"DROP TABLE IF EXISTS #{config.paths_table};",
|
|
86
85
|
"DROP TABLE IF EXISTS #{config.edge_table};",
|
|
87
|
-
"DROP FUNCTION IF EXISTS #{
|
|
88
|
-
"DROP FUNCTION IF EXISTS #{
|
|
89
|
-
"DROP FUNCTION IF EXISTS #{
|
|
90
|
-
"DROP FUNCTION IF EXISTS #{
|
|
91
|
-
"DROP FUNCTION IF EXISTS #{
|
|
92
|
-
"DROP FUNCTION IF EXISTS #{
|
|
93
|
-
"DROP FUNCTION IF EXISTS #{
|
|
94
|
-
"DROP FUNCTION IF EXISTS #{
|
|
95
|
-
"DROP FUNCTION IF EXISTS #{
|
|
86
|
+
"DROP FUNCTION IF EXISTS #{config.function_ref('lock')}(text);",
|
|
87
|
+
"DROP FUNCTION IF EXISTS #{config.function_ref('edge_insert_check')}();",
|
|
88
|
+
"DROP FUNCTION IF EXISTS #{config.function_ref('edge_insert_apply')}();",
|
|
89
|
+
"DROP FUNCTION IF EXISTS #{config.function_ref('edge_delete_apply')}();",
|
|
90
|
+
"DROP FUNCTION IF EXISTS #{config.function_ref('node_insert')}();",
|
|
91
|
+
"DROP FUNCTION IF EXISTS #{config.function_ref('node_update')}();",
|
|
92
|
+
"DROP FUNCTION IF EXISTS #{config.function_ref('node_delete')}();",
|
|
93
|
+
"DROP FUNCTION IF EXISTS #{config.function_ref('rebuild_paths')}();",
|
|
94
|
+
"DROP FUNCTION IF EXISTS #{config.function_ref('validate_paths')}();"
|
|
96
95
|
]
|
|
97
96
|
end
|
|
98
97
|
|
|
@@ -176,10 +175,20 @@ module DagMe
|
|
|
176
175
|
config.scope_columns.map { |c| "#{left}.#{c} IS DISTINCT FROM #{right}.#{c}" }.join(' OR ')
|
|
177
176
|
end
|
|
178
177
|
|
|
178
|
+
# Honors PostgreSQLAdapter.create_unlogged_tables (Rails test-env speed
|
|
179
|
+
# setting): a logged table cannot FK-reference an unlogged node table.
|
|
180
|
+
def create_table_clause
|
|
181
|
+
if ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables
|
|
182
|
+
'CREATE UNLOGGED TABLE'
|
|
183
|
+
else
|
|
184
|
+
'CREATE TABLE'
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
179
188
|
def edges_table_sql
|
|
180
189
|
defs = (col_defs(parent_cols) + col_defs(child_cols)).map { |d| " #{d}," }.join("\n")
|
|
181
190
|
<<~SQL
|
|
182
|
-
|
|
191
|
+
#{create_table_clause} #{config.edge_table} (
|
|
183
192
|
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
184
193
|
#{defs}
|
|
185
194
|
created_at timestamptz NOT NULL DEFAULT now()#{scope_column_defs},
|
|
@@ -195,7 +204,7 @@ module DagMe
|
|
|
195
204
|
def paths_table_sql
|
|
196
205
|
defs = (col_defs(anc_cols) + col_defs(desc_cols)).map { |d| " #{d}," }.join("\n")
|
|
197
206
|
<<~SQL
|
|
198
|
-
|
|
207
|
+
#{create_table_clause} #{config.paths_table} (
|
|
199
208
|
#{defs}
|
|
200
209
|
min_depth integer NOT NULL,
|
|
201
210
|
path_count numeric NOT NULL#{scope_column_defs},
|
|
@@ -211,7 +220,7 @@ module DagMe
|
|
|
211
220
|
# snapshot after the lock wait, so anything above READ COMMITTED is refused.
|
|
212
221
|
def lock_function_sql
|
|
213
222
|
<<~SQL
|
|
214
|
-
CREATE OR REPLACE FUNCTION #{config.
|
|
223
|
+
CREATE OR REPLACE FUNCTION #{config.function_ref('lock')}(scope_key text) RETURNS void
|
|
215
224
|
LANGUAGE plpgsql AS $$
|
|
216
225
|
BEGIN
|
|
217
226
|
IF current_setting('transaction_isolation') NOT IN ('read committed', 'read uncommitted') THEN
|
|
@@ -229,8 +238,7 @@ module DagMe
|
|
|
229
238
|
# reject cross-scope edges, stamp NEW, and lock the scope. Unscoped graphs
|
|
230
239
|
# just lock the single '' key.
|
|
231
240
|
def edge_check_preamble
|
|
232
|
-
|
|
233
|
-
return " PERFORM #{p}_lock('');" unless scoped?
|
|
241
|
+
return " PERFORM #{config.function_ref('lock')}('');" unless scoped?
|
|
234
242
|
|
|
235
243
|
stamps = config.scope_columns.map { |c| " NEW.#{c} := parent_row.#{c};" }.join("\n")
|
|
236
244
|
# FOR SHARE pins both node rows so a concurrent scope UPDATE cannot race
|
|
@@ -243,7 +251,7 @@ module DagMe
|
|
|
243
251
|
USING ERRCODE = '#{SQLSTATE_CROSS_SCOPE}';
|
|
244
252
|
END IF;
|
|
245
253
|
#{stamps}
|
|
246
|
-
PERFORM #{
|
|
254
|
+
PERFORM #{config.function_ref('lock')}(#{scope_key_expr('parent_row')});
|
|
247
255
|
SQL
|
|
248
256
|
end
|
|
249
257
|
|
|
@@ -279,7 +287,7 @@ module DagMe
|
|
|
279
287
|
|
|
280
288
|
def closure_edge_insert_check_sql
|
|
281
289
|
<<~SQL
|
|
282
|
-
CREATE OR REPLACE FUNCTION #{config.
|
|
290
|
+
CREATE OR REPLACE FUNCTION #{config.function_ref('edge_insert_check')}() RETURNS trigger
|
|
283
291
|
LANGUAGE plpgsql AS $$
|
|
284
292
|
#{edge_check_declarations}
|
|
285
293
|
BEGIN
|
|
@@ -300,7 +308,7 @@ module DagMe
|
|
|
300
308
|
def edge_insert_apply_sql
|
|
301
309
|
paths = config.paths_table
|
|
302
310
|
<<~SQL
|
|
303
|
-
CREATE OR REPLACE FUNCTION #{config.
|
|
311
|
+
CREATE OR REPLACE FUNCTION #{config.function_ref('edge_insert_apply')}() RETURNS trigger
|
|
304
312
|
LANGUAGE plpgsql AS $$
|
|
305
313
|
BEGIN
|
|
306
314
|
-- ancestors-incl-self of parent x descendants-incl-self of child
|
|
@@ -328,10 +336,10 @@ module DagMe
|
|
|
328
336
|
y_cols = pk_cols.map { |c| "y_#{c}" }
|
|
329
337
|
rect_defs = (col_defs(x_cols) + col_defs(y_cols)).map { |d| " #{d}," }.join("\n")
|
|
330
338
|
<<~SQL
|
|
331
|
-
CREATE OR REPLACE FUNCTION #{
|
|
339
|
+
CREATE OR REPLACE FUNCTION #{config.function_ref('edge_delete_apply')}() RETURNS trigger
|
|
332
340
|
LANGUAGE plpgsql AS $$
|
|
333
341
|
BEGIN
|
|
334
|
-
PERFORM #{
|
|
342
|
+
PERFORM #{config.function_ref('lock')}(#{scope_key_expr('OLD')});
|
|
335
343
|
|
|
336
344
|
-- The affected rectangle: every pair (x, y) with x reaching OLD's parent
|
|
337
345
|
-- and OLD's child reaching y lost `removed` paths through this edge.
|
|
@@ -401,7 +409,7 @@ module DagMe
|
|
|
401
409
|
|
|
402
410
|
def node_insert_function_sql
|
|
403
411
|
<<~SQL
|
|
404
|
-
CREATE OR REPLACE FUNCTION #{config.
|
|
412
|
+
CREATE OR REPLACE FUNCTION #{config.function_ref('node_insert')}() RETURNS trigger
|
|
405
413
|
LANGUAGE plpgsql AS $$
|
|
406
414
|
BEGIN
|
|
407
415
|
INSERT INTO #{config.paths_table} (#{list(anc_cols)}, #{list(desc_cols)}, min_depth, path_count#{scope_column_list})
|
|
@@ -414,10 +422,10 @@ module DagMe
|
|
|
414
422
|
|
|
415
423
|
def node_delete_function_sql
|
|
416
424
|
<<~SQL
|
|
417
|
-
CREATE OR REPLACE FUNCTION #{config.
|
|
425
|
+
CREATE OR REPLACE FUNCTION #{config.function_ref('node_delete')}() RETURNS trigger
|
|
418
426
|
LANGUAGE plpgsql AS $$
|
|
419
427
|
BEGIN
|
|
420
|
-
PERFORM #{config.
|
|
428
|
+
PERFORM #{config.function_ref('lock')}(#{scope_key_expr('OLD')});
|
|
421
429
|
-- Remove edges through their triggers so the closure shrinks
|
|
422
430
|
-- incrementally instead of relying on FK-cascade ordering.
|
|
423
431
|
DELETE FROM #{config.edge_table}
|
|
@@ -441,7 +449,7 @@ module DagMe
|
|
|
441
449
|
'-- edges only: nothing materialized to restamp'
|
|
442
450
|
end
|
|
443
451
|
<<~SQL
|
|
444
|
-
CREATE OR REPLACE FUNCTION #{config.
|
|
452
|
+
CREATE OR REPLACE FUNCTION #{config.function_ref('node_update')}() RETURNS trigger
|
|
445
453
|
LANGUAGE plpgsql AS $$
|
|
446
454
|
BEGIN
|
|
447
455
|
IF #{scope_distinct_expr('NEW', 'OLD')} THEN
|
|
@@ -461,22 +469,23 @@ module DagMe
|
|
|
461
469
|
end
|
|
462
470
|
|
|
463
471
|
def closure_trigger_sql
|
|
464
|
-
p = config.prefix
|
|
465
472
|
triggers = [
|
|
466
|
-
trigger_sql(
|
|
467
|
-
trigger_sql(
|
|
468
|
-
trigger_sql(
|
|
469
|
-
trigger_sql(
|
|
470
|
-
trigger_sql(
|
|
473
|
+
trigger_sql('edge_insert_check', 'BEFORE INSERT', config.edge_table),
|
|
474
|
+
trigger_sql('edge_insert_apply', 'AFTER INSERT', config.edge_table),
|
|
475
|
+
trigger_sql('edge_delete_apply', 'AFTER DELETE', config.edge_table),
|
|
476
|
+
trigger_sql('node_insert', 'AFTER INSERT', config.node_table),
|
|
477
|
+
trigger_sql('node_delete', 'BEFORE DELETE', config.node_table)
|
|
471
478
|
]
|
|
472
|
-
triggers << trigger_sql(
|
|
479
|
+
triggers << trigger_sql('node_update', 'BEFORE UPDATE', config.node_table) if scoped?
|
|
473
480
|
triggers
|
|
474
481
|
end
|
|
475
482
|
|
|
476
|
-
|
|
483
|
+
# Trigger names are plain identifiers; the executed function carries the
|
|
484
|
+
# schema qualification.
|
|
485
|
+
def trigger_sql(suffix, timing, table)
|
|
477
486
|
<<~SQL
|
|
478
|
-
CREATE TRIGGER #{
|
|
479
|
-
FOR EACH ROW EXECUTE FUNCTION #{
|
|
487
|
+
CREATE TRIGGER #{config.trigger_name(suffix)} #{timing} ON #{table}
|
|
488
|
+
FOR EACH ROW EXECUTE FUNCTION #{config.function_ref(suffix)}();
|
|
480
489
|
SQL
|
|
481
490
|
end
|
|
482
491
|
|
|
@@ -504,7 +513,7 @@ module DagMe
|
|
|
504
513
|
scope_join = scoped? ? "JOIN #{config.node_table} n ON #{eq(pk_cols, anc_cols, left: 'n', right: 'walk')}" : ''
|
|
505
514
|
scope_group = config.scope_columns.map { |c| ", n.#{c}" }.join
|
|
506
515
|
<<~SQL
|
|
507
|
-
CREATE OR REPLACE FUNCTION #{config.
|
|
516
|
+
CREATE OR REPLACE FUNCTION #{config.function_ref('rebuild_paths')}() RETURNS void
|
|
508
517
|
LANGUAGE plpgsql AS $$
|
|
509
518
|
BEGIN
|
|
510
519
|
LOCK TABLE #{config.node_table}, #{config.edge_table} IN SHARE ROW EXCLUSIVE MODE;
|
|
@@ -531,7 +540,7 @@ module DagMe
|
|
|
531
540
|
.map { |d| " #{d}," }.join("\n")
|
|
532
541
|
coalesced = (anc_cols + desc_cols).map { |c| "COALESCE(t.#{c}, s.#{c})" }.join(",\n ")
|
|
533
542
|
<<~SQL
|
|
534
|
-
CREATE OR REPLACE FUNCTION #{config.
|
|
543
|
+
CREATE OR REPLACE FUNCTION #{config.function_ref('validate_paths')}()
|
|
535
544
|
RETURNS TABLE(
|
|
536
545
|
#{returns}
|
|
537
546
|
stored_min_depth integer,
|
|
@@ -571,7 +580,7 @@ module DagMe
|
|
|
571
580
|
|
|
572
581
|
def cte_edge_insert_check_sql
|
|
573
582
|
<<~SQL
|
|
574
|
-
CREATE OR REPLACE FUNCTION #{config.
|
|
583
|
+
CREATE OR REPLACE FUNCTION #{config.function_ref('edge_insert_check')}() RETURNS trigger
|
|
575
584
|
LANGUAGE plpgsql AS $$
|
|
576
585
|
#{edge_check_declarations}
|
|
577
586
|
BEGIN
|
|
@@ -593,9 +602,8 @@ module DagMe
|
|
|
593
602
|
end
|
|
594
603
|
|
|
595
604
|
def cte_trigger_sql
|
|
596
|
-
|
|
597
|
-
triggers
|
|
598
|
-
triggers << trigger_sql("#{p}_node_update", 'BEFORE UPDATE', config.node_table) if scoped?
|
|
605
|
+
triggers = [trigger_sql('edge_insert_check', 'BEFORE INSERT', config.edge_table)]
|
|
606
|
+
triggers << trigger_sql('node_update', 'BEFORE UPDATE', config.node_table) if scoped?
|
|
599
607
|
triggers
|
|
600
608
|
end
|
|
601
609
|
end
|
data/lib/dag_me/graph.rb
CHANGED
|
@@ -51,7 +51,7 @@ module DagMe
|
|
|
51
51
|
return self unless config.closure?
|
|
52
52
|
|
|
53
53
|
model.connection_pool.with_connection do |conn|
|
|
54
|
-
conn.execute("SELECT #{conn.quote_table_name(
|
|
54
|
+
conn.execute("SELECT #{conn.quote_table_name(config.function_ref('rebuild_paths'))}();")
|
|
55
55
|
end
|
|
56
56
|
self
|
|
57
57
|
end
|
|
@@ -62,7 +62,7 @@ module DagMe
|
|
|
62
62
|
return [] unless config.closure?
|
|
63
63
|
|
|
64
64
|
model.connection_pool.with_connection do |conn|
|
|
65
|
-
conn.select_all("SELECT * FROM #{conn.quote_table_name(
|
|
65
|
+
conn.select_all("SELECT * FROM #{conn.quote_table_name(config.function_ref('validate_paths'))}();").to_a
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
|
data/lib/dag_me/task_helpers.rb
CHANGED
|
@@ -76,7 +76,7 @@ module DagMe
|
|
|
76
76
|
JOIN pg_class c ON c.oid = t.tgrelid
|
|
77
77
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
78
78
|
WHERE NOT t.tgisinternal
|
|
79
|
-
AND
|
|
79
|
+
AND #{namespace_filter(conn, config)}
|
|
80
80
|
AND t.tgname LIKE #{conn.quote("#{config.prefix}\\_%")}
|
|
81
81
|
SQL
|
|
82
82
|
(expected - installed).map { |name| bad("trigger #{name} missing") }
|
|
@@ -96,13 +96,21 @@ module DagMe
|
|
|
96
96
|
installed = conn.select_values(<<~SQL)
|
|
97
97
|
SELECT p.proname FROM pg_proc p
|
|
98
98
|
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
99
|
-
WHERE
|
|
99
|
+
WHERE #{namespace_filter(conn, config)}
|
|
100
100
|
AND p.proname LIKE #{conn.quote("#{config.prefix}\\_%")}
|
|
101
101
|
SQL
|
|
102
102
|
(expected - installed).map { |name| bad("function #{name} missing") }
|
|
103
103
|
.presence || [ok("functions (#{expected.length})")]
|
|
104
104
|
end
|
|
105
105
|
|
|
106
|
+
# Objects for schema-qualified node tables live in that schema regardless
|
|
107
|
+
# of search_path; unqualified ones resolve through it.
|
|
108
|
+
def namespace_filter(conn, config)
|
|
109
|
+
return "n.nspname = #{conn.quote(config.schema)}" if config.schema
|
|
110
|
+
|
|
111
|
+
'n.nspname = ANY (current_schemas(false))'
|
|
112
|
+
end
|
|
113
|
+
|
|
106
114
|
def closure_check(model, config)
|
|
107
115
|
return ok('closure: not materialized (recursive_cte)') unless config.closure?
|
|
108
116
|
return nil unless model.connection.table_exists?(config.paths_table)
|
data/lib/dag_me/version.rb
CHANGED