Repository navigation
Add CYCLE clause support for recursive CTEs - #134
heinrichf-cdgnm wants to merge 19 commits into
Conversation
69d22bd to
e9d1d1a
Compare
|
Pushed some cleanup on top.
|
|
@heinrichf-cdgnm Thank you for the contribution! I intend to review and hope to get this merged, but have been on vacation recently and am swamped with other work currently. Will get to it as soon as I can. |
millerdev
left a comment
There was a problem hiding this comment.
There are some things I'd like to seen cleaned up before this is merged.
| :param cycle_value: SQL literal assigned to the mark column when a | ||
| cycle is detected (default: "true"). Interpolated into the query as | ||
| written, so a string value must include its own quotes. | ||
| :param default_value: SQL literal assigned to the mark column when no | ||
| cycle is detected (default: "false"). Interpolated as written. | ||
| :param path_column: Name of the generated path column (default: | ||
| "path"). |
There was a problem hiding this comment.
These seem vulnerable to SQL injection. Would be better to interpret the value by type. So a Python boolean True is converted to true; a str is quoted; int, float, and datetime can be handled similarly. Maybe it could support an expression like Value or RawSQL if the user wants to do something more exotic?
There was a problem hiding this comment.
Done. to and default take a bool, int, float, str, date or datetime, a Value, or a RawSQL without params.
Postgres only accepts constants there AFAICT, so something like a placeholder, a cast or a signed number results in a syntax error. Values are therefore written into the SQL by Python type:
stris quoted (as an escape string constantE'...'if it contains a backslash)float->float8 '1.5'- negative
int->bigint '-1' dateanddatetime->date '...',timestamp '...'ortimestamptz '...'
The type of to then decides the output field of the mark column. For Value and RawSQL the output field is the expression's output_field,
|
Pushed changes according to the review, I hope I didn't miss anything. I did not resolve the conversations in case you would like to do that or there is more to be discussed. |
millerdev
left a comment
There was a problem hiding this comment.
I like where this is heading. Would still like to see some changes to compile_mark_value.
| self.cycle_value = cycle_value | ||
| self.default_value = default_value | ||
| self.cycle_sql, mark_field = compile_mark_value(cycle_value) | ||
| self.default_sql, _ = compile_mark_value(default_value) |
There was a problem hiding this comment.
Is there a test for (and does it break) if cycle_value and default_value have different types? Should that be detected here and maybe raise ValueError if the types do not match?
| ) | ||
|
|
||
|
|
||
| def compile_mark_value(value): |
There was a problem hiding this comment.
What about using Django's connection.ops.compose_sql() at SQL-compile time? It's better for a few reasons:
- Escaping: it uses the driver's own quoting, so you don't depend on the
E''/standard_conforming_stringsdetails thatquote_stringhandles by hand. - Types: it formats every type the docs list (
bool,int,float,str,date,datetime) with the driver's adapters. - Invalid input: the driver rejects values it can't represent, such as strings containing NUL (e.g.,
"abc\x00def").
def compile_mark_value(value, connection):
"""Get the SQL of a mark column value
PostgreSQL accepts only constants in TO and DEFAULT, not parameters
or casts, so the literal value is written into the SQL.
"""
return connection.ops.compose_sql("%s", [value]).replace("%", "%%")Replacing % to %% is still necessary because the result is embedded in a query that Django later runs with params, and compose_sql returns plain SQL with single %.
compile_mark_value is currently called in CycleClause.__init__, where there's no connection. The call will need to move into as_sql/the compiler, where compiler.connection is available. That's also more correct for multi-database setups.
A separate function could be used to get the output field. Not sure if it makes sense to do that here in CycleClause.__init__, or later at compile time?
Converting the USING path column to Python is involved with psycopg2. psycopg3 will convert it to a list of string tuples. I tried my best to document this in the code and in the docs.
The columns the clause adds are read through cte.col, e.g. .annotate(is_cycle=cte.col.is_cycle).
Closes #96