-
-
Notifications
You must be signed in to change notification settings - Fork 33k
Fixed #26167 -- Added support for functional indexes #8056
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Out of curiosity, what's the reason the example index can't be defined as Concat(Lower(F('name')), Value('blub')) |
@charettes Apparently, I haven't fully understood it, |
It's been a little while since I looked into this, but you can't use F() because:
You might be able to work around this by passing F.resolve_expression a fake query object with a resolve_ref method that simply verifies the column existing on the model, and returning the appropriate Col or Ref. Col looks like it outputs "table.column" while Ref outputs "column_alias" which can just be the column name. You could try building non-query support into the F expression by overloading the resolve_expression method, but that kind of defeats the purpose of using F anyway, while complicating the code. F doesn't do the column/reference verification, that's handled by query.resolve_ref. Hope that helps. Also, nice work! I really look forward to seeing this available. |
django/db/models/expressions.py
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These should be on F
. I fixed this in my rebase of #7517.
9fe867d
to
4d430fb
Compare
8eb1989
to
b2a6e82
Compare
f473af1
to
0d77c47
Compare
This is a second shot at how function based indexes could look like. $ cat app/models.py from django.db import models
from django.db.models.functions import Lower
class Foo(models.Model):
name = models.CharField(max_length=255)
class Meta:
indexes = [
models.Index(
[Lower('name').desc()],
name='some_func_index'
),
] $ python manage.py makemigrations -v 3
Migrations for 'app':
app/migrations/0001_initial.py
- Create model Foo
- Create index some_func_index for model foo $ cat app/migrations/0001_initial.py # Generated by Django 2.0.dev20170507133517 on 2017-05-07 13:52
from django.db import migrations, models
import django.db.models.expressions
import django.db.models.functions.base
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Foo',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
],
),
migrations.AddIndex(
model_name='foo',
index=models.Index(fields=[django.db.models.expressions.OrderBy(django.db.models.functions.base.Lower('name'), descending=True)], name='some_func_index'),
),
] On SQLite3$ python manage.py sqlmigrate app 0001 BEGIN;
--
-- Create model Foo
--
CREATE TABLE "app_foo" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(255) NOT NULL);
--
-- Create index some_func_index on field(s) OrderBy(Lower(F(name)), descending=True) of model foo
--
CREATE INDEX "some_func_index" ON "app_foo" (LOWER("name") DESC);
COMMIT; $ python manage.py migrate app
Operations to perform:
Apply all migrations: app
Running migrations:
Applying app.0001_initial... OK $ python manage.py dbshell SQLite version 3.16.2 2017-01-06 16:32:41
Enter ".help" for usage hints.
sqlite> .schema
CREATE TABLE IF NOT EXISTS "django_migrations" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "app" varchar(255) NOT NULL, "name" varchar(255) NOT NULL, "applied" datetime NOT NULL);
CREATE TABLE IF NOT EXISTS "app_foo" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(255) NOT NULL);
CREATE INDEX "some_func_index" ON "app_foo" (LOWER("name") DESC);
sqlite> .quit On PostgreSQL$ python manage.py sqlmigrate app 0001 --settings=testproject.pg_settings BEGIN;
--
-- Create model Foo
--
CREATE TABLE "app_foo" ("id" serial NOT NULL PRIMARY KEY, "name" varchar(255) NOT NULL);
--
-- Create index some_func_index on field(s) OrderBy(Lower(F(name)), descending=True) of model foo
--
CREATE INDEX "some_func_index" ON "app_foo" (LOWER("name") DESC);
COMMIT; $ python manage.py migrate app --settings=testproject.pg_settings
Operations to perform:
Apply all migrations: app
Running migrations:
Applying app.0001_initial... OK $ python manage.py dbshell --settings=testproject.pg_settings
|
tests/schema/tests.py
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that the index is named title_lower_id
this LOWER
check could check against that as well as lower(xxx)
-- please rename the index so there is only one match for LOWER
(same for TITLE
)
I found this error: ======================================================================
ERROR: test_func_index (schema.tests.SchemaTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/django/tests/schema/tests.py", line 1771, in test_func_index
sql = func_index.create_sql(Book, editor)
File "/django/django/db/models/indexes.py", line 78, in create_sql
sql_parameters = self.get_sql_create_template_values(model, schema_editor, using)
File "/django/django/db/models/indexes.py", line 58, in get_sql_create_template_values
expression = column_expression.resolve_expression(query)
File "/django/django/db/models/functions/datetime.py", line 60, in resolve_expression
field = copy.lhs.output_field
File "/django/django/utils/functional.py", line 31, in __get__
res = instance.__dict__[self.name] = self.func(instance)
File "/django/django/db/models/expressions.py", line 226, in output_field
raise FieldError("Cannot resolve expression type, unknown output_field")
django.core.exceptions.FieldError: Cannot resolve expression type, unknown output_field when I had added index with func_index = Index(fields=[ExtractMonth('pub_date')], name='pub_data_month_idx')
with connection.schema_editor() as editor:
sql = func_index.create_sql(Book, editor) |
tests/schema/tests.py
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self.assertTrue
-> self.assertIn
above and below as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤦♂️ Thanks!
9f1972f
to
4262296
Compare
4262296
to
a593989
Compare
django/db/models/expressions.py
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that you can remove "%s" %
, i.e.
return connection.ops.quote_name(self.alias), []
docs/ref/models/indexes.txt
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it's also worth to add test and mention in doc that it's possible to create an index with math expressions, e.g.:
Index(fields=[F('height')*F('weight')], name='mul_idx')
docs/ref/models/indexes.txt
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a similar restriction on Oracle (see doc). Function must be declared as DETERMINISTIC
.
Thanks @felixxm. Updated. |
daf02f7
to
292c455
Compare
tests/schema/tests.py
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
assertRaisesRegex
should be avoided, I believe, cc @timgraham
I've seen a pattern in the migration tests where self.assertIn
is used to check for parts of the message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
assertRaisesMessage
uses assertIn
so it works just as well without the need for the .*
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. Fixed.
docs/ref/models/indexes.txt
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use current
instead of 9.6
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
django/db/models/indexes.py
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inherit from django.db.NotSupportedError
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
43c87b7
to
0cd5fb9
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've only done cursory code review, but it's looking nice! I haven't actually executed any of the code yet.
I'd encourage you to write tests as if you were trying to break Django. What happens if two indexes are created with the same name on the same model? Same name on different models? Should we namespace them perhaps? etc
I think this will be good to merge before cutoff. It's in really good shape, and it sounds like others have tested it quite well.
django/db/backends/base/schema.py
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why? Is this an error or not?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While it's an error on platforms w/o support for expression indexes to create one, the presence of an index is often not required. Thus, I'm raising warning instead of blowing up. That's the only way I can imagine for 3rd party packages using expression indexes to use them while supporting all databases.
django/db/models/expressions.py
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm alias generally refers to the table name - not the column name. You're changing the meaning of alias depending on the index_col.
Maybe just pass None, 'name', index_col=True
?
django/db/models/indexes.py
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe we do something similar in order_by? Consider refactoring into a helper?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All the occurrences of similar things in the order_by()
and OrderBy()
context (django.db.models.sql.query:add_ordering
and django.db.models.sql.compiler:get_order_by
) are based on using ASC
/ DESC
over .asc()
/ .desc()
. Further, the compiler related functions lookup table aliases and whatnot which we don't have in the index context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lastly, the code doing ordering is already around. It's only updated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah no need to worry about this. I didn't look up the other function, it was just from memory. Hard to refactor into common method so forget it.
django/db/models/sql/query.py
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you should fix the args into here as mentioned above.
tests/schema/tests.py
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test is cute <
ff78154
to
b4f6cbf
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A small style comment
@skipUnlessDBFeature('supports_expression_indexes') | ||
def test_func_index_invalid_field(self): | ||
func_index = Index(fields=[Lower('blub')], name='dolor_idx') | ||
with self.assertRaisesMessage(ValueError, "Invalid reference to field 'blub' on model 'schema.Author'"): |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
@MarkusH I had a look in Please do rebase. |
Would be nice to have a test case using a subclass of from django.db import models, Func, F
from django.contrib.postgres.indexes import GistIndex
class Location(models.Model):
lat = models.FloatField()
lon = models.FloatField()
class Meta:
indexes = [
GistIndex(fields=[Func(F('lat'), F('lon'), function='ll_to_earth')]),
] should result in CREATE INDEX idx_name ON app_location USING gist(ll_to_earth("lat", "lng")); |
if not any(f.name == name for f in self.model._meta.concrete_fields): | ||
raise ValueError("Invalid reference to field '%s' on model '%s'" % (name, self.model._meta.label)) | ||
source_field = self.model._meta.get_field(name) | ||
return Col(None, source_field, index_col=True) # None refers to the table alias we don't have here |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any thoughts about using a Col
subclass here instead of this flag? You'd simply have to return IndexCol(None, source_field)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had a very similar requirement in my work on Check Constraints: 1a5ade0#diff-fe637a3ec6af9cbe2ee87293000e7c82R751
It's probably worthwhile checking if we can share some implementation details here.
contains_column_references = True | ||
|
||
def __init__(self, alias, target, output_field=None): | ||
def __init__(self, alias, target, output_field=None, index_col=False): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you should create a Col
subclass for this purpose instead.
e.g.
class IndexCol(Col):
def as_sql(self, compiler, connection):
return connection.ops.quote_name(self.target.column), []
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❤️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👏
I've been working on a https://github.com/schinckel/django-computed-field Ignoring the fact I have to currently sniff up the stack to resolve the query (I'm thinking about trying to use |
Any updates? |
@MarkusH this would be great to have in, any plans on getting back to it? |
Superseded by #11929 |
This is a first shot at it would or could look like. There are probably dozens more things to look into and consider.