Repository: DataDog/dd-trace-py

Issue:
TracedSQLiteCursor should implement SQLite iteration
### Which version of dd-trace-py are you using?

`datadog==0.41.0` and `datadog-lambda==3.60.0`

### Which version of pip are you using?

21.1.3

### Which version of the libraries are you using?

Complete `pip3 freeze` output:

```
attrs==22.1.0
awslambdaric==2.0.4
boto3==1.24.12
botocore==1.27.53
certifi==2022.6.15
chardet==5.0.0
charset-normalizer==2.1.0
datadog==0.41.0
datadog-lambda==3.60.0
ddtrace==0.61.2
decorator==5.1.1
future==0.18.2
idna==3.3
jmespath==1.0.1
mapbox-vector-tile==1.2.1
packaging==21.3
pmtiles==2.0.0
protobuf==3.20.1
pyclipper==1.3.0.post3
pyparsing==3.0.9
pyproj==3.3.1
python-dateutil==2.8.2
requests==2.28.1
s3transfer==0.6.0
sentry-sdk==1.8.0
Shapely==1.8.2
simplejson==3.17.2
six==1.16.0
tenacity==8.0.1
urllib3==1.26.11
wrapt==1.14.1
```

### How can we reproduce your problem?

Run a SQLite query and [use the result of `conn.execute()` as an iterator](https://docs.python.org/3/library/sqlite3.html#cursor-objects):

    row = next(conn.execute('select * from table'))

We are working around this bug by not using SQlite’s iterator feature:

    res = conn.execute('select * from table')
    row = res.fetchone()

### What is the result that you get?

```
ProcessUploadException: 'TracedSQLiteCursor' object is not an iterator
  File "/var/task/lib/python3.9/site-packages/sentry_sdk/integrations/aws_lambda.py", line 164, in sentry_handler
  File "/var/task/lib/python3.9/site-packages/sentry_sdk/_compat.py", line 54, in reraise
  File "/var/task/lib/python3.9/site-packages/sentry_sdk/integrations/aws_lambda.py", line 155, in sentry_handler
  File "/var/task/lib/python3.9/site-packages/datadog_lambda/wrapper.py", line 143, in __call__
  File "/var/task/felt_pipeline/library/instantiate_sentry.py", line 76, in decorated
  File "/var/task/felt_pipeline/lambdas/pipeline_create_feature_tiles.py", line 70, in lambda_handler
  File "/var/task/felt_pipeline/lambdas/pipeline_create_feature_tiles.py", line 109, in mbtiles_convert
```

### What is the result that you expected?

One row from the SQLite table. 



Discussion:

Resolving patch:
diff --git a/ddtrace/contrib/dbapi/__init__.py b/ddtrace/contrib/dbapi/__init__.py
--- a/ddtrace/contrib/dbapi/__init__.py
+++ b/ddtrace/contrib/dbapi/__init__.py
@@ -9,6 +9,7 @@
 from ...constants import SPAN_MEASURED_KEY
 from ...ext import SpanTypes
 from ...ext import sql
+from ...internal.compat import PY2
 from ...internal.logger import get_logger
 from ...internal.utils import ArgumentError
 from ...internal.utils import get_argument_value
@@ -47,6 +48,18 @@ def __init__(self, cursor, pin, cfg):
         self._self_last_execute_operation = None
         self._self_config = cfg or config.dbapi2
 
+    def __iter__(self):
+        return self.__wrapped__.__iter__()
+
+    def __next__(self):
+        return self.__wrapped__.__next__()
+
+    if PY2:
+
+        # Python 2 iterators use `next`
+        def next(self):  # noqa: A001
+            return self.__wrapped__.next()
+
     def _trace_method(self, method, name, resource, extra_tags, *args, **kwargs):
         """
         Internal function to trace the call to the underlying cursor method


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,5 +1,6 @@
 import sqlite3
 import time
+from typing import TYPE_CHECKING
 
 import pytest
 
@@ -18,6 +19,19 @@
 from tests.utils import assert_is_not_measured
 
 
+if TYPE_CHECKING:
+    from typing import Generator
+
+
+@pytest.fixture
+def patched_conn():
+    # type: () -> Generator[sqlite3.Cursor, None, None]
+    patch()
+    conn = sqlite3.connect(":memory:")
+    yield conn
+    unpatch()
+
+
 class TestSQLite(TracerTestCase):
     def setUp(self):
         super(TestSQLite, self).setUp()
@@ -351,3 +365,9 @@ def test_context_manager(self):
             cursor.fetchall()
             spans = self.get_spans()
             assert len(spans) == 1
+
+
+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