Repository: rucio/rucio

Issue:
temporary tables should not be used on right side of joins
Motivation
----------

In current form, temporary tables don't have index. The joins may be sub-optimal. Not sure how well the optimizer can take care of this case, but better be explicit about it.

Modification
------------
use the `join_from` sqlalchemy construct instead of `join` to explicitly define the left side of join. 




Discussion:

Resolving patch:
diff --git a/lib/rucio/core/replica.py b/lib/rucio/core/replica.py
--- a/lib/rucio/core/replica.py
+++ b/lib/rucio/core/replica.py
@@ -30,7 +30,7 @@
 import requests
 from dogpile.cache.api import NO_VALUE
 from six import string_types
-from sqlalchemy import func, and_, or_, exists, not_, update, delete, insert, Column
+from sqlalchemy import func, and_, or_, exists, not_, update, delete, insert
 from sqlalchemy.exc import DatabaseError, IntegrityError
 from sqlalchemy.orm import aliased
 from sqlalchemy.orm.exc import FlushError, NoResultFound
@@ -42,7 +42,6 @@
 from rucio.common import exception
 from rucio.common.cache import make_region_memcached
 from rucio.common.config import config_get, config_get_bool
-from rucio.common.schema import get_schema_value
 from rucio.common.types import InternalScope
 from rucio.common.utils import chunks, clean_surls, str_to_date, add_url_query
 from rucio.common.constants import SuspiciousAvailability
@@ -56,8 +55,7 @@
                                      BadFilesStatus, RuleState, BadPFNStatus)
 from rucio.db.sqla.session import (read_session, stream_session, transactional_session,
                                    DEFAULT_SCHEMA_NAME, BASE)
-from rucio.db.sqla.types import InternalScopeString, String
-from rucio.db.sqla.util import create_temp_table
+from rucio.db.sqla.util import create_scope_name_temp_table, create_association_temp_table
 from rucio.rse import rsemanager as rsemgr
 
 REGION = make_region_memcached(expiration_time=60)
@@ -1554,38 +1552,9 @@ def __delete_replicas(rse_id, files, ignore_availability=True, session=None):
         raise exception.ResourceTemporaryUnavailable('%s is temporary unavailable'
                                                      'for deleting' % replica_rse.rse)
 
-    columns = [
-        Column("scope", InternalScopeString(get_schema_value('SCOPE_LENGTH'))),
-        Column("name", String(get_schema_value('NAME_LENGTH'))),
-    ]
-    scope_name_temp_table = create_temp_table(
-        "delete_replicas",
-        *columns,
-        primary_key=columns,
-        session=session,
-    )
-    columns = [
-        Column("scope", InternalScopeString(get_schema_value('SCOPE_LENGTH'))),
-        Column("name", String(get_schema_value('NAME_LENGTH'))),
-    ]
-    scope_name_temp_table2 = create_temp_table(
-        "delete_replicas2",
-        *columns,
-        primary_key=columns,
-        session=session,
-    )
-    columns = [
-        Column("scope", InternalScopeString(get_schema_value('SCOPE_LENGTH'))),
-        Column("name", String(get_schema_value('NAME_LENGTH'))),
-        Column("child_scope", InternalScopeString(get_schema_value('SCOPE_LENGTH'))),
-        Column("child_name", String(get_schema_value('NAME_LENGTH'))),
-    ]
-    association_temp_table = create_temp_table(
-        "delete_replicas_association",
-        *columns,
-        primary_key=columns,
-        session=session,
-    )
+    scope_name_temp_table = create_scope_name_temp_table("delete_replicas", session=session)
+    scope_name_temp_table2 = create_scope_name_temp_table("delete_replicas2", session=session)
+    association_temp_table = create_association_temp_table("delete_replicas_association", session=session)
 
     session.bulk_insert_mappings(scope_name_temp_table, [{'scope': file['scope'], 'name': file['name']} for file in files])
 
@@ -1605,8 +1574,9 @@ def __delete_replicas(rse_id, files, ignore_availability=True, session=None):
     stmt = select(
         func.count(),
         func.sum(models.RSEFileAssociation.bytes),
-    ).join(
+    ).join_from(
         scope_name_temp_table,
+        models.RSEFileAssociation,
         and_(models.RSEFileAssociation.scope == scope_name_temp_table.scope,
              models.RSEFileAssociation.name == scope_name_temp_table.name,
              models.RSEFileAssociation.rse_id == rse_id)
@@ -1700,8 +1670,9 @@ def __cleanup_after_replica_deletion(scope_name_temp_table, scope_name_temp_tabl
         models.DataIdentifierAssociation.scope,
         models.DataIdentifierAssociation.name,
     ).distinct(
-    ).join(
+    ).join_from(
         scope_name_temp_table,
+        models.DataIdentifierAssociation,
         and_(scope_name_temp_table.scope == models.DataIdentifierAssociation.child_scope,
              scope_name_temp_table.name == models.DataIdentifierAssociation.child_name)
     ).join(
@@ -1732,8 +1703,9 @@ def __cleanup_after_replica_deletion(scope_name_temp_table, scope_name_temp_tabl
             models.DataIdentifierAssociation.child_scope,
             models.DataIdentifierAssociation.child_name,
         ).distinct(
-        ).join(
+        ).join_from(
             scope_name_temp_table,
+            models.DataIdentifierAssociation,
             and_(scope_name_temp_table.scope == models.DataIdentifierAssociation.child_scope,
                  scope_name_temp_table.name == models.DataIdentifierAssociation.child_name)
         ).outerjoin(
@@ -1810,8 +1782,9 @@ def __cleanup_after_replica_deletion(scope_name_temp_table, scope_name_temp_tabl
                 models.DataIdentifier.name,
                 models.DataIdentifier.did_type,
             ).distinct(
-            ).join(
+            ).join_from(
                 association_temp_table,
+                models.DataIdentifier,
                 and_(association_temp_table.scope == models.DataIdentifier.scope,
                      association_temp_table.name == models.DataIdentifier.name)
             ).where(
@@ -1852,8 +1825,9 @@ def __cleanup_after_replica_deletion(scope_name_temp_table, scope_name_temp_tabl
         models.CollectionReplica.scope,
         models.CollectionReplica.name,
     ).distinct(
-    ).join(
+    ).join_from(
         scope_name_temp_table,
+        models.CollectionReplica,
         and_(scope_name_temp_table.scope == models.CollectionReplica.scope,
              scope_name_temp_table.name == models.CollectionReplica.name),
     ).join(
@@ -1923,8 +1897,9 @@ def __cleanup_after_replica_deletion(scope_name_temp_table, scope_name_temp_tabl
     stmt = select(
         models.ConstituentAssociation
     ).distinct(
-    ).join(
+    ).join_from(
         scope_name_temp_table,
+        models.ConstituentAssociation,
         and_(scope_name_temp_table.scope == models.ConstituentAssociation.scope,
              scope_name_temp_table.name == models.ConstituentAssociation.name),
     ).outerjoin(
@@ -2051,8 +2026,9 @@ def __cleanup_after_replica_deletion(scope_name_temp_table, scope_name_temp_tabl
         ).distinct(
         ).where(
             models.DataIdentifier.is_archive == true()
-        ).join(
+        ).join_from(
             scope_name_temp_table,
+            models.DataIdentifier,
             and_(scope_name_temp_table.scope == models.DataIdentifier.scope,
                  scope_name_temp_table.name == models.DataIdentifier.name)
         ).join(
@@ -2541,16 +2517,7 @@ def list_and_mark_unlocked_replicas(limit, bytes_=None, rse_id=None, delay_secon
     total_bytes = 0
     rows = []
 
-    columns = [
-        Column("scope", InternalScopeString(get_schema_value('SCOPE_LENGTH'))),
-        Column("name", String(get_schema_value('NAME_LENGTH'))),
-    ]
-    temp_table_cls = create_temp_table(
-        "list_and_mark_unlocked_replicas",
-        *columns,
-        primary_key=columns,
-        session=session,
-    )
+    temp_table_cls = create_scope_name_temp_table("list_and_mark_unlocked_replicas", session=session)
 
     replicas_alias = aliased(models.RSEFileAssociation, name='replicas_alias')
 
@@ -2595,8 +2562,9 @@ def list_and_mark_unlocked_replicas(limit, bytes_=None, rse_id=None, delay_secon
             models.RSEFileAssociation.bytes,
             models.RSEFileAssociation.tombstone,
             models.RSEFileAssociation.state,
-        ).join(
+        ).join_from(
             temp_table_cls,
+            models.RSEFileAssociation,
             and_(models.RSEFileAssociation.scope == temp_table_cls.scope,
                  models.RSEFileAssociation.name == temp_table_cls.name,
                  models.RSEFileAssociation.rse_id == rse_id)
diff --git a/lib/rucio/core/request.py b/lib/rucio/core/request.py
--- a/lib/rucio/core/request.py
+++ b/lib/rucio/core/request.py
@@ -39,7 +39,7 @@
 from rucio.db.sqla import models, filter_thread_work
 from rucio.db.sqla.constants import RequestState, RequestType, LockState, RequestErrMsg, ReplicaState
 from rucio.db.sqla.session import read_session, transactional_session, stream_session
-from rucio.db.sqla.util import create_temp_table
+from rucio.db.sqla.util import create_id_temp_table
 
 RequestAndState = namedtuple('RequestAndState', ['request_id', 'request_state'])
 
@@ -413,15 +413,11 @@ def list_transfer_requests_and_source_replicas(
 
     use_temp_tables = config_get_bool('core', 'use_temp_tables', default=False)
     if rses and use_temp_tables:
-        temp_table_cls = create_temp_table(
-            "list_transfer_requests_and_source_replicas",
-            models.Column("rse_id", models.GUID()),
-            session=session,
-        )
+        temp_table_cls = create_id_temp_table("list_transfer_requests_and_source_replicas", session=session)
 
-        session.bulk_insert_mappings(temp_table_cls, [{'rse_id': rse_id} for rse_id in rses])
+        session.bulk_insert_mappings(temp_table_cls, [{'id': rse_id} for rse_id in rses])
 
-        sub_requests = sub_requests.join(temp_table_cls, temp_table_cls.rse_id == models.RSE.id)
+        sub_requests = sub_requests.join(temp_table_cls, temp_table_cls.id == models.RSE.id)
 
     sub_requests = filter_thread_work(session=session, query=sub_requests, total_threads=total_workers, thread_id=worker_number, hash_variable=partition_hash_var)
 
diff --git a/lib/rucio/db/sqla/util.py b/lib/rucio/db/sqla/util.py
--- a/lib/rucio/db/sqla/util.py
+++ b/lib/rucio/db/sqla/util.py
@@ -23,7 +23,7 @@
 import sqlalchemy
 from alembic import command, op
 from alembic.config import Config
-from sqlalchemy import func, inspect
+from sqlalchemy import func, inspect, Column
 from sqlalchemy.dialects.postgresql.base import PGInspector
 from sqlalchemy.exc import IntegrityError, DatabaseError
 from sqlalchemy.ext.declarative import declarative_base
@@ -33,11 +33,13 @@
 
 from rucio import alembicrevision
 from rucio.common.config import config_get
+from rucio.common.schema import get_schema_value
 from rucio.common.types import InternalAccount
 from rucio.core.account_counter import create_counters_for_new_account
 from rucio.db.sqla import models
 from rucio.db.sqla.constants import AccountStatus, AccountType, IdentityType
 from rucio.db.sqla.session import get_engine, get_session, get_dump_engine
+from rucio.db.sqla.types import InternalScopeString, String
 
 if TYPE_CHECKING:
     from typing import Optional, Union  # noqa: F401
@@ -353,3 +355,51 @@ class DeclarativeObj(base):
         session.execute(CreateTable(table, if_not_exists=True))
         session.query(DeclarativeObj).delete()
     return DeclarativeObj
+
+
+def create_scope_name_temp_table(name, session):
+    """
+    Create a temporary table with columns 'scope' and 'name'
+    """
+
+    columns = [
+        Column("scope", InternalScopeString(get_schema_value('SCOPE_LENGTH'))),
+        Column("name", String(get_schema_value('NAME_LENGTH'))),
+    ]
+    return create_temp_table(
+        name,
+        *columns,
+        primary_key=columns,
+        session=session,
+    )
+
+
+def create_association_temp_table(name, session):
+    """
+    Create a temporary table with columns 'scope', 'name', 'child_scope'and 'child_name'
+    """
+
+    columns = [
+        Column("scope", InternalScopeString(get_schema_value('SCOPE_LENGTH'))),
+        Column("name", String(get_schema_value('NAME_LENGTH'))),
+        Column("child_scope", InternalScopeString(get_schema_value('SCOPE_LENGTH'))),
+        Column("child_name", String(get_schema_value('NAME_LENGTH'))),
+    ]
+    return create_temp_table(
+        name,
+        *columns,
+        primary_key=columns,
+        session=session,
+    )
+
+
+def create_id_temp_table(name, session):
+    """
+    Create a temp table with a single id column of uuid type
+    """
+
+    return create_temp_table(
+        name,
+        Column("id", models.GUID()),
+        session=session,
+    )


Test patch: