Skip to content

A SQL generating language using Jinja templating that allows Python execution.

Notifications You must be signed in to change notification settings

ksmeeks0001/pysql

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PySQL

Author : Kevin Smeeks

PySQL is an extension built off of Jinja2. It adds a Python extension to execute code between {% python %}{% endpython %} tags. Variables set between these tags are made available to the template context. A SQL extension allows for results to be loaded into context. {% sql myvariable %}select * from mytable{% endsql %} will load the result of the query into myvariable as a list.

There are a few reasons behind the project. One, SQL does not allow variable table or column names. Two, when using Python to do work on the database I did not like that my SQL had to be contained in strings. I wanted to be able to write my SQL as I regularly would with the addition of Python when I needed it.

All Jinja template syntax is valid. Below will demonstrate the use of the Python and SQL extensions. SQL is generated by executing python pysql.py input_file_name.pysql. By default this will print the resulting SQL. You may pass a file to output the rendered template by using python pysql.py input_file_name.pysql -o my_output_file.sql.

A basic example

{% python %}
import datetime

today = datetime.date.today()
yyyymm = str(today.year)+str(today.month).zfill(2)
table_name = f"report_{yyyymm}"

{% endpython %}
select * from {{ table_name }};

Variables are accessible throughout the template and can be updated as you go.

{% python %}
table_name = "mytable1"
{% endpython %}
select * from {{ table_name }};
{% python %}
table_name = "mytable2"
{% endpython %}
select * from {{ table_name }};

wil result in

select * from mytable1;
select * from mytable2;

There are a few filters defined to help with building queries as well.

{% python %}

table_name = "my table"
mycolumns = ["mycol1", "mycol2", "mycol3"]
myvalues = ['hello', 5, None]

{% endpython %}
{% for value in myvalues %}
select {{ mycolumns|columns }} from {{ table_name|sql_escape }} where mycol1 {{ value|where }};
{% endfor %}

Will result in

select `mycol1`,`mycol2`,`mycol3` from `my table` where mycol1 = 'hello';
select `mycol1`,`mycol2`,`mycol3` from `my table` where mycol1 = 5;
select `mycol1`,`mycol2`,`mycol3` from `my table` where mycol1 IS NULL;

The sql_escape wraps table and column names in backticks. It is probably easier to use backticks yourself in most cases, but it used within the columns filter which takes a list of column names and wraps them in backticks, separating them by commas.

The where filter is used on the value within a condition. As seen in the example above, the '=' is added to the condition, strings are wrapped in quotes, numbers are left as is, and None is converted to IS NULL.

If you only want to escape a string with quotes, use the quote filter.

{% python %}
mystring = "My dog's house is red"
{% endpython %}
select * from mytable where mycol = {{ mystring|quote }};

outputs

select * from mytable where mycol = 'My dog\'s house is red';

Additionaly you can specify to use double quotes

{% python %}
mystring = "My dog's house is red"
{% endpython %}
select * from mytable where mycol = {{ mystring|quote("double") }};

outputs

select * from mytable where mycol = "My dog's house is red";

The where filter can be used for other operators as well.

{% python %}
from datetime import datetime
from decimal import Decimal

values = ["some string", 5, None, 5.2, datetime.now(), Decimal('4.7')]

me = {'firstname': 'kevin', 'dob': datetime.strptime('1975-04-16', '%Y-%m-%d').date()}

{% endpython %}

{% for value in values %}
select * from mytable where col1 {{ value|where('!=') }};
{% endfor %}

{% for value in values %}
select * from mytable where col1 {{ value|where('>') }};
{% endfor %}

select * from users where DOB {{ me.dob|where('>=') }};

{% python %}

from datetime import timedelta
me['dob'] += timedelta(days=365 * 5)

{% endpython %}

select * from users where DOB {{ me.dob|where('>=') }};

output

select * from mytable where col1 != 'some string';
select * from mytable where col1 != 5;
select * from mytable where col1 IS NOT NULL;
select * from mytable where col1 != '5.2';
select * from mytable where col1 != '2023-05-03 22:36:52.277321';
select * from mytable where col1 != '4.7';

select * from mytable where col1 > 'some string';
select * from mytable where col1 > 5;
select * from mytable where col1 > 'NULL';
select * from mytable where col1 > '5.2';
select * from mytable where col1 > '2023-05-03 22:36:52.277321';
select * from mytable where col1 > '4.7';

select * from users where DOB >= '1975-04-16';


select * from users where DOB >= '1980-04-14';

'!=' is converted to IS NOT NULL but other logical operators simply use the NULL value.

SQL Extension

There is also a SQL extension that will execute queries immediatly and are not part of the output. To use the SQL extension, create a configs.json file and set the PYSQL_DB_CONN_STRING key to a SQLAlchemy connection string. Rows are returned as a list of dictionaries. This can be useful when you need to use values from one table to generate SQL. For example :

python3 pysql.py examples/example7.pysql -c configs.json
{% sql rows %}select * from test.products{% endsql %}
{% python %}
tables = []
for row in rows:
    tables.append('items_' + row['Product'])
{% endpython %}
{% for table in tables %}
create table test.{{ table|sql_escape }} (

    ID int not null auto_increment,
    MPN varchar(64) not null,
    Brand varchar(128),
    CurrentPrice decimal(8,2),
    primary key(ID),
    index (MPN),
    index (Brand)
);
{% endfor %}

example output

create table test.`items_Cooktops` (

    ID int not null auto_increment,
    MPN varchar(64) not null,
    Brand varchar(128),
    CurrentPrice decimal(8,2),
    primary key(ID),
    index (MPN),
    index (Brand)
);
create table test.`items_Microwaves` (

    ID int not null auto_increment,
    MPN varchar(64) not null,
    Brand varchar(128),
    CurrentPrice decimal(8,2),
    primary key(ID),
    index (MPN),
    index (Brand)
);
create table test.`items_Ranges` (

    ID int not null auto_increment,
    MPN varchar(64) not null,
    Brand varchar(128),
    CurrentPrice decimal(8,2),
    primary key(ID),
    index (MPN),
    index (Brand)
);

About

A SQL generating language using Jinja templating that allows Python execution.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Languages