@@ -262,6 +262,44 @@ 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. Views reaching a source are excluded: the adapter
275+ refuses such a selection outright, which would make this vacuous."""
276+ views = [
277+ view
278+ for view in exe .db .views
279+ if view .read_then_write_input
280+ and (not view .temp or view in exe .temp_objects )
281+ ]
282+ self .rng .shuffle (views )
283+ for view in views :
284+ pairs = [
285+ (table_column , view_column )
286+ for table_column in table .columns
287+ for view_column in view .columns
288+ # A map has no equality operator, so it cannot drive an IN.
289+ if table_column .data_type == view_column .data_type
290+ and table_column .data_type != TextTextMap
291+ ]
292+ if not pairs :
293+ continue
294+ table_column , view_column = self .rng .choice (pairs )
295+ # The alias keeps the inner reference off the outer target, the
296+ # LIMIT bounds an expensive view body.
297+ return (
298+ f"{ table_column .name (True )} IN (SELECT rtw_src.{ view_column .name (True )} "
299+ f" FROM { view } AS rtw_src LIMIT 100)"
300+ )
301+ return None
302+
265303 def create_system_connection (
266304 self , exe : Executor , num_attempts : int = 10
267305 ) -> Connection :
@@ -1171,9 +1209,17 @@ def run(self, exe: Executor) -> bool:
11711209 if not tables :
11721210 return False
11731211 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 )
1212+ # Reading the insert target itself is the most contended shape a
1213+ # read-then-write can have. A view is the opposite: the target is
1214+ # written but not read, so a REFRESH view alone pins the selection's
1215+ # frontier, see `Action.view_predicate`.
1216+ sources = tables + [
1217+ view
1218+ for view in exe .db .views
1219+ if view .read_then_write_input
1220+ and (not view .temp or view in exe .temp_objects )
1221+ ]
1222+ source = table if self .rng .choice ([True , False ]) else self .rng .choice (sources )
11771223
11781224 column_names = ", " .join (column .name (True ) for column in table .columns )
11791225 # The cast is an identity cast: `expression` returns the requested type
@@ -1464,7 +1510,12 @@ def run(self, exe: Executor) -> bool:
14641510 f"{ c .name (True )} = { expression (c .data_type , table .columns , self .rng , kind = ExprKind .WRITE )} "
14651511 for c in set_columns
14661512 )
1467- query = f"UPDATE { table } SET { set_clause } WHERE { expression (Boolean , table .columns , self .rng , kind = ExprKind .WRITE )} "
1513+ predicate = expression (Boolean , table .columns , self .rng , kind = ExprKind .WRITE )
1514+ if self .rng .random () < 0.2 :
1515+ view_predicate = self .view_predicate (exe , table )
1516+ if view_predicate :
1517+ predicate = f"({ predicate } ) AND { view_predicate } "
1518+ query = f"UPDATE { table } SET { set_clause } WHERE { predicate } "
14681519 if self .rng .choice ([True , False ]):
14691520 self .stmt_id += 1
14701521 self .exe_prepared (query , f"update{ self .stmt_id } " , exe )
@@ -1516,7 +1567,8 @@ def errors_to_ignore(self, exe: Executor) -> list[str]:
15161567 "canceling statement due to statement timeout" ,
15171568 OCC_CONTENTION_EXHAUSTED_ERROR ,
15181569 ] + super ().errors_to_ignore (exe )
1519- if exe .db .scenario == Scenario .Rename :
1570+ # The predicate can name a view, which DDL drops concurrently.
1571+ if exe .db .complexity == Complexity .DDL or exe .db .scenario == Scenario .Rename :
15201572 errors += ["does not exist" ]
15211573 return errors
15221574
@@ -1553,7 +1605,14 @@ def run(self, exe: Executor) -> bool:
15531605 query += f" USING { using_table } "
15541606 query += f" WHERE { expression (Boolean , all_columns , self .rng , kind = ExprKind .WRITE )} "
15551607 elif self .rng .random () < 0.95 :
1556- query += f" WHERE { expression (Boolean , table .columns , self .rng , kind = ExprKind .WRITE )} "
1608+ predicate = expression (
1609+ Boolean , table .columns , self .rng , kind = ExprKind .WRITE
1610+ )
1611+ if self .rng .random () < 0.2 :
1612+ view_predicate = self .view_predicate (exe , table )
1613+ if view_predicate :
1614+ predicate = f"({ predicate } ) AND { view_predicate } "
1615+ query += f" WHERE { predicate } "
15571616 if self .rng .choice ([True , False ]):
15581617 self .stmt_id += 1
15591618 self .exe_prepared (query , f"delete{ self .stmt_id } " , exe )
0 commit comments