Skip to content

Commit

Permalink
Merge pull request #17 from tarsil/feature/add_index
Browse files Browse the repository at this point in the history
Feature/add index
  • Loading branch information
tarsil authored Feb 20, 2023
2 parents ccee3c6 + 9f638a0 commit 3e04a4d
Show file tree
Hide file tree
Showing 14 changed files with 352 additions and 47 deletions.
128 changes: 90 additions & 38 deletions docs/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,17 @@ field is **mandatory** and it will raise an `ImproperlyConfigured` error if no r

<sup>Default: `name of class pluralised`<sup>

* **unique_together** - The unique constrainsts for your model.

<sup>Default: `None`<sup>

* **abstract** - If the model is abstract or not. If is abstract, then it won't generate the
database table.

<sup>Default: `False`<sup>

* **unique_together** - The unique constrainsts for your model.

<sup>Default: `None`<sup>

* **indexes** - The extra custom indexes you want to add to the model

### Registry

Working with a [registry](./registry.md) is what makes **Saffier** dynamic and very flexible with
Expand Down Expand Up @@ -206,6 +208,60 @@ In this example, the `User` class will be represented by a `db_users` mapping in
in your codebase. The tablename is used **solely for SQL internal purposes**. You will
still access the given table in your codebase via main class.


### Abstract

As the name suggests, it is when you want to declare an abstract model.

Why do you need an abstract model in the first place? Well, for the same reason when you need to
declare an abstract class in python but for this case you simply don't want to generate a table
from that model declaration.

This can be useful if you want to hold common functionality across models and don't want to repeat
yourself.

The way of declaring an abstract model in **Saffier** is by passing `True` to the `abstract`
attribute in the [meta](#the-meta-class) class.

#### In a nutshell

In this document we already mentioned abstract models and how to use them but let us use some more
examples to be even clear.


```python hl_lines="10"
{!> ../docs_src/models/abstract/simple.py !}
```

This model itself does not do much alone. This simply creates a `BaseModel` and declares the
[registry](#registry) as well as declares the `abstract` as `True`.

#### Use abstract models to hold common functionality

Taking advantage of the abstract models to hold common functionality is usually the common use
case for these to be use in the first place.

Let us see a more complex example and how to use it.

```python hl_lines="10"
{!> ../docs_src/models/abstract/common.py !}
```

This is already quite a complex example where `User` and `Product` have both common functionality
like the `id` and `description` as well the `get_description()` function.

#### Limitations

You can do **almost everything** with abstract models and emphasis in **almost**.

Abstract models do not allow you to:

* **Declare** [managers](./managers.md).
* **Declare** [unique together](#unique-together)

This limitations are intentional as these operations should be done for [models](#declaring-models)
and not abstact models.

### Unique together

This is a very powerful tool being used by almost every single SQL database out there and extremely
Expand Down Expand Up @@ -305,55 +361,51 @@ This will make sure that `is_active` is also unique

For this we used a **list of tuples of strings as well as strings**.

## Abstract

As the name suggests, it is when you want to declare an abstract model.
### Indexes

Why do you need an abstract model in the first place? Well, for the same reason when you need to
declare an abstract class in python but for this case you simply don't want to generate a table
from that model declaration.
Sometimes you might want to add specific designed indexes to your models. Database indexes also
somes with costs and you **should always be careful** when creating one.

This can be useful if you want to hold common functionality across models and don't want to repeat
yourself.
If you are familiar with indexes you know what this means but if you are not, just have a quick
[read](https://www.codecademy.com/article/sql-indexes) and get yourself familiar.

The way of declaring an abstract model in **Saffier** is by passing `True` to the `abstract`
attribute in the [meta](#the-meta-class) class.
There are different ways of declaring an index.

### In a nutshell
Saffier provides an `Index` object that must be used when declaring models indexes or a
`ValueError` is raised.

In this document we already mentioned abstract models and how to use them but let us use some more
examples to be even clear.
```python
from saffier import Index
```
#### Parameters

The `Index` parameters are:

```python hl_lines="10"
{!> ../docs_src/models/abstract/simple.py !}
```
* **fields** - List of model fields in a string format.
* **name** - The name of the new index. If no name is provided, it will generate one, snake case
with a suffix `_idx` in the end. Example: `name_email_idx`.
* **suffix** - The suffix used to generate the index name when the `name` value is not provided.

This model itself does not do much alone. This simply creates a `BaseModel` and declares the
[registry](#registry) as well as declares the `abstract` as `True`.
Let us see some examples.

### Use abstract models to hold common functionality
#### Simple index

Taking advantage of the abstract models to hold common functionality is usually the common use
case for these to be use in the first place.
The simplest and cleanest way of declaring an index with **Saffier**. You declare it directly in
the model field.

Let us see a more complex example and how to use it.

```python hl_lines="10"
{!> ../docs_src/models/abstract/common.py !}
{!> ../docs_src/models/indexes/simple.py !}
```

This is already quite a complex example where `User` and `Product` have both common functionality
like the `id` and `description` as well the `get_description()` function.

### Limitations

You can do **almost everything** with abstract models and emphasis in **almost**.
#### With indexes in the meta

Abstract models do not allow you to:
```python hl_lines="15"
{!> ../docs_src/models/indexes/simple2.py !}
```

* **Declare** [managers](./managers.md).
* **Declare** [unique together](#unique-together)
#### With complex indexes in the meta

This limitations are intentional as these operations should be done for [models](#declaring-models)
and not abstact models.
```python hl_lines="16-19"
{!> ../docs_src/models/indexes/complex_together.py !}
```
10 changes: 10 additions & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Release Notes

## 0.2.0

### Added

- New [Index](./models.md#indexes) object allowing the creation of internal SQLAlchemy indexes.

### Changed

- Updated metaclass to validate the fields being added to `indexes`.

## 0.1.0

This is the initial release of Saffier.
Expand Down
19 changes: 19 additions & 0 deletions docs_src/models/indexes/complex_together.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import saffier
from saffier import Database, Index, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
name = saffier.CharField(max_length=255)
email = saffier.EmailField(max_length=70)
is_active = saffier.BooleanField(default=True)
status = saffier.CharField(max_length=255)

class Meta:
registry = models
unique_together = [
Index(fields=["name", "email"]),
Index(fields=["is_active", "statux"], name="active_status_idx"),
]
14 changes: 14 additions & 0 deletions docs_src/models/indexes/simple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import saffier
from saffier import Database, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
name = saffier.CharField(max_length=255)
email = saffier.EmailField(max_length=70, index=True)
is_active = saffier.BooleanField(default=True)

class Meta:
registry = models
15 changes: 15 additions & 0 deletions docs_src/models/indexes/simple2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import saffier
from saffier import Database, Index, Registry

database = Database("sqlite:///db.sqlite")
models = Registry(database=database)


class User(saffier.Model):
name = saffier.CharField(max_length=255)
email = saffier.EmailField(max_length=70)
is_active = saffier.BooleanField(default=True)

class Meta:
registry = models
indexes = [Index(fields=["email"])]
4 changes: 3 additions & 1 deletion saffier/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
__version__ = "0.1.0"
__version__ = "0.2.0"

from saffier.core.registry import Registry
from saffier.db.connection import Database
from saffier.db.constants import CASCADE, RESTRICT, SET_NULL
from saffier.db.datastructures import Index
from saffier.db.manager import Manager
from saffier.db.queryset import QuerySet
from saffier.exceptions import DoesNotFound, MultipleObjectsReturned
Expand Down Expand Up @@ -43,6 +44,7 @@
"EmailField",
"FloatField",
"ForeignKey",
"Index",
"IPAddressField",
"IntegerField",
"JSONField",
Expand Down
7 changes: 0 additions & 7 deletions saffier/core/unique.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,6 @@


class Uniqueness:
"""
A set-like class that tests for uniqueness of primitive types.
Ensures the `True` and `False` are treated as distinct from `1` and `0`,
and coerces non-hashable instances that cannot be added to sets,
into hashable representations that can.
"""

TRUE = Empty()
FALSE = Empty()
HASHABLE_TYPES = (int, bool, str, float, list, dict)
Expand Down
36 changes: 36 additions & 0 deletions saffier/db/datastructures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import typing

from pydantic import root_validator
from pydantic.dataclasses import dataclass


@dataclass
class Index:
"""
Class responsible for handling and declaring the database indexes.
"""

suffix: str = "idx"
max_name_length: int = 30
name: typing.Optional[str] = None
fields: typing.Optional[typing.List[str]] = None

@root_validator
def validate_data(cls, values):
name = values.get("name")

if name is not None and len(name) > cls.max_name_length:
raise ValueError(f"The max length of the index name must be 30. Got {len(name)}")

fields = values.get("fields")
if not isinstance(fields, (tuple, list)):
raise ValueError("Index.fields must be a list or a tuple.")

if fields and not all(isinstance(field, str) for field in fields):
raise ValueError("Index.fields must contain only strings with field names.")

if name is None:
suffix = values.get("suffix", cls.suffix)
values["name"] = f"{'_'.join(fields)}_{suffix}"

return values
1 change: 1 addition & 0 deletions saffier/db/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import typing
from math import isfinite


from saffier.core import formats
from saffier.core.base import ValidationResult
from saffier.core.datastructures import ArbitraryHashableBaseModel
Expand Down
19 changes: 19 additions & 0 deletions saffier/metaclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from saffier import fields as saffier_fields
from saffier.core.registry import Registry
from saffier.db.datastructures import Index
from saffier.db.manager import Manager
from saffier.exceptions import ImproperlyConfigured
from saffier.fields import BigIntegerField, Field
Expand All @@ -24,6 +25,7 @@ class MetaInfo:
"registry",
"tablename",
"unique_together",
"indexes",
"foreign_key_fields",
"parents",
"pk",
Expand All @@ -47,6 +49,7 @@ def __init__(self, meta: "Model.Meta") -> None:
self._model: typing.Type["Model"] = None
self.manager: typing.Type["Manager"] = getattr(meta, "manager", Manager())
self.unique_together: typing.Any = getattr(meta, "unique_together", None)
self.indexes: typing.Any = getattr(meta, "indexes", None)


def _check_model_inherited_registry(bases: typing.Tuple[typing.Type, ...]) -> Registry:
Expand Down Expand Up @@ -209,6 +212,9 @@ def __search_for_fields(base: typing.Type, attrs: DictAny) -> None:
if getattr(meta, "unique_together", None) is not None:
raise ImproperlyConfigured("unique_together cannot be in abstract classes.")

if getattr(meta, "indexes", None) is not None:
raise ImproperlyConfigured("indexes cannot be in abstract classes.")

# Handle the registry of models
if getattr(meta, "registry", None) is None:
if hasattr(new_class, "_db_model") and new_class._db_model:
Expand Down Expand Up @@ -236,6 +242,19 @@ def __search_for_fields(base: typing.Type, attrs: DictAny) -> None:
"The values inside the unique_together must be a string or a tuple of strings."
)

# Handle indexes
if getattr(meta, "indexes", None) is not None:
indexes = meta.indexes
if not isinstance(indexes, (list, tuple)):
value_type = type(indexes).__name__
raise ImproperlyConfigured(
f"indexes must be a tuple or list. Got {value_type} instead."
)
else:
for value in indexes:
if not isinstance(value, Index):
raise ValueError("Meta.indexes must be a list of Index types.")

registry = meta.registry
new_class.database = registry.database

Expand Down
Loading

0 comments on commit 3e04a4d

Please sign in to comment.