cbe_database methods for the generic dplyr::intersect()/
dplyr::union()/dplyr::setdiff() verbs (the same generics
dbplyr/dtplyr extend for their own backends) – the table-level
building blocks behind graph_intersect/graph_subtract/
graph_union. Given two as_database results (or any
plain list(nodes = ..., edges = ...) passed through as_database()
first), these find their common, exclusive, or combined rows. Operating at
this level (rather than only on graph objects) means they also work
directly on nodes/edges tables that never came from a graph –
e.g. two tables read back from read_workbook.
Arguments
- x, y
cbe_databaseobjects (seeas_database): named lists withnodesandedgesdata frames/tibbles.- node_by
Column(s) to match nodes on.
NULL(default) uses the first column of eachnodestable. A single string uses that column name on both sides. A named vector/stringc(x_col = y_col)matchesx'sx_coltoy'sy_col(as indplyr::*_join()).- edge_by
Column(s) to match edges on (default
c("from", "to"), which everyas_databasemethod produces).- ...
Passed on; unused.
Value
A cbe_database list(nodes = <tibble>, edges = <tibble>).
For intersect()/setdiff(), edges are always constrained to
reference only the returned nodes, so the result is a well-formed graph
database even if a matched/unmatched edge's endpoint didn't independently
match on the node side.
One consequence of that well-formedness constraint: an edge can vanish
from both intersect() and setdiff() at once. If edge
b->c doesn't literally match one of y's edges, it's
excluded from the intersection – but if node b is itself one of
y's nodes, b (and anything touching it) also drops out of
the setdiff result too, taking b->c with it. The edge belongs to
neither side; it isn't double-dropped or double-counted, just genuinely
unrepresentable in a well-formed result for either operation.
Details
Dispatch is on the cbe_database class (stamped by as_database),
not on igraph – igraph already registers its own
union.igraph() on this same generic, and S3 method tables are global,
so a union.igraph() defined here would silently collide with it.
graph_intersect/graph_subtract/graph_union
stay dedicated functions for exactly this reason, built on top of these
cbe_database methods via as_database/database_to_igraph.
Node matching defaults to the first column of each database's nodes
table – the node identifier column, which different sources name
differently (name for an igraph-derived database, id
for a visNetwork/funviewR one). When the two databases use different
column names, pass node_by explicitly using the same
c(x_col = y_col) convention as dplyr::*_join(by = ...); a
plain string means the same column name on both sides.
intersect()/setdiff(): attribute values always come
from x, never y, and are never merged. dplyr::semi_join()/
dplyr::anti_join() filter x's rows by whether a key matches in
y – they don't pull any of y's own columns across. This
matters most for graphs whose nodes carry rich, node-specific metadata –
e.g. a pipeline graph built from FilePath/FileUses/
FileOutputs objects (see compute_graph), where
stage, mtime, and description differ node by node.
Intersecting two snapshots of the same pipeline graph (say, today's vs.
yesterday's) for a node present in both keeps today's
mtime/stage, silently discarding yesterday's – there is no
reconciliation between the two. To compare attribute values themselves (not
just which nodes exist), run compare_df on the two
as_database results directly instead.
union(): attribute values are coalesced, not taken from one
side only. For a node/edge present in both x and y, shared
columns keep x's value where it's non-NA and fall back to
y's otherwise – so combining two databases never silently drops
data the way intersect()/setdiff() can. This mirrors
join_pipelines's existing union-and-coalesce behavior for
tbl_graph objects, generalized to any as_database source.
Examples
db_x <- as_database(list(
nodes = data.frame(name = c("a", "b", "c")),
edges = data.frame(from = c("a", "b"), to = c("b", "c"))
))
db_y <- as_database(list(
nodes = data.frame(name = c("a", "b")),
edges = data.frame(from = "a", to = "b")
))
dplyr::intersect(db_x, db_y)
#> $nodes
#> # A tibble: 2 × 1
#> name
#> <chr>
#> 1 a
#> 2 b
#>
#> $edges
#> # A tibble: 1 × 2
#> from to
#> <chr> <chr>
#> 1 a b
#>
#> attr(,"class")
#> [1] "cbe_database" "list"
dplyr::setdiff(db_x, db_y)
#> $nodes
#> # A tibble: 1 × 1
#> name
#> <chr>
#> 1 c
#>
#> $edges
#> # A tibble: 0 × 2
#> # ℹ 2 variables: from <chr>, to <chr>
#>
#> attr(,"class")
#> [1] "cbe_database" "list"
dplyr::union(db_x, db_y)
#> $nodes
#> # A tibble: 3 × 1
#> name
#> <chr>
#> 1 a
#> 2 b
#> 3 c
#>
#> $edges
#> # A tibble: 2 × 2
#> from to
#> <chr> <chr>
#> 1 a b
#> 2 b c
#>
#> attr(,"class")
#> [1] "cbe_database" "list"
# intersect()/setdiff(): attribute values come from x only, never merged
# with y's. Node "a" is present in both snapshots below with different
# `stage`/`mtime` (mirroring FilePath's own slots, see compute_graph()) --
# the intersection keeps today's values, not yesterday's.
snapshot_today <- as_database(list(
nodes = data.frame(
name = "a", stage = "02_analysis",
mtime = as.POSIXct("2026-09-18")
),
edges = data.frame(from = character(0), to = character(0))
))
snapshot_yesterday <- as_database(list(
nodes = data.frame(
name = "a", stage = "01_eda",
mtime = as.POSIXct("2026-09-17")
),
edges = data.frame(from = character(0), to = character(0))
))
dplyr::intersect(snapshot_today, snapshot_yesterday)$nodes$stage # "02_analysis"
#> [1] "02_analysis"
# union(): coalesces instead -- yesterday's mtime fills in where a node
# only exists in one snapshot, and today's non-NA values win on overlap.
dplyr::union(snapshot_today, snapshot_yesterday)$nodes
#> # A tibble: 1 × 3
#> name stage mtime
#> <chr> <chr> <dttm>
#> 1 a 02_analysis 2026-09-18 00:00:00
