Repository: DataDog/dd-trace-py

Issue:
Cannot call sqlite3.backup(db) on a TracedSQLite object
Thanks for taking the time for reporting an issue!

Before reporting an issue on dd-trace-py, please be sure to provide all
necessary information.

If you're hitting a bug, make sure that you're using the latest version of this
library.

### Which version of dd-trace-py are you using?
1.5.0
### Which version of pip are you using?
21.1.1
_ddtrace requires pip>=18 to install one of our pre-built wheels_

### Which version of the libraries are you using?

You can copy/paste the output of `pip freeze` here.

```
ddtrace==1.5.0
```

### How can we reproduce your problem?

```
from ddtrace import config, patch_all
import sqlite3

config.env = "test"  # the environment the application is in
config.service = "app"  # name of your application
config.version = "v1"  # version of your application
patch_all()

src = sqlite3.connect("1.db")
dst = sqlite3.connect("2.db")
with dst:
    src.backup(dst, pages=1)
dst.close()
src.close()
```

### What is the result that you get?

The following TypeError
```
TypeError: backup() argument 1 must be sqlite3.Connection, not TracedSQLite
```

### What is the result that you expected?

The function should succeed without error.



Discussion:
@JLHasson thank you for reporting, I have opened up #4246 to hopefully address this issue.
Thanks for the quick fix! How does the release process for dd-trace work, when will we see this in pypi?
Also, I'm not exactly sure the correct place to report this, but this honestly feels like a but in `sqlite3`. IMO instead of explicitly checking for a `sqlite3.Connection`, they should be doing the equivalent of `isinstance(sqlite3.Connection, o)` which should then work with a `TracedSQLite` since I assume it subclasses `sqlite3.Connection`?
> Thanks for the quick fix! How does the release process for dd-trace work, when will we see this in pypi?

Once the fix PR is merged we will backport and do a bug fix release (currently would be 1.5.1).


> Also, I'm not exactly sure the correct place to report this, but this honestly feels like a but in sqlite3

Personally I am not sure either, but it is fairly common to do strict type checking in C extensions.

https://discuss.python.org/ might be a great place to raise it if you wanted to!

Resolving patch:
diff --git a/ddtrace/contrib/sqlite3/patch.py b/ddtrace/contrib/sqlite3/patch.py
--- a/ddtrace/contrib/sqlite3/patch.py
+++ b/ddtrace/contrib/sqlite3/patch.py
@@ -1,6 +1,7 @@
 import os
 import sqlite3
 import sqlite3.dbapi2
+import sys
 
 from ddtrace import config
 from ddtrace.vendor import wrapt
@@ -75,3 +76,13 @@ def __init__(self, conn, pin=None, cursor_cls=None):
     def execute(self, *args, **kwargs):
         # sqlite has a few extra sugar functions
         return self.cursor().execute(*args, **kwargs)
+
+    # backup was added in Python 3.7
+    if sys.version_info >= (3, 7, 0):
+
+        def backup(self, target, *args, **kwargs):
+            # sqlite3 checks the type of `target`, it cannot be a wrapped connection
+            # https://github.com/python/cpython/blob/4652093e1b816b78e9a585d671a807ce66427417/Modules/_sqlite/connection.c#L1897-L1899
+            if isinstance(target, TracedConnection):
+                target = target.__wrapped__
+            return self.__wrapped__.backup(target, *args, **kwargs)


Test patch:
diff --git a/tests/contrib/sqlite3/test_sqlite3.py b/tests/contrib/sqlite3/test_sqlite3.py
--- a/tests/contrib/sqlite3/test_sqlite3.py
+++ b/tests/contrib/sqlite3/test_sqlite3.py
@@ -1,4 +1,5 @@
 import sqlite3
+import sys
 import time
 from typing import TYPE_CHECKING
 
@@ -371,3 +372,12 @@ def test_iterator_usage(patched_conn):
     """Ensure sqlite3 patched cursors can be used as iterators."""
     rows = next(patched_conn.execute("select 1"))
     assert len(rows) == 1
+
+
+@pytest.mark.skipif(sys.version_info < (3, 7), reason="Connection.backup was added in Python 3.7")
+def test_backup(patched_conn):
+    """Ensure sqlite3 patched connections backup function can be used"""
+    destination = sqlite3.connect(":memory:")
+
+    with destination:
+        patched_conn.backup(destination, pages=1)