sqla-raw
An opinionated, minimalist library for fetching data from a SQLAlchemy connection, when you don't need or want an ORM. You know how to write SQL; sqla-raw makes it E-Z to send that raw SQL to your database and get results, saving a lot of DBAPI boilerplate and providing a simple and consistent interface with a result format that is straightfoward to introspect.
Really not much more than a single method (raw.db.result()) that submits raw SQL via a SQLAlchemy Engine connection. By default, db.result() returns all results as a list of SQLAlchemy RowMapping dictionaries, keyed by column names. (See 'Usage – Options' below for other options)
For convenience, result_from_file() and result_by_name() allow you to store your SQL in separate local files for submission to the database via result()
Installation
uv add "sqla-raw[pg]" (or pip install sqla-raw[pg])
Usage
Configure your database connection string by setting $DATABASE_URL in your environment.
>>> from raw import db
>>> x = db.result("select version()");
>>> x
[{'version': 'PostgreSQL 16.3 (Debian 16.3-1.pgdg110+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit'}]
Because it's SQLAlchemy, you can safely use named parameters in your SQL string with colon-prepended :key format, and assign values in kwargs.
>>> db.result("select :foo as bar", foo="baz")
[{'bar': 'baz'}]
Jinja templating
You can also use Jinja2 templating syntax to interpolate the query, if desired. db.result() inspects the query for template tags ("{%.*%}") and renders the template to SQL before submitting if tags are present. (It uses a SandboxedEnvironment for some measure of injection safety, but avoid this option with untrusted inputs, for obvious reasons.)
>>> sql = """
... select * from a_table
... {% if filter_value %}
... where a_column = :filter_value
... {% endif %}
... """
>>> db.result(sql)
[{'a_column': 1}, {'a_column': 2}, {'a_column': 7}, {'a_column': 9}]
>>> db.result(sql, filter_value=7)
[{'a_column': 7}]
Options
Passing argument returns to db.result() (or result_from_file()) overrides the default result formatting: returns="tuples" brings back a list of Row named tuples with row values instead of dictionaries, and returns="proxy" returns the plain SQLAlchemy CursorResult object directly, for further handling by the caller. The "proxy" option allows access to methods (e.g. fetchone() or fetchmany() ) that sqla-raw default usage hides behind its facade; it can also be good for SQL statements (such as inserts without returning or DDL) that are not expected to return results — although by default these will return an empty list.
SQL file handling
For longer or more complex queries, you may find it more convenient and maintainable to save your SQL in its own file, rather than include it inline as a string in your Python program. Doing so also allows the queries to be tested and/or reused in your preferred database client tool. sqla-raw provides two ways to do this:
-
result_from_file()takes a path (any file-like object should also work) and reads your query from there, rather than taking a SQL string argument directly. Contents of the file are handed off to result() so the rest functions identically. -
result_by_name()looks up files with a.sqlextension in a local directory — it looks in${PWD}/query_filesby default, or you may specify any arbitrary filesystem location by setting$QUERY_PATHin the environment. Thequery_nameargument is the stem of the desired file, i.e. the base name of the file without the.sqlextension.
SQLAlchemy Engine invocation
By default, Engine instantiation is handled implicitly on first call to result(); subsequent calls use a connection from the pool. The default connection string for the Engine is set by DATABASE_URL in the environment, and all other Engine settings use SQLAlchemy defaults. This allows you to simply call result() and start querying $DATABASE_URL immediately with a minimum of fuss.
In case you require multiple database connections, or more control over Engine parameters, db.engine() wraps sqlalchemy.create_engine(), so you can set a different connection string or pass additional settings as keyword arguments (see https://docs.sqlalchemy.org/en/latest/core/engines.html for options). Once db.engine() is explicitly invoked, the engine so instantiated remains as the active connection pool unless changed again.
Snowflake database keypair authentication
Assuming you have a Snowflake user set up with keypair authentication, db.engine() can be configured to connect that way by supplying a PEM-formatted private key in the environment, in either of two ways:
PRIVATE_KEY_PATH— a path to your private key file. Best for local development, where the key is a file on disk anyway.PRIVATE_KEY— the key itself, as a string. Best for containers and hosted platforms, where injecting a config value is easier than mounting a file. Because such platforms often can't hold multi-line values, the line breaks may be given as literal\nsequences and they will be restored; a real PEM never contains that sequence, so passing a key with genuine newlines works too.
In both cases, set PRIVATE_KEY_PASSPHRASE to the passphrase for the key, or leave it unset if the key is not encrypted.
Set one or the other, not both. If both are set, PRIVATE_KEY_PATH takes precedence and PRIVATE_KEY is ignored. That rule is only a tiebreaker, though, and not a good thing to rely on: when the two hold different keys, PRIVATE_KEY_PASSPHRASE can only match one of them, so you are as likely as not to get a decryption error that points nowhere near the actual mistake. Pick whichever suits your deployment and leave the other unset.
Note that the key is only read for connection strings beginning with snowflake; it is ignored for other databases.
As with other database drivers, snowflake-sqlalchemy is optional and not a strict dependency of sqla-raw so will need to be installed separately for Snowflake connections.
Exception handling
Obviously, when interacting with a database, any number of things can go wrong, that may or may not be the fault of your code. Besides obvious syntax errors, inputs to parameters might be the wrong type, the database could be unreachable, credentials incorrect or changed, etc. Early development versions of sqla-raw tried to catch any database exceptions and return them formatted like results, on the theory that any calling program wouldn't want to halt execution on such errors. On further reflection, it doesn't seem like a library should be making that decision, and sqla-raw as of version 1.x allows any exceptions it may encounter to be raised in the usual way. Any calling code that does not wish to halt on these exceptions may of course simply wrap the call to any raw.db method in a try/except block itself. In hindsight, it probably should have been clear this was the right way to do it all along.
Tests
pytest tests are located in tests/. This project uses uv for environment and dependency management, so no separate virtualenv setup is needed:
uv sync # create .venv and install runtime + dev dependencies
uv run pytest --cov
Dependencies are declared in pyproject.toml and locked in uv.lock. To add a dependency, use uv add <package> (or uv add --dev <package> for a development-only one) rather than editing the lockfile by hand; uv lock --upgrade refreshes all pins.
Releasing
Bump version in pyproject.toml, then:
uv build
uv publish
git tag vX.Y.Z # the version you just set
git push --tags
Alternatives and prior art
These are all fine projects, and if sqla-raw appeals to you at all, you owe it to yourself to take a look at them. These and sqla-raw are all similar tools with similar SQL-first, non-ORM philosophies. I haven't benchmarked performance for any one of them, but 3 out of 4 use SQLAlchemy under the covers, and I'd be surprised if there are big differences among at least those three. Until some notable difference in performance turns up, the best choice for you is most likely a matter of taste.
- aiosql
- Supports standard and async I/O
- Turns SQL files into callable methods
- Nothing wrong with that, but different from the interface chosen for
sqla-raw(which takes the SQL or file name as argument to a singleresult()method) - Relies on special comments in the SQL
- Nothing wrong with that, but different from the interface chosen for
- Not SQLAlchemy; supports a more limited set of database drivers
- Doesn't handle database connect instantiation (expects to be given a conn object)
- PugSQL
- Based on Clojure's HugSQL library
- Uses SQLAlchemy
- Similar API to
aiosql, with commented SQL files used to create methods- Also generates modules from folders of SQL files, and can load multiple such modules
- Records
- Another SQLAlchemy facade, and a big inspiration for
sqla-raw Doesn't seem to be actively maintained(This may have improved somewhat – it's been updated in the last year)- Formats results as a specialized
Recordclass, based ontablib- Again, nothing wrong with that —
sqla-rawfavors a standard list-of-dicts format for results instead, as lighter weight and easier to introspect
- Again, nothing wrong with that —
- Another SQLAlchemy facade, and a big inspiration for
Release files for sqla-raw 1.8.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sqla_raw-1.8.0.tar.gz | 16.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sqla_raw-1.8.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size:27.3 kB
Release files / sqla_raw-1.8.0.tar.gz
| Download URL | sqla_raw-1.8.0.tar.gz |
|---|---|
| Size | 16.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9acf3febeb2895cf9004d45141c2532928ab17e801c57478b944645a9e9c8883
|
|
BLAKE2b-256 checksum How to use checksums |
e0575f68ce1aea9bcaea2bb13c412b1ada2d54a987781c148302ae3524fa0f50
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.2 {"installer":{"name":"uv","version":"0.10.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / sqla_raw-1.8.0-py3-none-any.whl
| Download URL | sqla_raw-1.8.0-py3-none-any.whl |
|---|---|
| Size | 10.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a0105a01c3506a3202bd942b8d9b0baacdc5f6ba3cb3324440804d9c038dabad
|
|
BLAKE2b-256 checksum How to use checksums |
5fbd4f117ea888e3f61af4c35d17a4ff8da630df580212c596f088f1bd5406e0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.2 {"installer":{"name":"uv","version":"0.10.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|