{"id":29354220,"url":"https://github.com/stenci/uniquery","last_synced_at":"2026-05-08T15:18:41.716Z","repository":{"id":303637357,"uuid":"1015686698","full_name":"stenci/UniQuery","owner":"stenci","description":"Raw SQL in. Typed Python Objects out. The only ORM that just does ORM.","archived":false,"fork":false,"pushed_at":"2025-07-08T16:06:35.000Z","size":43,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-07-08T17:25:10.795Z","etag":null,"topics":["code-generation","data-models","introspection","object-relational-mapping","orm","postgresql","python","raw-sql","sql","sql-join","sqlglot","sqlite","typed-orm","uniquery"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stenci.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-07-07T22:20:03.000Z","updated_at":"2025-07-08T16:06:38.000Z","dependencies_parsed_at":"2025-07-08T17:25:48.202Z","dependency_job_id":"95708f38-ee9e-413f-abb3-760e06b49450","html_url":"https://github.com/stenci/UniQuery","commit_stats":null,"previous_names":["stenci/uniquery"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/stenci/UniQuery","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stenci%2FUniQuery","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stenci%2FUniQuery/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stenci%2FUniQuery/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stenci%2FUniQuery/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stenci","download_url":"https://codeload.github.com/stenci/UniQuery/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stenci%2FUniQuery/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264384389,"owners_count":23599612,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["code-generation","data-models","introspection","object-relational-mapping","orm","postgresql","python","raw-sql","sql","sql-join","sqlglot","sqlite","typed-orm","uniquery"],"created_at":"2025-07-09T03:11:46.195Z","updated_at":"2026-05-08T15:18:41.705Z","avatar_url":"https://github.com/stenci.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# UniQuery\n\n**Raw SQL in. Typed Python Objects out. The only ORM that just does ORM.**\n\n## Table of Contents\n\n- [Overview](#overview)\n- [Introduction](#introduction)\n- [Traditional ORMs vs UniQuery](#traditional-orms-vs-uniquery)\n- [Installation](#installation)\n- [Getting Started](#getting-started)\n- [Features](#features)\n- [Architecture Details](#architecture-details)\n- [License](#license)\n\n## Overview\n\nHere’s what UniQuery is about: raw SQL in, typed Python objects out — with relationships handled automatically.\n\n```python\nfrom my_models import db_config, UniQuerySession, QueryResult\n\nwith UniQuerySession(db_config) as session:\n    with session.transaction() as tr:\n        tr.query(q := QueryResult(), \"\"\"\n            SELECT *\n            FROM clients\n            FULL OUTER JOIN clients_cars ON clients.id = clients_cars.client_id\n            FULL OUTER JOIN cars ON clients_cars.car_id = cars.id\n        \"\"\")\n\nprint(q.cars[0].clients[0].name)\nprint(q.clients[0].cars[0].make)\nprint(q.cars_dict['Car1'].make)\n```\n\n- Write raw SQL with a `JOIN`\n- UniQuery automatically populates `q.cars` and `q.clients`\n- Each `Car` object gets its related `Client` objects as `.clients`\n- Each `Client` object includes a `.cars` list of related cars\n\n## Introduction\n\nUniQuery is not a traditional ORM — it doesn’t create tables, manage migrations, or build queries for you.\n\nInstead, it’s designed for developers who prefer writing raw SQL and want Python objects back — with relationships handled automatically.\n\nMost traditional ORMs hide SQL behind layers of abstraction, which leads to limitations, hidden performance problems, and extra learning curves. UniQuery instead assumes you already know SQL and builds a thin, powerful interface that adds convenience without taking control away.\n\nDatabase design is best handled using tools like **DataGrip** or **Azure Data Studio** — not an ORM. I don’t want to use an ORM to define my schema. I want to use the right tools for that. An ORM is supposed to mean _Object Relational Mapping_, and that’s exactly what I need: a library that, after I run the SQL I wrote myself (with the right tools), gives me back lists and dictionaries of properly typed Python objects, connected through their relationships, not something that limits my SQL, slows down my queries, or gives me weak type hints.\n\n**UniQuery** is a Python library that provides a unified interface for working with relational databases by automatically generating data model classes from your database schema and enabling intuitive querying and data manipulation. It supports multiple database backends (currently SQLite and PostgreSQL) and uses introspection to create Python classes for each database table. With UniQuery, you can write standard SQL queries and receive the results as Python objects with their inter-table relationships automatically handled.\n\n## Traditional ORMs vs UniQuery\n\n| Feature               | Traditional ORMs                                                                                 | UniQuery                                                                                       |\n| --------------------- | ------------------------------------------------------------------------------------------------ |------------------------------------------------------------------------------------------------|\n| **Schema Definition** | You define the classes, and they generate the database schema.                                   | You start with an existing database, and generate the classes from it using `generate_models`. |\n| **Drawbacks**         | Trial and error to get schema right; advanced DB features often inaccessible due to abstraction. | No abstraction: use all advanced DB features directly.                                         |\n| **Querying**          | Must use the ORM's query builder — often unintuitive and limiting for complex queries.           | Write pure SQL in your DB dialect — any query your DB supports is valid.                       |\n| **Type Hinting**      | Often incomplete or incorrect due to dynamic typing and query builder complexity.                | Fully automatic and accurate via `generate_models`.                                            |\n| **Schema Sync**       | Easy to get out of sync by editing classes directly.                                             | Update schema → rerun `generate_models` → classes are synced (custom methods preserved).       |\n| **1+N Problem**       | Requires manual optimization or advanced ORM knowledge to avoid.                                 | Make one SQL query, UniQuery normalizes everything automatically in one round trip.            |\n| **Migrations**        | Painful: learn ORM-specific migration DSL and keep classes backward-compatible.                  | Just run your SQL migrations and regenerate models. Done.                                      |\n\n---\n\n## Installation\n\nInstall for SQLite only:\n```bash\npip install uniquery\n```\n\nInstall for Postgres:\n```bash\npip install uniquery[postgres]\n```\n\nInstall via pip from GitHub:\n```bash\npip install git+https://github.com/stenci/UniQuery.git\n```\n\n## Getting Started\n\nUsing UniQuery involves a simple 4+1 step process:\n\n### Step 1: Create Your Database (Using Your Preferred Tools)\n\nDesign and create your database schema using tools like **DataGrip**, **Azure Data Studio**, or plain SQL. Ensure that every table — including link tables used for many-to-many relations — has a **primary key**. UniQuery requires this for proper model generation and relationship mapping.\n\n### Step 2: Generate Python Models\n\nRun the `generate_models()` function to introspect the database and generate a Python module with ORM model classes and schema metadata:\n\n```python\nfrom uniquery.uniquery_sqlite import ModelGenerator\nModelGenerator.generate_models(\"path/to/your_database.sqlite\", \"my_models.py\")\n```\n\nThis creates:\n- One `UniQueryModel` subclass per table\n- A `QueryResult` container class\n- A `db_config` dictionary with connection/schema info\n- Clearly marked **editable regions** in the file where you can safely add custom methods\n\n### Step 3: Customize Models (Optional but Recommended)\n\nOpen the generated `my_models.py` and add helper methods or computed properties inside the editable regions (`# region custom code` / `# endregion`). These edits are preserved when regenerating models.\n\n### Step 4: Connect, Query, and Use ORM Models\n\nYou can now connect to the database and run SQL queries. UniQuery maps the results to model instances and populates relationships automatically.\n\n```python\nfrom my_models import UniQuerySession, QueryResult, db_config\n\nwith UniQuerySession(db_config) as session:\n    with session.transaction() as tr:\n        tr.query(q := QueryResult(), \"SELECT * FROM cars\")\n\nprint(q.cars[0].make)\n```\n\n- `q.cars` is a list of `Car` instances.\n- `q.cars_dict` maps primary keys (e.g. car IDs) to `Car` objects.\n- Relationships like `.clients` on each car will only be populated if your query joins the `clients` table — see the [Overview](#overview) for an example.\n\n### Step 5 (Optional): Regenerate Models After Schema Changes\n\nIf your database structure changes, simply rerun:\n\n```python\nModelGenerator.generate_models(\"path/to/your_database.sqlite\", \"my_models.py\")\n```\n\nUniQuery will update the model definitions while preserving all your customizations in the editable regions.\n\n---\n\n## Features\n\n* **Automatic Model Generation:** UniQuery can introspect an existing database and generate Python model classes for each table. The `ModelGenerator` utility creates a models file with classes (subclasses of `UniQueryModel`) representing tables, including their fields and relationships.\n  * Note: Every table in the database must have a primary key, including link tables used for many-to-many relationships.\n\n* **Multi-Database Support:** The library is designed to work with different SQL databases. It includes implementations for SQLite and PostgreSQL, each with appropriate handling of connections, SQL syntax, and placeholders.\n\n* **Unified Query Interface:** Write regular SQL queries (including JOINs and complex selects) and let UniQuery handle the rest. When you execute a query through UniQuery, it automatically builds Python objects for each table involved in the query and organizes them into a `QueryResult` container. If your query involves multiple tables, UniQuery populates the corresponding lists in the `QueryResult` (e.g. `query_result.orders` and `query_result.customers` for an orders-customers join) and sets up the relationships between objects. For example, in a scenario with a many-to-many relation between *clients* and *cars*, each `Car` instance will have a `.clients` list of related clients, and each `Client` instance a `.cars` list – all populated automatically from the query results.\n\n* **Relationship Handling:** UniQuery infers one-to-many, many-to-one, and many-to-many relationships from foreign keys in your schema and makes navigating these relations easy. After a query, related records can be accessed as attributes on the model instances. For instance, if an **Invoice** table has a foreign key to **SalesRep**, querying invoices will allow each invoice object to have a `.salesrep` attribute, and each salesrep object will have an `.invoices` list of Invoice objects. Many-to-many link tables are recognized (if a table consists solely of two foreign keys plus a primary key) and the related objects on both sides are connected through convenient list attributes.\n\n* **Naming Conventions:** UniQuery uses a consistent pattern:\n  - Model classes are singular (e.g. `Client`, `CarModel`)\n  - On the one side of a relationship, attributes are singular (e.g. `client.salesrep`)\n  - On the many side, attributes are plural (e.g. `salesrep.clients`)\n  - Many-to-many relations use plural lists on both sides\n  This helps make code predictable and type-safe. Attribute names are generated using singularization/pluralization with support for common irregular forms.\n\n* **Choosing the Right JOIN**: In many cases, a `LEFT OUTER JOIN` or even a `FULL OUTER JOIN` is not only acceptable but ideal. It ensures that UniQuery can construct complete object lists for both sides of the join, even when one side has no corresponding match. This allows, for instance, a `Client` object with no `Car` entries (or vice versa) to still be included in the result, with its `.cars` or `.clients` list simply being empty. Since UniQuery populates the relationships based on joined data, outer joins are often the most natural way to retrieve and link all involved objects at once.\n\n* **Transaction Management:** UniQuery provides a high-level transaction API via context managers. You work with a `UniQuerySession` (database connection context) and open transactions using `session.transaction()`. Within a transaction, you can execute queries and make changes. The transaction can be committed or rolled back as needed. The API supports nested transactions with savepoints for databases that allow it (so you can safely handle partial rollbacks). Simply exiting the transaction context (or calling `abort()` on it) will roll back uncommitted changes, whereas calling `commit()` will persist them and keep the transaction open for further operations.\n\n* **Data Manipulation (CRUD Operations):** The model objects returned by UniQuery are active records – you can create, update, or delete using intuitive methods:\n\n  * *Create:* Use the class method `YourModel.create_record(transaction, **fields)` to instantiate a new object within an active transaction. After setting any necessary fields, call `.save()` on the instance to insert it into the database.\n  * *Read:* Fetch data by writing SQL queries with `transaction.query()`, or use the convenience class method `YourModel.get_by_pk_value(transaction, primary_key)` to retrieve a single record by primary key.\n  * *Update:* Modify the attributes of an object and call its `.save()` method to update the corresponding row in the database. UniQuery will perform an INSERT or UPDATE as appropriate (it handles upserts where supported).\n  * *Delete:* Call an object's `.delete_record()` method to remove that record from the database. This will execute a DELETE statement for you under the hood.\n\n* **Convenient Query Results:** The `QueryResult` object holds results for each table involved in a query and enables easy access via lists and dictionaries. For example, after a join query, you can access `query_result.TableA` (list of TableA objects) and `query_result.TableB` (list of TableB objects), and also use `query_result.TableA_dict[id_value]` to get a specific object by its primary key.\n\n* **Logging and Debugging:** If you enable SQL logging (by passing `log_sql=True` when creating a session), UniQuery will print out the SQL commands being executed. These statements are fully rendered with parameters already applied, making them immediately executable in your SQL shell or database console. This is especially useful for debugging, profiling, or understanding how your queries are constructed and executed.\n\n* **Transaction Success Exceptions:** In some frameworks like **CherryPy**, control flow exceptions such as `HTTPRedirect` are used to indicate success. You can configure UniQuery to treat these as successful — not causing a rollback — by passing them to `succeed_exceptions`.\n  ```python\n  from cherrypy import HTTPRedirect\n  \n  with session.transaction(succeed_exceptions=(HTTPRedirect,)) as tr:\n      ...\n      raise HTTPRedirect(\"/done\")  # This will NOT cause a rollback\n  ```\n\n#### Query Requirements and Limitations\n\nWhen using `Transaction.query`, the SQL query must return columns from one or more actual tables in the schema. If you explicitly list columns for a table (instead of using `SELECT *`), you must include that table’s primary key among the returned columns so that UniQuery can map the results correctly. Queries must not return calculated or derived columns (e.g., expressions, functions, or aliases not tied directly to schema fields). If you need to access all result columns — including calculated values — use the `execute` method with `get_dicts=True` instead.\n\n_**TODO**: Add support for calculated columns and aggregate queries in `Transaction.query`._\n\n### Architecture Details\nUniQuery’s architecture is built around a set of core classes that abstract database operations and mapping:\n\n* **Model Classes:** For each database table, UniQuery uses a generated subclass of `UniQueryModel` to represent that table. These classes contain a nested `Meta` class with metadata about the table (like `table_name`, `primary_key`, list of `columns`, and relationship mappings). The fields of the table and relationships to other tables are represented as class attributes (for type hinting) and are populated at runtime. Each model class also gets utility methods like `create_record` and `delete_record` injected during generation.\n\n* **Session and Transaction:** A `UniQuerySession` represents a connection to the database. There are separate session classes for each supported DB dialect (e.g., `uniquery_sqlite.UniQuerySession` and `uniquery_postgres.UniQuerySession`), both inheriting from a common `UniQuerySessionBase`. The session is used as a context manager to open/close the database connection. Within a session, you create a `Transaction` (also database-specific subclass) which manages a database cursor and the scope for executing queries or modifications. The transaction implements context manager methods `__enter__` and `__exit__` to begin and end (commit/rollback) the transaction. This design abstracts away the differences in how each DB handles transactions (for example, SQLite supports different modes like DEFERRED or IMMEDIATE, while PostgreSQL uses savepoints for nested transactions).\n \n  UniQuery automatically handles nested transactions. When you start a new transaction inside an existing one, it creates a savepoint if the database supports it (e.g. SQLite or PostgreSQL). This lets you safely nest logic blocks without worrying about conflicts — inner transactions can roll back independently, while the outer transaction remains open until explicitly committed or aborted.\n\n* **Query Processing:** When you call `transaction.query(query_result, sql, params)`, UniQuery parses the SQL query using the **sqlglot** library to determine which tables are involved and to properly handle SQL parameter placeholders. If UniQuery cannot determine the tables involved in a query (e.g. with UNIONs or CTEs), you can pass an explicit list of model classes using the `models` argument in `transaction.query(...)`. The order of models must match the order of columns returned by the SQL query. If there’s a mismatch, UniQuery will raise a `WrongNumberOfColumnsInQuery` exception.\n It then executes the query and iterates over the returned rows. For each table in the query, a `UniQueryTable` object is created internally to accumulate results. Each row from the cursor is processed and turned into model instances for the respective tables using the schema information from `db_config`. UniQuery sets each field value on these instances and then establishes relations:\n\n  * One-to-many and many-to-one links are set by linking objects via their foreign keys (e.g., setting the `.customer` reference on an Order, and adding that Order to the `.orders` list on the corresponding Customer).\n  * Many-to-many relationships are resolved by recognizing link tables and connecting the objects on either side (e.g., adding a `Car` to a `Client`’s `.cars` list and vice versa for a `clients_cars` association table).\n    After mapping, the `QueryResult` object is populated with lists and dictionaries of the results for each table. All these steps happen behind the scenes in the `Transaction.query()` implementation.\n\n* **Data Persistence:** The model instances carry a reference to the transaction they belong to (through their internal `_table` object). When you call `instance.save()`, UniQuery determines whether to perform an INSERT or an UPDATE. If the object is new (its primary key attribute was `None` before save), an INSERT is issued, and UniQuery captures the last inserted ID (if autoincrement) to update the object. If the object already exists (primary key is set), it performs an upsert (INSERT ... ON CONFLICT DO UPDATE for PostgreSQL or a REPLACE/UPDATE equivalent for SQLite). The deletion via `delete_record()` simply executes a DELETE statement for that primary key. By abstracting this logic, UniQuery allows you to persist changes to objects without writing SQL manually.\n\n## License\n\nLicensed under the [MIT License](./LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstenci%2Funiquery","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstenci%2Funiquery","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstenci%2Funiquery/lists"}