Skip to content

Commit

Permalink
feat: first poc implementation (#2)
Browse files Browse the repository at this point in the history
* Implements a MongoDB backend for Django, which supports CRUD operations, relations, single-table inheritance and single-table OneToOne relations.

Not implemented yet are joins, aggregations (other than count), and embedded document arrays.

* chore: ruff + black + pre-commit

* fix: bug in row item lookup

* fix: getter for null values

* fix: get prep value

* chore: guard rhs sub-query expressions / fix aggregation result

* fix: use proper pk field if order by pk

* chore: reset migrations, mongodb as secondary, better distinct aggreation

* fix: post-filter search results + more docs

* chore: search improvements

* chore: search improvements

* chore: update version tag

* chore: github actions

* Change version to 0.19

* Change version to 0.20

* chore: search query bug

* chore: fmt

* Change version to 0.21

* chore: tests for mongodb search

* fix: wrong init file name

* fix: wrong init file name

* fix: unused fixture

* fix: implements NothingNode

* Change version to 0.22

* chore: refactoring query nodes for reuse

* chore: db prep

* fix: operator rename

* chore: publish

* chore: publish (fix)

* Change version to 0.19

* chore: small cleanup

* chore: skip empty rhs search filters

* chore: add support for autocomplete search

* chore: add shorthand to retrieve collection from cursor

* feat: implements raw mongo query

* feat: search fixes + test update

* chore: update pyproject.toml + django 5.0 support

---------

Co-authored-by: PyPI Poetry Publish Bot <admin@code-specialist.com>
  • Loading branch information
gersmann and PyPI Poetry Publish Bot authored Apr 7, 2024
1 parent 96030a1 commit 55c3faf
Show file tree
Hide file tree
Showing 50 changed files with 2,998 additions and 0 deletions.
50 changes: 50 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Test

on:
push:
branches:
- '*'

jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.11'

- name: Setup poetry cache
uses: actions/cache@v2
with:
path: ~/.cache/pypoetry
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
restore-keys: |
${{ runner.os }}-poetry-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install poetry
poetry config virtualenvs.create false
poetry install
- name: Run linters
run: |
poetry run ruff .
poetry run black --check .
- name: Start MongoDB
uses: supercharge/mongodb-github-action@1.10.0
with:
mongodb-version: 6.0

- name: Run tests
env:
MONGODB_URL: mongodb://localhost:27017
CI: true
run: |
poetry run pytest
22 changes: 22 additions & 0 deletions .github/workflows/test_and_release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Test and release on tag

on:
release:
types: [ published ]

jobs:
publish-service-client-package:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Publish PyPi package
uses: code-specialist/pypi-poetry-publish@v1
with:
ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PUBLISH_REGISTRY_PASSWORD: ${{ secrets.PYPI_TOKEN }}
BRANCH: main
PACKAGE_DIRECTORY: ./django_mongodb/
PYTHON_VERSION: 3.11
POETRY_VERSION: 1.7.1
POETRY_CORE_VERSION: 1.8.1
34 changes: 34 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Mac
.DS_Store

# Windows
Thumbs.db

# Editor
.idea/
.vscode

# vi
*~

# General
log/
*.log

# Django
/db.sqlite3
/.mypy_cache/
.env
/public/
__pycache__

oidc.key
/.ruff_cache/
/copilot/
/.coverage

/media/mail_media/*

/track/data

/dist/
16 changes: 16 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files

- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: "v0.3.5"
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
setup_local_db:
@echo "Creating local database..."
@atlas deployments setup local --type local --port 3307 --force
@echo "Local database created."

start_local_db:
@echo "Starting local database..."
@atlas deployments start local
@echo "Local database started."
162 changes: 162 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
## Django backend for MongoDB

Django backend for MongoDB.

Supports:
- Column mappings to MongoDB documents
- Single table (collection) inheritance and single table OneToOne relationships
- Filters (filter/exclude)

## Setup / Configuration

Not supported as primary database, as Django contrib apps rely on Integer primary keys in built in migrations (and
because it is a use case that is not a priority at the moment).

```python
# settings.py
DATABASES = {
# or any other primary databse
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
},
# mongodb database, Client constructor options are passe in 'CLIENT', the database name in 'NAME'
"mongodb": {
"ENGINE": "django_mongodb",
"NAME": "django_mongodb",
"CONN_MAX_AGE": 120,
"CLIENT": {
"host": os.environ.get("MONGODB_URL"),
},
},
}
# A database is required
DATABASE_ROUTERS = ["testproject.router.DatabaseRouter"]
```

Using the database in models requires a DatabaseRouter, which could look like this
```python
class DatabaseRouter:
def db_for_read(self, model, **hints):
if model._meta.app_label == "mymongoapp":
return "default"
return "default"

def db_for_write(self, model, **hints):
if model._meta.app_label == "mymongoapp":
return "default"
return "default"

def allow_relation(self, obj1, obj2, **hints):
if obj1._meta.app_label == obj2._meta.app_label:
return True
return None

def allow_migrate(self, db, app_label, model_name=None, **hints):
if app_label == "mymongoapp":
# we are disabling migrations, as MongoDB is schema-less. Alerts, such as renaming fields, etc. are not supported
return False
return None
```

Finally we are going to change the default primary key of the app using MongoDB (if that is the case, otherwise add
ObjectIdAutoField to the models, where you need it).

```python
# apps.py
class TestappConfig(AppConfig):
default_auto_field = "django_mongodb.models.ObjectIdAutoField"
name = "mymongoapp"
```

### Defining Models
A simple model, in an app, which has `ObjectIdAutoField` as `default_auto_field`

```python
class MyModel(models.Model):
json_field = JSONField()
name = models.CharField(max_length=100)
datetime_field = models.DateTimeField(auto_now_add=True)
time_field = models.TimeField(auto_now_add=True)
date_field = models.DateField(auto_now_add=True)
```

Single table inheritance

```python
class SameTableChild(MyModel):
my_model_ptr = models.OneToOneField(
MyModel,
on_delete=models.CASCADE,
parent_link=True,
related_name="same_table_child",
# pointer to the primary key of the parent model
db_column="_id",
)
extended = models.CharField(max_length=100)

class Meta:
# We are using the parent collection as db_table
db_table = "mymongoapp_mymodel"
```

Single table `OneToOne` relationships

```python
class SameTableOneToOne(models.Model):
dummy_model = models.OneToOneField(
MyModel,
primary_key=True,
on_delete=models.CASCADE,
related_name="extends",
db_column="_id",
)
extra = models.CharField(max_length=100)

class Meta:
# we are using the same collection to persist one-to-one relationships
db_table = "mymongoapp_mymodel"
```

### Querying

```python
# get all objects
MyModel.objects.all()

# get all objects, which have a name in list ["foo", "bar"]
MyModel.objects.filter(name_in=["foo", "bar"])

# select related with single table inheritance and one to one relationships
MyModel.objects.select_related("same_table_child", "extends").all()

# simple aggregations
MyModel.objects.filter(name_in=["foo", "bar"]).count()
```

### Search
Using the `prefer_search()` extension of MongoQueryset, we can use the `$search` operator of MongoDB to query,
if we have search indexes configured on the model.

```python
MyModel.objects.prefer_search().filter(name="foo").all()
```

PostgreSQL search vectors map down to MongoDB search indexes, so we can use the same syntax as with PostgreSQL.

```python
class MyModel(models.Model):
name = models.CharField(max_length=100)
```

```python
MyModel.objects.annotate(search=SearchVector('name')).filter(search=SearchQuery('foo')).all()
```

### Raw Queries

```python
with connections["mongodb"].cursor() as cursor:
doc = cursor.collections["my_collection"].find_one()
assert isinstance(doc["_id"], ObjectId)
```
1 change: 1 addition & 0 deletions django_mongodb/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.22"
Loading

0 comments on commit 55c3faf

Please sign in to comment.