Skip to content

Commit

Permalink
Add option for span and document readme and examples
Browse files Browse the repository at this point in the history
  • Loading branch information
jacklinke committed Apr 27, 2022
1 parent db12d09 commit cd5a03a
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 28 deletions.
40 changes: 27 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,36 @@ When using Postgres, the set-returning functions allow us to easily create seque

This project makes it possible to create such sequences, which can then be used with Django QuerySets. For instance, assuming you have an Order model, you can create a set of sequential dates and then annotate each with the number of orders placed on that date. This will ensure you have no date gaps in the resulting QuerySet. To get the same effect without this package, additional post-processing of the QuerySet with Python would be required.

## Terminology

Although this packages is named django-generate-series based on Postgres' [`generate_series` set-returning function](https://www.postgresql.org/docs/current/functions-srf.html), mathematically we are creating a [sequence](https://en.wikipedia.org/wiki/Sequence) rather than a [series](https://en.wikipedia.org/wiki/Series_(mathematics)).

- **sequence**: Formally, "a list of objects (or events) which have been ordered in a sequential fashion; such that each member either comes before, or after, every other member."

In django-generate-series, we can generate sequences of integers, decimals, dates, datetimes, as well as the equivalent ranges of each of these types.

- **term**: The *n*th item in the sequence, where '*n*th' can be found using the id of the model instance.

This is the name of the field in the model which contains the term value.

## API

The package includes a `generate_series` function from which you can create your own series-generating QuerySets. The field type passed into the function as `output_field` determines the resulting type of series that can be created.

### generate_series arguments

- ***start*** - The value at which the sequence should begin (required)
- ***stop*** - The value at which the sequence should end. For range types, this is the lower value of the final term (required)
- ***step*** - How many values to step from one term to the next. For range types, this is the step from the lower value of one term to the next. (required for non-integer types)
- ***span*** - For range types other than date and datetime, this determines the span of the lower value of a term and its upper value (optional, defaults to 1 if neeeded in the query)
- ***output_field*** - A django model field class, one of BigIntegerField, IntegerField, DecimalField, DateField, DateTimeField, BigIntegerRangeField, IntegerRangeField, DecimalRangeField, DateRangeField, or DateTimeRangeField. (required)
- ***include_id*** - If set to True, an auto-incrementing `id` field will be added to the QuerySet.
- ***max_digits*** - For decimal types, specifies the maximum digits
- ***decimal_places*** - For decimal types, specifies the number of decimal places
- ***default_bounds*** - In Django 4.1+, allows specifying bounds for list and tuple inputs. See [Django docs](https://docs.djangoproject.com/en/dev/releases/4.1/#django-contrib-postgres)

## Basic Examples

```python
# Create a bunch of sequential integers
integer_sequence_queryset = generate_series(
Expand Down Expand Up @@ -69,7 +95,7 @@ Result:
...
2023-04-27

*Note: See the docs and the example project in the tests directory for further examples of usage.*
*Note: See [the docs](https://django-generate-series.readthedocs.io/en/latest/usage_examples.html) and the example project in the tests directory for further examples of usage.*

## Usage with partial

Expand All @@ -84,15 +110,3 @@ int_and_id_series = partial(generate_series, include_id=True, output_field=BigIn

qs = int_and_id_series(1, 100)
```

## Terminology

Although this packages is named django-generate-series based on Postgres' [`generate_series` set-returning function](https://www.postgresql.org/docs/current/functions-srf.html), mathematically we are creating a [sequence](https://en.wikipedia.org/wiki/Sequence) rather than a [series](https://en.wikipedia.org/wiki/Series_(mathematics)).

- **sequence**: Formally, "a list of objects (or events) which have been ordered in a sequential fashion; such that each member either comes before, or after, every other member."

In django-generate-series, we can generate sequences of integers, decimals, dates, datetimes, as well as the equivalent ranges of each of these types.

- **term**: The *n*th item in the sequence, where '*n*th' can be found using the id of the model instance.

This is the name of the field in the model which contains the term value.
41 changes: 29 additions & 12 deletions django_generate_series/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,23 @@ def __init__(
start,
stop,
step=None,
span=None,
include_id=False,
model: AbstractBaseSeriesModel = None,
):
self.term = type(model._meta.get_field("term"))
self.start = start
self.stop = stop
self.step = step
self.span = span
self.include_id = include_id
self.range = False
self.field_type = FieldType.INTEGER

# Verify the input params match for the type of model field used

# ToDo: Check span type

if issubclass(self.term, (models.DecimalField, pg_models.DecimalRangeField)):
self.check_params(
start_type=[int, Decimal],
Expand Down Expand Up @@ -184,6 +188,7 @@ def get_raw_query(self):
if self.field_type == FieldType.DATETIME:
if self.include_id:
sql = """
--- %s
SELECT
row_number() over () as id,
"term"
Expand All @@ -196,13 +201,15 @@ def get_raw_query(self):
"""
else:
sql = """
--- %s
SELECT tstzrange((lag(a) OVER()), a, '[)') AS term
FROM generate_series(timestamptz %s, timestamptz %s, interval %s)
AS a OFFSET 1
"""
elif self.field_type == FieldType.DATE:
if self.include_id:
sql = """
--- %s
SELECT
row_number() over () as id,
"term"
Expand All @@ -217,6 +224,7 @@ def get_raw_query(self):
"""
else:
sql = """
--- %s
SELECT daterange((lag(a.n) OVER()), a.n, '[)') AS term
FROM (
SELECT generate_series(date %s, date %s, interval %s)::date
Expand All @@ -231,13 +239,13 @@ def get_raw_query(self):
"term"
FROM
(
SELECT numrange(a, a + 1) AS term
SELECT numrange(a, a + %s) AS term
FROM generate_series(%s, %s, %s) a
) AS seriesquery
"""
else:
sql = """
SELECT numrange(a, a + 1) AS term
SELECT numrange(a, a + %s) AS term
FROM generate_series(%s, %s, %s) a
"""
elif self.field_type == FieldType.BIGINTEGER:
Expand All @@ -248,13 +256,13 @@ def get_raw_query(self):
"term"
FROM
(
SELECT int8range(a, a + 1) AS term
SELECT int8range(a, a + %s) AS term
FROM generate_series(%s, %s, %s) a
) AS subquery
"""
else:
sql = """
SELECT int8range(a, a + 1) AS term
SELECT int8range(a, a + %s) AS term
FROM generate_series(%s, %s, %s) a
"""
else:
Expand All @@ -266,20 +274,21 @@ def get_raw_query(self):
"term"
FROM
(
SELECT int4range(a, a + 1) AS term
SELECT int4range(a, a + %s) AS term
FROM generate_series(%s, %s, %s) a
) AS seriesquery
"""
else:
sql = """
SELECT int4range(a, a + 1) AS term
SELECT int4range(a, a + %s) AS term
FROM generate_series(%s, %s, %s) a
"""
else:
if self.field_type == FieldType.DATE:
# Must specify this one, or defaults timestamptz rather than date
if self.include_id:
sql = """
--- %s
SELECT
row_number() over () as id,
"term"
Expand All @@ -290,10 +299,14 @@ def get_raw_query(self):
) AS seriesquery
"""
else:
sql = "SELECT generate_series(%s, %s, %s)::date term"
sql = """
--- %s
SELECT generate_series(%s, %s, %s)::date term
"""
else:
if self.include_id:
sql = """
--- %s
SELECT
row_number() over () as id,
"term"
Expand All @@ -304,7 +317,10 @@ def get_raw_query(self):
) AS seriesquery
"""
else:
sql = "SELECT generate_series(%s, %s, %s) term"
sql = """
--- %s
SELECT generate_series(%s, %s, %s) term
"""

return sql

Expand All @@ -323,7 +339,7 @@ def get_from_clause_wrapper(*args, **kwargs):
result, params = get_from_clause_method(*args, **kwargs)
wrapper = source.raw_query
result[0] = f"{wrapper} AS {tuple(compiler.query.alias_map)[0]}"
params = (source.start, source.stop, source.step or 1) + tuple(params)
params = (source.span, source.start, source.stop, source.step or 1) + tuple(params)

return result, params

Expand All @@ -346,12 +362,12 @@ def __init__(self, *args, query=None, _series_func=None, **kwargs):
class GenerateSeriesManager(models.Manager):
"""Custom manager for creating series"""

def _generate_series(self, start, stop, step=None, include_id=False):
def _generate_series(self, start, stop, step=None, span=None, include_id=False):

# def series_func(cls, *args):
def series_func(cls):
model = self.model
return FromRaw(model=model, start=start, stop=stop, step=step, include_id=include_id)
return FromRaw(model=model, start=start, stop=stop, step=step, span=span, include_id=include_id)

return GenerateSeriesQuerySet(self.model, using=self._db, _series_func=series_func)

Expand All @@ -368,6 +384,7 @@ def generate_series(
start: Union[int, date, datetime, datetimetz],
stop: Union[int, date, datetime, datetimetz],
step: Optional[Union[int, str]] = None,
span: Optional[int] = 1,
*,
output_field: models.Field,
include_id: Optional[bool] = False,
Expand All @@ -376,7 +393,7 @@ def generate_series(
default_bounds: Optional[Union[str, None]] = None,
):
model_class = _make_model_class(output_field, include_id, max_digits, decimal_places, default_bounds)
return model_class.objects._generate_series(start, stop, step, include_id)
return model_class.objects._generate_series(start, stop, step, span, include_id)


@lru_cache(maxsize=128)
Expand Down
Loading

0 comments on commit cd5a03a

Please sign in to comment.