@@ -262,6 +262,40 @@ def insert_batch_size(self, exe: Executor, table: Table) -> int:
262262 share = max (1 , MAX_ROWS // exe .db .num_threads )
263263 return self .rng .randint (1 , min (available , share ))
264264
265+ def view_predicate (self , exe : Executor , table : Table ) -> str | None :
266+ """A predicate over a view, adding a second input to a read-then-write's
267+ selection.
268+
269+ The selection's frontier is the minimum over its inputs, and an UPDATE
270+ or DELETE reads its target too, so the table holds it near the wall
271+ clock while a REFRESH view is free to sit far ahead. Neither may reach
272+ the write timestamp, which comes from the timeline's oracle.
273+ `InsertSelectAction` covers the view-only case. None when no view offers
274+ a comparable column."""
275+ views = [
276+ view for view in exe .db .views if not view .temp or view in exe .temp_objects
277+ ]
278+ self .rng .shuffle (views )
279+ for view in views :
280+ pairs = [
281+ (table_column , view_column )
282+ for table_column in table .columns
283+ for view_column in view .columns
284+ # A map has no equality operator, so it cannot drive an IN.
285+ if table_column .data_type == view_column .data_type
286+ and table_column .data_type != TextTextMap
287+ ]
288+ if not pairs :
289+ continue
290+ table_column , view_column = self .rng .choice (pairs )
291+ # The alias keeps the inner reference off the outer target, the
292+ # LIMIT bounds an expensive view body.
293+ return (
294+ f"{ table_column .name (True )} IN (SELECT rtw_src.{ view_column .name (True )} "
295+ f" FROM { view } AS rtw_src LIMIT 100)"
296+ )
297+ return None
298+
265299 def create_system_connection (
266300 self , exe : Executor , num_attempts : int = 10
267301 ) -> Connection :
@@ -1171,9 +1205,14 @@ def run(self, exe: Executor) -> bool:
11711205 if not tables :
11721206 return False
11731207 table = self .rng .choice (tables )
1174- # Reading the insert target itself makes the target a read dependency
1175- # too, the most contended shape a read-then-write can have.
1176- source = table if self .rng .choice ([True , False ]) else self .rng .choice (tables )
1208+ # Reading the insert target itself is the most contended shape a
1209+ # read-then-write can have. A view is the opposite: the target is
1210+ # written but not read, so a REFRESH view alone pins the selection's
1211+ # frontier, see `Action.view_predicate`.
1212+ sources = tables + [
1213+ view for view in exe .db .views if not view .temp or view in exe .temp_objects
1214+ ]
1215+ source = table if self .rng .choice ([True , False ]) else self .rng .choice (sources )
11771216
11781217 column_names = ", " .join (column .name (True ) for column in table .columns )
11791218 # The cast is an identity cast: `expression` returns the requested type
@@ -1464,7 +1503,12 @@ def run(self, exe: Executor) -> bool:
14641503 f"{ c .name (True )} = { expression (c .data_type , table .columns , self .rng , kind = ExprKind .WRITE )} "
14651504 for c in set_columns
14661505 )
1467- query = f"UPDATE { table } SET { set_clause } WHERE { expression (Boolean , table .columns , self .rng , kind = ExprKind .WRITE )} "
1506+ predicate = expression (Boolean , table .columns , self .rng , kind = ExprKind .WRITE )
1507+ if self .rng .random () < 0.2 :
1508+ view_predicate = self .view_predicate (exe , table )
1509+ if view_predicate :
1510+ predicate = f"({ predicate } ) AND { view_predicate } "
1511+ query = f"UPDATE { table } SET { set_clause } WHERE { predicate } "
14681512 if self .rng .choice ([True , False ]):
14691513 self .stmt_id += 1
14701514 self .exe_prepared (query , f"update{ self .stmt_id } " , exe )
@@ -1516,7 +1560,8 @@ def errors_to_ignore(self, exe: Executor) -> list[str]:
15161560 "canceling statement due to statement timeout" ,
15171561 OCC_CONTENTION_EXHAUSTED_ERROR ,
15181562 ] + super ().errors_to_ignore (exe )
1519- if exe .db .scenario == Scenario .Rename :
1563+ # The predicate can name a view, which DDL drops concurrently.
1564+ if exe .db .complexity == Complexity .DDL or exe .db .scenario == Scenario .Rename :
15201565 errors += ["does not exist" ]
15211566 return errors
15221567
@@ -1553,7 +1598,14 @@ def run(self, exe: Executor) -> bool:
15531598 query += f" USING { using_table } "
15541599 query += f" WHERE { expression (Boolean , all_columns , self .rng , kind = ExprKind .WRITE )} "
15551600 elif self .rng .random () < 0.95 :
1556- query += f" WHERE { expression (Boolean , table .columns , self .rng , kind = ExprKind .WRITE )} "
1601+ predicate = expression (
1602+ Boolean , table .columns , self .rng , kind = ExprKind .WRITE
1603+ )
1604+ if self .rng .random () < 0.2 :
1605+ view_predicate = self .view_predicate (exe , table )
1606+ if view_predicate :
1607+ predicate = f"({ predicate } ) AND { view_predicate } "
1608+ query += f" WHERE { predicate } "
15571609 if self .rng .choice ([True , False ]):
15581610 self .stmt_id += 1
15591611 self .exe_prepared (query , f"delete{ self .stmt_id } " , exe )
0 commit comments