Re: Generated Field

2022-12-24 Thread Jeremy Nauta
I'd love to help implement this if we can find a rough syntax! I've made a 
proof of concept in Postgres, and there are two outstanding limitations to 
address:

- The generated field value is not set until the model is reloaded from the 
database
- The `GENERATED ALWAYS` expression requires an argument to be injected in 
the the sql expression, but this is not currently possible

*from django.db.backends.utils import CursorWrapper*

*from django.db.models import Expression, Field*

*from django.db.models.sql import Query*



*class GeneratedField(Field):*

*"""*

*Wrapper field used to support generated columns in postgres.*

*"""*


*def __init__(self, expression: Expression, db_collation: str = None, 
*args, **kwargs):*

*"""*

*:param expression: DB expression used to calculate the 
auto-generated field value*

*"""*


*self.expression = expression*

*self.db_collation = db_collation*


*kwargs['editable'] = False  # This field can never be edited*

*kwargs['blank'] = True  # This field never requires a value to be 
set*

*kwargs['null'] = True  # This field never requires a value to be 
set*


*super().__init__(*args, **kwargs)*


*def _compile_expression(self, cursor: CursorWrapper, sql: str, params: 
dict):*

*"""*

*Compiles SQL and its associated parameters into a full SQL query. 
Usually sql params are kept*

*separate until `cursor.execute()` is called, but this is not 
possible since this function*

*must return a single sql string.*

*"""*


*return cursor.mogrify(sql, params).decode()*


*def db_type(self, connection):*

*"""*

*Called when calculating SQL to create DB column (e.g. DB 
migrations)*

*
https://docs.djangoproject.com/en/4.1/ref/models/fields/#django.db.models.Field.db_type*

*"""*


*db_type = 
self.expression.output_field.db_type(connection=connection)*


*# Convert any F() references to concrete field names*

*query = Query(model=self.model, alias_cols=False)*

*expression = self.expression.resolve_expression(query, 
allow_joins=False)*


*# Compile expression into SQL*

*expression_sql, params = expression.as_sql(*

*compiler=connection.ops.compiler('SQLCompiler')(*

*query, connection=connection, using=None*

*),*

*connection=connection,*

*)*


*with connection.cursor() as cursor:*

*expression_sql = self._compile_expression(*

*cursor=cursor, sql=expression_sql, params=params*

*)*


*return f'{db_type} GENERATED ALWAYS AS ({expression_sql}) STORED'*


*def rel_db_type(self, connection):*

*"""*

*Called when calculating SQL to reference DB column*

*
https://docs.djangoproject.com/en/4.1/ref/models/fields/#django.db.models.Field.rel_db_type*

*"""*

*return self.expression.output_field.db_type(connection=connection)*


*def deconstruct(self):*

*"""*

*Add custom field properties to allow migrations to deconstruct 
field*

*
https://docs.djangoproject.com/en/4.1/ref/models/fields/#django.db.models.Field.deconstruct*

*"""*

*name, path, args, kwargs = super().deconstruct()*

*kwargs['expression'] = self.expression*

*if self.db_collation is not None:*

*kwargs['db_collation'] = self.db_collation*

*return name, path, args, kwargs*



*class GeneratedFieldQuerysetMixin:*

*"""*

*Must be added to queryset classes*

*"""*


*def _insert(self, objs, fields, *args, **kwargs):*

*if getattr(self.model, '_generated_fields', None) and fields:*

*# Don't include generated fields when performing a 
`model.objects.bulk_create()`*

*fields = [f for f in fields if f not in 
self.model._generated_fields()]*


*return super()._insert(objs, fields, *args, **kwargs)*



*class GeneratedFieldModelMixin:*

*"""*

*Must be added to model class*

*"""*


*def _generated_fields(cls) -> list[Field]:*


*"""*

*:return all fields of the model that are generated*

*"""*


*return [*

*f*

*for f in cls._meta.fields*

*if isinstance(f, GeneratedField)*

*]*


*def _do_insert(self, manager, using, fields, *args, **kwargs):*

*generated_fields = self._generated_fields()*

*if generated_fields and fields:*

*# Don't include generated fields when performing a `save()` or 
`create()`*

*fields = [f for f in fields if f not in generated_fields]*


*return super()._do_insert(manager, using, fields, *args, **kwargs)*


*def _do_update(self, base_qs, using, pk_val, values, *args, **kwargs):*

*generated_fields = self._generated_fields()*

*

Re: Generated Field

2022-12-24 Thread 'schinckel' via Django developers (Contributions to Django itself)
I believe there are a bunch of similarities between the requirements of 
generated fields and my project 
django-shared-property: https://django-shared-property.readthedocs.io/en/latest/

On Sunday, December 25, 2022 at 10:23:10 AM UTC+10:30 jeremy...@gmail.com 
wrote:

> I'd love to help implement this if we can find a rough syntax! I've made a 
> proof of concept in Postgres, and there are two outstanding limitations to 
> address:
>
> - The generated field value is not set until the model is reloaded from 
> the database
> - The `GENERATED ALWAYS` expression requires an argument to be injected in 
> the the sql expression, but this is not currently possible
>
> *from django.db.backends.utils import CursorWrapper*
>
> *from django.db.models import Expression, Field*
>
> *from django.db.models.sql import Query*
>
>
>
> *class GeneratedField(Field):*
>
> *"""*
>
> *Wrapper field used to support generated columns in postgres.*
>
> *"""*
>
>
> *def __init__(self, expression: Expression, db_collation: str = None, 
> *args, **kwargs):*
>
> *"""*
>
> *:param expression: DB expression used to calculate the 
> auto-generated field value*
>
> *"""*
>
>
> *self.expression = expression*
>
> *self.db_collation = db_collation*
>
>
> *kwargs['editable'] = False  # This field can never be edited*
>
> *kwargs['blank'] = True  # This field never requires a value to be 
> set*
>
> *kwargs['null'] = True  # This field never requires a value to be 
> set*
>
>
> *super().__init__(*args, **kwargs)*
>
>
> *def _compile_expression(self, cursor: CursorWrapper, sql: str, 
> params: dict):*
>
> *"""*
>
> *Compiles SQL and its associated parameters into a full SQL query. 
> Usually sql params are kept*
>
> *separate until `cursor.execute()` is called, but this is not 
> possible since this function*
>
> *must return a single sql string.*
>
> *"""*
>
>
> *return cursor.mogrify(sql, params).decode()*
>
>
> *def db_type(self, connection):*
>
> *"""*
>
> *Called when calculating SQL to create DB column (e.g. DB 
> migrations)*
>
> *
> https://docs.djangoproject.com/en/4.1/ref/models/fields/#django.db.models.Field.db_type
>  
> *
>
> *"""*
>
>
> *db_type = 
> self.expression.output_field.db_type(connection=connection)*
>
>
> *# Convert any F() references to concrete field names*
>
> *query = Query(model=self.model, alias_cols=False)*
>
> *expression = self.expression.resolve_expression(query, 
> allow_joins=False)*
>
>
> *# Compile expression into SQL*
>
> *expression_sql, params = expression.as_sql(*
>
> *compiler=connection.ops.compiler('SQLCompiler')(*
>
> *query, connection=connection, using=None*
>
> *),*
>
> *connection=connection,*
>
> *)*
>
>
> *with connection.cursor() as cursor:*
>
> *expression_sql = self._compile_expression(*
>
> *cursor=cursor, sql=expression_sql, params=params*
>
> *)*
>
>
> *return f'{db_type} GENERATED ALWAYS AS ({expression_sql}) STORED'*
>
>
> *def rel_db_type(self, connection):*
>
> *"""*
>
> *Called when calculating SQL to reference DB column*
>
> *
> https://docs.djangoproject.com/en/4.1/ref/models/fields/#django.db.models.Field.rel_db_type
>  
> *
>
> *"""*
>
> *return 
> self.expression.output_field.db_type(connection=connection)*
>
>
> *def deconstruct(self):*
>
> *"""*
>
> *Add custom field properties to allow migrations to deconstruct 
> field*
>
> *
> https://docs.djangoproject.com/en/4.1/ref/models/fields/#django.db.models.Field.deconstruct
>  
> *
>
> *"""*
>
> *name, path, args, kwargs = super().deconstruct()*
>
> *kwargs['expression'] = self.expression*
>
> *if self.db_collation is not None:*
>
> *kwargs['db_collation'] = self.db_collation*
>
> *return name, path, args, kwargs*
>
>
>
> *class GeneratedFieldQuerysetMixin:*
>
> *"""*
>
> *Must be added to queryset classes*
>
> *"""*
>
>
> *def _insert(self, objs, fields, *args, **kwargs):*
>
> *if getattr(self.model, '_generated_fields', None) and fields:*
>
> *# Don't include generated fields when performing a 
> `model.objects.bulk_create()`*
>
> *fields = [f for f in fields if f not in 
> self.model._generated_fields()]*
>
>
> *return super()._insert(objs, fields, *args, **kwargs)*
>
>
>
> *class GeneratedFieldModelMixin:*
>
> *"""*
>
> *Must be added