{"id":28469796,"url":"https://github.com/answerdotai/fastsql","last_synced_at":"2026-01-16T09:02:09.002Z","repository":{"id":252141606,"uuid":"836899089","full_name":"AnswerDotAI/fastsql","owner":"AnswerDotAI","description":null,"archived":false,"fork":false,"pushed_at":"2026-01-09T23:50:11.000Z","size":583,"stargazers_count":64,"open_issues_count":3,"forks_count":5,"subscribers_count":9,"default_branch":"main","last_synced_at":"2026-01-10T21:43:08.459Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://answerdotai.github.io/fastsql","language":"Jupyter Notebook","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AnswerDotAI.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","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,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-08-01T19:40:43.000Z","updated_at":"2026-01-09T23:49:37.000Z","dependencies_parsed_at":"2024-11-16T13:32:12.333Z","dependency_job_id":"9abdf65e-df7c-41a0-bc85-6447476d0287","html_url":"https://github.com/AnswerDotAI/fastsql","commit_stats":null,"previous_names":["answerdotai/fastalchemy"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/AnswerDotAI/fastsql","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AnswerDotAI%2Ffastsql","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AnswerDotAI%2Ffastsql/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AnswerDotAI%2Ffastsql/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AnswerDotAI%2Ffastsql/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AnswerDotAI","download_url":"https://codeload.github.com/AnswerDotAI/fastsql/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AnswerDotAI%2Ffastsql/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28478049,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-16T06:30:42.265Z","status":"ssl_error","status_checked_at":"2026-01-16T06:30:16.248Z","response_time":107,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":[],"created_at":"2025-06-07T09:08:42.228Z","updated_at":"2026-01-16T09:02:08.902Z","avatar_url":"https://github.com/AnswerDotAI.png","language":"Jupyter Notebook","readme":"# fastsql\n\n\n\u003c!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! --\u003e\n\n## Install\n\n    pip install fastsql\n\n## Overview\n\n``` python\nfrom fastcore.utils import *\nfrom fastcore.net import urlsave\nfrom fastsql import *\nfrom fastsql.core import NotFoundError\n```\n\nWe demonstrate `fastsql`‘s features here using the ’chinook’ sample\ndatabase.\n\n``` python\nurl = 'https://github.com/lerocha/chinook-database/raw/master/ChinookDatabase/DataSources/Chinook_Sqlite.sqlite'\npath = Path('chinook.sqlite')\nif not path.exists(): urlsave(url, path)\n```\n\n``` python\ndb = database(\"chinook.sqlite\"); db\n```\n\n    Database(sqlite:///chinook.sqlite)\n\nDatabases have a `t` property that lists all tables:\n\n``` python\ndt = db.t\ndt\n```\n\n    Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track\n\nYou can use this to grab a single table…:\n\n``` python\n# artist = dt.artists\n# artist\n```\n\n``` python\nartist = dt.Artist\nartist\n```\n\n    \u003cTable Artist (ArtistId, Name)\u003e\n\n…or multiple tables at once:\n\n``` python\ndt['Artist','Album','Track','Genre','MediaType']\n```\n\n    [\u003cTable Artist (ArtistId, Name)\u003e,\n     \u003cTable Album (AlbumId, Title, ArtistId)\u003e,\n     \u003cTable Track (TrackId, Name, AlbumId, MediaTypeId, GenreId, Composer, Milliseconds, Bytes, UnitPrice)\u003e,\n     \u003cTable Genre (GenreId, Name)\u003e,\n     \u003cTable MediaType (MediaTypeId, Name)\u003e]\n\nIt also provides auto-complete in Jupyter, IPython, and nearly any other\ninteractive Python environment:\n\n\u003cimg src=\"index_files/figure-commonmark/5134025a-1-image.png\"\nwidth=\"180\" /\u003e\n\nYou can check if a table is in the database already:\n\n``` python\n'Artist' in dt\n```\n\n    True\n\nColumn work in a similar way to tables, using the `c` property:\n\n``` python\nac = artist.c\nac\n```\n\n    ArtistId, Name\n\nAuto-complete works for columns too:\n\n\u003cimg src=\"index_files/figure-commonmark/954ad8db-1-image.png\"\nwidth=\"140\" /\u003e\n\nColumns, tables, and view stringify in a format suitable for including\nin SQL statements. That means you can use auto-complete in f-strings.\n\n``` python\nqry = f\"select * from {artist} where {ac.Name} like 'AC/%'\"\nprint(qry)\n```\n\n    select * from \"Artist\" where \"Artist\".\"Name\" like 'AC/%'\n\nYou can view the results of a select query using `q`:\n\n``` python\ndb.q(qry)\n```\n\n    [{'ArtistId': 1, 'Name': 'AC/DC'}]\n\nViews can be accessed through the `v` property:\n\n``` python\nalbum = dt.Album\n\nacca_sql = f\"\"\"select {album}.*\nfrom {album} join {artist} using (ArtistId)\nwhere {ac.Name} like 'AC/%'\"\"\"\n\ndb.create_view(\"AccaDaccaAlbums\", acca_sql, replace=True)\nacca_dacca = db.q(f\"select * from {db.v.AccaDaccaAlbums}\")\nacca_dacca\n```\n\n    [{'AlbumId': 1,\n      'Title': 'For Those About To Rock We Salute You',\n      'ArtistId': 1},\n     {'AlbumId': 4, 'Title': 'Let There Be Rock', 'ArtistId': 1}]\n\n## Dataclass support\n\nA `dataclass` type with the names, types, and defaults of the tables is\ncreated using `dataclass()`:\n\n``` python\nalbum_dc = album.dataclass()\n```\n\n``` python\nalbum_dc\n```\n\n    fastsql.core.Album\n\nLet’s try it:\n\n``` python\nalbum_obj = album_dc(**acca_dacca[0])\nalbum_obj\n```\n\n    Album(AlbumId=1, Title='For Those About To Rock We Salute You', ArtistId=1)\n\nYou can get the definition of the dataclass using fastcore’s\n`dataclass_src` – everything is treated as nullable, in order to handle\nauto-generated database values:\n\n``` python\nsrc = dataclass_src(album_dc)\nhl_md(src, 'python')\n```\n\n``` python\n@dataclass\nclass Album:\n    AlbumId: int | None = UNSET\n    Title: str | None = UNSET\n    ArtistId: int | None = UNSET\n```\n\nBecause `dataclass()` is dynamic, you won’t get auto-complete in editors\nlike vscode – it’ll only work in dynamic environments like Jupyter and\nIPython. For editor support, you can export the full set of dataclasses\nto a module, which you can then import from:\n\n``` python\ncreate_mod(db, 'db_dc')\n```\n\n``` python\nimport sys\nsys.path.insert(0, '.')\nfrom db_dc import Track\nTrack()\n```\n\n    Track(TrackId=UNSET, Name=UNSET, AlbumId=UNSET, MediaTypeId=UNSET, GenreId=UNSET, Composer=UNSET, Milliseconds=UNSET, Bytes=UNSET, UnitPrice=UNSET)\n\nIndexing into a table does a query on primary key:\n\n``` python\ndt.Track[1]\n```\n\n    Track(TrackId=1, Name='For Those About To Rock (We Salute You)', AlbumId=1, MediaTypeId=1, GenreId=1, Composer='Angus Young, Malcolm Young, Brian Johnson', Milliseconds=343719, Bytes=11170334, UnitPrice=Decimal('0.99'))\n\nThere’s a shortcut to select from a table – just call it as a function.\nIf you’ve previously called `dataclass()`, returned iterms will be\nconstructed using that class by default. There’s lots of params you can\ncheck out, such as `limit`:\n\n``` python\nalbum(limit=2)\n```\n\n    [Album(AlbumId=1, Title='For Those About To Rock We Salute You', ArtistId=1),\n     Album(AlbumId=2, Title='Balls to the Wall', ArtistId=2)]\n\nPass a truthy value as `with_pk` and you’ll get tuples of primary keys\nand records:\n\n``` python\nalbum(with_pk=1, limit=2)\n```\n\n    [(1,\n      Album(AlbumId=1, Title='For Those About To Rock We Salute You', ArtistId=1)),\n     (2, Album(AlbumId=2, Title='Balls to the Wall', ArtistId=2))]\n\nIndexing also uses the dataclass by default:\n\n``` python\nalbum[5]\n```\n\n    Album(AlbumId=5, Title='Big Ones', ArtistId=3)\n\nIf you set `xtra` fields, then indexing is also filtered by those. As a\nresult, for instance in this case, nothing is returned since album 5 is\nnot created by artist 1:\n\n``` python\nalbum.xtra(ArtistId=1)\n\ntry: album[5]\nexcept NotFoundError: print(\"Not found\")\n```\n\n    Not found\n\nThe same filtering is done when using the table as a callable:\n\n``` python\nalbum()\n```\n\n    [Album(AlbumId=1, Title='For Those About To Rock We Salute You', ArtistId=1),\n     Album(AlbumId=4, Title='Let There Be Rock', ArtistId=1)]\n\n## Core design\n\nThe following methods accept `**kwargs`, passing them along to the first\n`dict` param:\n\n- `create`\n- `transform`\n- `transform_sql`\n- `update`\n- `insert`\n- `upsert`\n- `lookup`\n\nWe can access a table that doesn’t actually exist yet:\n\n``` python\ndt\n```\n\n    Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track\n\n``` python\ncats = dt.Cats\ncats\n```\n\n    \u003cTable Cats (does not exist yet)\u003e\n\nWe can use keyword arguments to now create that table:\n\n``` python\ncats.create(id=int, name=str, weight=float, uid=int, pk='id')\nhl_md(cats.schema, 'sql')\n```\n\n``` sql\nCREATE TABLE \"Cats\" (\n    id INTEGER, \n    name VARCHAR, \n    weight FLOAT, \n    uid INTEGER, \n    PRIMARY KEY (id)\n)\n```\n\nIt we set `xtra` then the additional fields are used for `insert`,\n`update`, and `delete`:\n\n``` python\ncats.xtra(uid=2)\ncat = cats.insert(name='meow', weight=6)\n```\n\nThe inserted row is returned, including the xtra ‘uid’ field.\n\n``` python\ncat\n```\n\n    {'id': 1, 'name': 'meow', 'weight': 6.0, 'uid': 2}\n\nUsing `**` in `update` here doesn’t actually achieve anything, since we\ncan just pass a `dict` directly – it’s just to show that it works:\n\n``` python\ncat['name'] = \"moo\"\ncat['uid'] = 1\ncats.update(**cat)\ncats()\n```\n\n    [{'id': 1, 'name': 'moo', 'weight': 6.0, 'uid': 2}]\n\nAttempts to update or insert with xtra fields are ignored.\n\nAn error is raised if there’s an attempt to update a record not matching\n`xtra` fields:\n\n``` python\ncats.xtra(uid=1)\ntry: cats.update(**cat)\nexcept NotFoundError: print(\"Not found\")\n```\n\n    Not found\n\nThis all also works with dataclasses:\n\n``` python\ncats.xtra(uid=2)\ncats.dataclass()\ncat = cats[1]\ncat\n```\n\n    Cats(id=1, name='moo', weight=6.0, uid=2)\n\n``` python\ncats.drop()\ncats\n```\n\n    \u003cTable Cats (id, name, weight, uid)\u003e\n\nAlternatively, you can create a table from a class. If it’s not already\na dataclass, it will be converted into one. In either case, the\ndataclass will be created (or modified) so that `None` can be passed to\nany field (this is needed to support fields such as automatic row ids).\n\n``` python\nclass Cat: id:int; name:str; weight:float; uid:int\n```\n\n``` python\ncats = db.create(Cat)\n```\n\n``` python\nhl_md(cats.schema, 'sql')\n```\n\n``` sql\nCREATE TABLE cat (\n    id INTEGER, \n    name VARCHAR, \n    weight FLOAT, \n    uid INTEGER, \n    PRIMARY KEY (id)\n)\n```\n\n``` python\ncat = Cat(name='咪咪', weight=9)\ncats.insert(cat)\n```\n\n    Cat(id=1, name='咪咪', weight=9.0, uid=None)\n\n## Manipulating data\n\nWe try to make the following methods as flexible as possible. Wherever\npossible, they support Python dictionaries, dataclasses, and classes.\n\n### .insert()\n\nCreates a record. Returns an instance of the updated record.\n\nInsert using a dictionary.\n\n``` python\ncats.insert({'name': 'Rex', 'weight': 12.2})\n```\n\n    Cat(id=2, name='Rex', weight=12.2, uid=None)\n\nInsert using a dataclass.\n\n``` python\nCatDC = cats.dataclass()\ncats.insert(CatDC(name='Tom', weight=10.2))\n```\n\n    Cat(id=3, name='Tom', weight=10.2, uid=None)\n\nInsert using a standard Python class\n\n``` python\ncat = cats.insert(Cat(name='Jerry', weight=5.2))\n```\n\n### .update()\n\nUpdates a record using a Python dict, dataclass, or object, and returns\nan instance of the updated record.\n\nUpdating from a Python dict:\n\n``` python\ncats.update(dict(id=cat.id, name='Jerry', weight=6.2))\n```\n\n    Cat(id=4, name='Jerry', weight=6.2, uid=None)\n\nUpdating from a dataclass:\n\n``` python\ncats.update(CatDC(id=cat.id, name='Jerry', weight=6.3))\n```\n\n    Cat(id=4, name='Jerry', weight=6.3, uid=None)\n\nUpdating using a class:\n\n``` python\ncats.update(Cat(id=cat.id, name='Jerry', weight=5.7))\n```\n\n    Cat(id=4, name='Jerry', weight=5.7, uid=None)\n\n### .delete()\n\nRemoving data is done by providing the primary key value of the record.\n\n``` python\n# Farewell Jerry!\ncats.delete(cat.id)\n```\n\n    Cat(id=4, name='Jerry', weight=5.7, uid=None)\n\n### Multi-field primary keys\n\nPass a collection of strings to create a multi-field pk:\n\n``` python\nclass PetFood: catid:int; food:str; qty:int\npetfoods = db.create(PetFood, pk=['catid','food'])\nprint(petfoods.schema)\n```\n\n    CREATE TABLE pet_food (\n        catid INTEGER, \n        food VARCHAR, \n        qty INTEGER, \n        PRIMARY KEY (catid, food)\n    )\n\nYou can index into these using multiple values:\n\n``` python\npf = petfoods.insert(PetFood(1, 'tuna', 2))\npetfoods[1,'tuna']\n```\n\n    PetFood(catid=1, food='tuna', qty=2)\n\nUpdates work in the usual way:\n\n``` python\npf.qty=3\npetfoods.update(pf)\n```\n\n    PetFood(catid=1, food='tuna', qty=3)\n\nYou can also use `upsert` to update if the key exists, or insert\notherwise:\n\n``` python\npf.qty=1\npetfoods.upsert(pf)\npetfoods()\n```\n\n    [PetFood(catid=1, food='tuna', qty=1)]\n\n``` python\npf.food='salmon'\npetfoods.upsert(pf)\npetfoods()\n```\n\n    [PetFood(catid=1, food='tuna', qty=1), PetFood(catid=1, food='salmon', qty=1)]\n\n`delete` takes a tuple of keys:\n\n``` python\npetfoods.delete((1, 'tuna'))\npetfoods()\n```\n\n    [PetFood(catid=1, food='salmon', qty=1)]\n\n## Migrations\n\nFastSQL supports schema migrations to evolve your database over time.\nMigrations are SQL or Python files stored in a migrations directory,\nnumbered sequentially.\n\nThe database tracks the current schema version in a `_meta` table. When\nyou run migrations, only unapplied migrations are executed.\n\nLet’s create a migration to add a priority field to our cats table:\n\n``` python\n# Create migrations directory\nmig_dir = Path('cat_migrations')\nmig_dir.mkdir(exist_ok=True)\n\n# Create a migration to add priority column\nmigration_sql = 'alter table cat add column color text default \"unknown\";'\n(mig_dir / '1-add_color_to_cat.sql').write_text(migration_sql)\n```\n\n    56\n\nCheck the current schema version (will be 0 initially):\n\n``` python\nprint(f\"Current version: {db.version}\")\n```\n\n    Current version: 0\n\nRun the migration:\n\n``` python\ndb.migrate('cat_migrations')\n```\n\n    Applied migration 1: 1-add_color_to_cat.sql\n\nThe database version is now updated, and the table structure reflects\nthe change:\n\n``` python\nprint(f\"New version: {db.version}\")\nprint(f\"\\nUpdated schema:\")\ncats = dt.cat\nhl_md(cats.schema, 'sql')\n```\n\n    New version: 1\n\n    Updated schema:\n\n``` sql\nCREATE TABLE cat (\n    id INTEGER, \n    name VARCHAR, \n    weight FLOAT, \n    uid INTEGER, \n    color TEXT DEFAULT \"unknown\", \n    PRIMARY KEY (id)\n)\n```\n\nExisting records now have the priority field with the default value, and\nnew records can use it too:\n\n``` python\ncats.insert({'name': 'Mr. Snuggles', 'weight': 8.5, 'color': 'tuxedo'})\ncats()\n```\n\n    [Cat(id=1, name='咪咪', weight=9.0, uid=None, color='unknown'),\n     Cat(id=2, name='Rex', weight=12.2, uid=None, color='unknown'),\n     Cat(id=3, name='Tom', weight=10.2, uid=None, color='unknown'),\n     Cat(id=4, name='Mr. Snuggles', weight=8.5, uid=None, color='tuxedo')]\n\nIf you run `migrate()` again, it won’t reapply migrations that have\nalready been applied:\n\n``` python\ndb.migrate('cat_migrations')  # No output - migration already applied\n```\n\nMigrations can also be Python scripts. Create a file like\n`2-update_data.py` that accepts the database connection string as a\ncommand line argument to perform more complex data transformations.\nPython migration scripts must handle their own commits:\n\n``` python\n# migrations/2-update_data.py\nimport sys\nfrom fastsql import database\n\nconn_str = sys.argv[1]\ndb = database(conn_str)\n\n# Perform complex data transformations\nfor cat in db.t.cat():\n    if cat.weight \u003e 10:\n        db.t.cat.update({'id': cat.id, 'priority': 1})\n\n# Python migrations must commit their own changes\ndb.conn.commit()\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fanswerdotai%2Ffastsql","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fanswerdotai%2Ffastsql","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fanswerdotai%2Ffastsql/lists"}