21  Has a partner

Which of your rows have a partner in the other table? Sometimes that is the whole question: you do not want the other table’s columns, only to keep the rows that appear in it. That is a question about a row, so it is written as one, and the verb that takes a question is keep.

The other table is stocked, a list of one product:

product
Widget
run('
  sales
    then keep where matching(stocked, by [product])
')
date region product quantity revenue cost
2025-11-03 West Widget 4 100 40
2025-12-22 North Widget 6 150 60
2026-01-09 East Widget 8 200 80
2026-04-11 West Widget 2 50 20
2026-08-24 North Widget 3 75 30
2026-10-02 West Widget 5 125 50
sales |> keep(matching(stocked, by = product))
date region product quantity revenue cost
2025-11-03 West Widget 4 100 40
2025-12-22 North Widget 6 150 60
2026-01-09 East Widget 8 200 80
2026-04-11 West Widget 2 50 20
2026-08-24 North Widget 3 75 30
2026-10-02 West Widget 5 125 50
sales >> keep(matching(stocked, by = col.product))
date region product quantity revenue cost
2025-11-03 West Widget 4 100 40
2025-12-22 North Widget 6 150 60
2026-01-09 East Widget 8 200 80
2026-04-11 West Widget 2 50 20
2026-08-24 North Widget 3 75 30
2026-10-02 West Widget 5 125 50

sales then keep where matching(stocked, by [product])

Nothing was added. The columns are the ones sales started with, and only the rows changed, which is why this is not spelled join. Negate it and you get exactly the rows the first one dropped.

run('
  sales
    then keep where (not matching(stocked, by [product]))
')
date region product quantity revenue cost
2025-11-17 East Gadget 2 120 80
2025-12-05 West Doohickey 3 120 75
2026-01-26 West Gadget 5 300 200
2026-02-14 North Doohickey 2 80 50
2026-03-03 East Doohickey 5 200 125
2026-05-06 North Gadget 4 240 160
2026-06-19 East Sprocket 10 150 120
2026-07-08 West Sprocket 6 90 72
2026-09-15 East Gadget 1 60 40
sales |> keep(!matching(stocked, by = product))
date region product quantity revenue cost
2025-11-17 East Gadget 2 120 80
2025-12-05 West Doohickey 3 120 75
2026-01-26 West Gadget 5 300 200
2026-02-14 North Doohickey 2 80 50
2026-03-03 East Doohickey 5 200 125
2026-05-06 North Gadget 4 240 160
2026-06-19 East Sprocket 10 150 120
2026-07-08 West Sprocket 6 90 72
2026-09-15 East Gadget 1 60 40
sales >> keep(~matching(stocked, by = col.product))
date region product quantity revenue cost
2025-11-17 East Gadget 2 120 80
2025-12-05 West Doohickey 3 120 75
2026-01-26 West Gadget 5 300 200
2026-02-14 North Doohickey 2 80 50
2026-03-03 East Doohickey 5 200 125
2026-05-06 North Gadget 4 240 160
2026-06-19 East Sprocket 10 150 120
2026-07-08 West Sprocket 6 90 72
2026-09-15 East Gadget 1 60 40

by works here exactly as it does on join, and leaving it out means the same thing: god matches on the column names both tables share. The note naming its choice arrives beside the answer, as it does for join.

That sameness is the whole point, so it extends to keys the two tables name differently. Write is between them, this table’s name first, exactly as another table does.

listed names the same products under a column of its own:

item
Widget
Gadget
run('
  sales
    then keep where matching(listed, by [product] is [item])
    then take 3
')
date region product quantity revenue cost
2025-11-03 West Widget 4 100 40
2025-11-17 East Gadget 2 120 80
2025-12-22 North Widget 6 150 60
sales |> keep(matching(listed, by = product == item)) |> take(3)
date region product quantity revenue cost
2025-11-03 West Widget 4 100 40
2025-11-17 East Gadget 2 120 80
2025-12-22 North Widget 6 150 60
sales >> keep(matching(listed, by = col.product == col.item)) >> take(3)
date region product quantity revenue cost
2025-11-03 West Widget 4 100 40
2025-11-17 East Gadget 2 120 80
2025-12-22 North Widget 6 150 60

sales then keep where matching(listed, by [product] is [item]) then take 3

With no by at all, god says which column it used:

run('
  sales
    then keep where matching(stocked)
')
date region product quantity revenue cost
2025-11-03 West Widget 4 100 40
2025-12-22 North Widget 6 150 60
2026-01-09 East Widget 8 200 80
2026-04-11 West Widget 2 50 20
2026-08-24 North Widget 3 75 30
2026-10-02 West Widget 5 125 50
sales |> keep(matching(stocked))
date region product quantity revenue cost
2025-11-03 West Widget 4 100 40
2025-12-22 North Widget 6 150 60
2026-01-09 East Widget 8 200 80
2026-04-11 West Widget 2 50 20
2026-08-24 North Widget 3 75 30
2026-10-02 West Widget 5 125 50
sales >> keep(matching(stocked))
date region product quantity revenue cost
2025-11-03 West Widget 4 100 40
2025-12-22 North Widget 6 150 60
2026-01-09 East Widget 8 200 80
2026-04-11 West Widget 2 50 20
2026-08-24 North Widget 3 75 30
2026-10-02 West Widget 5 125 50

sales then keep where matching(stocked)

There is one thing matching guarantees that join cannot. If the other table has three rows for a product, a join gives you three rows back for each of yours. A question does not: a row either has a partner or it does not, and how many it has never reaches the answer.

restocks <- data.frame(
  product   = c("Widget", "Widget", "Widget"),
  restocked = c(10, 20, 30),
  stringsAsFactors = FALSE
)

nrow(collect(sales |> keep(matching(restocks, by = product))))
[1] 6
nrow(collect(sales |> join(restocks, by = product, unmatched = "none")))
[1] 18
restocks = pd.DataFrame({
    "product":   ["Widget", "Widget", "Widget"],
    "restocked": [10, 20, 30],
})

print(len(collect(sales >> keep(matching(restocks, by = col.product)))))
6
print(len(collect(
    sales >> join(restocks, by = col.product, unmatched = "none"))))
18

matching is the whole question keep asks rather than one part of it, so it does not combine with and or or. Ask it in its own step.

collect(sales |> keep(matching(products, by = product) & revenue > 100))
Error:
! 
illegal: `matching` chooses whole rows, so it is the whole question `keep` asks rather than one part of it. Ask it in its own step: `then keep where matching(products, by [id]) then keep where ...`
  |
2 |   then keep where (matching(products, by [product]) and ([revenue] > 100))
  |                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
try:
    collect(sales
      >> keep(matching(products, by = col.product) & (col.revenue > 100)))
except GodError as refusal:
    print(refusal)

illegal: `matching` chooses whole rows, so it is the whole question `keep` asks rather than one part of it. Ask it in its own step: `then keep where matching(products, by [id]) then keep where ...`
  |
2 |   then keep where (matching(products, by [product]) and ([revenue] > 100))
  |                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
run('
  sales
    then keep where matching(products, by [product])
    then keep where ([revenue] > 100)
')
date region product quantity revenue cost
2025-11-17 East Gadget 2 120 80
2025-12-05 West Doohickey 3 120 75
2025-12-22 North Widget 6 150 60
2026-01-09 East Widget 8 200 80
2026-01-26 West Gadget 5 300 200
2026-03-03 East Doohickey 5 200 125
2026-05-06 North Gadget 4 240 160
2026-10-02 West Widget 5 125 50
sales |> keep(matching(products, by = product)) |> keep(revenue > 100)
date region product quantity revenue cost
2025-11-17 East Gadget 2 120 80
2025-12-05 West Doohickey 3 120 75
2025-12-22 North Widget 6 150 60
2026-01-09 East Widget 8 200 80
2026-01-26 West Gadget 5 300 200
2026-03-03 East Doohickey 5 200 125
2026-05-06 North Gadget 4 240 160
2026-10-02 West Widget 5 125 50
(sales
  >> keep(matching(products, by = col.product))
  >> keep(col.revenue > 100))
date region product quantity revenue cost
2025-11-17 East Gadget 2 120 80
2025-12-05 West Doohickey 3 120 75
2025-12-22 North Widget 6 150 60
2026-01-09 East Widget 8 200 80
2026-01-26 West Gadget 5 300 200
2026-03-03 East Doohickey 5 200 125
2026-05-06 North Gadget 4 240 160
2026-10-02 West Widget 5 125 50

The two keep steps read as one sentence: first the rows with a partner, then the rows whose revenue is over 100. However long the pipeline grows, matching can only remove rows, which is the property the word exists to guarantee.

That property is what separates it from a join, because the two sentences look alike and mean different things.

sales |> keep(matching(products, by = product)) |> show_steps()
sales date :text region :text product :text quantity :number revenue :number cost :number keep where matching(products, by [product]) date region =product quantity revenue cost products =product maker :text matched on product · no columns cross, only rows go only the rows that match products — never more than it started with
show_steps(sales >> keep(matching(products, by = col.product)))
sales date :text region :text product :text quantity :number revenue :number cost :number keep where matching(products, by [product]) date region =product quantity revenue cost products =product maker :text matched on product · no columns cross, only rows go only the rows that match products — never more than it started with

products gets a line of its own, the same as it would under a join, and its maker column stays where it is. Nothing crossed over, and the last line says the rows can only be fewer. A join in the same place says they may multiply. That difference is the whole reason both words exist.