Show HN: Sqlbind a Python library to compose raw SQL
github.com
github.com
I love Django, including aspects of the ORM (its very easy to write schema, migrations are pretty easy to use, even queries are very concise and easy to write) but I have no idea what the SQL will look like except in the simplest cases.
I often do not particularly care, and it is easy enough to see them when I do, but I feel a bit icky about the extra layer of stuff I do not really understand.
> In truth the best way to do the data layer is to use stored procs with a generated binding at the application layer. This is absolutely safe from injection, and is wicked fast as well.
[Who must keep stored procedures in sync with migrations,]
1. Almost nobody in the team knows SQL, stored procedures more so.
2. It's not possible to deploy and test stored procedures with the usual tooling. Actually I don't know what's the usual way to do it in projects that go with that approach.
I don't really understand the confusion here, you write a stored procedure, you document it, test it, peer review it, then stuff it into the database. You then write code that uses it. No sarcasm, but what's the problem?
I think they're referring to ongoing automated tests, and not one-off tests.
That works; it's just maybe thousands of times slower than unit tests, and much more work to create and maintain.
cursor.mogrify(*queryset.query.sql_with_params())
Alternatively, you can set the log level of the `django.db.backends` logger to DEBUG to see all executed queries> Django gives you two ways of performing raw SQL queries: you can use `Manager.raw()` to perform raw queries and return model instances, or you can avoid the model layer entirely and execute custom SQL directly.
https://stackoverflow.com/questions/1074212/how-can-i-see-th... has :
MyModel.objects.all().query.sql_with_params()
str(MyModel.objects.all().query)
And: from django.db import connection
from myapp.models import SomeModel
queryset = SomeModel.objects.filter(foo='bar')
sql_query, params = queryset.query.as_sql(None, connection)
with connection.connection.cursor(cursor_factory=DictCursor) as cursor:
cursor.execute(sql_query, params)
data = cursor.fetchall()
But that's still not backend-specific SQL?There should be an interface method for this. Why does psycopg call it mogrify?
https://django-debug-toolbar.readthedocs.io/en/latest/panels... :
> debug_toolbar.panels.sql.SQLPanel: SQL queries including time to execute and links to EXPLAIN each query
But debug toolbars mostly don't work with APIs.
https://github.com/django-query-profiler/django-query-profil... :
> Django query profiler - one profiler to rule them all. Shows queries, detects N+1 and gives recommendations on how to resolve them
https://github.com/jazzband/django-silk :
> Silk is a live profiling and inspection tool for the Django framework. Silk intercepts and stores HTTP requests and database queries before presenting them in a user interface for further inspection
debug_toolbar and silk are both tools that show you what queries were executed by your running application. They're both good tools, but neither quite solves the problem of giving you the executable SQL for a specific queryset (e.g., in a repl).
Maybe it should be called dialectize() or to_sql(parametrized=True)
I understand why it doesn't allow that, and probably never can, but it would be nice when you need author OLAP queries but want to continue to use as much of the ORM as you can.
Guessing .annotation() for runtime type info? .filter() avoids re-executing query just to get a subset?
How do the (canonical utility methods) relate to nested JOINs? Because field types of nested sets aren't visible?
TIA.
If the ORM were to allow you do build a raw query, using multiple tables, views, nested queries, etc - it would be difficult for it to then allow those utility methods. The ORM wouldn't have the Model class and it's associated fields to use in while translating the method's args into SQL.
- https://docs.djangoproject.com/en/5.0/topics/db/queries/
- https://docs.djangoproject.com/en/5.0/ref/models/querysets/
you can also print qs.query, qs.explain("analyze"), etc etc
Why don't you print QuerySet.query ?
that's really on you as Django makes it very easy to find the answer to that because Python makes it very easy to inspect objects at runtime
Using the quickstart example, visually both lines are very similar and both are functional, but one of them may introduce a SQL injection:
sql = f'SELECT * FROM table WHERE field1 = {q/value1} AND field2 > {q/value2}'
sql = f'SELECT * FROM table WHERE field1 = {value1} AND field2 > {value2}'e.g. You must use the Q mechanism and doesn’t work if you forget it.
I guess the q part is the query parameter for the prepared statement. I’d still be a bit antsy here given the ease of a mistake.
I just don’t get what it’s providing over using my db’s module. Usually the dialect is close but different enough to require changes to queries.
QParams = sqlbind.Dialect.some_dialect
@QParams
def make_my_query(value1: str, value2: int):
return f'SELECT * FROM table WHERE field1 = {value1} AND field2 > {value2}'
data = connection.execute(*make_my_query(foo, bar))
Obviously this wouldn't work as-is, QParams would need to be modified to support this. The decorator would wrap the method, sanitising the args before they're even passed to the wrapped function.Edit: actually, I might've misunderstood what sqlbind is doing internally, maybe this approach doesn't quite make sense
Edit 2: Like so: https://github.com/baverman/sqlbind/issues/1
QParams = sqlbind.Dialect.some_dialect
@QParams
def make_my_query(value1: str, value2: int):
# SELECT * FROM table WHERE field1 = ? AND field2 > ?
return f'SELECT * FROM table WHERE field1 = {value1} AND field2 > {value2}'
data = connection.execute(*make_my_query(foo, bar), [foo, bar]) ps = f'SELECT * FROM table WHERE field1 = {q/value1} AND field2 > {q/value2}'
# SELECT * FROM table WHERE field1 = ? AND field2 > ?
However, I would imagine that if any external input is passed through to this framework, then there might still be the possibility of SQL injection attacks passing through this framework and ending up in the prepared statement SQL. filters = [q.field == value1, q.field2 > value2]
sql = f'SELECT * FROM table {WHERE(*filters)}'
It's a little harder to misuse.Similar idea, more fleshed out, doesn't require all the weird interpolation stuff and special python functions which map to SQL grammar.
Almost nothing to memorize so you can use the library.
The SQL it outputs is extremely readable and cleanly formatted.
Norm:
def get_users(cursor, user_ids, only_girls=False, minimum_age=0):
s = (SELECT('first_name', 'age')
.FROM('people')
.WHERE('user_ids IN :user_ids') # doesn't work in sqlite
.bind(user_ids=user_ids))
if only_girls:
s = s.WHERE(gender='f')
if minimum_age:
s = (s.WHERE('age >= :minimum_age')
.bind(minimum_age=minimum_age))
return cursor.run_query(s)
VS sqlbind: def get_users(cursor, user_ids, only_girls=False, minimum_age=0):
q = QParams()
filters = [
q.user_ids.IN(user_ids), # renders into FALSE if user_ids are empty and supports SQlite.
q.cond(only_girls, "gender = 'f'"),
q.age >= truthy/minimum_age,
]
sql = f'''\
SELECT first_name, age
FROM people
{WHERE(*filters)}
'''
return cursor.execute(sql, q)I enjoy working with SQL directly, though I understand it has pros and cons. If you need specially crafted queries, it is a must though.
My lib is at https://monazita.gitlab.io/monazita/ and show HN is at https://news.ycombinator.com/item?id=39467742
I want to write a sql query I can deploy against test and prod, so I need to be able to parameterize table names to some extend. Then there are values, as shown here, with all the footguns that entails. But in the end I also want to be able to have IDE niceties while developing. Autocomplete on column names and table names and inline be able to see types and those kind if things.
And I have never seen anything that can give you all those things.
Not sure it allows you to parameterize table names but the basic idea is codegen from sql queries so you are working with go code (autocompletion etc).
… are your “test" and “prod" different tables in the same database? :S
But if I remember correctly, when I'm executing bigquery against parquet files in a bucket, they are kinda all in the big database of global bucket names on GCP.
I've had the best experience with embrace-sql which translates sql-files with magic comment-delimited parametrized queries to python-callable modules: https://pypi.org/project/embrace/
https://pandas.pydata.org/docs/reference/api/pandas.DataFram...
a) let me write actual SQL, not a python DSL that generates SQL
b) be placeholder-safe
c) be composable
Though it was somewhat intentionally limited to what I needed to support for my own needs at the time.
Are the query params stored in `q`? Which is also updated when it's inserted in the query itself? Why does a `sqlbind.Dialect.default()` turn into an `str`?
And... what's the difference between that and normal parameterized queries? It just seems weirder and with a lot of side effect magic going on.
So in effect, the slash behavior is just a spicy way to append or set (depending on which dialect option you choose).
1. https://github.com/manifold-systems/manifold/blob/master/man...
SQLAlchemy to SQL is borderline trivial, you just call `str(statement)` and it will output SQL. [Here's the docpage for if you want to select dialects and optionally inline parameters](https://docs.sqlalchemy.org/en/20/faq/sqlexpressions.html#ho...).
You don't have to use the modeling part of an ORM. Just the built-in security, session, and connection pool handling is already valuable. And you can already do:
```
params = [123]
session.query("SELECT * FROM my_table WHERE userID = ?", params)
```
Why introduce another library just for that?