Skip to content

[CRITICAL] django-cf D1 backend runs tests against production database, causing data loss #221

Description

@Phat-pham99

Summary

When running Django tests with django-cf's D1 backend (django_cf.d1_api or django_cf.db.backends.d1), Django's test runner does not create a separate test database. Instead, it connects directly to the production D1 database and wipes all data during test setup/teardown.

Impact

This is a critical data-loss bug. My production Cloudflare D1 was wiped out. But luckily the Cloudflare D1 has Time travel enabled by default. Therefore I was able to restore the data.

Steps to Reproduce

  1. Configure Django with the D1 API backend:
DATABASES = {
    'default': {
        'ENGINE': 'django_cf.d1_api',  # or django_cf.db.backends.d1
        'CLOUDFLARE_DATABASE_ID': '...',
        'CLOUDFLARE_ACCOUNT_ID': '...',
        'CLOUDFLARE_TOKEN': '...',
    }
}
  1. Have existing data in the D1 database (e.g., tables from previous migrations).
  2. Run python manage.py test.

Expected Behavior

Django's test runner should either:

  • Create a separate, isolated test database on D1, or
  • Raise a clear error/warning that testing against D1 is unsafe, or
  • Use an in-memory SQLite database for tests by default.

Actual Behavior

The test runner executes migrations, flushes data, and runs tests directly against the configured D1 database. Since D1 does not support database-level cloning or rollback (transactions are disabled), all production data is permanently lost.

Root Cause Analysis

Looking at the D1 backend implementation:

  • In django_cf/db/backends/d1/base.py, DatabaseWrapper inherits from CFDatabaseWrapper.
  • CFDatabaseCreation inherits from Django's SQLiteDatabaseCreation.
  • The backend does not override the test database creation methods (_create_test_db, _destroy_test_db, etc.) to account for D1's serverless nature.
  • Unlike SQLite (which creates a local file), D1 is a remote HTTP API. Django cannot simply create a "test_" prefixed database file.
  • The supports_transactions = False flag is set, but Django still attempts to flush and migrate the "test" schema, which in practice targets the same database.

Environment

  • django-cf version: >=0.1.1 (tested on 0.1.1, likely affects 0.2.x as well)
  • Django version: >=5.0
  • Python version: >=3.10

Suggested Fix

Short-term (Immediate)

Override DatabaseCreation in the D1 backend to raise a ImproperlyConfigured exception if manage.py test is detected, with a message like:

"Running Django tests against Cloudflare D1 is not supported because D1 does not support isolated test databases. Please configure a separate test database (e.g., SQLite or a dedicated D1 database) using the TEST setting."

Long-term

Implement proper test database isolation, such as:

  1. SQLite fallback for tests: If TEST configuration is not provided, default to an in-memory SQLite database for tests.
  2. D1 test database creation: Allow users to specify a separate CLOUDFLARE_DATABASE_ID under DATABASES['default']['TEST'], and create/destroy that via the D1 API during test runs.

Example safe configuration pattern:

DATABASES = {
    'default': {
        'ENGINE': 'django_cf.db.backends.d1',
        'CLOUDFLARE_DATABASE_ID': os.getenv('CLOUDFLARE_DATABASE_ID'),
        'TEST': {
            'ENGINE': 'django.db.backends.sqlite3',
            'NAME': ':memory:',
        },
    }
}

Additional Context

This bug has already caused complete data loss in a production D1 database. I discovered this after running python manage.py test and finding all tables and data erased. Because D1 does not support transactions or point-in-time recovery, the data is unrecoverable.

Please prioritize this issue, as it poses a serious risk to anyone using django-cf with D1 in production.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions