@@ -412,6 +412,9 @@ class UsageError(DBError):
412412 display_name = "usage error"
413413
414414
415+ TEMPORARY_DB_PATHS = {':memory:' , '' }
416+
417+
415418class LocalConnectionFactory :
416419 """Maintain a set of connections to the same database, one per thread.
417420
@@ -450,8 +453,6 @@ def __init__(
450453 self .kwargs = kwargs
451454 if kwargs .get ('uri' ): # pragma: no cover
452455 raise NotImplementedError ("is_private() does not work for uri=True" )
453- if path .startswith (':file:' ): # pragma: no cover
454- raise NotImplementedError ("file: connections are not supported" )
455456 self .kwargs ['uri' ] = True
456457 self .attached : dict [str , str ] = {}
457458 self ._local = _LocalConnectionFactoryState ()
@@ -595,25 +596,27 @@ def attach(self, name: str, path: str) -> None:
595596 if name in self .attached : # pragma: no cover
596597 raise ValueError (f"database already attached: { name !r} " )
597598
598- uri_path = self ._make_uri (path )
599- self .attached [name ] = uri_path
599+ self .attached [name ] = path
600600 db = self ._local .db
601601 assert db is not None
602- self ._attach (db , name , uri_path )
602+ self ._attach (db , name , path )
603603
604604 def _attach (self , db : sqlite3 .Connection , name : str , path : str ) -> None :
605- db .execute ("ATTACH DATABASE ? AS ?;" , (path , name ))
605+ db .execute ("ATTACH DATABASE ? AS ?;" , (self . _make_uri ( path ) , name ))
606606
607607 def is_private (self ) -> bool :
608608 return self ._is_private (self .path )
609609
610610 def _make_uri (self , path : str ) -> str :
611- # keeping sqlite special names intact
612- if path in (':memory:' , '' ):
613- return path
614- # make the path full, otherwise it cannot be turned into URI
615- abs_path = Path (path ).resolve (strict = False )
616- uri = abs_path .as_uri ()
611+ # SQLite allows relative paths by skipping the // after file:,
612+ # but pathlib.Path.as_uri() does not support this;
613+ # however, temporary databases *must* be relative
614+ if path in TEMPORARY_DB_PATHS :
615+ # arbitrary paths would need to quote ? to avoid qs injection
616+ uri = f"file:{ path } "
617+ else :
618+ # bypasses SQLite's resolution of relative paths, should be fine
619+ uri = Path (path ).absolute ().as_uri ()
617620
618621 if self .read_only :
619622 uri += "?mode=ro"
@@ -637,7 +640,7 @@ def _is_private(path: str) -> bool:
637640 https://www.sqlite.org/uri.html
638641
639642 """
640- return path in [ ':memory:' , '' ]
643+ return path in TEMPORARY_DB_PATHS
641644
642645
643646class _LocalConnectionFactoryState (threading .local ):
0 commit comments