Draft

The Loop That Kept Going

Conversation 3.3 — Push Execution and Query Compilation

The loop that kept going

Ada · 01

In 3.2, pausing after a result let the consumer decide whether to continue. Now the consumer wants the complete appearance relation. After each returned tuple, it inserts the projected answer and immediately asks for another. What does its outer loop look like?

Alice
loop:
    match J2.next():
        None: break
        Some(w):
            answers.insert((w.Actor, w.Schedule, w.Address))

The action after a return is always the same. The consumer is not making a new decision between these results.

Ada · 02

We could keep batching those returns. Here is another possibility: give the producer that action once. What could it do when a result is ready?

Alice

Perform the action and continue its loop. It would no longer have to return one tuple and wait for an immediate request to resume.

We would need another way to stop early if the consumer later wanted that.

Ada · 03

Use the same cinema arrays and indexes. Keep the same indexed plan and the same appearance(c,s,a) query. What should happen when the first movie–showing match is ready?

Alice

Use its Theater to look up Location. For each address, send the projected actor, showing time, and address to the result collector.

Then continue to the next showing for the current movie.

Ada · 04

Write that control flow. lookup_P and lookup_L still enumerate the exact matching tuples defined in 3.2. The collector answers is a set.

Alice
for u in scan(M):
    for v in lookup_P(u.Title):
        for w in lookup_L(v.Theater):
            answers.insert((u.Actor, v.Schedule, w.Address))

Those are the same nested lookups. But a completed match now continues directly into the action that uses it.

Ada · 05

Which answers does the first Movies tuple cause?

Alice

First (actor1,18:00,addr1), then (actor1,21:00,addr1). Only then does the outer loop move to actor2.

The trace agrees with the pull plan's first two results.

Ada · 06

Did the Title-match offset disappear?

Alice

It is still needed by the inner loop. The loop and its local variables now hold the progress that the resumable join previously stored between calls.

We changed where control waits. We did not make iteration stateless.

Ada · 07

Could I implement that action as an ordinary function call for every tuple?

Alice

Yes. That would be push execution, with a runtime call to the consumer. It would not yet be a query compiler.

Ada · 08

Suppose our runtime consumer is a generic plan node. For each tuple it inspects whether it represents a selection, projection, or join, then performs that operation. Does its node kind change between actor1 and actor2?

Alice

No. Its plan and attribute choices stay fixed while the tuples change. That repeated interpretation is a possible cost of this implementation.

Ada · 09

Can a program built for this particular plan make those fixed choices before reading the tuples?

Alice

It could put the chosen tests and field accesses directly in the loop body. Then the loop would operate on the tuple values without asking what kind of plan node to interpret each time.

That motivates specialization. It does not tell us whether its cost will pay off for a small query.

Ada · 10

Should we hand-write a new loop body whenever a user changes the query?

Alice

No. The plan already describes the required operations. We need to turn those descriptions into code systematically.

That is the compiler's job. How does it compose the pieces?

Ada · 11

First compile a smaller plan over Movies:

$$\pi_{\mathrm{Title}} \bigl(\sigma_{\mathrm{Director}=\mathrm{Bergman}}(M)\bigr). $$

Let $k(u)$ mean “generate the code that uses the current tuple expression $u$.” What code should the scan generator put around $k(u)$?

Alice

It emits a loop over Movies, then places the code generated by $k(u)$ inside that loop:

for u in M:
    <code generated by k(u)>

Here u is a name in the program being generated. We are not reading one particular Movies tuple during compilation.

Ada · 12

What should selection put between that loop and its consumer?

Alice

An if whose body contains the consumer's code:

for u in M:
    if u.Director == "Bergman":
        <code for the selected tuple's consumer>
Ada · 13

The consumer inserts Title into a result set. Finish the program and run it on the three Movies tuples.

Alice
answers = empty_set()
for u in M:
    if u.Director == "Bergman":
        answers.insert(u.Title)

Actor1 inserts filmA. Actor2 inserts filmA again, leaving the set unchanged. Actor3 fails the condition. The answer is {filmA}.

Ada · 14

Suppose I delete the set and simply emit Title at each successful test.

Alice

Then I emit filmA twice. Fusing the operations did not give permission to drop duplicate elimination from relational projection.

Ada · 15

Neumann calls the compiler-side methods produce and consume. Does consume run once for actor1 and again for actor2 when the query executes?

Alice

I was about to say yes. But it generated the loop body. That body runs for actor1 and actor2 later.

The compiler's traversal of the plan and the generated program's traversal of tuples are different computations.

Ada · 16

Which parts of the selection example are available during generation?

Alice

The Movies schema, the selected Director field, the constant Bergman, and the requested Title field. The actual movie values arrive at runtime.

That matches R.3: leave runtime holes in the program we construct.

Ada · 17

Return to the three-way indexed plan. Describe how its loops are built, starting at the final collector.

Alice

The collector generates insertion of (Actor,Schedule,Address).

$J_2$ asks $J_1$ to produce a tuple, then generates a Theater lookup loop around that insertion. $J_1$ asks the Movies scan to produce a tuple, then generates a Title lookup loop around $J_2$'s code.

The result is the three nested loops we traced. We reached them through the physical plan.

Ada · 18

Could the compiler silently replace the inner Title lookup with one membership test?

Alice

No. Actor1 has two showings. Existence would tell us that at least one exists, but it would not enumerate both times.

Changing control flow must preserve the access contract too.

Ada · 19

Remove the pre-existing Title index. If every movie scans Pariscope, what work repeats for the two filmA actors?

Alice

Both search the same showing array for the same Title. We still need both actors' matches, but we could share the work of organizing Pariscope.

Ada · 20

Suppose we pay for one traversal that groups showings by Title in a temporary hash table. Then each movie probes its group. This is the build-then-probe hash join previewed in 3.2. Write its two phases.

Alice

A build loop over Pariscope:

H = empty_map_of_lists()
for v in P:
    H[v.Title].append(v)

for u in M:
    for v in H.get_or_empty(u.Title):
        for w in lookup_L(v.Theater):
            answers.insert((u.Actor, v.Schedule, w.Address))

The table keeps both filmA showings. Keeping one would lose an answer.

Ada · 21

Can we start probing after the first filmA showing enters $H$, and treat that lookup as complete?

Alice

No. The 21:00 showing has not arrived yet. This build-then-probe algorithm must finish the build before the probe phase uses the table as complete.

Push execution did not remove that dependency.

Ada · 22

What is stored at the boundary between those phases?

Alice

The hash table of build tuples, or references to them. Not the complete movie–showing join result.

The probe phase can still pass each joined tuple directly into the Location lookup.

Ada · 23

The consumer wants just the first answer. In the pull version it stops calling next. What must the push version arrange?

Alice

The consumer must signal that it is finished, and the loops must propagate that signal outward. Breaking only the Location loop would let the next showing continue producing answers.

Early termination is part of the control protocol, not an automatic consequence of this loop syntax.

Ada · 24

Have we proved that the generated version is faster?

Alice

We have proved no timing claim. It may avoid calls and keep values close to their uses. It also costs time to generate and compile code.

A compiler may inline a pull implementation too. We must inspect or measure the implementations we actually compare.

Ada · 25

Try a different input: one Movies tuple, two matching showings, and no matching Location. Compare the complete executions.

Alice

Both versions try the two showing matches and find two empty Location lookups. Both return the empty answer set.

The pull call works internally until it returns None. The push loops finish without inserting anything. Empty output does not mean no work.

Ada · 26

And if I change only the batch size or the storage layout?

Alice

That is another choice. It does not follow from choosing pull or push.

We can now ask separately which tuples are required, how to find them, and who drives the next step.

Recap

Execution and generation

The indexed pull and push executions enumerate the same witnesses under the same access contracts. Pull retains continuation state between requests; synchronous push continues into the consumer while its loops remain active.

A push-style compiler builds those consumer bodies during code generation. The resulting program later processes tuples. Neumann's produce/consume interface describes this generation process. Build dependencies, retained data, duplicate elimination, and early-stop protocols still require explicit implementations. Correctness comes from preserving the chosen plan's result; performance requires a separate argument or measurement.