{"id":45919815,"url":"https://github.com/k98kurz/sqloquent","last_synced_at":"2026-04-02T22:26:56.375Z","repository":{"id":170431263,"uuid":"609976740","full_name":"k98kurz/sqloquent","owner":"k98kurz","description":"Python 3+ SQL ORM system inspired by Eloquent","archived":false,"fork":false,"pushed_at":"2025-11-17T20:39:49.000Z","size":583,"stargazers_count":1,"open_issues_count":13,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-11-17T21:19:27.312Z","etag":null,"topics":["orm","sql"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"isc","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/k98kurz.png","metadata":{"files":{"readme":"readme.md","changelog":"changelog.md","contributing":null,"funding":".github/FUNDING.yml","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,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"polar":"pycelium","github":"k98kurz"}},"created_at":"2023-03-05T19:40:56.000Z","updated_at":"2025-11-17T20:39:53.000Z","dependencies_parsed_at":"2024-11-10T17:26:02.199Z","dependency_job_id":"8b3bc01b-8144-426d-aa40-66e0ca828243","html_url":"https://github.com/k98kurz/sqloquent","commit_stats":null,"previous_names":["k98kurz/simplesqlorm","k98kurz/sqloquent"],"tags_count":23,"template":false,"template_full_name":null,"purl":"pkg:github/k98kurz/sqloquent","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/k98kurz%2Fsqloquent","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/k98kurz%2Fsqloquent/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/k98kurz%2Fsqloquent/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/k98kurz%2Fsqloquent/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/k98kurz","download_url":"https://codeload.github.com/k98kurz/sqloquent/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/k98kurz%2Fsqloquent/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29928998,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-27T19:37:42.220Z","status":"online","status_checked_at":"2026-02-28T02:00:07.010Z","response_time":90,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["orm","sql"],"created_at":"2026-02-28T08:36:27.453Z","updated_at":"2026-04-02T22:26:56.367Z","avatar_url":"https://github.com/k98kurz.png","language":"Python","funding_links":["https://polar.sh/pycelium","https://github.com/sponsors/k98kurz"],"categories":[],"sub_categories":[],"readme":"# Sqloquent\n\nThis is a SQL library with included bindings for sqlite. Inspired by Laravel and\nin particular Eloquent.\n\n## Overview\n\nThis package provides a set of interfaces and classes to make using a SQL\ndatabase easier and simpler, both synchronously and by using asyncio. (See\nsection below for full list.)\n\nThe primary features are the `SqlQueryBuilder`, `SqlModel`, and `HashedModel` base\nclasses (or `AsyncSqlQueryBuilder`, `AsyncSqlModel`, and `AsyncHashedModel` for use\nwith asyncio) and ORM system. The `SqlQueryBuilder` uses a builder pattern to build\nand execute a query from various clauses. The `SqlModel` handles encoding,\npersisting, reading, and decoding models that correspond to rows. The query builder\ncan be used without a model, in which case a dynamic model will be created. Any\ngrouping will result in `get` returning `Row`s, and joining will result in `get`\nreturning `JoinedModel`s.\n\nBelow are some examples of using the query builder without models:\n\n```python\nfrom sqloquent import SqlQueryBuilder\n\nquery = lambda table, columns: SqlQueryBuilder(\n    table=table, columns=columns, connection_info='temp.db'\n)\n\n# count the number of matches\nsqb = query('some_table', ['id', 'etc']).join(\n    'some_other_table',\n    on=['id', 'some_id'],\n    joined_table_columns=['id', 'some_id', 'data']\n)\ncount = sqb.count()\n\n# chunk through them 1000 at a time\nfor chunk in sqb.chunk(1000):\n    for joined_model in chunk:\n        ...\n\n# or just get them all\nresults = sqb.get()\n\n# or use a condition\nresults = query('some_table', ['id', 'etc']).where(contains={'etc': 'something'}).get()\n# or equivalently\nresults = query('some_table', ['id', 'etc']).contains(etc='something').get()\n```\n\nOr for asyncio:\n\n```python\nfrom asyncio import run\nfrom sqloquent.asyncql import AsyncSqlQueryBuilder\n\nquery = lambda table, columns: AsyncSqlQueryBuilder(\n    table=table, columns=columns, connection_info='temp.db'\n)\n\nsqb = query('some_table', ['id', 'etc']).join(\n    'some_other_table',\n    on=['id', 'some_id'],\n    joined_table_columns=['id', 'some_id', 'data']\n)\n\n# count the number of matches\ncount = run(sqb.count())\n\n# chunk through them 1000 at a time\nasync def chunk_it(sqb):\n    async for chunk in sqb.chunk(1000):\n        for joined_model in chunk:\n            ...\nrun(chunk_it(sqb))\n\n# or just get them all\nresults = run(sqb.get())\n\n# or use a condition\nresults = run(query('some_table', ['id', 'etc']).where(contains={'etc': 'something'}).get())\n# or equivalently\nresults = run(query('some_table', ['id', 'etc']).contains(etc='something').get())\n```\n\nThe base classes have a default binding to sqlite3 via the `SqliteContext`\nclass, but they can be coupled to any SQL database client. See the \"Usage\"\nsection below for detailed instructions for the latter.\n\nAdditionally, three classes, `DeletedModel`, `HashedModel`, and `Attachment`\nhave been supplied to allow easy implementation of a system that includes a\ncryptographic audit trail. `DeletedModel` corresponds to any deleted model from\na class that extends `HashedModel` and includes a `restore` method that can\nrestore the deleted record.\n\nThere is an included CLI tool that generates code scaffolding for models and\nmigrations, as well as track, apply, rollback, and refresh migrations. See the\n[dox.md](https://github.com/k98kurz/sqloquent/blob/v0.7.4/docs/dox.md) and\n[asyncql_dox.md](https://github.com/k98kurz/sqloquent/blob/v0.7.4/docs/asyncql_dox.md)\nfiles generated by [autodox](https://pypi.org/project/autodox) for full\ndocumentation, or read\n[interfaces.md](https://github.com/k98kurz/sqloquent/blob/v0.7.4/docs/interfaces.md)\nand\n[async_interfaces.md](https://github.com/k98kurz/sqloquent/blob/v0.7.4/docs/async_interfaces.md)\nfor documentation on just the interfaces or\n[tools.md](https://github.com/k98kurz/sqloquent/blob/v0.7.4/docs/tools.md) for\ninformation about the bundled tools.\n\n## Status\n\nSee open issues that are planned for a future release\n[here](https://github.com/k98kurz/sqloquent/issues?q=is%3Aissue+is%3Aopen+label%3Aplanned).\nHistoric changes are summarized in the\n[changelog](https://github.com/k98kurz/sqloquent/blob/master/changelog.md).\n\nCurrently, only the basic sqlite3 types (affinities) of text, blob, integer,\nreal, numeric, and boolean are supported by the migration system. Support for\nall data types is planned for a future release:\n[issue #8](https://github.com/k98kurz/sqloquent/issues/8).\n\n## Setup and Usage\n\nRequires Python 3.10+. This has not been tested with older Python versions.\n\n### Setup\n\n```bash\npip install sqloquent\n```\n\nTo use the async version, instead install with the following:\n\n```bash\npip install sqloquent[asyncql]\n```\n\n### Quick Start\n\nGetting started is pretty quick using the sqloquent CLI:\n\n```bash\nexport CONNECTION_STRING='database.db' MAKE_WITH_CONNSTRING=1\nmkdir -p migrations\nmkdir -p models\nsqloquent make model GraphNode --columns \"id=str,details=bytes,parent_ids=str\" \u003e models/GraphNode.py\ncat \u003c\u003cEOF \u003e\u003e models/__init__.py\nfrom .GraphNode import GraphNode\nfrom sqloquent import contains, within\n\nGraphNode.parents = contains(GraphNode, GraphNode, 'parent_ids')\nGraphNode.children = within(GraphNode, GraphNode, 'parent_ids')\n__all__ = [\n    GraphNode,\n]\nEOF\n\nsqloquent make migration --model GraphNode models/GraphNode.py \u003e migrations/001_create_graph_nodes.py\nsqloquent automigrate migrations/\n```\n\nThen, the model can be used within an app or script:\n\n```python\nfrom models import GraphNode\n\nprint(GraphNode.query().count())\nparent = GraphNode.insert({'details': b'parent: 1234'})\nchild = GraphNode.insert({'details': b'child: 4321', 'parent_ids': parent.id})\nprint(GraphNode.query().count())\nprint([c.id for c in parent.children], child.id)\nprint([p.id for p in child.parents], parent.id)\n```\n\n### Usage\n\nThere are two primary ways to use this package: either with a bundled sqlite3\ncoupling or with a custom coupling to an arbitrary SQL database client. The\ncryptographic audit trail features can be used with any SQL database coupling.\n\nNote that if you create a custom async DB coupling, you will also need to create\na non-async coupling to use the migration system. Also note that at the moment,\nthis library has only been tested with sqlite3.\n\n#### CLI Tool\n\nFor ease of development, a CLI tool is included which can be used for generating\ncode scaffolds/boilerplates and for managing migrations. After installing via\npip, run `sqloquent` in the terminal to view the help text.\n\nThe CLI tool can generate models and migrations, including the ability to\ngenerate migrations from completed models. Migrations can be handled manually or\nusing an automatic method that tracks migrations via a `migrations` table. To\nuse the migration tools, the environment variable `CONNECTION_STRING` should be\nset either in the CLI environment or in a .env file, e.g.\n`CONNECTION_STRING=path/to/file.db`. To insert this connection string into\ngenerated scaffold code, also define a `MAKE_WITH_CONNSTRING` environment\nvariable and set it to anything other than \"false\" or \"0\"; this is a convenience\nfeature for working with sqlite3, since that is the only bundled coupling, but\noverwriting the `connection_info` attribute on models at the app execution entry\npoint is probably a better strategy -- if using another SQL binding, the\nconnection info should be injected into the context manager (see section about\nbinding to other SQL databases/engines below).\n\nAdditionally, the functionality exposed by the CLI tool can be accessed\nprogrammatically through `sqloquent.tools`.\n\n#### Connection Information\n\nConnection information can be bound or injected in several places:\n\n- Bound to each individual model\n- Injected into the query builder\n- Bound to the query builder\n- Bound to the db context manager\n\nItems higher in the list will override those lower in the list. For example, if\nyou set the `connection_info` attribute on a model class or instance, it will be\nused for interactions with the db originating from that model class or instance,\nrespectively. If you set the `connection_info` attribute on the query builder\nclass, it will be used, but if you pass it as a parameter to initialize a query\nbuilder, that paramter will be used instead.\n\n#### Connection Pooling\n\nIn applications that involve high database throughput, it is possible to use\nconnection pooling to improve performance/reduce IO overhead. The syntax is as\nfollows:\n\n```python\nimport SomeSqlModel # assuming the connection_info is set on the model\nfrom sqloquent import SqliteContext\n\ndef main():\n    with SqliteContext(SomeSqlModel.connection_info) as cursor:\n        ... # application logic goes here\n```\n\n#### Examples\n\nThe most thorough examples are the integration tests. The model files for the\nfirst can be found\n[here](https://github.com/k98kurz/sqloquent/tree/master/tests/integration_vectors/models),\nand the test itself is\n[here](https://github.com/k98kurz/sqloquent/blob/master/tests/test_integration.py#L73).\n\nThe async versions can be found here:\n- [models](https://github.com/k98kurz/sqloquent/tree/master/tests/integration_vectors/asyncmodels)\n- [test](https://github.com/k98kurz/sqloquent/blob/master/tests/test_async_integration.py#L74)\n\nThe models were scaffolded using the CLI tool, then the details filled out in\neach. The relations were set up in the `__init__.py` file. The integration test\ngenerates migrations from these classes using the CLI tool, automigrates using\nthe CLI tool, then does inserts/updates/deletes and checks the db for\ncorrectness. (These files provide a basic schema for correspondent banking.)\n\nThe second integration test is outlined in the \"Using the ORM\" section below.\n\n#### Models\n\nModels should extend `SqlModel` or a model that extends `SqlModel` and couples\nto another database client. To use the supplied sqlite3 coupling without the\ncryptographic features, extend the `SqlModel`, filling these attributes as shown\nbelow:\n\n- `table: str`: the name of the table\n- `columns: tuple[str]`: the ordered tuple of column names\n- annotations for columns as desired\n\nAdditionally, set up any relevant relations using the ORM helper methods.\n\nThe CLI tool will produce a scaffold for a model. For example, running\n`sqloquent make model Thing --hashed --columns \"id,name,stuff=str|None\"` will\nproduce the following:\n\n```python\nfrom sqloquent import HashedModel\n\n\nclass stuff(HashedModel):\n    connection_info: str = ''\n    table: str = 'things'\n    id_column: str = 'id'\n    columns: tuple[str] = ('id', 'name', 'stuff')\n    id: str\n    name: str\n    stuff: str|None\n```\n\nSpecify `--async` to use an async model. For example, running\n`sqloquent make model Person --columns id,name --async` will produce the\nfollowing:\n\n```python\nfrom sqloquent.asyncql import AsyncSqlModel\n\n\nclass Person(AsyncSqlModel):\n    connection_info: str = ''\n    table: str = 'persons'\n    id_column: str = 'id'\n    columns: tuple[str] = ('id', 'name')\n    id: str\n    name: str\n```\n\nBelow is a more complex example with relations.\n\n```python\nfrom __future__ import annotations\nfrom sqloquent import SqlModel, has_many, belongs_to, RelatedModel, RelatedCollection\nimport json\n\nconnection_string = ''\n\nwith open('.env', 'r') as f:\n    lines = f.readlines()\n    for l in lines:\n        if l[:18] == 'CONNECTION_STRING=':\n            connection_string = l[18:-1]\n\nclass ModelA(SqlModel):\n    connection_info = connection_string\n    table: str = 'model_a'\n    columns: tuple = ('id', 'name', 'details')\n    id: str\n    name: str\n    details: str\n    model_b: RelatedCollection\n\n    @property\n    def details(self) -\u003e dict:\n        \"\"\"Decode json str to dict.\"\"\"\n        return json.loads(self.data.get('details', '{}'))\n\n    @details.setter\n    def details(self, val: dict) -\u003e None:\n        \"\"\"Sets details and encodes to json str.\"\"\"\n        self.data['details'] = json.dumps(val or '{}')\n\nclass ModelB(SqlModel):\n    connection_info = connection_string\n    table: str = 'model_b'\n    columns: tuple = ('id', 'name', 'model_a_id', 'number')\n    id: str\n    name: str\n    model_a_id: str\n    number: int\n    model_a: RelatedModel\n\n\nModelA.model_b = has_many(ModelA, ModelB, 'model_a_id')\nModelB.model_a = belongs_to(ModelB, ModelA, 'model_a_id')\n\n\nif __name__ == \"__main__\":\n    model_a = ModelA.insert({'name': 'Some ModelA'})\n    model_b = ModelB({'name': 'Some ModelB'})\n    model_b.details = {'something': 'important(?)'}\n    model_b.save()\n    assert type(model_a.details) is dict\n    assert type(model_b.details) is dict\n    model_b.model_a = model_a\n    model_b.model_a().save()\n    model_a.model_b().reload()\n    assert model_a.model_b[0].data['id'] == model_b.id\n    assert model_a.model_b[0].id == model_b.id\n    ModelA.query().delete()\n    ModelB.query().delete()\n    print(\"success\")\n```\n\nTo use this, save the code snippet as \"example.py\" and run the following to set\nup the database and then run the script:\n\n```bash\nsqloquent make migration --model ModelA example.py \u003e model_a_migration.py\nsqloquent make migration --model ModelB example.py \u003e model_b_migration.py\nsqloquent migrate model_a_migration.py\nsqloquent migrate model_b_migration.py\npython example.py\n```\n\nIt is noteworthy that every column in the `columns` class attribute will be\nmade into a property that accesses the underlying data stored in the `data`\ndict (the annotation just helps the code editor/LSP pick up on this). This will\nnot work for any column name that collides with an existing class attribute or\nmethod, and the behavior can be disabled by adding a class attribute called\n`disable_column_property_mapping`; all row data will still be accessible via the\n`data` attribute on each instance regardless of name collision or feature\ndisabling.\n\nAs of v0.5.2, models will contain a `data_original` dict in addition to the\n`data` dict to track changes between database operations, and these will be\nvisible to most event handlers through the `self` kwarg.\n\nIf you do not want to use the bundled ORM system, set up any relevant relations\nwith `_{related_name}: RelatedModel` attributes and\n`{related_name}(self, reload: bool = False)` methods. Dicts should be encoded\nusing `json.dumps` and stored in text columns. More flexibility can be gained at\nthe expense of performance by using the packify package, e.g. to encode sets or\nclasses that implement the `packify.Packable` interface.\n\n```python\nfrom sqloquent import SqlModel\n\n\nclass ModelA(SqlModel):\n    table: str = 'model_a'\n    columns: tuple = ('id', 'name', 'details')\n    id: str\n    name: str\n    _model_b: ModelB|None = None\n    _details: dict|None = None\n\n    def model_b(self, reload: bool = False) -\u003e list[ModelB]:\n        \"\"\"The related model.\"\"\"\n        if self._model_b is None or reload:\n            self._model_b = ModelB.query({'model_a_id': self.data['id']}).get()\n        return self._model_b\n\n    def set_model_b(self, model_b: ModelB) -\u003e ModelA:\n        \"\"\"Helper method to save some lines.\"\"\"\n        model_b.data['model_a_id'] = self.data['id']\n        model_b._model_a = self\n        model_b.save()\n        self._model_b = model_b\n        return self\n\n    @property\n    def details(self) -\u003e dict:\n        \"\"\"Decode json str to dict.\"\"\"\n        return json.loads(self.data.get('details', '{}'))\n\n    @details.setter\n    def details(self, val: dict) -\u003e None:\n        \"\"\"Sets details and encodes to json str.\"\"\"\n        self.data['details'] = json.dumps(val or '{}')\n\nclass ModelB(SqlModel):\n    table: str = 'model_b'\n    columns: tuple = ('id', 'name', 'model_a_id', 'number')\n    id: str\n    name: str\n    model_a_id: str\n    number: int\n    _model_a: ModelA|None = None\n\n    def model_a(self, reload: bool = False) -\u003e Optional[ModelA]:\n        \"\"\"Return the related model.\"\"\"\n        if self._model_a is None or reload:\n            self._model_a = ModelA.find(self.data['model_a_id'])\n        return self._model_a\n\n    def set_model_a(self, model_a: ModelA) -\u003e ModelB:\n        \"\"\"Helper method to save some lines.\"\"\"\n        self.data['model_a_id'] = model_a.data['id']\n        self._model_a = model_a\n        model_a._model_b = self\n        return self.save()\n```\n\n#### Using the ORM\n\nThe ORM is comprised of 6 classes inheriting from `Relation` and implementing\nthe `RelationProtocol`: `HasOne`, `HasMany`, `BelongsTo`, `BelongsToMany`,\n`Contains`, and `Within`. The async version is equivalent with `Async` prefixes.\nNote that currently the async ORM may create `ResourceWarning`s when properties\nare accessed.\n\nThe recommended way to use the ORM is with the helper functions:\n\n```python\nfrom sqloquent import SqlModel, HashedModel, has_one, belongs_to, contains, within\n\nclass User(SqlModel):\n    ...\n\nclass Avatar(SqlModel):\n    columns = ('id', 'url', 'user_id')\n\nUser.avatar = has_one('user_id', User, Avatar)\nAvatar.user = belongs_to('user_id', Avatar, User)\n\nclass DAGItem(HashedModel):\n    columns = ('id', 'details', 'parent_ids')\n\nDAGItem.parents = contains('parent_ids', DAGItem, DAGItem)\nDAGItem.children = within('parent_ids', DAGItem, DAGItem)\n```\n\nThere are six helper functions for setting up relations between models: `has_one`,\n`has_many`, `belongs_to`, `belongs_to_many`, `contains`, and `within`.\n(Same are available for async, but with an `async_` prefix.)\n\n\u003cdetails\u003e\n\u003csummary\u003eAnother example\u003c/summary\u003e\n\n```python\nfrom __future__ import annotations\nfrom sqloquent import (\n    SqlModel, RelatedCollection, RelatedModel,\n    has_one, has_many, belongs_to, belongs_to_many,\n)\n\nclass User(SqlModel):\n    table = 'users'\n    columns = ('id', 'name')\n    friends: RelatedCollection\n    friendships: RelatedCollection\n    avatar: RelatedModel\n    posts: RelatedCollection\n\nclass Avatar(SqlModel):\n    table = 'avatars'\n    columns = ('id', 'url', 'user_id')\n    user: RelatedModel\n\nclass Post(SqlModel):\n    table = 'posts'\n    columns = ('id', 'content', 'user_id')\n    author: RelatedModel\n\nclass Friendship(SqlModel):\n    table = 'friendships'\n    columns = ('id', 'user1_id', 'user2_id')\n    user1: RelatedModel\n    user2: RelatedModel\n\n    @classmethod\n    def insert(cls, data: dict) -\u003e Friendship | None:\n        # also set inverse relationship\n        result = super().insert(data)\n        if result:\n            super().insert({\n                **data,\n                'user1_id': data['user2_id'],\n                'user2_id': data['user1_id'],\n            })\n\n    @classmethod\n    def insert_many(cls, items: list[dict]) -\u003e int:\n        inverse = [\n            {\n                'user1_id': item['user2_id'],\n                'user2_id': item['user1_id']\n            }\n            for item in items\n        ]\n        return super().insert_many([*items, *inverse])\n\n    def delete(self):\n        # first delete the inverse\n        self.query().equal('user1_id', self.data['user2_id']).equal(\n            'user2_id', self.data['user1_id']\n        ).delete()\n        super().delete()\n\nUser.avatar = has_one(User, Avatar)\nAvatar.user = belongs_to(Avatar, User)\n\nUser.posts = has_many(User, Post)\nPost.author = belongs_to(Post, User)\n\nUser.friendships = has_many(User, Friendship, 'user1_id')\nUser.friends = belongs_to_many(User, User, Friendship, 'user1_id', 'user2_id')\n\nFriendship.user1 = belongs_to(Friendship, User, 'user1_id')\nFriendship.user2 = belongs_to(Friendship, User, 'user2_id')\n```\n\nThe relations can then be used as follows:\n\n```python\n# add users\nalice: User = User.insert({\"name\": \"Alice\"})\nbob: User = User.insert({\"name\": \"Bob\"})\n\n# add avatars\nalice.avatar().secondary = Avatar.insert({\n    \"url\": \"http://www.perseus.tufts.edu/img/newbanner.png\",\n})\nalice.avatar().save()\nbob.avatar = Avatar.insert({\n    \"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/9/90\"\n    \"/Walrus_(Odobenus_rosmarus)_on_Svalbard.jpg/1200px-Walrus_(Odobe\"\n    \"nus_rosmarus)_on_Svalbard.jpg\",\n})\nbob.avatar().save()\n\n# add a friendship\nbob.friends = [alice]\nbob.friends().save()\nbob.friendships().reload()\nalice.friendships().reload()\nalice.friends().reload()\n```\n\nThe above is included in the second integration test:\n- [models](https://github.com/k98kurz/sqloquent/blob/master/tests/integration_vectors/models2.py)\n- [test](https://github.com/k98kurz/sqloquent/blob/master/tests/test_integration.py#L371)\n\u003c/details\u003e\n\nNB: polymorphic relations are not supported. See the `Attachment` class for an\nexample of how to implement polymorphism if necessary.\n\n\u003cdetails\u003e\n\u003csummary\u003eAnother example of the Contains and Within relations\u003c/summary\u003e\n\n```python\nfrom sqloquent import (\n    HashedModel, RelatedCollection, RelatedModel, contains, within,\n)\n\nclass DAGItem(HashedModel):\n    table = 'dag'\n    columns = ('id', 'details', 'parent_ids')\n    parents: RelatedCollection\n    children: RelatedCollection\n\n    @classmethod\n    def insert(cls, data: dict) -\u003e DAGItem|None:\n        # \"\"\"For better type hinting.\"\"\"\n        return super().insert(data)\n\n    @classmethod\n    def insert_many(cls, items: list[dict]) -\u003e int:\n        # \"\"\"For better type hinting.\"\"\"\n        return super().insert_many(items)\n\nDAGItem.parents = contains(DAGItem, DAGItem, 'parent_ids')\nDAGItem.children = within(DAGItem, DAGItem, 'parent_ids')\n```\n\nWhich can be used as follows:\n\n```python\n# create parents\nparent1 = DAGItem.insert({'details': 'parent 1'})\nparent2 = DAGItem.insert({'details': 'parent 2'})\n\n# create children\nchild1 = DAGItem({'details': 'child 1'})\nchild1.parents = [parent1, parent2]\nchild1.parents().save()\n\nchild2 = DAGItem({'details': 'child 2'})\nchild2.parents = [parent1]\nchild2.parents().save()\n\n# reload relation\nparent1.children().reload()\nparent2.children().reload()\nassert len(parent1.children) == 2\nassert len(parent2.children) == 1\n```\n\u003c/details\u003e\n\n#### Table Construction\n\nThe package as it stands relies upon text or varchar type `id` columns. The\n`SqlModel` uses a hexadecimal uuid4 as a GUID, while the `HashedModel` uses the\nsha256 of the deterministically encoded record content as a GUID. This can be\nchanged for use with autoincrementing int id columns by extending `SqlModel` and\noverriding the `insert` and `insert_many` methods to prevent setting the id via\n`cls.generate_id()`. However, this is not recommended unless the autoincrement\nid can be reliably discerned from the db cursor and there are no concerns about,\nsay, synchronizing between instances using a CRDT.\n\nUse one of the variants of the `sqloquent make migration` command to create a\nmigration scaffold, then edit the result as necessary. If you specify the\n`--model name path/to/model/file` variant, the resultant source will include a\nunique index on the id column and simple indices on all other columns. This will\nalso parse any class annotations that map to names of columns. For example,\ngiven the following class,\n\n```python\nfrom sqloquent import SqlModel, Default\nclass Thing(SqlModel):\n    table = 'things'\n    columns = ('id', 'name', 'amount', 'is_nothing')\n    id: str\n    name: bytes|Default[b'something']\n    amount: int|None\n    is_nothing: bool|None|Default[True]\n```\n\nthe `make migration --model` command will produce the following migration:\n\n```python\nfrom sqloquent import Migration, Table\n\n\ndef create_table_things() -\u003e list[Table]:\n    t = Table.create('things')\n    t.text('id').unique()\n    t.blob('name').default(b'something').index()\n    t.integer('amount').nullable().index()\n    t.boolean('is_nothing').default(True).nullable().index()\n    ...\n    return [t]\n\ndef drop_table_things() -\u003e list[Table]:\n    return [Table.drop('things')]\n\ndef migration(connection_string: str = '') -\u003e Migration:\n    migration = Migration(connection_string)\n    migration.up(create_table_things)\n    migration.down(drop_table_things)\n    return migration\n```\n\nThis should provide a decent scaffold for migrations, allowing the user of this\npackage to model their data first as classes if desired. If some custom SQL is\nnecessary, it can be added using a callback.\n\n\u003cdetails\u003e\n\u003csummary\u003eExample pseudocode scaffold for adding custom SQL to a migration\u003c/summary\u003e\n\n```python\ndef add_custom_sql(clauses: list[str]) -\u003e list[str]:\n    clauses.append(\"do something sql-y\")\n    return clauses\n\ndef create_table_things() -\u003e list[Table]:\n    t = Table.create('things')\n    t.text('id').unique()\n    t.blob('name').index()\n    t.integer('amount').nullable().index()\n    t.boolean('is_nothing').nullable().default(True).index()\n    t.custom(add_custom_sql)\n    ...\n    return [t]\n```\n\u003c/details\u003e\n\nExamine the generated SQL of any migration using the\n`sqloquent examine path/to/migration/file` command. The above example (less the\ncustom sql pseudocode) will generate the following:\n\n```sql\n/**** generated up/apply sql ****/\nbegin;\ncreate table if not exists \"things\" (\"id\" text, \"name\" blob default (x'736f6d657468696e67'), \"amount\" integer, \"is_nothing\" boolean default True);\ncreate unique index if not exists udx_things_id on \"things\" (\"id\");\ncreate index if not exists idx_things_name on \"things\" (\"name\");\ncreate index if not exists idx_things_amount on \"things\" (\"amount\");\ncreate index if not exists idx_things_is_nothing on \"things\" (\"is_nothing\");\ncommit;\n\n/**** generated down/undo sql ****/\nbegin;\ndrop table if exists \"things\";\ncommit;\n```\n\nFor comprehensive documentation on the automatic migration system, including data\ntype compatibility, automatic migration tracking, and complete workflow examples,\nsee the\n[Migration Guide](https://github.com/k98kurz/sqloquent/blob/v0.7.4/docs/migrations.md).\n\n#### Model Event Hooks\n\nAs of v0.5.0, `SqlModel`, `HashedModel`, `DeletedModel`, `Attachment`,\n`AsyncSqlModel`, `AsyncHashedModel`, `AsyncDeletedModel`, and `AsyncAttachment`\nhave an event hook system. Each has the following class methods:\n\n- `add_hook(event: str, hook: Callable)`\n- `remove_hook(event: str, hook: Callable)`\n- `clear_hooks(event: str = None)`\n- `invoke_hooks(event, *args, **kwargs)`\n\nThe async version of `invoke_hooks` will detect when an event handler returns a\ncoroutine and will await them if `parallel_events=True` is passed in (relevant\nother methods as described below have `parallel_events=False` default argument).\nAdditionally, these methods contain checks to ensure that subclasses will have\ntheir own `_event_hooks` dictionary to avoid conflicts with parent classes (i.e.\nso that every class has its own unique event hooks).\n\nThe following events are shared by all models:\n\n- `before_insert`\n- `after_insert`\n- `before_insert_many`\n- `after_insert_many`\n- `before_update`\n- `after_update`\n- `before_save`\n- `after_save`\n- `before_delete`\n- `after_delete`\n- `before_reload`\n- `after_reload`\n\n`DeletedModel` and `AsyncDeletedModel` also have the following unique events:\n\n- `before_restore`\n- `after_restore`\n\nCallbacks will receive one positional arg (the class calling the event hook) and\nthe rest will be keyword args. Callbacks should accept `**kwargs` and check it\nfor expected values necessary for handling the event. The `kwargs` will at a\nminimum contain the string event name under the key \"event\".\n\nTo manage these events, use the methods on the class. For example:\n\n```python\n\nclass Thing(SqlModel):\n    table = 'things'\n    columns = ('id', 'name')\n\ndef make_handler(event):\n    def handle_event(*args, **kwargs):\n        print(f'{event} called')\n    return handle_event\n\nThing.add_hook('before_insert', make_handler('before_insert'))\nthing = Thing.insert({'name': 'Stuff'}) # prints \"before_insert called\" before all insert logic\nThing.insert({'name': 'Another Stuff'}, suppress_events = True) # event handler not called\n\nThing.add_hook('after_delete', make_handler('after_delete'))\nthing.delete() # prints \"after_delete called\" after db operation\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eAsync version\u003c/summary\u003e\n\n```python\nclass Thing(AsyncSqlModel):\n    table = 'things'\n    columns = ('id', 'name')\n\ndef make_handler(event):\n    async def handle_event(*args, **kwargs):\n        await asyncio.sleep(0.1)\n        print(f'{event} called')\n    return handle_event\n\nThing.add_hook('before_insert', make_handler('before_insert1'))\nThing.add_hook('before_insert', make_handler('before_insert2'))\nThing.add_hook('after_delete', make_handler('after_delete'))\n\nasync def test_events():\n    # prints \"before_insert1 called\" and then \"before_insert2\" before all insert logic\n    thing = await Thing.insert({'name': 'Stuff'})\n\n    # prints \"before_insert1 called\" and \"before_insert2\" in unknowable order\n    thing = await Thing.insert({'name': 'Stuff'}, parallel_events=True)\n\n    # event handlers not called\n    await Thing.insert({'name': 'Another Stuff'}, suppress_events = True)\n\n    # prints \"after_delete called\" after db operation\n    await thing.delete()\n```\n\u003c/details\u003e\n\nIf you overwrite any hooked methods and use `super().hookedmethod()`, you should\nadd calls to `invoke_hooks` to manage events for this overwritten method\ndirectly, and you should pass `suppress_events=True` to `super().hookedmethod()`\ncalls to avoid duplicate events.\n\n\u003cdetails\u003e\n\u003csummary\u003eExample\u003c/summary\u003e\n\n```python\nclass Thing(SqlModel):\n    @classmethod\n    def insert(cls, data: dict, /, *, suppress_events=False) -\u003e Thing:\n        \"\"\"Overwrite for some reason, probably custom logic.\"\"\"\n        if not suppress_events:\n            cls.invoke_hooks('before_insert', data=data)\n        ...\n        something = super().insert(data, suppress_events=True) # no duplicate events\n        ...\n        if not suppress_events:\n            cls.invoke_hooks('after_insert', data=data, something=something)\n        return something\n```\n\u003c/details\u003e\n\nNote that calls to `invoke_hooks` should pass all arguments other than the event\nname as keyword arguments.\n\n#### Coupling to a Different SQL Database Client\n\nA guide for using this library with a differen SQL database client is provided\n[here](https://github.com/k98kurz/sqloquent/blob/v0.7.4/docs/how_to_couple.md).\n\n#### Using the Cryptographic Features\n\nIf a cryptographic audit trail is desirable, use an inheritance pattern to couple\nthe supplied classes to the desired `ModelProtocol` implementation, or simply change\nthe `connection_info` attribute to use with sqlite3. The simplest and recommended\nway is to subclass from `HashedModel` or `AsyncHashedModel`, e.g. by executing\n`sqloquent make model ModelName --hashed [--async] --columns id=str,etc` for a code\nscaffold. The `DeletedModel` or `AsyncDeletedModel` will need to be monkey-patched\nwith the correct connection info to use `HashedModel` or subclasses.\n\nThe below example demonstrates how to monkey-patch everything together.\n\n```python\nfrom .dbcxm import SomeDBContextImplementation\nfrom sqloquent import HashedModel, DeletedModel, Attachment, SqlQueryBuilder\n\nenv_db_file_path = 'some_file.db'\nenv_connstring = 'host=localhost,port=69,user=admin,password=admin'\n\n# option 1: inheritance\nclass CustomQueryBuilder(SqlQueryBuilder):\n    def __init__(self, model_or_table, **kwargs,):\n        return super().__init__(model_or_table, SomeDBContextImplementation, **kwargs)\n\nclass NewModel(HashedModel, SomeDBModel):\n    connection_info = env_connstring\n    query_builder_class = CustomQueryBuilder\n\n# option 2: bind the classes\nHashedModel.connection_info = env_db_file_path\nHashedModel.query_builder_class = CustomQueryBuilder\nDeletedModel.connection_info = env_db_file_path\nDeletedModel.query_builder_class = CustomQueryBuilder\nAttachment.connection_info = env_db_file_path\nAttachment.query_builder_class = CustomQueryBuilder\n```\n\nThe latter must be done exactly once. The value supplied for `connection_info`\nshould be set with some environment configuration system, but here it is only\npoorly mocked.\n\n## Interfaces, Classes, Functions, and Tools\n\nBelow are lists of interfaces, classes, errors, and functions.\n\n\u003cdetails\u003e\n\u003csummary\u003eInterfaces\u003c/summary\u003e\n\n- CursorProtocol(Protocol)\n- DBContextProtocol(Protocol)\n- ModelProtocol(Protocol)\n- JoinedModelProtocol(Protocol)\n- RowProtocol(Protocol)\n- QueryBuilderProtocol(Protocol)\n- RelationProtocol(Protocol)\n- RelatedModel(ModelProtocol)\n- RelatedCollection(Protocol)\n- ColumnProtocol(Protocol)\n- TableProtocol(Protocol)\n- MigrationProtocol(Protocol)\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eClasses\u003c/summary\u003e\n\nClasses implement the protocols or extend the classes indicated.\n\n- SqlModel(ModelProtocol)\n- SqlQueryBuilder(QueryBuilderProtocol)\n- SqliteContext(DBContextProtocol)\n- DeletedModel(SqlModel)\n- HashedModel(SqlModel)\n- Attachment(HashedModel)\n- Row(RowProtocol)\n- JoinedModel(JoinedModelProtocol)\n- JoinSpec\n- Relation(RelationProtocol)\n- HasOne(Relation)\n- HasMany(HasOne)\n- BelongsTo(HasOne)\n- BelongsToMany(Relation)\n- Contains(HasMany)\n- Within(HasMany)\n- Column(ColumnProtocol)\n- Table(TableProtocol)\n- Migration(MigrationProtocol)\n- AsyncSqlModel(AsyncModelProtocol)\n- AsyncSqlQueryBuilder(AsyncQueryBuilderProtocol)\n- AsyncSqliteContext(AsyncDBContextProtocol)\n- AsyncDeletedModel(AsyncSqlModel)\n- AsyncHashedModel(AsyncSqlModel)\n- AsyncAttachment(AsyncHashedModel)\n- AsyncJoinedModel(AsyncJoinedModelProtocol)\n- AsyncRelation(AsyncRelationProtocol)\n- AsyncHasOne(AsyncRelation)\n- AsyncHasMany(AsyncHasOne)\n- AsyncBelongsTo(AsyncHasOne)\n- AsyncBelongsToMany(AsyncRelation)\n- AsyncContains(AsyncHasMany)\n- AsyncWithin(AsyncHasMany)\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eFunctions\u003c/summary\u003e\n\nThe package includes some ORM helper functions for setting up relations and some\nother useful functions.\n\n- `dynamic_sqlmodel`\n- `has_one`\n- `has_many`\n- `belongs_to`\n- `belongs_to_many`\n- `contains`\n- `within`\n- `get_index_name`\n- `async_dynamic_sqlmodel`\n- `async_has_one`\n- `async_has_many`\n- `async_belongs_to`\n- `async_belongs_to_many`\n- `async_contains`\n- `async_within`\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eTools\u003c/summary\u003e\n\nThe package includes a set of tools with a CLI invocation script.\n\n- `make_migration_create`\n- `make_migration_alter`\n- `make_migration_drop`\n- `make_migration_from_model`\n- `make_migration_from_model_path`\n- `publish_migrations`\n- `make_model`\n- `migrate`\n- `rollback`\n- `refresh`\n- `examine`\n- `automigrate`\n- `autorollback`\n- `autorefresh`\n\u003c/details\u003e\n\n## More Resources\n\nCheck out the [Pycelium discord server](https://discord.gg/b2QFEJDX69). If you\nexperience a problem, please discuss it on the Discord server. All suggestions\nfor improvement are also welcome, and the best place for that is also Discord.\nIf you experience a bug and do not use Discord, open an issue on Github.\n\n## Tests\n\nOpen a terminal in the root directory and run the following to set up:\n\n```bash\npython -m venv venv\nsource venv/bin/activate\npip install -r requirements.txt\n```\n\nFor Windows, replace `source venv/bin/activate` with\n`source venv/Scripts/activate` if using a POSIX-compliant shell or\n`venv\\Scripts\\activate.bat` for command prompt.\n\nThen run the tests with the following for Unix:\n\n```bash\nfind tests -name test_*.py -print -exec python {} \\;\n```\n\nOr for Windows:\n\n```\npython tests/test_async_classes.py\npython tests/test_async_integration.py\npython tests/test_async_relations.py\npython tests/test_classes.py\npython tests/test_relations.py\npython tests/test_integration.py\npython tests/test_migration.py\npython tests/test_regressions.py\npython tests/test_tools.py\n```\n\nThe tests demonstrate the intended (and actual) behavior of the classes, as\nwell as some contrived examples of how they are used. Perusing the tests will be\ninformative to anyone seeking to use/break this package, especially the\nintegration test which demonstrates the full package. There are currently 494\nunit tests + 6 e2e/integration tests + 4 regression tests.\n\n## ISC License\n\nCopyright (c) 2026 Jonathan Voss (k98kurz)\n\nPermission to use, copy, modify, and/or distribute this software\nfor any purpose with or without fee is hereby granted, provided\nthat the above copyright notice and this permission notice appear in\nall copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL\nWARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE\nAUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR\nCONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS\nOF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,\nNEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN\nCONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fk98kurz%2Fsqloquent","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fk98kurz%2Fsqloquent","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fk98kurz%2Fsqloquent/lists"}