The aiosql project is for writing SQL to interact with a database. Most database libraries are intended to reduce the amount of SQL developers need to write, aiosql takes an alternative approach. Why?
- Alternatives are good. No approach fits all use cases, no matter how predominant.
- SQL is the most expressive and performant way to interact with a SQL database.
- Investigating where a query came from is simpler when it is source controlled, named, and written by a human.
- Writing SQL in files gives you built-in compatibility with powerful SQL tools like DataGrip and psql.
ORMs and SQL Query Builders offer object interfaces to generate and execute SQL. They exist to ease development, not to make it simpler. Inheriting object hierarchies, mixing data with behaviors, mirroring a database schema, and generating SQL are not simple. ORMs are introduced early in a project's life when requirements are limited and the need to move fast is paramount. As a project grows, ORM objects and their relations grow too, they become a source of complexity and coupling.
aiosql doesn't solve these problems directly either, your application will still get more complex with time. You can write bad SQL and bad python. But, with aiosql there is no mandate that all interaction with the database go through a complex network of related python objects that mirror a database schema. The only mandates are that you write SQL to talk to the database and python to use the data. From there you start with a system in which the database and the application are intentionally separate and independent from each other so they can change independently. The architecture of your application and the boundaries you choose between it and the database is left to you.
The documentation for projects like SQLAlchemy and Django DB can give you a better vision for the class of problems that ORMs do solve and the productivity gains they intend. Please choose these projects over aiosql if you find that they fit the needs of your application better.
This section goes over the three ways to make SQL queries available for execution in python. You'll learn the basics of defining queries so aiosql can find them and turn them into methods on a
Queries object. For more details reference the Defining SQL Queries documentation.
From a SQL File¶
SQL can be loaded by providing a path to a
.sql file. Below is a blogs.sql file that defines two queries.
-- name: get_all_blogs select blogid, userid, title, content, published from blogs; -- name: get_user_blogs -- Get blogs with a fancy formatted published date and author field select b.blogid, b.title, strftime('%Y-%m-%d %H:%M', b.published) as published, u.username as author from blogs b inner join users u on b.userid = u.userid where u.username = :username order by b.published desc;
--name: <name_of_method> comments and the
:username substitution variable. The comments that start with
--name: are the magic of aiosql. They are used by
aiosql.from_path to parse the file into separate methods accessible by the name. The
aiosql.from_path function takes a path to a sql file or directory and the name of the database driver intended for use with the methods.
queries = aiosql.from_path("blogs.sql", "sqlite3")
In the case of blogs.sql we expect the following two methods to be available. The
username parameter of
get_user_blogs will substitute in for the
:username variable in the SQL.
def get_all_blogs(self) -> List: pass def get_user_blogs(self, username: str) -> List: pass
From a SQL String¶
SQL can be loaded from a string as well. The result below is the same as the first example above that loads from a SQL file.
sql_str = """ -- name: get_all_blogs select blogid, userid, title, content, published from blogs; -- name: get_user_blogs -- Get blogs with a fancy formatted published date and author field select b.blogid, b.title, strftime('%Y-%m-%d %H:%M', b.published) as published, u.username as author from blogs b inner join users u on b.userid = u.userid where u.username = :username order by b.published desc; """ queries = aiosql.from_str(sql_str, "sqlite3")
Queries object here will have two methods:
queries.get_all_blogs(conn) queries.get_user_blogs(conn, username="johndoe")
From a Directory of SQL Files¶
Loading a directory of SQL files loads all of the queries defined in those files into a single object. The
example/sql directory below contains three
.sql files and can be loaded using
example/sql ├── blogs.sql ├── create_schema.sql └── users.sql
queries = aiosql.from_path("example/sql", "sqlite3")
queries object will have a mixture of methods from all the files.
Don't name queries the same in various files in the same directory. The last one loaded will win. See Subdirectories below to namespace queries.
Introducing subdirectories allows namspacing queries. This provides a way to further organize and group queries conceptually. For instance, you could define blog queries separate from user queries access them on distinct properties of the queries object.
Assume the blogs.sql and users.sql files both contain a
--name: get_all query.
example/sql ├── blogs │ └── blogs.sql ├── create_schema.sql └── users └── users.sql
queries = aiosql.from_path("example/sql", "sqlite3")
Queries object has two nested
get_all methods accessible on attributes
.users. The attributes reflect the names of the subdirectories.
Calling Query Methods¶
The connection or
conn is always the first argument to an aiosql method. The
conn is an open connection to a database driver that your aiosql method can use for executing the sql it contains. Controlling connections outside of aiosql queries means you can call multiple queries and control them under one transaction, or otherwise set connection level properties that affect driver behavior.
For more see: Leveraging Driver Specific Features.
In the examples throughout this page a
conn object has been passed. Here is a more code complete example that shows the connection creation and call to
aiosql.from_path that make a queries object.
>>> import sqlite3 >>> import aiosql >>> conn = sqlite3.connect("./mydb.sql") >>> # Note the "sqlite3" driver_adapter argument is what tells >>> # aiosql it should be expecting a sqlite3 connection object. >>> queries = aiosql.from_path("./blogs.sql", "sqlite3") >>> queries.get_all_blogs(conn) [(1, 1, 'What I did Today', 'I mowed the lawn, washed some clothes, and ate a burger.\n' '\n' 'Until next time,\n' 'Bob', '2017-07-28'), (2, 3, 'Testing', 'Is this thing on?\n', '2018-01-01'), (3, 1, 'How to make a pie.', '1. Make crust\n2. Fill\n3. Bake\n4. Eat\n', '2018-11-23')]
-- name: get_user_blogs -- Get blogs with a fancy formatted published date and author field select b.blogid, b.title, strftime('%Y-%m-%d %H:%M', b.published) as published, u.username as author from blogs b inner join users u on b.userid = u.userid where u.username = :username order by b.published desc;
aiosql allows paremeterization of queries by parsing values like
:username in the above query and having the resultant method expect an inbound argument to substitute for
You can call the
get_user_blogs function with plain arguments or keyword arguments with the name of the subsitution variable.
>>> import sqlite3 >>> import aiosql >>> conn = sqlite3.connect("./mydb.sql") >>> queries = aiosql.from_path("./blogs.sql", "sqlite3") >>> >>> # Using keyword args >>> queries.get_user_blogs(conn, username="bobsmith") [(3, 'How to make a pie.', '2018-11-23 00:00', 'bobsmith'), (1, 'What I did Today', '2017-07-28 00:00', 'bobsmith')] >>> >>> # Using positional argument >>> queries.get_user_blogs(conn, "janedoe") [(2, 'Testing', '2018-01-01 00:00', 'janedoe')]
When passing positional arguments aiosql will apply them in the order that the substitutions appear in your SQL. This can be convenient and clear in some cases, but confusing in others. You might want to choose to always name your arguments for clarity.