Skip to content
Back to Journal

From one sentence to a controlled data change.

In my previous post, I introduced OrbitRows and a question that has guided the work: why should a simple change to a spreadsheet require so much effort? Since then, I’ve built a first working pipeline that takes an instruction, breaks it into smaller decisions, and follows them through to a change in the data. To show how it works, and where it still needs improvement, I’ll follow one merchant’s sentence from the initial request to the resulting prices.

A different role for the language model.

The principle behind this version is simple:

Jev chooses, algorithm builds, SQLite confirms.

Jev answers bounded questions using options prepared by the program. It does not generate the SQL or write the JSON structures.

Code assembles those decisions into an operation. Database lookups connect words in the request to stored values, and SQLite executes the resulting statements.

This gives me intermediate outputs to inspect. If the result is wrong, I can investigate whether the failure came from splitting the request, identifying a value, choosing a column, or constructing a condition.

One instruction to follow through the pipeline.

For the demonstration, I used a SQLite store containing six tables and this request:

Lower the price of the Volt and Casa variants heavier than 0.05 kg by 30%, but never below their cost, then show the names and new prices of those, cheapest first.

The instruction contains two actions, a weight conversion, a minimum-price constraint, and references connecting different parts of the sentence.

The walkthrough follows it through seven steps.

Technical Walkthrough: From One Merchant Sentence to SQL.

Each step adds one kind of decision. Jev, the language model, only picks among options the code prepares; the algorithm builds every tree; SQLite confirms values and runs the result.

Every block below is the pipeline’s own output; the store was changed inside a savepoint and rolled back.

Step 1: Requests.

GLiNER2 + Jev + CorPipe

Cuts the sentence into requests, links them, resolves the words pointing back.

Two requests, linked by THEN. “their” points to the variants of r1, “those” in r2 to the same variants.

Output (JSON)
JSON
{
  "requests": [
    {
      "id": "r1",
      "text": "Lower the price of the Volt and Casa variants heavier than 0.05 kg by 30%, but never below their cost",
      "waits_for": null,
      "input": "snapshot"
    },
    {
      "id": "r2",
      "text": "show the names and new prices of those, cheapest first.",
      "waits_for": "r1",
      "input": "rows"
    }
  ],
  "links": "r1 THEN r2",
  "references": [
    {
      "text": "their",
      "target": "the Volt and Casa variants heavier than 0.05 kg",
      "request": "r1",
      "target_request": "r1"
    },
    {
      "text": "those",
      "target": "the Volt and Casa variants heavier than 0.05 kg",
      "request": "r2",
      "target_request": "r1"
    }
  ]
}

Step 2: Operation.

Jev

One kind of SQL statement per request, and what a request takes from an earlier one.

r1 changes data (update); r2 reads (select) the rows r1 changed (affected_rows).

Output (JSON)
JSON
{
  "r1": {
    "statement": "update",
    "reports_back": false
  },
  "r2": {
    "statement": "select",
    "reports_back": true,
    "input": "affected_rows"
  }
}

Step 3: Values.

Value linker + GLiNER2 + SQLite + Jev

Finds the words that are stored values and where the store holds them.

“Volt” is a brand stored twice (Volt, volt), “Casa” a brand; “0.05 kg” is a number for later steps.

Output (JSON)
JSON
[
  {
    "text": "Volt",
    "role": "lookup",
    "column": "t_products.c_brand",
    "match": "exact",
    "stored": [
      "Volt",
      "volt"
    ]
  },
  {
    "text": "Casa",
    "role": "lookup",
    "column": "t_products.c_brand",
    "match": "exact",
    "stored": [
      "Casa"
    ]
  },
  {
    "text": "0.05 kg",
    "role": "number"
  }
]

Step 4: Sources.

Jev

The table each request acts on and the tables it reads.

r1 changes variants and reads products (the brands); r2 starts from the rows r1 changed.

Output (JSON)
JSON
{
  "r1": {
    "target": {
      "type": "database_table",
      "reference": "t_variants"
    },
    "reads": [
      "t_products"
    ]
  },
  "r2": {
    "from": {
      "type": "request_result",
      "request_id": "r1"
    },
    "reads": [
      "t_products"
    ]
  }
}

Step 5: Parts.

Jev chooses a template, algorithm builds the tree

What each request changes or shows.

“by 30%” becomes × 0.7; “never below their cost” becomes a floor, kept safe when the cost is empty. r2 shows the price and the product name.

Output (JSON)
JSON
{
  "r1": [
    {
      "column": "t_variants.c_price_mad",
      "value": {
        "type": "function",
        "name": "greatest",
        "arguments": [
          {
            "type": "arithmetic",
            "operator": "*",
            "left": {
              "type": "column",
              "source": "t_variants",
              "reference": "c_price_mad"
            },
            "right": {
              "type": "literal",
              "value": 0.7
            }
          },
          {
            "type": "function",
            "name": "coalesce",
            "arguments": [
              {
                "type": "column",
                "source": "t_variants",
                "reference": "c_cost_mad"
              },
              {
                "type": "arithmetic",
                "operator": "*",
                "left": {
                  "type": "column",
                  "source": "t_variants",
                  "reference": "c_price_mad"
                },
                "right": {
                  "type": "literal",
                  "value": 0.7
                }
              }
            ]
          }
        ]
      }
    }
  ],
  "r2": [
    {
      "column": "t_variants.c_price_mad",
      "aggregate": "value"
    },
    {
      "column": "t_products.c_name",
      "aggregate": "value"
    }
  ]
}

Step 6: Conditions.

Parameters + SQLite + Jev

Which columns choose the rows, one condition each, then how they combine and sort.

“heavier than 0.05 kg” becomes weight_g > 50 (converted to grams); the brands become one IN list; both must hold. r2 adds no condition of its own and sorts by price.

Output (JSON)
JSON
{
  "conditions": {
    "r1": {
      "columns": [
        "t_variants.c_weight_g",
        "t_products.c_brand"
      ],
      "predicates": [
        {
          "column": "t_variants.c_weight_g",
          "operator": ">",
          "rows": 14,
          "condition": {
            "type": "comparison",
            "operator": ">",
            "left": {
              "type": "column",
              "source": "t_variants",
              "reference": "c_weight_g"
            },
            "right": {
              "type": "literal",
              "value": 50.0
            }
          }
        },
        {
          "column": "t_products.c_brand",
          "operator": "in",
          "rows": 6,
          "condition": {
            "type": "membership",
            "expression": {
              "type": "column",
              "source": "t_products",
              "reference": "c_brand"
            },
            "operator": "in",
            "values": [
              {
                "type": "literal",
                "value": "Volt"
              },
              {
                "type": "literal",
                "value": "volt"
              },
              {
                "type": "literal",
                "value": "Casa"
              }
            ]
          }
        }
      ]
    },
    "r2": {
      "columns": [],
      "rows_from": "r1",
      "predicates": []
    }
  },
  "parameters (r1)": [
    {
      "text": "0.05 kg",
      "value": 0.05,
      "unit": {
        "kind": "physical_unit",
        "unit": "kg"
      }
    },
    {
      "text": "30%",
      "value": 30.0,
      "unit": {
        "kind": "percentage",
        "unit": "%"
      }
    }
  ],
  "grouping and sort": {
    "r1": {
      "where": {
        "type": "all",
        "children": [
          {
            "type": "comparison",
            "operator": ">",
            "left": {
              "type": "column",
              "source": "t_variants",
              "reference": "c_weight_g"
            },
            "right": {
              "type": "literal",
              "value": 50.0
            }
          },
          {
            "type": "membership",
            "expression": {
              "type": "column",
              "source": "t_products",
              "reference": "c_brand"
            },
            "operator": "in",
            "values": [
              {
                "type": "literal",
                "value": "Volt"
              },
              {
                "type": "literal",
                "value": "volt"
              },
              {
                "type": "literal",
                "value": "Casa"
              }
            ]
          }
        ]
      }
    },
    "r2": {
      "rows_from": "r1",
      "where": null,
      "order_by": [
        {
          "expression": {
            "type": "column",
            "source": "t_variants",
            "reference": "c_price_mad"
          },
          "direction": "asc"
        }
      ]
    }
  }
}

Step 7: Assembly.

Code + SQLGlot + SQLite

One complete statement per request, checked and run in a savepoint that is rolled back.

The brand condition follows the foreign key variants → products; r1 returns the ids it changed, r2 reads exactly those.

Output (JSON)
JSON
{
  "program": [
    {
      "request": "r1",
      "statement": "update"
    },
    {
      "request": "r2",
      "statement": "select"
    }
  ],
  "rows changed by r1": 6,
  "rows returned by r2": 6,
  "errors": []
}

One validated statement per request.

The AST is plain JSON checked by Pydantic: every node has a fixed shape, so a malformed tree is refused before it reaches the database.

r1 · update (AST)
JSON
{
  "id": "r1",
  "type": "update",
  "target": {
    "type": "database_table",
    "reference": "t_variants",
    "alias": "v"
  },
  "scope": {
    "type": "filtered",
    "where": {
      "type": "all",
      "children": [
        {
          "type": "comparison",
          "operator": ">",
          "left": {
            "type": "column",
            "source": "v",
            "reference": "c_weight_g"
          },
          "right": {
            "type": "literal",
            "value": 50.0
          }
        },
        {
          "type": "membership",
          "operator": "in",
          "expression": {
            "type": "column",
            "source": "v",
            "reference": "c_product_id"
          },
          "query": {
            "id": "q1",
            "type": "select",
            "from": {
              "type": "database_table",
              "reference": "t_products",
              "alias": "p"
            },
            "columns": [
              {
                "expression": {
                  "type": "column",
                  "source": "p",
                  "reference": "c_product_id"
                },
                "alias": "c0"
              }
            ],
            "scope": {
              "type": "filtered",
              "where": {
                "type": "membership",
                "expression": {
                  "type": "column",
                  "source": "p",
                  "reference": "c_brand"
                },
                "operator": "in",
                "values": [
                  {
                    "type": "literal",
                    "value": "Volt"
                  },
                  {
                    "type": "literal",
                    "value": "volt"
                  },
                  {
                    "type": "literal",
                    "value": "Casa"
                  }
                ]
              }
            }
          }
        }
      ]
    }
  },
  "assignments": [
    {
      "column": {
        "type": "column",
        "source": "v",
        "reference": "c_price_mad"
      },
      "value": {
        "type": "function",
        "name": "greatest",
        "arguments": [
          {
            "type": "arithmetic",
            "operator": "*",
            "left": {
              "type": "column",
              "source": "v",
              "reference": "c_price_mad"
            },
            "right": {
              "type": "literal",
              "value": 0.7
            }
          },
          {
            "type": "function",
            "name": "coalesce",
            "arguments": [
              {
                "type": "column",
                "source": "v",
                "reference": "c_cost_mad"
              },
              {
                "type": "arithmetic",
                "operator": "*",
                "left": {
                  "type": "column",
                  "source": "v",
                  "reference": "c_price_mad"
                },
                "right": {
                  "type": "literal",
                  "value": 0.7
                }
              }
            ]
          }
        ]
      }
    }
  ]
}
r2 · select (AST)
JSON
{
  "id": "r2",
  "type": "select",
  "from": {
    "type": "database_table",
    "reference": "t_variants",
    "alias": "v"
  },
  "scope": {
    "type": "filtered",
    "where": {
      "type": "membership",
      "operator": "in",
      "expression": {
        "type": "column",
        "source": "v",
        "reference": "c_variant_id"
      },
      "values": [
        {
          "type": "literal",
          "value": 107
        },
        {
          "type": "literal",
          "value": 108
        },
        {
          "type": "literal",
          "value": 109
        },
        {
          "type": "literal",
          "value": 110
        },
        {
          "type": "literal",
          "value": 111
        },
        {
          "type": "literal",
          "value": 116
        }
      ]
    }
  },
  "columns": [
    {
      "expression": {
        "type": "column",
        "source": "v",
        "reference": "c_price_mad"
      },
      "alias": "price_mad"
    },
    {
      "expression": {
        "type": "subquery",
        "query": {
          "id": "q1",
          "type": "select",
          "from": {
            "type": "database_table",
            "reference": "t_products",
            "alias": "p"
          },
          "columns": [
            {
              "expression": {
                "type": "column",
                "source": "p",
                "reference": "c_name"
              },
              "alias": "c0"
            }
          ],
          "scope": {
            "type": "filtered",
            "where": {
              "type": "comparison",
              "operator": "=",
              "left": {
                "type": "column",
                "source": "p",
                "reference": "c_product_id"
              },
              "right": {
                "type": "column",
                "source": "v",
                "reference": "c_product_id"
              }
            }
          },
          "limit": 1
        }
      },
      "alias": "name"
    }
  ],
  "order_by": [
    {
      "expression": {
        "type": "column",
        "source": "v",
        "reference": "c_price_mad"
      },
      "direction": "asc"
    }
  ]
}

Compiled by SQLGlot for SQLite.

Formatted for reading. The update runs with RETURNING so the second request reads exactly the rows it changed.

r1 · change the prices

SQL
UPDATE variants AS v
SET price_mad = MAX(v.price_mad * 0.7,
                    COALESCE(v.cost_mad, v.price_mad * 0.7))
WHERE v.weight_g > 50
  AND v.product_id IN (
        SELECT p.product_id
        FROM products AS p
        WHERE p.brand IN ('Volt', 'volt', 'Casa'))
RETURNING variant_id;   -- 107, 108, 109, 110, 111, 116

r2 · show those, cheapest first

SQL
SELECT v.price_mad,
       (SELECT p.name
        FROM products AS p
        WHERE p.product_id = v.product_id
        LIMIT 1) AS name
FROM variants AS v
WHERE v.variant_id IN (107, 108, 109, 110, 111, 116)   -- the rows r1 changed
ORDER BY v.price_mad ASC
The SQL exactly as generated
SQL
UPDATE "variants" AS "v" SET "price_mad" = MAX(("v"."price_mad" * 0.7), COALESCE("v"."cost_mad", ("v"."price_mad" * 0.7))) WHERE ("v"."weight_g" > 50.0) AND ("v"."product_id" IN (SELECT "p"."product_id" AS "c0" FROM "products" AS "p" WHERE "p"."brand" IN ('Volt', 'volt', 'Casa')));

SELECT "v"."price_mad" AS "price_mad", (SELECT "p"."name" AS "c0" FROM "products" AS "p" WHERE "p"."product_id" = "v"."product_id" LIMIT 1) AS "name" FROM "variants" AS "v" WHERE "v"."variant_id" IN (107, 108, 109, 110, 111, 116) ORDER BY "v"."price_mad" ASC NULLS LAST;

Six variants change, two are left alone.

VariantProductBrandWeightPriceCostNew price
107USB-C CableVolt80 g1204584
108Wireless Mousevolt95 g18090126
109Travel MugCasa300 g15070105
110Travel MugCasa300 g15070105
111Desk LampCasa1200 g400250280
116Mug 100%Casa280 g1406098
106USB-C CableVolt50 gnot changed: exactly 50 g is not heavier than 50 g
117Adapter_USBVolt35 gnot changed: 35 g

Prices in MAD. 30% off keeps every price above its cost here, so the floor never applies; it would for a variant whose cost is above 70% of its price. “Volt” is stored with two spellings (Volt, volt); both are matched.

r2 returns

NameNew price
USB-C Cable84
Mug 100%98
Travel Mug105
Travel Mug105
Wireless Mouse126
Desk Lamp280

What the merchant is asked.

Any answer below 0.7 is kept but shown to the merchant. Here the doubts are minor and the program is right; filtering such doubts so only the ones that change the result reach the merchant is a planned improvement.

  • Values: role uncertain (0.64)
  • Sources: uses table uncertain (0.56)
  • Conditions: chooses by t_products.c_name uncertain (0.49)

The same prompts, written directly by Codex.

26 end-to-end prompts (A01–A26). Codex (GPT-5.6 Terra, medium effort) received the same schema and stored values and answered with SQL only. Both programs ran on a fresh copy of the store and were scored on every table’s final content and every read’s rows.

MeasureCodex (SQL only)Our pipeline
Time per prompt5.3 s5.2 s (median 4.8 s)
First prompt after starting–≈ 41 s (local models load once)
Tokens per prompt≈ 3,400≈ 25,300 in + 3,300 out
Calls to the model1≈ 8.5 (Jev, bounded choices)
Cost per prompt (published prices)≈ $0.010–0.015≈ $0.001
Cost per 1,000 prompts≈ $10–15≈ $1
Right, no question1911
Right, with a question–13
Wrong, but the merchant is asked–2
Wrong, with no question70

Between the two runs, I fixed requests that combine two actions in one sentence, such as “Show the new prices of the black variants once you have lowered them by 5%”. The flagged errors went from 5 to 2. The cases that were wrong now come out right, but with a question, which is why “right, no question” moved from 12 to 11.

Codex’s seven silent answers, reviewed

PromptWhat Codex didVerdict
Show the new prices of the black variants once you have lowered them by 5%showed computed prices, never applied the updatewrong: the store is unchanged
Cut the price of every variant by 40%, never below the cost plus 10skipped the variants without a costwrong: not every variant
Show the variants that are not black and cost more than 300used the cost columndebatable: the pipeline read it the same way
Show the 3 suppliers with the longest lead timechose which columns to showpresentation only
Three prompts asking for the names of changed variantsone name per product (DISTINCT)presentation only

Codex writes fluent SQL quickly; when it is wrong, nothing tells the merchant. The pipeline now answers just as fast once started. It reads more tokens, yet costs less, because Jev’s input is about fifty times cheaper and its output is free.

Prices and caveats

Jev (jev-latest): $0.042 per million input tokens, output free. GPT-5.6 Terra: $2.00–2.50 per million input, $12–15 per million output; reasoning counts as output. Both come from third-party pages, not the providers’ own price lists; TypeSafe says it cannot yet prove its price is not subsidized. Codex reports one token total (lower when its context is cached), so its cost is a range. In this test Codex ran under a ChatGPT subscription; the comparison uses API prices. The pipeline also uses local models (GLiNER2, CorPipe, embeddings), which cost server time but no tokens.

Sources: eesel.ai, Jev pricing ·flaviocopes.com, Jev cost ·aiweekly.co ·anotherwrapper.com, GPT-5.6 Terra ·pricepertoken.com, GPT-5.6 Terra

Limits: 26 prompts, several written to test the pipeline’s own weak points; a strict scorer counts presentation differences as wrong for both.

Testing the outcome, including the mistakes.

Nevertheless, one failure reflects the concern that started this project: the baseline showed calculated “new prices” without saving the requested update.

The pipeline’s two incorrect outcomes were flagged. That creates a review opportunity; it does not guarantee the user will identify and correct the mistake.

There were no unflagged errors in this 26-prompt test. That is an observed result, not a guarantee for future requests.

The trade-offs are now visible too.

Speed was the main trade-off when I first published: about 24 seconds per prompt. Profiling one request showed that the language model was not the problem: Jev’s calls took about 3 seconds. Two local tasks were repeating work:

  • The coreference model (CorPipe) was reloaded for every prompt: about 14 seconds. It now stays loaded, and its answers are identical on all 203 saved prompts.
  • The unit of each column (“weight_g” is in grams) was read again by a language model 56 times per prompt, although the store has only 16 columns of that kind: about 13 seconds. Units depend on the store, not on the prompt, so they are now read once.

The pipeline now answers in about 5 seconds per prompt, the same as Codex. The first prompt after starting still takes about 40 seconds while the local models load. The lesson is general: work that depends only on the store should be done once, not for every request.

The report also estimates roughly ten times lower model API cost per prompt. That estimate uses third-party pricing and excludes local compute, so it remains provisional.

The test set is small, includes prompts targeting the pipeline’s weak points, and contains phrasings it still fails to interpret correctly.

There is another practical limitation: unnecessary questions.

In the demo, three decisions were flagged even though they did not change the correct result. Currently, scores below 0.7 are surfaced for review, but the score alone does not tell me whether another answer would change the operation.

The next step: asking better questions.

OrbitRows now carries an instruction through identifiable requests, stored values, expressions, conditions, and executable statements. With speed now on par with a direct LLM, my next work is to improve the interpretations that still fail, build recovery for approved changes, and reduce questions that add no useful control. The aim is a workflow where people can understand what will happen to their data and give their attention to decisions that actually matter.

More from the Journal.

All posts

Have a project in mind?

Let’s work together