From f7b6661935fd193583b82bbd5ac6aff55e282edf Mon Sep 17 00:00:00 2001 From: Justin Copeland Date: Fri, 30 Sep 2022 15:24:12 -0500 Subject: [PATCH] add mysql support --- README.md | 18 +++++++++--------- setup.py | 2 +- sql_compiler/compilers/__init__.py | 3 +++ sql_compiler/compilers/mysql.py | 26 +++++++++++++++++++++++++- 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 82c128d..cc06ce4 100644 --- a/README.md +++ b/README.md @@ -3,24 +3,24 @@ A light-weight module to generate usable SQL from a Django QuerySet. ## Backend Support -Currently, `django-sql-compiler` only supports connections made via the `django.db.backends.postgresql` backend. +Currently, `django-sql-compiler` only supports connections made via the `django.db.backends.postgresql` and `django.db.backends.mysql` backends. ## About Django SQL Compiler The Django ORM is very useful for abstracting away SQL queries from the focus of the developer. This is very useful for preventing SQL injection attacks and generating queries programmatically using applied logic in your Django app. However, very complex queries (such as those used in reporting, analytics, or data science projects) can be difficult -or impossible to create with the Django ORM alone. The ORM provides the `.raw` query method and exposes the raw database +or impossible to create with the Django ORM alone. The ORM provides the `.raw` query method and exposes the raw database `connection` objects which can be used to execute arbitrary SQL against the database. In doing so, we lose the benefit -of the ORM with respect to dynamically adding components to the query (such as filters in a `WHERE` clause) in a way that +of the ORM with respect to dynamically adding components to the query (such as filters in a `WHERE` clause) in a way that prevents injection attacks. -The base Django `QuerySet` object has a `Query` object available at the `.query` property. Casting this `Query` object as a `str` -prints out what looks like a valid SQL query. However, this version of the query is not properly escaped or quoted, meaning +The base Django `QuerySet` object has a `Query` object available at the `.query` property. Casting this `Query` object as a `str` +prints out what looks like a valid SQL query. However, this version of the query is not properly escaped or quoted, meaning it's not **actually** valid SQL unless there are no dynamic components (such as filters from user input) in the `QuerySet`. -`django-sql-compiler` aims to provide a way to generate clean, usable SQL from a given `QuerySet`, which can be used in -tandem with a raw SQL query to give SQL users more flexibility in querying their Django-connected database while still +`django-sql-compiler` aims to provide a way to generate clean, usable SQL from a given `QuerySet`, which can be used in +tandem with a raw SQL query to give SQL users more flexibility in querying their Django-connected database while still retaining the Django ORM for security and dynamic query generation purposes. ## Usage @@ -79,7 +79,7 @@ filtered_query_set = MyModel.objects.filter( field_one__lte=request.data.get('field_one_filter', 0), field_two__in=request.data.get('field_two_filter', []) ) - + more_complex_query = """ select @@ -94,4 +94,4 @@ more_complex_results = MyModel.objects.raw(more_complex_query) ``` Now, users who are more familiar with SQL rather than the Django ORM can use the ORM for security and conveniently -generating SQL queries and use SQL for the rest of their transformations. \ No newline at end of file +generating SQL queries and use SQL for the rest of their transformations. diff --git a/setup.py b/setup.py index 3ff14a0..a05671d 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='django-sql-compiler', - version='0.1.1', + version='0.2.3', description="A light-weight module to generate usable SQL from a Django QuerySet.", long_description=open('README.md').read(), long_description_content_type='text/markdown', diff --git a/sql_compiler/compilers/__init__.py b/sql_compiler/compilers/__init__.py index 537901b..8d43a2e 100644 --- a/sql_compiler/compilers/__init__.py +++ b/sql_compiler/compilers/__init__.py @@ -1,6 +1,8 @@ from .postgresql import PostgresCompiler, PostgresTenantsCompiler, Psycopg2Compiler +from .mysql import MySQLCompiler __all__ = [ + 'MySQLCompiler', 'PostgresCompiler', 'PostgresTenantsCompiler', 'Psycopg2Compiler', @@ -9,6 +11,7 @@ ] compilers = [ + MySQLCompiler, PostgresCompiler, PostgresTenantsCompiler, Psycopg2Compiler diff --git a/sql_compiler/compilers/mysql.py b/sql_compiler/compilers/mysql.py index 1c900ac..d26f815 100644 --- a/sql_compiler/compilers/mysql.py +++ b/sql_compiler/compilers/mysql.py @@ -6,4 +6,28 @@ class MySQLCompiler(BaseSQLCompiler): backend_name = 'django.db.backends.mysql' def compile_sql(self, connection, query, params): - pass + with connection.cursor() as cursor: + compiled_sql = self.mogrify(cursor, query, params) + return compiled_sql + + def mogrify(self, cursor, query, args=None): + db = cursor.connection + if isinstance(query, str): + query = query.encode(db.encoding) + + if args is not None: + if isinstance(args, dict): + nargs = {} + for key, item in args.items(): + if isinstance(key, str): + key = key.encode(db.encoding) + nargs[key] = db.literal(item) + args = nargs + else: + args = tuple(map(db.literal, args)) + try: + query = query % args + except TypeError as m: + raise ProgrammingError(str(m)) + + return query