Repository: litestar-org/litestar

Issue:
StaticFilesConfig and virtual directories
I'm trying to write a ``FileSystemProtocol`` to load files from the package data using [importlib_resources](https://importlib-resources.readthedocs.io/en/latest/using.html#). But because ``directories`` is defined as ``DirectoryPath``, pydantic checks if the given directories exist in the local filesystem. 

This is not generally true, especially in any kind of virtual filesystem (e.g. a zipped package). I think this condition should be relaxed to support virtual filesystems.

https://github.com/starlite-api/starlite/blob/9bb6dcd57c10a591377cf8e3a537e9292566d5b9/starlite/config/static_files.py#L32


Discussion:
I agree. Do you want to submit a PR?

Resolving patch:
diff --git a/litestar/contrib/sqlalchemy/repository.py b/litestar/contrib/sqlalchemy/repository.py
--- a/litestar/contrib/sqlalchemy/repository.py
+++ b/litestar/contrib/sqlalchemy/repository.py
@@ -345,7 +345,7 @@ async def update_many(self, data: list[ModelT]) -> list[ModelT]:
         """
         data_to_update: list[dict[str, Any]] = [v.to_dict() if isinstance(v, self.model_type) else v for v in data]  # type: ignore
         with wrap_sqlalchemy_exception():
-            if self.session.bind.dialect.update_executemany_returning:
+            if self.session.bind.dialect.update_executemany_returning and self.session.bind.dialect.name != "oracle":
                 instances = list(
                     await self.session.scalars(  # type: ignore
                         update(self.model_type).returning(self.model_type),
@@ -831,7 +831,11 @@ def update_many(self, data: list[ModelT]) -> list[ModelT]:
         """
         data_to_update: list[dict[str, Any]] = [v.to_dict() if isinstance(v, self.model_type) else v for v in data]  # type: ignore
         with wrap_sqlalchemy_exception():
-            if self.session.bind and self.session.bind.dialect.update_executemany_returning:
+            if (
+                self.session.bind
+                and self.session.bind.dialect.update_executemany_returning
+                and self.session.bind.dialect.name != "oracle"
+            ):
                 instances = list(
                     self.session.scalars(
                         update(self.model_type).returning(self.model_type),
diff --git a/litestar/contrib/sqlalchemy/types.py b/litestar/contrib/sqlalchemy/types.py
--- a/litestar/contrib/sqlalchemy/types.py
+++ b/litestar/contrib/sqlalchemy/types.py
@@ -3,6 +3,8 @@
 import uuid
 from typing import TYPE_CHECKING, Any, cast
 
+from sqlalchemy.dialects.oracle import BLOB as ORA_BLOB
+from sqlalchemy.dialects.oracle import RAW as ORA_RAW
 from sqlalchemy.dialects.postgresql import JSONB as PG_JSONB
 from sqlalchemy.dialects.postgresql import UUID as PG_UUID
 from sqlalchemy.types import BINARY, CHAR, BigInteger, Integer, TypeDecorator
@@ -24,7 +26,7 @@
 class GUID(TypeDecorator):
     """Platform-independent GUID type.
 
-    Uses PostgreSQL's UUID type, otherwise uses
+    Uses PostgreSQL's UUID type, Oracle's RAW(16) type, otherwise uses
     BINARY(16) or CHAR(32), storing as stringified hex values.
 
     Will accept stringified UUIDs as a hexstring or an actual UUID
@@ -41,6 +43,8 @@ def __init__(self, *args: Any, binary: bool = True, **kwargs: Any) -> None:
     def load_dialect_impl(self, dialect: Dialect) -> Any:
         if dialect.name == "postgresql":
             return dialect.type_descriptor(PG_UUID())
+        if dialect.name == "oracle":
+            return dialect.type_descriptor(ORA_RAW(16))
         if self.binary:
             return dialect.type_descriptor(BINARY(16))
         return dialect.type_descriptor(CHAR(32))
@@ -53,6 +57,8 @@ def process_bind_param(self, value: bytes | str | uuid.UUID | None, dialect: Dia
         value = self.to_uuid(value)
         if value is None:
             return value
+        if dialect.name == "oracle":
+            return value.bytes
         return value.bytes if self.binary else value.hex
 
     def process_result_value(self, value: bytes | str | uuid.UUID | None, dialect: Dialect) -> uuid.UUID | None:
@@ -75,8 +81,8 @@ def to_uuid(value: Any) -> uuid.UUID | None:
         return cast("uuid.UUID | None", value)
 
 
-JSON = _JSON().with_variant(PG_JSONB, "postgresql")
+JSON = _JSON().with_variant(PG_JSONB, "postgresql").with_variant(ORA_BLOB, "oracle")
 """Platform-independent JSON type.
 
-    Uses JSONB type for postgres, otherwise uses the generic JSON data type.
+    Uses JSONB type for postgres, BLOB for Oracle, otherwise uses the generic JSON data type.
 """


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -23,13 +23,16 @@
     cast,
 )
 
+import anyio
 import asyncmy
 import asyncpg
+import oracledb
 import pytest
 from _pytest.fixtures import FixtureRequest
 from _pytest.nodes import Item
 from fakeredis.aioredis import FakeRedis
 from freezegun import freeze_time
+from oracledb.exceptions import DatabaseError, OperationalError
 from pytest_docker.plugin import Services
 from pytest_lazyfixture import lazy_fixture
 from redis.asyncio import Redis as AsyncRedis
@@ -50,6 +53,7 @@
 from litestar.stores.memory import MemoryStore
 from litestar.stores.redis import RedisStore
 from litestar.testing import RequestFactory
+from litestar.utils.sync import AsyncCallable
 
 if TYPE_CHECKING:
     from types import ModuleType
@@ -479,6 +483,45 @@ async def postgres_service(docker_ip: str, docker_services: Services) -> None:
     await wait_until_responsive(timeout=30.0, pause=0.1, check=postgres_responsive, host=docker_ip)
 
 
+def oracle_responsive(host: str) -> bool:
+    """
+    Args:
+        host: docker IP address.
+
+    Returns:
+        Boolean indicating if we can connect to the database.
+    """
+
+    try:
+        conn = oracledb.connect(
+            host=host,
+            port=1512,
+            user="app",
+            service_name="xepdb1",
+            password="super-secret",
+        )
+        with conn.cursor() as cursor:
+            cursor.execute("SELECT 1 FROM dual")
+            resp = cursor.fetchone()
+        return bool(resp[0] == 1)
+    except (OperationalError, DatabaseError):  # pyright: ignore
+        return False
+
+
+@docker_service_registry.register("oracle")
+async def oracle_service(docker_ip: str, docker_services: Services) -> None:  # pylint: disable=unused-argument
+    """Starts containers for required services, fixture waits until they are
+    responsive before returning.
+
+    Args:
+        docker_ip:
+        docker_services:
+    """
+    # oracle takes a while to mount and open initially.
+    await anyio.sleep(20)
+    await wait_until_responsive(timeout=30.0, pause=0.1, check=AsyncCallable(oracle_responsive), host=docker_ip)
+
+
 # the monkeypatch fixture does not work with session scoped dependencies
 @pytest.fixture(autouse=True, scope="session")
 def disable_warn_implicit_sync_to_thread() -> Generator[None, None, None]:
diff --git a/tests/contrib/sqlalchemy/repository/sqlalchemy_sync_tests.py b/tests/contrib/sqlalchemy/repository/sqlalchemy_sync_tests.py
--- a/tests/contrib/sqlalchemy/repository/sqlalchemy_sync_tests.py
+++ b/tests/contrib/sqlalchemy/repository/sqlalchemy_sync_tests.py
@@ -43,9 +43,12 @@ def seed_db(
     with engine.begin() as conn:
         base.orm_registry.metadata.drop_all(conn)
         base.orm_registry.metadata.create_all(conn)
-        conn.execute(insert(Author).values(raw_authors))
-        conn.execute(insert(Ingredient).values(raw_ingredients))
-        conn.execute(insert(Store).values(raw_stores))
+        for author in raw_authors:
+            conn.execute(insert(Author).values(author))
+        for ingredient in raw_ingredients:
+            conn.execute(insert(Ingredient).values(ingredient))
+        for store in raw_stores:
+            conn.execute(insert(Store).values(store))
 
 
 def test_filter_by_kwargs_with_incorrect_attribute_name(author_repo: AuthorSyncRepository) -> None:
diff --git a/tests/contrib/sqlalchemy/repository/test_sqlalchemy_oracledb.py b/tests/contrib/sqlalchemy/repository/test_sqlalchemy_oracledb.py
new file mode 100644
--- /dev/null
+++ b/tests/contrib/sqlalchemy/repository/test_sqlalchemy_oracledb.py
@@ -0,0 +1,306 @@
+"""Unit tests for the SQLAlchemy Repository implementation for psycopg."""
+from __future__ import annotations
+
+import platform
+import sys
+from typing import Any, Generator
+
+import pytest
+from sqlalchemy import Engine, NullPool, create_engine
+from sqlalchemy.orm import Session, sessionmaker
+
+from tests.contrib.sqlalchemy.models import (
+    AuthorSyncRepository,
+    BookSyncRepository,
+    IngredientSyncRepository,
+    StoreSyncRepository,
+)
+from tests.contrib.sqlalchemy.repository import sqlalchemy_sync_tests as st
+
+pytestmark = [
+    pytest.mark.skipif(sys.platform != "linux", reason="docker not available on this platform"),
+    pytest.mark.skipif(platform.uname()[4] != "x86_64", reason="oracle not available on this platform"),
+    pytest.mark.usefixtures("oracle_service"),
+]
+
+
+@pytest.mark.sqlalchemy_oracledb
+@pytest.fixture(name="engine")
+def fx_engine(docker_ip: str) -> Engine:
+    """Postgresql instance for end-to-end testing.
+
+    Args:
+        docker_ip: IP address for TCP connection to Docker containers.
+
+    Returns:
+        Async SQLAlchemy engine instance.
+    """
+    return create_engine(
+        "oracle+oracledb://:@",
+        thick_mode=False,
+        connect_args={
+            "user": "app",
+            "password": "super-secret",
+            "host": docker_ip,
+            "port": 1512,
+            "service_name": "xepdb1",
+            "encoding": "UTF-8",
+            "nencoding": "UTF-8",
+        },
+        echo=True,
+        poolclass=NullPool,
+    )
+
+
+@pytest.mark.sqlalchemy_oracledb
+@pytest.fixture(
+    name="session",
+)
+def fx_session(
+    engine: Engine,
+    raw_authors: list[dict[str, Any]],
+    raw_books: list[dict[str, Any]],
+    raw_stores: list[dict[str, Any]],
+    raw_ingredients: list[dict[str, Any]],
+) -> Generator[Session, None, None]:
+    session = sessionmaker(bind=engine)()
+    st.seed_db(engine, raw_authors, raw_books, raw_stores, raw_ingredients)
+    try:
+        yield session
+    finally:
+        session.rollback()
+        session.close()
+
+
+@pytest.mark.sqlalchemy_oracledb
+@pytest.fixture(name="author_repo")
+def fx_author_repo(session: Session) -> AuthorSyncRepository:
+    return AuthorSyncRepository(session=session)
+
+
+@pytest.mark.sqlalchemy_oracledb
+@pytest.fixture(name="book_repo")
+def fx_book_repo(session: Session) -> BookSyncRepository:
+    return BookSyncRepository(session=session)
+
+
+@pytest.fixture(name="store_repo")
+def fx_store_repo(session: Session) -> StoreSyncRepository:
+    return StoreSyncRepository(session=session)
+
+
+@pytest.fixture(name="ingredient_repo")
+def fx_ingredient_repo(session: Session) -> IngredientSyncRepository:
+    return IngredientSyncRepository(session=session)
+
+
+@pytest.mark.sqlalchemy_oracledb
+def test_filter_by_kwargs_with_incorrect_attribute_name(author_repo: AuthorSyncRepository) -> None:
+    """Test SQLALchemy filter by kwargs with invalid column name.
+
+    Args:
+        author_repo (AuthorRepository): The author mock repository
+    """
+    st.test_filter_by_kwargs_with_incorrect_attribute_name(author_repo=author_repo)
+
+
+@pytest.mark.sqlalchemy_oracledb
+def test_repo_count_method(author_repo: AuthorSyncRepository, store_repo: StoreSyncRepository) -> None:
+    """Test SQLALchemy count.
+
+    Args:
+        author_repo (AuthorRepository): The author mock repository
+    """
+    st.test_repo_count_method(author_repo=author_repo, store_repo=store_repo)
+
+
+@pytest.mark.sqlalchemy_oracledb
+def test_repo_list_and_count_method(
+    raw_authors: list[dict[str, Any]],
+    author_repo: AuthorSyncRepository,
+    raw_stores: list[dict[str, Any]],
+    store_repo: StoreSyncRepository,
+) -> None:
+    """Test SQLALchemy list with count.
+
+    Args:
+        raw_authors (list[dict[str, Any]]): list of authors pre-seeded into the mock repository
+        author_repo (AuthorRepository): The author mock repository
+        raw_stores (list[dict[str, Any]]): list of stores pre-seeded into the mock repository
+        store_repo (StoreRepository): The store mock repository
+    """
+    st.test_repo_list_and_count_method(
+        raw_authors=raw_authors, author_repo=author_repo, raw_stores=raw_stores, store_repo=store_repo
+    )
+
+
+@pytest.mark.sqlalchemy_oracledb
+def test_repo_list_and_count_method_empty(book_repo: BookSyncRepository) -> None:
+    """Test SQLALchemy list with count.
+
+    Args:
+        raw_authors (list[dict[str, Any]]): list of authors pre-seeded into the mock repository
+        author_repo (AuthorRepository): The author mock repository
+    """
+
+    st.test_repo_list_and_count_method_empty(book_repo=book_repo)
+
+
+@pytest.mark.sqlalchemy_oracledb
+def test_repo_list_method(
+    raw_authors: list[dict[str, Any]],
+    author_repo: AuthorSyncRepository,
+    raw_stores: list[dict[str, Any]],
+    store_repo: StoreSyncRepository,
+) -> None:
+    """Test SQLALchemy list.
+
+    Args:
+        raw_authors (list[dict[str, Any]]): list of authors pre-seeded into the mock repository
+        author_repo (AuthorRepository): The author mock repository
+        raw_stores (list[dict[str, Any]]): list of stores pre-seeded into the mock repository
+        store_repo (StoreRepository): The store mock repository
+    """
+    st.test_repo_list_method(
+        raw_authors=raw_authors, author_repo=author_repo, raw_stores=raw_stores, store_repo=store_repo
+    )
+
+
+@pytest.mark.sqlalchemy_oracledb
+def test_repo_add_method(
+    raw_authors: list[dict[str, Any]],
+    author_repo: AuthorSyncRepository,
+    raw_stores: list[dict[str, Any]],
+    store_repo: StoreSyncRepository,
+) -> None:
+    """Test SQLALchemy list.
+
+    Args:
+        raw_authors (list[dict[str, Any]]): list of authors pre-seeded into the mock repository
+        author_repo (AuthorRepository): The author mock repository
+        raw_stores (list[dict[str, Any]]): list of stores pre-seeded into the mock repository
+        store_repo (StoreRepository): The store mock repository
+    """
+    st.test_repo_add_method(
+        raw_authors=raw_authors, author_repo=author_repo, raw_stores=raw_stores, store_repo=store_repo
+    )
+
+
+@pytest.mark.sqlalchemy_oracledb
+def test_repo_add_many_method(raw_authors: list[dict[str, Any]], author_repo: AuthorSyncRepository) -> None:
+    """Test SQLALchemy Add Many.
+
+    Args:
+        raw_authors (list[dict[str, Any]]): list of authors pre-seeded into the mock repository
+        author_repo (AuthorRepository): The author mock repository
+    """
+    st.test_repo_add_many_method(raw_authors=raw_authors, author_repo=author_repo)
+
+
+@pytest.mark.sqlalchemy_oracledb
+def test_repo_update_many_method(author_repo: AuthorSyncRepository) -> None:
+    """Test SQLALchemy Update Many.
+
+    Args:
+        author_repo (AuthorRepository): The author mock repository
+    """
+    st.test_repo_update_many_method(author_repo=author_repo)
+
+
+@pytest.mark.sqlalchemy_oracledb
+def test_repo_exists_method(author_repo: AuthorSyncRepository) -> None:
+    """Test SQLALchemy exists.
+
+    Args:
+        author_repo (AuthorRepository): The author mock repository
+    """
+    st.test_repo_exists_method(author_repo=author_repo)
+
+
+@pytest.mark.sqlalchemy_oracledb
+def test_repo_update_method(author_repo: AuthorSyncRepository) -> None:
+    """Test SQLALchemy Update.
+
+    Args:
+        author_repo (AuthorRepository): The author mock repository
+    """
+    st.test_repo_update_method(author_repo=author_repo)
+
+
+@pytest.mark.sqlalchemy_oracledb
+def test_repo_delete_method(author_repo: AuthorSyncRepository) -> None:
+    """Test SQLALchemy delete.
+
+    Args:
+        author_repo (AuthorRepository): The author mock repository
+    """
+    st.test_repo_delete_method(author_repo=author_repo)
+
+
+@pytest.mark.sqlalchemy_oracledb
+def test_repo_delete_many_method(author_repo: AuthorSyncRepository) -> None:
+    """Test SQLALchemy delete many.
+
+    Args:
+        author_repo (AuthorRepository): The author mock repository
+    """
+    st.test_repo_delete_many_method(author_repo=author_repo)
+
+
+@pytest.mark.sqlalchemy_oracledb
+def test_repo_get_method(author_repo: AuthorSyncRepository) -> None:
+    """Test SQLALchemy Get.
+
+    Args:
+        author_repo (AuthorRepository): The author mock repository
+    """
+    st.test_repo_get_method(author_repo=author_repo)
+
+
+@pytest.mark.sqlalchemy_oracledb
+def test_repo_get_one_or_none_method(author_repo: AuthorSyncRepository) -> None:
+    """Test SQLALchemy Get One.
+
+    Args:
+        author_repo (AuthorRepository): The author mock repository
+    """
+    st.test_repo_get_one_or_none_method(author_repo=author_repo)
+
+
+@pytest.mark.sqlalchemy_oracledb
+def test_repo_get_one_method(author_repo: AuthorSyncRepository) -> None:
+    """Test SQLALchemy Get One.
+
+    Args:
+        author_repo (AuthorRepository): The author mock repository
+    """
+    st.test_repo_get_one_method(author_repo=author_repo)
+
+
+@pytest.mark.sqlalchemy_oracledb
+def test_repo_get_or_create_method(author_repo: AuthorSyncRepository) -> None:
+    """Test SQLALchemy Get or create.
+
+    Args:
+        author_repo (AuthorRepository): The author mock repository
+    """
+    st.test_repo_get_or_create_method(author_repo=author_repo)
+
+
+@pytest.mark.sqlalchemy_oracledb
+def test_repo_get_or_create_match_filter(author_repo: AuthorSyncRepository) -> None:
+    """Test SQLALchemy Get or create with a match filter
+
+    Args:
+        author_repo (AuthorRepository): The author mock repository
+    """
+    st.test_repo_get_or_create_match_filter(author_repo=author_repo)
+
+
+def test_repo_upsert_method(author_repo: AuthorSyncRepository) -> None:
+    """Test SQLALchemy upsert.
+
+    Args:
+        author_repo (AuthorRepository): The author mock repository
+    """
+    st.test_repo_upsert_method(author_repo=author_repo)
diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml
--- a/tests/docker-compose.yml
+++ b/tests/docker-compose.yml
@@ -18,6 +18,14 @@ services:
       MYSQL_DATABASE: db
       MYSQL_ROOT_HOST: "%"
       LANG: C.UTF-8
+  oracle:
+    image: gvenzl/oracle-xe:latest-faststart
+    ports:
+      - "1512:1521" # use a non-standard port here
+    environment:
+      ORACLE_PASSWORD: super-secret
+      APP_USER_PASSWORD: super-secret
+      APP_USER: app
   redis:
     image: redis
     ports: