{"id":19567763,"url":"https://github.com/cjauvin/little_pger","last_synced_at":"2025-10-13T10:10:53.463Z","repository":{"id":1683776,"uuid":"2411287","full_name":"cjauvin/little_pger","owner":"cjauvin","description":"🐘 PostgreSQL query building with plain data structures.","archived":false,"fork":false,"pushed_at":"2024-12-31T18:23:41.000Z","size":50,"stargazers_count":50,"open_issues_count":3,"forks_count":3,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-09-13T18:25:55.907Z","etag":null,"topics":["postgresql","psycopg2","python","sql","sql-query"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cjauvin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","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}},"created_at":"2011-09-18T20:35:48.000Z","updated_at":"2025-09-03T23:16:59.000Z","dependencies_parsed_at":"2025-02-28T07:15:56.393Z","dependency_job_id":"254cb54f-a197-4aca-aa35-21f074fcee77","html_url":"https://github.com/cjauvin/little_pger","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/cjauvin/little_pger","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cjauvin%2Flittle_pger","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cjauvin%2Flittle_pger/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cjauvin%2Flittle_pger/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cjauvin%2Flittle_pger/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cjauvin","download_url":"https://codeload.github.com/cjauvin/little_pger/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cjauvin%2Flittle_pger/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279014676,"owners_count":26085554,"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","status":"online","status_checked_at":"2025-10-13T02:00:06.723Z","response_time":61,"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":["postgresql","psycopg2","python","sql","sql-query"],"created_at":"2024-11-11T05:41:15.494Z","updated_at":"2025-10-13T10:10:53.449Z","avatar_url":"https://github.com/cjauvin.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Little_PGer.py\n\n## What is it?\n\nIt's a thin layer just a tad above SQL, for use with Postgres and\n[psycopg2](http://www.initd.org/psycopg/), when you want to wrap\nqueries in a convenient way, using plain data structures (but you\ndon't feel like using an ORM, for some reason).\n\n## Why?\n\nOf course `psycopg2` already does a very fine job on its own, but in\nthe context of webapp backend development, I often found myself\nwanting for an extra-frictionless way of shuffling around Ajax/JSON\ndata. As composing raw SQL queries quickly induces string-manipulation\nfatigue, I gradually evolved `little_pger` for that simple purpose.\n\nIf you want to know more about it, I have also discussed its use in\nsome particular contexts, on my blog:\n\n* \u003chttp://cjauvin.blogspot.com/2012/10/a-tribute-to-unsung-pattern.html\u003e\n\n* \u003chttp://cjauvin.blogspot.com/2013/04/impossibly-lean-access-control-with.html\u003e\n\n## To install\n\n    $ pip install little_pger\n\nor\n\n    $ pip install -e git+git@github.com:cjauvin/little_pger.git#egg=little_pger\n\nNote that `psycopg2` will be automatically installed if it isn't\nalready.\n\n## Testing it with this README document\n\nNote that this `README.md` file can be executed as a test suite. To do\nso, simply create a dummy database (that you can destroy afterward):\n\n    $ createdb little_pger_test -U \u003cyour_pg_user\u003e\n\nand set this variable appropriately for your setup:\n\n```python\n\u003e\u003e\u003e pg_user = '' # an empty string works when the OS and PG users share the same name\n\n```\n\nThen simply execute the script with:\n\n    $ python -m doctest -f -v README.md\n\nLet's go!\n\n```python\n\u003e\u003e\u003e from little_pger import LittlePGer\n\n```\n\nThe first and mandatory parameter to a `LittlePGer` object is a\nconnection, either as a string or as a `psycopg2` object (resulting\nfrom `psycopg2.connect`). A `LittlePGer` object can be used in two\nways. The first is as a context manager, which implies that the\ntransaction is encapsulated under the `with` statement (with a\n`rollback` or `commit` performed automatically at exit):\n\n```python\n\u003e\u003e\u003e conn_str = 'dbname=little_pger_test user={}'.format(pg_user)\n\u003e\u003e\u003e with LittlePGer(conn=conn_str, commit=False) as pg:\n...     _ = pg.pg_version # (9, 5, 0) for me, perhaps not for you\n\n```\n\nYou can also use it without the context manager:\n\n```python\n\u003e\u003e\u003e pg = LittlePGer(conn=conn_str, commit=False)\n\u003e\u003e\u003e _ = pg.pg_version # (9, 5, 0) for me, perhaps not for you\n\n```\n\nin which case you are in charge of managing the transaction\nyourself. In this document we will not use the context manager because\nit makes things easier on the eyes.\n\n## Insert and update\n\nSuppose we have two SQL tables:\n\n```python\n\u003e\u003e\u003e pg.sql(\"\"\"\n...     create table book (\n...         book_id serial primary key,\n...         author_id int,\n...         title text,\n...         n_pages int,\n...         topics text[]\n...     )\n... \"\"\")\n\n\u003e\u003e\u003e pg.sql(\"\"\"\n...     create table author (\n...         author_id serial primary key,\n...         name text\n...      )\n... \"\"\")\n\n```\n\nyou can `insert` a new `book`, along with its `author`:\n\n```python\n\u003e\u003e\u003e book = pg.insert('book', values={'title': 'PG is Fun!'})\n\u003e\u003e\u003e author = pg.insert('author', values={'name': 'Joe Foo', 'author_id': 100})\n\n```\n\nand `update` it:\n\n```python\n\u003e\u003e\u003e book = pg.update(\n...     'book', set={'author_id': author['author_id'], 'n_pages': 200},\n...     where={'book_id': book['book_id']}\n... )\n\u003e\u003e\u003e sorted(book.items()) # just to clamp the field order\n[('author_id', 100), ('book_id', 1), ('n_pages', 200), ('title', 'PG is Fun!'), ('topics', None)]\n\n```\n\nAs shown above, `insert` and `update` by default return a `dict`\nrecord. However, `insert` has a convenient `return_id` keyword\nargument, which means that the primary key value of the newly\ncreated record should be returned directly:\n\n```python\n\u003e\u003e\u003e pg.insert(\n...     'book', values={'title': 'Python and PG, a Love Story'},\n...     return_id=True\n... )\n2\n\n```\n\n## Upsert\n\nEven though `upsert` only appeared recently (with PG 9.5),\n`little_pger` supports it for every version of PG, with a \"fake\nimplementation\" (i.e. check existence, then insert or update\naccordingly) in the cases where it is not natively supported (and when\nit is, a \"real\" implementation is used). Both implementations are\nsimplified versions where the primary key is implicitly used to\ndetermine uniqueness.\n\n```python\n\u003e\u003e\u003e # does not yet exist, will be created\n\u003e\u003e\u003e book_id = pg.upsert('book', set={'title': 'A Boring Story'}, return_id=True)\n\u003e\u003e\u003e book_id\n3\n\n\u003e\u003e\u003e # already exists, will be updated\n\u003e\u003e\u003e book = pg.upsert('book', values={'n_pages': 123, 'book_id': book_id})\n\u003e\u003e\u003e book_id, book['book_id']\n(3, 3)\n\n```\n\n`insert`, `update` and `upsert` all have a convenient `filter_values`\nparameter which, if used, will remove any item in the `values` dict\nthat doesn't belong to the target table. Without it here, an exception\nwould be thrown, as the `book` table does not have a `publisher`\ncolumn:\n\n```python\n\u003e\u003e\u003e _ = pg.upsert(\n...     'book', filter_values=True,\n...      values={'book_id': book_id, 'publisher': 'Joe North'}\n... )\n\n```\n\n## Select\n\nTo `select` all books:\n\n```python\n\u003e\u003e\u003e books = pg.select('book')\n\u003e\u003e\u003e len(books)\n3\n\n```\n\nor a particular book:\n\n```python\n\u003e\u003e\u003e books = pg.select('book', where={'book_id': book_id})\n\u003e\u003e\u003e len(books)\n1\n\n```\n\nor:\n\n```python\n\u003e\u003e\u003e book = pg.select1('book', where={'book_id': book_id})\n\u003e\u003e\u003e type(book)\n\u003cclass 'psycopg2.extras.RealDictRow'\u003e\n\n```\n\nIt's easy to (inner) join books and authors:\n\n```python\n\u003e\u003e\u003e book = pg.select1(\n...     'book', join='author', where={'book_id': 1}\n... )\n\u003e\u003e\u003e sorted(book.items()) # just to clamp the field order\n[('author_id', 100), ('book_id', 1), ('n_pages', 200), ('name', 'Joe Foo'), ('title', 'PG is Fun!'), ('topics', None)]\n\n```\n\nor left join them:\n\n```python\n\u003e\u003e\u003e book_author = pg.select1(\n...     'book', left_join='author', where={'book_id': 2}\n... )\n\u003e\u003e\u003e sorted(book_author.items()) # just to clamp the field order\n[('author_id', None), ('book_id', 2), ('n_pages', None), ('name', None), ('title', 'Python and PG, a Love Story'), ('topics', None)]\n\n```\n\nUsing a `tuple` value in the `where` clause:\n\n```python\n\u003e\u003e\u003e books = pg.select('book', where={'book_id': (1, 2, 3)})\n\u003e\u003e\u003e len(books)\n3\n\n```\n\ntranslates to a SQL query using the `in` operator:\n\n```sql\nselect * from book where book_id in (1, 2, 3)\n```\n\nMake sure that you do not use `tuple`s and `list`s interchangeably\nwhen working with `psycopg2` and `little_pger`, as they are used for\nvery different purposes. Python arrays translate into PG arrays (note\nthat the `book.topics` column has type `text[]`):\n\n```python\n\u003e\u003e\u003e book = pg.update(\n...     'book', set={'topics': ['database', 'programming']},\n...     where={'book_id': 1}\n... )\n\u003e\u003e\u003e book['topics']\n['database', 'programming']\n\n```\n\nYou can use operators other than `=`, like this:\n\n```python\n\u003e\u003e\u003e books = pg.select('book', where={('book_id', '\u003c='): 2})\n\u003e\u003e\u003e len(books)\n2\n\n```\n\nUsing a `set` (instead of a `tuple` or a `list`) will result in a\nthird type of semantics:\n\n```python\n\u003e\u003e\u003e pg.select1(\n...     'book', where={('title', 'like'): {'%PG%', '%Fun%'}}\n... )['title']\n'PG is Fun!'\n\n```\n\nwhich translates to:\n\n```sql\nselect * from book where title like '%PG%' and title like '%Fun%'\n```\n\nwhich can be a powerful way to implement an autocomplete mechanism,\n[as I explain in more details elsewhere](http://cjauvin.blogspot.ca/2012/10/a-tribute-to-unsung-pattern.html).\n\nUntil now we have assumed `*` selection, but the `what` keyword allows\nfor more flexibility:\n\n```python\n\u003e\u003e\u003e res = pg.select(\n...     'book', what={'*':1, 'title is not null': 'has_title'}\n... )\n\u003e\u003e\u003e [book['has_title'] for book in res]\n[True, True, True]\n\n```\n\nSimilarly:\n\n```python\n\u003e\u003e\u003e res = pg.select(\n...     'book', left_join='author',\n...      what=['name', 'count(*)'],\n...      group_by='name', order_by='count desc'\n... )\n\u003e\u003e\u003e res[0]['name'], int(res[0]['count'])\n(None, 2)\n\u003e\u003e\u003e res[1]['name'], int(res[1]['count'])\n('Joe Foo', 1)\n\n```\n\n## Delete\n\nThe `delete` function includes an option to \"tighten\" the\nprimary key sequence, to make sure that if you delete a row with some\nID that is the maximum one currently existing, it will be reused the\nnext time you create a new row (in other words: it prevents \"gaps\" in\nthe ID sequences).\n\nWithout `tighten_sequence`:\n\n```python\n\u003e\u003e\u003e pg.delete('book', where={'book_id': 3})\n\u003e\u003e\u003e pg.insert('book', return_id=True)\n4\n\n```\n\nWith it:\n\n```python\n\u003e\u003e\u003e pg.delete('book', where={'book_id': 4}, tighten_sequence=True)\n\u003e\u003e\u003e pg.insert('book', return_id=True)\n3\n\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcjauvin%2Flittle_pger","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcjauvin%2Flittle_pger","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcjauvin%2Flittle_pger/lists"}