Commit Graph

10940 Commits

Author SHA1 Message Date
Jussi Saurio
dbdf60a628 extract common functionality 2025-11-19 14:29:33 +02:00
Jussi Saurio
745cdc3aa2 Align trigger sql rewrite behavior with sqlite
SQLite doesn't rewrite INSERT lists or WHEN clause, it instead
lets the trigger go "stale" and will cause runtime errors. This
may not be great behavior, but it's compatible...
2025-11-19 14:29:33 +02:00
Jussi Saurio
5b1c69a9d0 fix ai slop with more ai slop 2025-11-19 14:29:33 +02:00
Jussi Saurio
a0a1bd6637 Triggers: fix issues with ALTER TABLE
- Disallow DROP COLUMN on columns referenced in triggers
- Propagate RENAME COLUMN to trigger SQL definitions

DROP COLUMN is not allowed when the column is mentioned in a trigger
on the table the column is dropped from, eg:

```
turso> CREATE TABLE t(x,y);
turso> CREATE TRIGGER foo BEFORE INSERT ON t BEGIN INSERT INTO t VALUES (NEW.x); END;
turso> ALTER TABLE t DROP COLUMN x;
  × Parse error: cannot drop column "x": it is referenced in trigger foo
```

However, it is allowed if the trigger is on another table:

```
turso> CREATE TABLE t(x,y);
turso> CREATE TABLE u(x,y);
turso> CREATE TRIGGER bar BEFORE INSERT ON t BEGIN INSERT INTO u(y) VALUES (NEW.x); END;
turso> ALTER TABLE u DROP COLUMN y;
turso> INSERT INTO t VALUES (1,1);
  × Parse error: table u has no column named y
```

Nearly all of the code here is vibecoded. I first asked Cursor Composer to create
an initial implementation. Then, I asked it to try to discover edge cases using the
`turso` and `sqlite3` CLIs, and write tests+fixes for the edge cases found.

The code is a bit slop, but this isn't a particularly performance-critical use case
and it should solve most of the issues with RENAME and DROP COLUMN.
2025-11-19 14:29:33 +02:00
Jussi Saurio
fb31fd56ba Merge 'Simulator: refactor and simplify InteractionPlan' from Pedro Muniz
Depends on #3775 - to remove noise from this PR.
## Motivation
In my continued efforts in making the simulator more accessible and
simpler to work with, I have over time simplified and optimized some
parts of the codebase like query generation and decision making so that
more people from the community can contribute and enhance the simulator.
This PR is one more step in that direction.
Before this PR, our `InteractionPlan` stored `Vec<Interactions>`.
`Interactions` are a higher level collection that will generate a list
of `Interaction` (yes I know the naming can be slightly confusing
sometimes. Maybe we can change it later as well. Especially because
`Interactions` are mainly just `Property`). However, this architecture
imposed a problem when MVCC enters the picture. MVCC requires us to make
sure that DDL statements are executed serially. To avoid adding even
more complexity to plan generation, I opted on previous PRs to check
before emitting an `Interaction` for execution, if the interaction is a
DDL statement, and if it is, I emit a `Commit` for each connection still
in a transaction. This worked slightly fine, but as we do not store the
actual execution of interactions in the interaction plan, only the
higher level `Interactions`, this meant that I had to do some
workarounds to modify the `Interactions` inside the plan to persist the
`Commit` I generated on demand.
## Problem
However, I was stupid and overlooked the fact that for certain
properties that allow queries to be generated in the middle (referenced
as extensional queries in the code), we cannot specify the connection
that should execute that query, meaning if a DDL statement occurred
there, the simulator could emit the query but could not save it properly
in the plan to reproduce in shrinking. So to correct and make
interaction generation/emission less brittle, I refactored the
`InteractionPlan` so that it stores `Vec<Interaction>` instead.
## Implications
- `Interaction` is not currently serializable using `Serde` due to the
fact that it stores a function in `Assertion`. This means that we cannot
serialize the plan into a `plan.json`. Which to me is honestly fine, as
the only things that used `plan.json` was `--load` and `--watch`
options. Which are options almost nobody really used.
- For load, instead of generating the whole plan it just read the plan
from disk. The workaround for that right now is just load the `cli_opts`
that were last run for that particular seed and use those exact options
to run the simulation.
- For watch, currently there is not workaround but, @alpaylan told me
has some plans to make assertions serializable by embedding a custom
language into the `plan.sql` file, meaning we will probably not need a
json file at all to store the interaction plan. And this embedded
language will make it much easier to bring back a more proper watch
mode.
- The current shrinking algorithms all have some notion of properties
and removal of properties, but `Interaction` do not have this concept.
So I added some metadata to interactions and a origin ID to each
`Interaction` so that we can search through the list of interactions
using binary search to get all of the interactions that are part of the
same `Property`. To support this, I added an `InteractionBuilder` and
some utilities to iterate and remove properties in the `InteractionPlan`
## Conclusion
Overall, this code simplifies emission of interactions and ensures the
`InteractionPlan` always stores the actual interactions that get
executed. This also decouples more query generation logic from query
emission logic.

Closes #3774
2025-11-19 11:10:51 +02:00
Pekka Enberg
687d9faf37 Turso 0.4.0-pre.2 2025-11-19 09:40:08 +02:00
Jussi Saurio
92f47dffb0 Merge 'Trigger support' from Jussi Saurio
## Trigger Support
This PR adds support for triggers:
- `CREATE TRIGGER`
- `DROP TRIGGER`
Supported
- `BEFORE/AFTER INSERT`
- `BEFORE/AFTER DELETE`
- `BEFORE/AFTER UPDATE [OF <col1,col2,col3>]`
Not supported:
- `INSTEAD OF`
- `TEMPORARY`
### Implementation details
- Triggers are executed within a new `Insn::Program` instruction. The
spec of the insn differs a bit from SQlite: we store a `Statement`
inside that instruction that we can `reset()` for every invocation.
- Like Sqlite, trigger programs take `NEW` and `OLD` rows as program
parameters.
Whenever there are triggers that would fire as the result of a DML
statement:
- `DELETE` writes the rows being deleted into a `RowSet` first.
- `UPDATE` and `INSERT` write the rows being updated into an ephemeral
table first.
### Other shit
Also added `EXPLAIN` support - the bytecode plans for trigger
subprograms are appended after the main program.
### AI disclosure
Used Cursor quite a bit for generating boilerplate code for this - you
can blame all the bad code on the AI of course 🤡
### Follow-ups:
1. ALTER TABLE ops need to rewrite the sql in the CREATE TRIGGER
statement e.g. if a column is renamed. Columns cannot be dropped if
referenced in triggers.
2. Fix weird rowid -1 fallback:
https://github.com/tursodatabase/turso/pull/3979#issuecomment-3547999449

Closes #3979
2025-11-19 08:42:41 +02:00
Pere Diaz Bou
72bf195f4b Merge 'core/mvcc/cursor: rowid don't seek first rowid' from Pere Diaz Bou
rowid should only try to use the current's position. So if we are not
pointing to a `Loaded` row, then it should return None
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Change `rowid()` to return `None` unless cursor is on a `Loaded` row,
removing the implicit seek from `BeforeFirst`.
>
> - **Core MVCC Cursor (`core/mvcc/cursor.rs`)**:
>   - Adjust `rowid()` behavior: remove implicit first-row seek when
`BeforeFirst`; return `None` unless position is `Loaded`.
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
8848775a71. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Reviewed-by: Jussi Saurio <jussi.saurio@gmail.com>

Closes #3977
2025-11-18 19:51:19 +01:00
Jussi Saurio
ad753281b6 Remove unneeded too_many_arguments annotation 2025-11-18 18:41:45 +02:00
Jussi Saurio
129ee8c82b Remove more AI-generated unnecessary code 2025-11-18 17:24:10 +02:00
Jussi Saurio
2cbc83a01c triggers: add ParamMap abstraction to reduce code noise a bit 2025-11-18 17:08:22 +02:00
Jussi Saurio
11528cff12 Remove weird AI-innovated negative index hack 2025-11-18 16:56:27 +02:00
Jussi Saurio
2674145937 Avoid allocation when no triggers exist 2025-11-18 15:40:06 +02:00
Jussi Saurio
d33c294380 remove unhelpful comment 2025-11-18 15:39:53 +02:00
Jussi Saurio
5c1ebbd011 Use VecDeque for trigger storage for similar reasons as indexes do 2025-11-18 15:19:01 +02:00
Jussi Saurio
e1dee4a072 triggers: add a lot of different kinds of tests 2025-11-18 15:19:01 +02:00
Jussi Saurio
9aa09d5ccf Add EXPLAIN support for trigger subprograms
They get printed after the parent program.
2025-11-18 15:19:01 +02:00
Jussi Saurio
423a1444d1 Don't crash if table cursor is already opened 2025-11-18 15:19:01 +02:00
Jussi Saurio
7f536506c3 Clear deferred_seeks for cursor when it is closed
Sometimes the deferred seek never happens, so we don't want it to
dangle if the same cursor is reused for another seek
2025-11-18 15:19:01 +02:00
Jussi Saurio
d398f12471 triggers: subprograms shouldnt commit or use the transaction opcode 2025-11-18 15:19:01 +02:00
Jussi Saurio
be6f8ab8b3 state.end_statement() should not be called separately in cases where abort() already does it 2025-11-18 15:19:01 +02:00
Jussi Saurio
7a12e184a8 Only reset FK violation counter if stmt was rolled back
In the case of trigger subprograms the statement didn't roll back,
since the parent program will roll it back.
2025-11-18 15:19:01 +02:00
Jussi Saurio
770c6eef9f triggers: subprograms dont use transactions 2025-11-18 15:19:01 +02:00
Jussi Saurio
70267f8710 triggers: add translation logic for INSERT triggers 2025-11-18 15:19:01 +02:00
Jussi Saurio
e28301dc2e triggers: add translation logic for UPDATE triggers 2025-11-18 15:19:01 +02:00
Jussi Saurio
516dae5b6a triggers: add translation logic for DELETE triggers 2025-11-18 15:19:01 +02:00
Jussi Saurio
5b037b0f75 resolve labels for RowSetRead insn 2025-11-18 15:19:01 +02:00
Jussi Saurio
7d1543fcc5 triggers: take triggers into account in optimizer decision
- optimize the select plan used for the RowSet in DELETE
- require ephemeral table when UPDATE involves triggers
2025-11-18 15:19:01 +02:00
Jussi Saurio
78ce3c8658 triggers: add capability for DeletePlan to write the write set into a RowSet first
This is needed for safe DELETE when there are DELETE triggers on the affected
table.
2025-11-18 15:19:01 +02:00
Jussi Saurio
e60e37da7d triggers: add execution plumbing to translation and vdbe layers 2025-11-18 15:19:01 +02:00
Jussi Saurio
3d00686f48 triggers: translation functions for DDL 2025-11-18 12:18:07 +02:00
Jussi Saurio
d4b487eebc triggers: add in-memory schema entries 2025-11-18 12:14:27 +02:00
Preston Thorpe
e61234d522 Merge 'translate/insert: Implement INSERT OR REPLACE' from Preston Thorpe
This PR implements support for `INSERT OR REPLACE INTO t`.
For `OR IGNORE`, we currently rewrite this internally to an `ON CONFLICT
DO NOTHING`, and I was hopeful we could do this with OR REPLACE, however
it seems SQLite actually deletes the row and then proceeds to insert, so
we could not simply rewrite this to an `ON CONFLICT DO UPDATE SET
col=excluded.col`, as this would result in differing rowid's when
compared to SQLite.

Reviewed-by: Jussi Saurio <jussi.saurio@gmail.com>

Closes #3972
2025-11-17 18:50:15 -05:00
PThorpe92
56f35ad4cd cargo fmt 2025-11-17 12:22:55 -05:00
PThorpe92
c3185d0b8c Properly handle foreign keys for INSERT OR REPLACE 2025-11-17 12:19:33 -05:00
Pere Diaz Bou
8848775a71 core/mvcc/cursor: rowid don't seek first rowid
rowid should only try to use the current's position. So if we are not
pointing to a `Loaded` row, then it should return None
2025-11-17 16:17:52 +01:00
pedrocarlo
c2be60b007 add pragma to shrinking 2025-11-17 11:45:02 -03:00
pedrocarlo
f09b73c768 remove Span, as interaction ID is enough to determine membership of a property 2025-11-17 11:45:02 -03:00
pedrocarlo
2aab33b714 find_interactions_range only check for interaction id to determine membership 2025-11-17 11:45:02 -03:00
pedrocarlo
4fd0896538 remove extension queries from other types of properties 2025-11-17 11:45:02 -03:00
pedrocarlo
9d439556ca if table changed names, add its previous names to depending tables when shrinking 2025-11-17 11:45:02 -03:00
pedrocarlo
af31e74d9f add depending tables to assertions to delete them if needed in shrinking 2025-11-17 11:45:02 -03:00
pedrocarlo
087d5f59a1 fix execution ticks not ticking enough 2025-11-17 11:45:02 -03:00
pedrocarlo
836d115853 create interaction plan correct in main.rs 2025-11-17 11:45:02 -03:00
pedrocarlo
2c8754985b refactor shrinking to use utilities in the InteractionPlan to iterate over properties, instead of handrolling property iteration 2025-11-17 11:45:02 -03:00
pedrocarlo
a21f7675dd - update interaction stats on demand instead of reading the entire plan
to calculate metrics per generation step 
- simplify generation as we now only store `Interaction`. So now we can 
  funnel most of the logic for interaction generation, metric update,
  and Interaction append in the `PlanGenerator::next`.
2025-11-17 11:45:02 -03:00
pedrocarlo
157a5cf10a - add Interaction Builder to simplify code for building an interaction.
Modify `generation/property.rs` to use the Builder
- add additional metadata to `Interaction` to give more context for
  shrinking and iterating over interactions that originated from the
  same interaction.
- add Iterator like utilities for `InteractionPlan` to facilitate
  iterating over interactions that came from the same property:
2025-11-17 11:45:02 -03:00
pedrocarlo
c088a653e6 move interaction stats to metrics 2025-11-17 11:45:02 -03:00
pedrocarlo
2fe39d40bb add Span and PropertyMetadata structs 2025-11-17 11:45:02 -03:00
pedrocarlo
a4f0f2364d disable Watch Mode until we can properly serialize interaction plan 2025-11-17 11:45:02 -03:00