5555use OCP \PreConditionNotMetException ;
5656use OCP \Profiler \IProfiler ;
5757use Psr \Log \LoggerInterface ;
58- use SensitiveParameter ;
5958
6059class Connection extends PrimaryReadReplicaConnection {
6160 /** @var string */
@@ -80,6 +79,8 @@ class Connection extends PrimaryReadReplicaConnection {
8079 /** @var DbDataCollector|null */
8180 protected $ dbDataCollector = null ;
8281
82+ protected $ tableDirtyWrites = [];
83+
8384 /**
8485 * Initializes a new instance of the Connection class.
8586 *
@@ -256,13 +257,35 @@ public function prepare($sql, $limit = null, $offset = null): Statement {
256257 * @throws \Doctrine\DBAL\Exception
257258 */
258259 public function executeQuery (string $ sql , array $ params = [], $ types = [], QueryCacheProfile $ qcp = null ): Result {
260+ $ tables = $ this ->getQueriedTables ($ sql );
261+ if (count (array_intersect ($ this ->tableDirtyWrites , $ tables )) === 0 && !$ this ->isTransactionActive ()) {
262+ // No tables read that could have been written already in the same request and no transaction active
263+ // so we can switch back to the replica for reading as long as no writes happen that switch back to the primary
264+ $ this ->ensureConnectedToReplica ();
265+ $ this ->logger ->debug ('no dirty table reads: ' . $ sql , ['tables ' => $ this ->tableDirtyWrites , 'reads ' => $ tables ]);
266+ } else {
267+ // Read to a table that was previously written to
268+ // While this might not necessarily mean that we did a read after write it is an indication for a code path to check
269+ $ this ->logger ->debug ('dirty table reads: ' . $ sql , ['tables ' => $ this ->tableDirtyWrites , 'reads ' => $ tables , 'exception ' => new \Exception ()]);
270+ }
271+
259272 $ sql = $ this ->replaceTablePrefix ($ sql );
260273 $ sql = $ this ->adapter ->fixupStatement ($ sql );
261274 $ this ->queriesExecuted ++;
262275 $ this ->logQueryToFile ($ sql );
263276 return parent ::executeQuery ($ sql , $ params , $ types , $ qcp );
264277 }
265278
279+ /**
280+ * Helper function to get the list of tables affected by a given query
281+ * used to track dirty tables that received a write with the current request
282+ */
283+ private function getQueriedTables (string $ sql ): array {
284+ $ re = '/(\*PREFIX\*[A-z0-9_-]+)/mi ' ;
285+ preg_match_all ($ re , $ sql , $ matches , PREG_SET_ORDER );
286+ return array_map ([$ this , 'replaceTablePrefix ' ], $ matches [0 ] ?? []);
287+ }
288+
266289 /**
267290 * @throws Exception
268291 */
@@ -289,6 +312,9 @@ public function executeUpdate(string $sql, array $params = [], array $types = []
289312 * @throws \Doctrine\DBAL\Exception
290313 */
291314 public function executeStatement ($ sql , array $ params = [], array $ types = []): int {
315+ $ tables = $ this ->getQueriedTables ($ sql );
316+ $ this ->tableDirtyWrites = array_merge ($ this ->tableDirtyWrites , $ tables );
317+ $ this ->logger ->error ('dirty table writes: ' . $ sql , ['tables ' => $ this ->tableDirtyWrites ]);
292318 $ sql = $ this ->replaceTablePrefix ($ sql );
293319 $ sql = $ this ->adapter ->fixupStatement ($ sql );
294320 $ this ->queriesExecuted ++;
0 commit comments