{"html_url": "https://github.com/simonw/sqlite-utils/issues/235#issuecomment-1304539296", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/235", "id": 1304539296, "node_id": "IC_kwDOCGYnMM5NwbCg", "user": {"value": 559711, "label": "ryascott"}, "created_at": "2022-11-05T12:40:12Z", "updated_at": "2022-11-05T12:40:12Z", "author_association": "NONE", "body": "I had the problem this morning when running:\r\n\r\n`Python==3.9.6\r\n sqlite3.sqlite_version==3.37.0\r\n sqlite-utils==3.30\r\n`\r\n\r\nI upgraded to:\r\n`Python ==3.10.8 \r\n sqlite3.sqlite_version==3.37.2\r\n sqlite-utils==3.30\r\n`\r\n\r\nand the error did not appear anymore.\r\n\r\nHope this helps\r\nRyan\r\n\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 810618495, "label": "Extract columns cannot create foreign key relation: sqlite3.OperationalError: table sqlite_master may not be modified"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/511#issuecomment-1304320521", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/511", "id": 1304320521, "node_id": "IC_kwDOCGYnMM5NvloJ", "user": {"value": 7908073, "label": "chapmanjacobd"}, "created_at": "2022-11-04T22:54:09Z", "updated_at": "2022-11-04T22:59:54Z", "author_association": "CONTRIBUTOR", "body": "I ran `PRAGMA integrity_check` and it returned `ok`. but then I tried restoring from a backup and I didn't get this `IntegrityError: constraint failed` error. So I think it was just something wrong with my database. If it happens again I will first try to reindex and see if that fixes the issue", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1436539554, "label": "[insert_all, upsert_all] IntegrityError: constraint failed"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/511#issuecomment-1304078945", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/511", "id": 1304078945, "node_id": "IC_kwDOCGYnMM5Nuqph", "user": {"value": 7908073, "label": "chapmanjacobd"}, "created_at": "2022-11-04T19:38:36Z", "updated_at": "2022-11-04T20:13:17Z", "author_association": "CONTRIBUTOR", "body": "Even more bizarre, the source db only has one record and the target table has no conflicting record:\r\n\r\n```\r\n875 0.3s lb:/ (main|\u271a2) [0|0]\ud83c\udf3a sqlite-utils tube_71.db 'select * from media where path = \"https://archive.org/details/088ghostofachanceroygetssackedrevengeofthelivinglunchdvdripxvidphz\"' | jq\r\n[\r\n {\r\n \"size\": null,\r\n \"time_created\": null,\r\n \"play_count\": 1,\r\n \"language\": null,\r\n \"view_count\": null,\r\n \"width\": null,\r\n \"height\": null,\r\n \"fps\": null,\r\n \"average_rating\": null,\r\n \"live_status\": null,\r\n \"age_limit\": null,\r\n \"uploader\": null,\r\n \"time_played\": 0,\r\n \"path\": \"https://archive.org/details/088ghostofachanceroygetssackedrevengeofthelivinglunchdvdripxvidphz\",\r\n \"id\": \"088ghostofachanceroygetssackedrevengeofthelivinglunchdvdripxvidphz/074 - Home Away from Home, Rainy Day Robot, Odie the Amazing DVDRip XviD [PhZ].mkv\",\r\n \"ie_key\": \"ArchiveOrg\",\r\n \"playlist_path\": \"https://archive.org/details/088ghostofachanceroygetssackedrevengeofthelivinglunchdvdripxvidphz\",\r\n \"duration\": 1424.05,\r\n \"tags\": null,\r\n \"title\": \"074 - Home Away from Home, Rainy Day Robot, Odie the Amazing DVDRip XviD [PhZ].mkv\"\r\n }\r\n]\r\n875 0.3s lb:/ (main|\u271a2) [0|0]\ud83e\udd67 sqlite-utils video.db 'select * from media where path = \"https://archive.org/details/088ghostofachanceroygetssackedrevengeofthelivinglunchdvdripxvidphz\"' | jq\r\n[]\r\n```\r\n\r\nI've been able to use this code successfully several times before so not sure what's causing the issue.\r\n\r\nI guess the way that I'm handling multiple databases is an issue, though it hasn't ever inserted into the source db, not sure what's different. The only reasonable explanation is that it is trying to insert into the source db from the source db for some reason? Or maybe sqlite3 is checking the source db for primary key violation because the table name is the same", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1436539554, "label": "[insert_all, upsert_all] IntegrityError: constraint failed"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1882#issuecomment-1302818784", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1882", "id": 1302818784, "node_id": "IC_kwDOBm6k_c5Np2_g", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-04T00:25:18Z", "updated_at": "2022-11-04T16:12:39Z", "author_association": "OWNER", "body": "On that basis I think the core API design should change to this:\r\n```\r\nPOST /db/-/create\r\nAuthorization: Bearer xxx\r\nContent-Type: application/json\r\n{\r\n \"name\": \"my new table\",\r\n \"columns\": [\r\n {\r\n \"name\": \"id\",\r\n \"type\": \"integer\"\r\n },\r\n {\r\n \"name\": \"title\",\r\n \"type\": \"text\"\r\n }\r\n ]\r\n \"pk\": \"id\"\r\n}\r\n```\r\nThis leaves room for a `\"rows\": []` key at the root too. Having that as a child of `\"table\"` felt unintuitive to me, and I didn't like the way this looked either:\r\n\r\n```json\r\n{\r\n \"table\": {\r\n \"name\": \"my_new_table\"\r\n },\r\n \"rows\": [\r\n {\"id\": 1, \"title\": \"Title\"}\r\n ]\r\n}\r\n```\r\nWeird to have the table `name` nested inside `table` when `rows` wasn't.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1435294468, "label": "`/db/-/create` API for creating tables"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/50#issuecomment-1303660293", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/50", "id": 1303660293, "node_id": "IC_kwDOCGYnMM5NtEcF", "user": {"value": 7908073, "label": "chapmanjacobd"}, "created_at": "2022-11-04T14:38:36Z", "updated_at": "2022-11-04T14:38:36Z", "author_association": "CONTRIBUTOR", "body": "where did you see the limit as 999? I believe the limit has been 32766 for quite some time. If you could detect which one this could speed up batch insert of some types of data significantly", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 473083260, "label": "\"Too many SQL variables\" on large inserts"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1217#issuecomment-1303301786", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1217", "id": 1303301786, "node_id": "IC_kwDOBm6k_c5Nrs6a", "user": {"value": 31312775, "label": "mattmalcher"}, "created_at": "2022-11-04T11:37:52Z", "updated_at": "2022-11-04T11:37:52Z", "author_association": "NONE", "body": "All seems to work well, but there are some glitches to do with proxies, see #1883 .\r\n\r\nExcited to use this :)", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 802513359, "label": "Possible to deploy as a python app (for Rstudio connect server)?"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1217#issuecomment-1303299509", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1217", "id": 1303299509, "node_id": "IC_kwDOBm6k_c5NrsW1", "user": {"value": 31312775, "label": "mattmalcher"}, "created_at": "2022-11-04T11:35:13Z", "updated_at": "2022-11-04T11:35:13Z", "author_association": "NONE", "body": "The following worked for deployment to RStudio / Posit Connect\r\n\r\nAn app.py along the lines of:\r\n\r\n```python\r\nfrom pathlib import Path\r\n\r\nfrom datasette.app import Datasette\r\n\r\nexample_db = Path(__file__).parent / \"data\" / \"example.db\"\r\n\r\n# use connect 'Content URL' setting here to set app to /datasette/\r\nds = Datasette(files=[example_db], settings={\"base_url\": \"/datasette/\"})\r\n\r\nds._startup_invoked = True\r\nds_app = ds.app()\r\n```\r\nThen to deploy, from within a virtualenv with `rsconnect-python`\r\n```sh\r\nrsconnect write-manifest fastapi -p $VIRTUAL_ENV/bin/python -e app:ds_app -o .\r\nrsconnect deploy manifest manifest.json -n -t \"Example Datasette\"\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 802513359, "label": "Possible to deploy as a python app (for Rstudio connect server)?"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1882#issuecomment-1302818153", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1882", "id": 1302818153, "node_id": "IC_kwDOBm6k_c5Np21p", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-04T00:23:58Z", "updated_at": "2022-11-04T00:23:58Z", "author_association": "OWNER", "body": "I made a decision here that this endpoint should also accept an optional `\"rows\": [...]` list which is used to automatically create the table using a schema derived from those example rows (which then get inserted):\r\n\r\n- https://github.com/simonw/datasette/issues/1862#issuecomment-1302817807", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1435294468, "label": "`/db/-/create` API for creating tables"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1862#issuecomment-1302817807", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1862", "id": 1302817807, "node_id": "IC_kwDOBm6k_c5Np2wP", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-04T00:23:13Z", "updated_at": "2022-11-04T00:23:13Z", "author_association": "OWNER", "body": "I don't like this on `/db/table/-/insert` - I think it makes more sense to optionally pass a `\"rows\"` key to the `/db/-/create` endpoint instead.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1425011030, "label": "Create a new table from one or more records, `sqlite-utils` style"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1862#issuecomment-1302817500", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1862", "id": 1302817500, "node_id": "IC_kwDOBm6k_c5Np2rc", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-04T00:22:31Z", "updated_at": "2022-11-04T00:22:31Z", "author_association": "OWNER", "body": "Maybe this is a feature added to the existing `/db/table/-/insert` endpoint?\r\n\r\nBit weird that you can call that endpoint for a table that doesn't exist yet, but it fits the `sqlite-utils` way of creating tables which I've found very pleasant over the past few years.\r\n\r\nSo perhaps the API looks like this:\r\n\r\n```\r\nPOST ///-/insert\r\nContent-Type: application/json\r\nAuthorization: Bearer dstok_\r\n{\r\n \"create_table\": true,\r\n \"rows\": [\r\n {\r\n \"column1\": \"value1\",\r\n \"column2\": \"value2\"\r\n },\r\n {\r\n \"column1\": \"value3\",\r\n \"column2\": \"value4\"\r\n }\r\n ]\r\n}\r\n```\r\nThe `create_table` option will cause the table to be created if it doesn't already exist.\r\n\r\nThat means I probably also need a `\"pk\": \"...\"` column for setting a primary key if the table is being created ... and maybe other options that I invent for this other feature too?\r\n- #1882", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1425011030, "label": "Create a new table from one or more records, `sqlite-utils` style"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1871#issuecomment-1302815105", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1871", "id": 1302815105, "node_id": "IC_kwDOBm6k_c5Np2GB", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-04T00:17:23Z", "updated_at": "2022-11-04T00:17:23Z", "author_association": "OWNER", "body": "I'll probably enhance it a bit more though, I want to provide a UI that lists all the tables you can explore and lets you click to pre-fill the forms with them.\r\n\r\nThough at that point what should I do about the other endpoints? Probably list those too. Gets a bit complex, especially with the row-level update and delete endpoints.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1427293909, "label": "API explorer tool"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1871#issuecomment-1302814693", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1871", "id": 1302814693, "node_id": "IC_kwDOBm6k_c5Np1_l", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-04T00:16:36Z", "updated_at": "2022-11-04T00:16:36Z", "author_association": "OWNER", "body": "I can close this issue once I fix it so it no longer hard-codes a potentially invalid example endpoint:\r\n\r\nhttps://github.com/simonw/datasette/blob/bcc781f4c50a8870e3389c4e60acb625c34b0317/datasette/templates/api_explorer.html#L24-L26\r\n\r\nhttps://github.com/simonw/datasette/blob/bcc781f4c50a8870e3389c4e60acb625c34b0317/datasette/templates/api_explorer.html#L34-L35", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1427293909, "label": "API explorer tool"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1881#issuecomment-1302813449", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1881", "id": 1302813449, "node_id": "IC_kwDOBm6k_c5Np1sJ", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-04T00:14:07Z", "updated_at": "2022-11-04T00:14:07Z", "author_association": "OWNER", "body": "Tool is now live here: https://latest-1-0-dev.datasette.io/-/permissions\r\n\r\nNeeds root perms, so access this first: https://latest-1-0-dev.datasette.io/login-as-root", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1434094365, "label": "Tool for simulating permission checks against actors"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1881#issuecomment-1302812918", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1881", "id": 1302812918, "node_id": "IC_kwDOBm6k_c5Np1j2", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-04T00:13:05Z", "updated_at": "2022-11-04T00:13:05Z", "author_association": "OWNER", "body": "Has tests now.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1434094365, "label": "Tool for simulating permission checks against actors"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1863#issuecomment-1302790013", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1863", "id": 1302790013, "node_id": "IC_kwDOBm6k_c5Npv99", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-03T23:32:30Z", "updated_at": "2022-11-03T23:32:30Z", "author_association": "OWNER", "body": "I'm not going to allow updates to primary keys. If you need to do that, you can instead delete the record and then insert a new one with the new primary keys you wanted - or maybe use a custom SQL query.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1425029242, "label": "Update a single record in an existing table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1851#issuecomment-1294224185", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1851", "id": 1294224185, "node_id": "IC_kwDOBm6k_c5NJEs5", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-27T23:18:24Z", "updated_at": "2022-11-03T23:26:05Z", "author_association": "OWNER", "body": "So new API design is:\r\n\r\n```\r\nPOST /db/table/-/insert\r\nAuthorization: Bearer xxx\r\nContent-Type: application/json\r\n{\r\n \"row\": {\r\n \"id\": 1,\r\n \"name\": \"New record\"\r\n }\r\n}\r\n```\r\nReturns:\r\n```\r\n201 Created\r\n{\r\n \"row\": [{\r\n \"id\": 1,\r\n \"name\": \"New record\"\r\n }]\r\n}\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1421544654, "label": "API to insert a single record into an existing table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1863#issuecomment-1302785086", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1863", "id": 1302785086, "node_id": "IC_kwDOBm6k_c5Npuw-", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-03T23:24:33Z", "updated_at": "2022-11-03T23:24:56Z", "author_association": "OWNER", "body": "Thinking more about validation: I'm considering if this should validate that columns which are defined as SQLite foreign keys are being updated to values that exist in those other tables.\r\n\r\nI like the sound of this. It seems like a sensible default behaviour for Datasette. And it fits with the fact that Datasette treats foreign keys specially elsewhere in the interface.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1425029242, "label": "Update a single record in an existing table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1863#issuecomment-1302760549", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1863", "id": 1302760549, "node_id": "IC_kwDOBm6k_c5Npoxl", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-03T22:43:04Z", "updated_at": "2022-11-03T23:21:31Z", "author_association": "OWNER", "body": "The `id=(int, ...)` thing is weird, but is apparently Pydantic syntax for a required field?\r\n\r\nhttps://cs.github.com/starlite-api/starlite/blob/28ddc847c4cb072f0d5d21a9ecd5259711f12ec9/docs/usage/11-data-transfer-objects.md#L161 confirms:\r\n\r\n> 1. For required fields use a tuple of type + ellipsis, for example `(str, ...)`.\r\n> 2. For optional fields use a tuple of type + `None`, for example `(str, None)`\r\n> 3. To set a default value use a tuple of type + default value, for example `(str, \"Hello World\")`", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1425029242, "label": "Update a single record in an existing table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1863#issuecomment-1302760382", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1863", "id": 1302760382, "node_id": "IC_kwDOBm6k_c5Npou-", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-03T22:42:47Z", "updated_at": "2022-11-03T22:42:47Z", "author_association": "OWNER", "body": "```python\r\nprint(create_model('document', id=(int, ...), title=(str, None)).schema_json(indent=2))\r\n```\r\n```json\r\n{\r\n \"title\": \"document\",\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"id\": {\r\n \"title\": \"Id\",\r\n \"type\": \"integer\"\r\n },\r\n \"title\": {\r\n \"title\": \"Title\",\r\n \"type\": \"string\"\r\n }\r\n },\r\n \"required\": [\r\n \"id\"\r\n ]\r\n}\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1425029242, "label": "Update a single record in an existing table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1863#issuecomment-1302759174", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1863", "id": 1302759174, "node_id": "IC_kwDOBm6k_c5NpocG", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-03T22:40:47Z", "updated_at": "2022-11-03T22:40:47Z", "author_association": "OWNER", "body": "I'm considering Pydantic for this, see:\r\n- https://github.com/simonw/datasette/issues/1882#issuecomment-1302716350\r\n\r\nIn particular the `create_model()` method: https://pydantic-docs.helpmanual.io/usage/models/#dynamic-model-creation\r\n\r\nThis would give me good validation. It would also, weirdly, give me the ability to output JSON schema. Maybe I could have this as the JSON schema for a row?\r\n\r\n`/db/table/-/json-schema`", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1425029242, "label": "Update a single record in an existing table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1882#issuecomment-1302716350", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1882", "id": 1302716350, "node_id": "IC_kwDOBm6k_c5Npd--", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-03T21:51:14Z", "updated_at": "2022-11-03T22:35:54Z", "author_association": "OWNER", "body": "Validating this JSON object is getting a tiny bit complex. I'm tempted to adopt https://pydantic-docs.helpmanual.io/ at this point.\r\n\r\nThe `create_model` example on https://stackoverflow.com/questions/66168517/generate-dynamic-model-using-pydantic/66168682#66168682 is particularly relevant, especially when I work on this issue:\r\n\r\n- #1863\r\n\r\n```python\r\nfrom pydantic import create_model\r\n\r\nd = {\"strategy\": {\"name\": \"test_strat2\", \"periods\": 10}}\r\n\r\nStrategy = create_model(\"Strategy\", **d[\"strategy\"])\r\n\r\nprint(Strategy.schema_json(indent=2))\r\n```\r\n`create_model()`: https://pydantic-docs.helpmanual.io/usage/models/#dynamic-model-creation", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1435294468, "label": "`/db/-/create` API for creating tables"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1882#issuecomment-1302721916", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1882", "id": 1302721916, "node_id": "IC_kwDOBm6k_c5NpfV8", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-03T21:58:50Z", "updated_at": "2022-11-03T21:59:17Z", "author_association": "OWNER", "body": "Mocked up a quick HTML+JavaScript form for creating that JSON structure using some iteration against Copilot prompts:\r\n```html\r\n
\r\n/* JSON format:\r\n{\r\n  \"table\": {\r\n      \"name\": \"my new table\",\r\n      \"columns\": [\r\n          {\r\n              \"name\": \"id\",\r\n              \"type\": \"integer\"\r\n          },\r\n          {\r\n              \"name\": \"title\",\r\n              \"type\": \"text\"\r\n          }\r\n      ]\r\n     \"pk\": \"id\"\r\n  }\r\n}\r\n\r\nHTML form with Javascript for creating this JSON:\r\n*/
\r\n
\r\n \r\n
\r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n

Current columns:

\r\n
    \r\n \r\n\r\n\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1435294468, "label": "`/db/-/create` API for creating tables"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1882#issuecomment-1302715662", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1882", "id": 1302715662, "node_id": "IC_kwDOBm6k_c5Npd0O", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-03T21:50:27Z", "updated_at": "2022-11-03T21:50:27Z", "author_association": "OWNER", "body": "API design for this:\r\n```\r\nPOST /db/-/create\r\nAuthorization: Bearer xxx\r\nContent-Type: application/json\r\n{\r\n \"table\": {\r\n \"name\": \"my new table\",\r\n \"columns\": [\r\n {\r\n \"name\": \"id\",\r\n \"type\": \"integer\"\r\n },\r\n {\r\n \"name\": \"title\",\r\n \"type\": \"text\"\r\n }\r\n ]\r\n \"pk\": \"id\"\r\n }\r\n}\r\n```\r\nSupported column types are:\r\n\r\n- `integer`\r\n- `text`\r\n- `float` (even though SQLite calls it a \"real\")\r\n- `blob`\r\n\r\nThis matches my design for `sqlite-utils`: https://sqlite-utils.datasette.io/en/stable/cli.html#cli-create-table", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1435294468, "label": "`/db/-/create` API for creating tables"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1843#issuecomment-1302679026", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1843", "id": 1302679026, "node_id": "IC_kwDOBm6k_c5NpU3y", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-03T21:22:42Z", "updated_at": "2022-11-03T21:22:42Z", "author_association": "OWNER", "body": "Docs for the new `db.close()` method: https://docs.datasette.io/en/latest/internals.html#db-close", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1408757705, "label": "Intermittent \"Too many open files\" error running tests"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1843#issuecomment-1302678384", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1843", "id": 1302678384, "node_id": "IC_kwDOBm6k_c5NpUtw", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-03T21:21:59Z", "updated_at": "2022-11-03T21:21:59Z", "author_association": "OWNER", "body": "I added extra debug info to `/-/threads` to see this for myself:\r\n\r\n```diff\r\ndiff --git a/datasette/app.py b/datasette/app.py\r\nindex 02bd38f1..16579e28 100644\r\n--- a/datasette/app.py\r\n+++ b/datasette/app.py\r\n@@ -969,6 +969,13 @@ class Datasette:\r\n \"threads\": [\r\n {\"name\": t.name, \"ident\": t.ident, \"daemon\": t.daemon} for t in threads\r\n ],\r\n+ \"file_connections\": {\r\n+ db.name: [\r\n+ [dict(r) for r in conn.execute(\"pragma database_list\").fetchall()]\r\n+ for conn in db._all_file_connections\r\n+ ]\r\n+ for db in self.databases.values()\r\n+ },\r\n }\r\n # Only available in Python 3.7+\r\n if hasattr(asyncio, \"all_tasks\"):\r\n```\r\nOutput after hitting refresh on a few `/fixtures` tables to ensure more threads started:\r\n\r\n```\r\n \"file_connections\": {\r\n \"_internal\": [],\r\n \"fixtures\": [\r\n [\r\n {\r\n \"seq\": 0,\r\n \"name\": \"main\",\r\n \"file\": \"/Users/simon/Dropbox/Development/datasette/fixtures.db\"\r\n }\r\n ],\r\n [\r\n {\r\n \"seq\": 0,\r\n \"name\": \"main\",\r\n \"file\": \"/Users/simon/Dropbox/Development/datasette/fixtures.db\"\r\n }\r\n ],\r\n [\r\n {\r\n \"seq\": 0,\r\n \"name\": \"main\",\r\n \"file\": \"/Users/simon/Dropbox/Development/datasette/fixtures.db\"\r\n }\r\n ]\r\n ]\r\n },\r\n```\r\nI decided not to ship this feature though as it leaks the names of internal database files.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1408757705, "label": "Intermittent \"Too many open files\" error running tests"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1843#issuecomment-1302634332", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1843", "id": 1302634332, "node_id": "IC_kwDOBm6k_c5NpJ9c", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-03T20:34:56Z", "updated_at": "2022-11-03T20:34:56Z", "author_association": "OWNER", "body": "Confirmed that calling `conn.close()` on each SQLite file-based connection is the way to fix this problem.\r\n\r\nI'm adding a `db.close()` method (sync, not async - I tried async first but it was really hard to cause every thread in the pool to close its threadlocal database connection) which loops through all known open file-based connections and closes them.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1408757705, "label": "Intermittent \"Too many open files\" error running tests"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1843#issuecomment-1302574330", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1843", "id": 1302574330, "node_id": "IC_kwDOBm6k_c5No7T6", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-03T19:30:22Z", "updated_at": "2022-11-03T19:30:22Z", "author_association": "OWNER", "body": "This is affecting me a lot at the moment, on my laptop (runs fine in CI).\r\n\r\nHere's a change to `conftest.py` which highlights the problem - it cause a failure the moment there are more than 5 open files according to `psutil`:\r\n\r\n```diff\r\ndiff --git a/tests/conftest.py b/tests/conftest.py\r\nindex f4638a14..21d433c1 100644\r\n--- a/tests/conftest.py\r\n+++ b/tests/conftest.py\r\n@@ -1,6 +1,7 @@\r\n import httpx\r\n import os\r\n import pathlib\r\n+import psutil\r\n import pytest\r\n import re\r\n import subprocess\r\n@@ -192,3 +193,8 @@ def ds_unix_domain_socket_server(tmp_path_factory):\r\n yield ds_proc, uds\r\n # Shut it down at the end of the pytest session\r\n ds_proc.terminate()\r\n+\r\n+\r\n+def pytest_runtest_teardown(item: pytest.Item) -> None:\r\n+ open_files = psutil.Process().open_files()\r\n+ assert len(open_files) < 5\r\n```\r\nThe first error I get from this with `pytest --pdb -x` is here:\r\n\r\n```\r\ntests/test_api.py ............E\r\n>>>>> traceback >>>>>\r\n\r\nitem = \r\n\r\n def pytest_runtest_teardown(item: pytest.Item) -> None:\r\n open_files = psutil.Process().open_files()\r\n> assert len(open_files) < 5\r\nE AssertionError: assert 5 < 5\r\nE + where 5 = len([popenfile(path='/private/var/folders/wr/hn3206rs1yzgq3r49bz8nvnh0000gn/T/tmpfglrt4p2/fixtures.db', fd=14), popenfile(... fd=19), popenfile(path='/private/var/folders/wr/hn3206rs1yzgq3r49bz8nvnh0000gn/T/tmphdi5b250/fixtures.dot.db', fd=20)])\r\n\r\n/Users/simon/Dropbox/Development/datasette/tests/conftest.py:200: AssertionError\r\n>>>>> entering PDB >>>>>\r\n\r\n>>>>> PDB post_mortem (IO-capturing turned off) >>>>>\r\n> /Users/simon/Dropbox/Development/datasette/tests/conftest.py(200)pytest_runtest_teardown()\r\n-> assert len(open_files) < 5\r\n```\r\nThat's this test:\r\n\r\nhttps://github.com/simonw/datasette/blob/2ec5583629005b32cb0877786f9681c5d43ca33f/tests/test_api.py#L656-L673\r\n\r\nWhich uses this fixture:\r\n\r\nhttps://github.com/simonw/datasette/blob/2ec5583629005b32cb0877786f9681c5d43ca33f/tests/fixtures.py#L228-L231\r\n\r\nWhich calls this function:\r\n\r\nhttps://github.com/simonw/datasette/blob/2ec5583629005b32cb0877786f9681c5d43ca33f/tests/fixtures.py#L105-L122\r\n\r\nSo now I'm suspicious that, even though the fixture is meant to be session scoped, the way I'm using `with tempfile.TemporaryDirectory() as tmpdir:` is causing a whole load of files to be created and held open which are not later closed.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1408757705, "label": "Intermittent \"Too many open files\" error running tests"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1855#issuecomment-1301646670", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1855", "id": 1301646670, "node_id": "IC_kwDOBm6k_c5NlY1O", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-03T05:11:26Z", "updated_at": "2022-11-03T05:11:26Z", "author_association": "OWNER", "body": "That still needs comprehensive tests before I land it.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1423336089, "label": "`datasette create-token` ability to create tokens with a reduced set of permissions"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1855#issuecomment-1301646493", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1855", "id": 1301646493, "node_id": "IC_kwDOBm6k_c5NlYyd", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-03T05:11:06Z", "updated_at": "2022-11-03T05:11:06Z", "author_association": "OWNER", "body": "Built a prototype of the above:\r\n\r\n```diff\r\ndiff --git a/datasette/default_permissions.py b/datasette/default_permissions.py\r\nindex 32b0c758..f68aa38f 100644\r\n--- a/datasette/default_permissions.py\r\n+++ b/datasette/default_permissions.py\r\n@@ -6,8 +6,8 @@ import json\r\n import time\r\n \r\n \r\n-@hookimpl(tryfirst=True)\r\n-def permission_allowed(datasette, actor, action, resource):\r\n+@hookimpl(tryfirst=True, specname=\"permission_allowed\")\r\n+def permission_allowed_default(datasette, actor, action, resource):\r\n async def inner():\r\n if action in (\r\n \"permissions-debug\",\r\n@@ -57,6 +57,44 @@ def permission_allowed(datasette, actor, action, resource):\r\n return inner\r\n \r\n \r\n+@hookimpl(specname=\"permission_allowed\")\r\n+def permission_allowed_actor_restrictions(actor, action, resource):\r\n+ if actor is None:\r\n+ return None\r\n+ _r = actor.get(\"_r\")\r\n+ if not _r:\r\n+ # No restrictions, so we have no opinion\r\n+ return None\r\n+ action_initials = \"\".join([word[0] for word in action.split(\"-\")])\r\n+ # If _r is defined then we use those to further restrict the actor\r\n+ # Crucially, we only use this to say NO (return False) - we never\r\n+ # use it to return YES (True) because that might over-ride other\r\n+ # restrictions placed on this actor\r\n+ all_allowed = _r.get(\"a\")\r\n+ if all_allowed is not None:\r\n+ assert isinstance(all_allowed, list)\r\n+ if action_initials in all_allowed:\r\n+ return None\r\n+ # How about for the current database?\r\n+ if action in (\"view-database\", \"view-database-download\", \"execute-sql\"):\r\n+ database_allowed = _r.get(\"d\", {}).get(resource)\r\n+ if database_allowed is not None:\r\n+ assert isinstance(database_allowed, list)\r\n+ if action_initials in database_allowed:\r\n+ return None\r\n+ # Or the current table? That's any time the resource is (database, table)\r\n+ if not isinstance(resource, str) and len(resource) == 2:\r\n+ database, table = resource\r\n+ table_allowed = _r.get(\"t\", {}).get(database, {}).get(table)\r\n+ # TODO: What should this do for canned queries?\r\n+ if table_allowed is not None:\r\n+ assert isinstance(table_allowed, list)\r\n+ if action_initials in table_allowed:\r\n+ return None\r\n+ # This action is not specifically allowed, so reject it\r\n+ return False\r\n+\r\n+\r\n @hookimpl\r\n def actor_from_request(datasette, request):\r\n prefix = \"dstok_\"\r\n\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1423336089, "label": "`datasette create-token` ability to create tokens with a reduced set of permissions"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1881#issuecomment-1301639741", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1881", "id": 1301639741, "node_id": "IC_kwDOBm6k_c5NlXI9", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-03T04:58:21Z", "updated_at": "2022-11-03T04:58:21Z", "author_association": "OWNER", "body": "The whole `database_name` or `(database_name, table_name)` tuple for resource is a bit of a code smell. Maybe this is a chance to tidy that up too?", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1434094365, "label": "Tool for simulating permission checks against actors"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1881#issuecomment-1301639370", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1881", "id": 1301639370, "node_id": "IC_kwDOBm6k_c5NlXDK", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-03T04:57:21Z", "updated_at": "2022-11-03T04:57:21Z", "author_association": "OWNER", "body": "The plugin hook would be called `register_permissions()`, for consistency with `register_routes()` and `register_commands()`.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1434094365, "label": "Tool for simulating permission checks against actors"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1881#issuecomment-1301638918", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1881", "id": 1301638918, "node_id": "IC_kwDOBm6k_c5NlW8G", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-03T04:56:06Z", "updated_at": "2022-11-03T04:56:06Z", "author_association": "OWNER", "body": "I've also introduced a new concept of a permission abbreviation, which like the permission name needs to be globally unique.\r\n\r\nThat's a problem for plugins - they might just be able to guarantee that their permission long-form name is unique among other plugins (through sensible naming conventions) but the thing where they declare a initial-letters-only abbreviation is far more risky.\r\n\r\nI think abbreviations are optional - they are provided for core permissions but plugins are advised not to use them.\r\n\r\nAlso Datasette could check that the installed plugins do not provide conflicting permissions on startup and refuse to start if they do.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1434094365, "label": "Tool for simulating permission checks against actors"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1881#issuecomment-1301638156", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1881", "id": 1301638156, "node_id": "IC_kwDOBm6k_c5NlWwM", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-03T04:54:00Z", "updated_at": "2022-11-03T04:54:00Z", "author_association": "OWNER", "body": "If I have the permissions defined like this:\r\n```python\r\nPERMISSIONS = (\r\n Permission(\"view-instance\", \"vi\", False, False, True),\r\n Permission(\"view-database\", \"vd\", True, False, True),\r\n Permission(\"view-database-download\", \"vdd\", True, False, True),\r\n Permission(\"view-table\", \"vt\", True, True, True),\r\n Permission(\"view-query\", \"vq\", True, True, True),\r\n Permission(\"insert-row\", \"ir\", True, True, False),\r\n Permission(\"delete-row\", \"dr\", True, True, False),\r\n Permission(\"drop-table\", \"dt\", True, True, False),\r\n Permission(\"execute-sql\", \"es\", True, False, True),\r\n Permission(\"permissions-debug\", \"pd\", False, False, False),\r\n Permission(\"debug-menu\", \"dm\", False, False, False),\r\n)\r\n```\r\nInstead of just calling them by their undeclared names in places like this:\r\n```python\r\nawait self.ds.permission_allowed(\r\n request.actor, \"execute-sql\", database, default=True\r\n)\r\n```\r\nOn the one hand I can ditch that confusing `default=True` option - whether a permission is on by default becomes a characteristic of that `Permission()` itself, which feels much neater.\r\n\r\nOn the other hand though, plugins that introduce their own permissions - like https://datasette.io/plugins/datasette-edit-schema - will need a way to register those permissions with Datasette core. Probably another plugin hook.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1434094365, "label": "Tool for simulating permission checks against actors"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1881#issuecomment-1301635906", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1881", "id": 1301635906, "node_id": "IC_kwDOBm6k_c5NlWNC", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-03T04:48:09Z", "updated_at": "2022-11-03T04:48:09Z", "author_association": "OWNER", "body": "I built this prototype on the http://127.0.0.1:8001/-/allow-debug page, which is open to anyone to visit.\r\n\r\nBut... I just realized that using this tool can leak information - you can use it to guess the names of invisible databases and tables and run theoretical permission checks against them.\r\n\r\nUsing the tool also pollutes the list of permission checks that show up on the root-anlo `/-/permissions` page.\r\n\r\nSo.... I'm going to restrict the usage of this tool to users with access to `/-/permissions` and put it on that page instead.\r\n\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1434094365, "label": "Tool for simulating permission checks against actors"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1881#issuecomment-1301635340", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1881", "id": 1301635340, "node_id": "IC_kwDOBm6k_c5NlWEM", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-03T04:46:41Z", "updated_at": "2022-11-03T04:46:41Z", "author_association": "OWNER", "body": "Built this prototype:\r\n\r\n![prototype](https://user-images.githubusercontent.com/9599/199649219-f146e43b-bfb5-45e6-9777-956f21a79887.gif)\r\n\r\nIn building it I realized I needed to know which permissions took a table, a database, both or neither. So I had to bake that into the code.\r\n\r\nHere's the prototype so far (which includes a prototype of the logic for the `_r` field on actor, see #1855):\r\n\r\n```diff\r\ndiff --git a/datasette/default_permissions.py b/datasette/default_permissions.py\r\nindex 32b0c758..f68aa38f 100644\r\n--- a/datasette/default_permissions.py\r\n+++ b/datasette/default_permissions.py\r\n@@ -6,8 +6,8 @@ import json\r\n import time\r\n \r\n \r\n-@hookimpl(tryfirst=True)\r\n-def permission_allowed(datasette, actor, action, resource):\r\n+@hookimpl(tryfirst=True, specname=\"permission_allowed\")\r\n+def permission_allowed_default(datasette, actor, action, resource):\r\n async def inner():\r\n if action in (\r\n \"permissions-debug\",\r\n@@ -57,6 +57,44 @@ def permission_allowed(datasette, actor, action, resource):\r\n return inner\r\n \r\n \r\n+@hookimpl(specname=\"permission_allowed\")\r\n+def permission_allowed_actor_restrictions(actor, action, resource):\r\n+ if actor is None:\r\n+ return None\r\n+ _r = actor.get(\"_r\")\r\n+ if not _r:\r\n+ # No restrictions, so we have no opinion\r\n+ return None\r\n+ action_initials = \"\".join([word[0] for word in action.split(\"-\")])\r\n+ # If _r is defined then we use those to further restrict the actor\r\n+ # Crucially, we only use this to say NO (return False) - we never\r\n+ # use it to return YES (True) because that might over-ride other\r\n+ # restrictions placed on this actor\r\n+ all_allowed = _r.get(\"a\")\r\n+ if all_allowed is not None:\r\n+ assert isinstance(all_allowed, list)\r\n+ if action_initials in all_allowed:\r\n+ return None\r\n+ # How about for the current database?\r\n+ if action in (\"view-database\", \"view-database-download\", \"execute-sql\"):\r\n+ database_allowed = _r.get(\"d\", {}).get(resource)\r\n+ if database_allowed is not None:\r\n+ assert isinstance(database_allowed, list)\r\n+ if action_initials in database_allowed:\r\n+ return None\r\n+ # Or the current table? That's any time the resource is (database, table)\r\n+ if not isinstance(resource, str) and len(resource) == 2:\r\n+ database, table = resource\r\n+ table_allowed = _r.get(\"t\", {}).get(database, {}).get(table)\r\n+ # TODO: What should this do for canned queries?\r\n+ if table_allowed is not None:\r\n+ assert isinstance(table_allowed, list)\r\n+ if action_initials in table_allowed:\r\n+ return None\r\n+ # This action is not specifically allowed, so reject it\r\n+ return False\r\n+\r\n+\r\n @hookimpl\r\n def actor_from_request(datasette, request):\r\n prefix = \"dstok_\"\r\ndiff --git a/datasette/templates/allow_debug.html b/datasette/templates/allow_debug.html\r\nindex 0f1b30f0..ae43f0f5 100644\r\n--- a/datasette/templates/allow_debug.html\r\n+++ b/datasette/templates/allow_debug.html\r\n@@ -35,7 +35,7 @@ p.message-warning {\r\n \r\n

    Use this tool to try out different actor and allow combinations. See Defining permissions with \"allow\" blocks for documentation.

    \r\n \r\n-
    \r\n+\r\n
    \r\n

    \r\n \r\n@@ -55,4 +55,82 @@ p.message-warning {\r\n \r\n {% if result == \"False\" %}

    Result: deny

    {% endif %}\r\n \r\n+

    Test permission check

    \r\n+\r\n+

    This tool lets you simulate an actor and a permission check for that actor.

    \r\n+\r\n+\r\n+ \r\n+
    \r\n+

    \r\n+ \r\n+
    \r\n+
    \r\n+

    \r\n+

    \r\n+ \r\n+

    \r\n+

    \r\n+
    \r\n+
    \r\n+ \r\n+
    \r\n+\r\n+\r\n+\r\n+ \r\n+\r\n {% endblock %}\r\ndiff --git a/datasette/views/special.py b/datasette/views/special.py\r\nindex 9922a621..d46fc280 100644\r\n--- a/datasette/views/special.py\r\n+++ b/datasette/views/special.py\r\n@@ -1,6 +1,8 @@\r\n import json\r\n+from datasette.permissions import PERMISSIONS\r\n from datasette.utils.asgi import Response, Forbidden\r\n from datasette.utils import actor_matches_allow, add_cors_headers\r\n+from datasette.permissions import PERMISSIONS\r\n from .base import BaseView\r\n import secrets\r\n import time\r\n@@ -138,9 +140,34 @@ class AllowDebugView(BaseView):\r\n \"error\": \"\\n\\n\".join(errors) if errors else \"\",\r\n \"actor_input\": actor_input,\r\n \"allow_input\": allow_input,\r\n+ \"permissions\": PERMISSIONS,\r\n },\r\n )\r\n \r\n+ async def post(self, request):\r\n+ vars = await request.post_vars()\r\n+ actor = json.loads(vars[\"actor\"])\r\n+ permission = vars[\"permission\"]\r\n+ resource_1 = vars[\"resource_1\"]\r\n+ resource_2 = vars[\"resource_2\"]\r\n+ resource = []\r\n+ if resource_1:\r\n+ resource.append(resource_1)\r\n+ if resource_2:\r\n+ resource.append(resource_2)\r\n+ resource = tuple(resource)\r\n+ result = await self.ds.permission_allowed(\r\n+ actor, permission, resource, default=\"USE_DEFAULT\"\r\n+ )\r\n+ return Response.json(\r\n+ {\r\n+ \"actor\": actor,\r\n+ \"permission\": permission,\r\n+ \"resource\": resource,\r\n+ \"result\": result,\r\n+ }\r\n+ )\r\n+\r\n \r\n class MessagesDebugView(BaseView):\r\n name = \"messages_debug\"\r\n```\r\n\r\n\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1434094365, "label": "Tool for simulating permission checks against actors"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1855#issuecomment-1301594495", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1855", "id": 1301594495, "node_id": "IC_kwDOBm6k_c5NlMF_", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-03T03:11:17Z", "updated_at": "2022-11-03T03:11:17Z", "author_association": "OWNER", "body": "Maybe the way to do this is through a new standard mechanism on the actor: a set of additional restrictions, e.g.:\r\n\r\n```\r\n{\r\n \"id\": \"root\",\r\n \"_r\": {\r\n \"a\": [\"ir\", \"ur\", \"dr\"],\r\n \"d\": {\r\n \"fixtures\": [\"ir\", \"ur\", \"dr\"]\r\n },\r\n \"t\": {\r\n \"fixtures\": {\r\n \"searchable\": [\"ir\"]\r\n }\r\n }\r\n}\r\n```\r\n`\"a\"` is \"all permissions\" - these apply to everything.\r\n`\"d\"` permissions only apply to the specified database\r\n`\"t\"` permissions only apply to the specified table\r\n\r\nThe way this works is there's a default [permission_allowed(datasette, actor, action, resource)](https://docs.datasette.io/en/stable/plugin_hooks.html#id25) hook which only consults these, and crucially just says NO if those rules do not match.\r\n\r\nIn this way it would apply as an extra layer of permission rules over the defaults (which for this `root` instance would all return yes).", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1423336089, "label": "`datasette create-token` ability to create tokens with a reduced set of permissions"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1880#issuecomment-1301043042", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1880", "id": 1301043042, "node_id": "IC_kwDOBm6k_c5NjFdi", "user": {"value": 525934, "label": "amitkoth"}, "created_at": "2022-11-02T18:20:14Z", "updated_at": "2022-11-02T18:20:14Z", "author_association": "NONE", "body": "Follow on question - is all memory use @simonw - for both datasette and SQLlite confined to the \"query time\" itself i.e. the memory use is relevant only to a particular transaction or query - and then subsequently released?", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1433576351, "label": "Datasette with many and large databases > Memory use"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1871#issuecomment-1299607082", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1871", "id": 1299607082, "node_id": "IC_kwDOBm6k_c5Ndm4q", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-02T05:45:31Z", "updated_at": "2022-11-02T05:45:31Z", "author_association": "OWNER", "body": "I'm going to add a link to the Datasette API docs for the current running version of Datasette, e.g. to https://docs.datasette.io/en/0.63/json_api.html", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1427293909, "label": "API explorer tool"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1871#issuecomment-1299600257", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1871", "id": 1299600257, "node_id": "IC_kwDOBm6k_c5NdlOB", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-02T05:36:40Z", "updated_at": "2022-11-02T05:36:40Z", "author_association": "OWNER", "body": "The API Explorer should definitely link to the `/-/create-token` page for users who have permission though.\r\n\r\nAnd it should probably go in the Datasette application menu?", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1427293909, "label": "API explorer tool"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1871#issuecomment-1299599461", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1871", "id": 1299599461, "node_id": "IC_kwDOBm6k_c5NdlBl", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-02T05:35:36Z", "updated_at": "2022-11-02T05:36:15Z", "author_association": "OWNER", "body": "Here's a slightly wild idea: what if there was a button on `/-/api` that you could click to turn on \"API explorer mode\" for the rest of the Datasette interface - which sets a cookie, and that cookie means you then see \"API explorer\" links in all sorts of other relevant places in the Datasette UI (maybe tucked away in cog menus).\r\n\r\nOnly reason I don't want to show these to everyone is that I don't think this is a very user-friendly feature: if you don't know what an API is I don't want to expose you to it unnecessarily.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1427293909, "label": "API explorer tool"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1871#issuecomment-1299598570", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1871", "id": 1299598570, "node_id": "IC_kwDOBm6k_c5Ndkzq", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-02T05:34:28Z", "updated_at": "2022-11-02T05:34:28Z", "author_association": "OWNER", "body": "This is pretty useful now. Two features I still want to add:\r\n\r\n- The ability to link to the API explorer such that the form is pre-filled with material from the URL. Need to guard against clickjacking first though, so no-one can link to it in an invisible iframe and trick the user into hitting POST.\r\n- Some kind of list of endpoints so people can click links to start using the API explorer. A list of every table the user can write to with each of their `/db/table/-/insert` endpoints for example.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1427293909, "label": "API explorer tool"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1871#issuecomment-1299597066", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1871", "id": 1299597066, "node_id": "IC_kwDOBm6k_c5NdkcK", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-02T05:32:22Z", "updated_at": "2022-11-02T05:32:22Z", "author_association": "OWNER", "body": "Demo of the latest API explorer:\r\n\r\n![explorer](https://user-images.githubusercontent.com/9599/199406184-1292df42-25ea-4daf-8b54-ca26170ec1ea.gif)\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1427293909, "label": "API explorer tool"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1871#issuecomment-1299388341", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1871", "id": 1299388341, "node_id": "IC_kwDOBm6k_c5Ncxe1", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-02T00:24:28Z", "updated_at": "2022-11-02T00:25:00Z", "author_association": "OWNER", "body": "I want JSON syntax highlighting.\r\n\r\nhttps://github.com/luyilin/json-format-highlight is an MIT licensed tiny highlighter that looks decent for this.\r\n\r\nhttps://unpkg.com/json-format-highlight@1.0.1/dist/json-format-highlight.js", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1427293909, "label": "API explorer tool"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1871#issuecomment-1299349741", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1871", "id": 1299349741, "node_id": "IC_kwDOBm6k_c5NcoDt", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-01T23:22:55Z", "updated_at": "2022-11-01T23:22:55Z", "author_association": "OWNER", "body": "It's weird that the API explorer only lets you explore POST APIs. It should probably also let you explore GET APIs, or be renamed.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1427293909, "label": "API explorer tool"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1879#issuecomment-1299098458", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1879", "id": 1299098458, "node_id": "IC_kwDOBm6k_c5Nbqta", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-01T20:27:40Z", "updated_at": "2022-11-01T20:33:52Z", "author_association": "OWNER", "body": "https://github.com/simonw/datasette-x-forwarded-host/blob/main/datasette_x_forwarded_host/__init__.py could happen in core controlled by:\r\n\r\n`--setting trust_forwarded_host 1`", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1432037325, "label": "Make it easier to fix URL proxy problems"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1879#issuecomment-1299102108", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1879", "id": 1299102108, "node_id": "IC_kwDOBm6k_c5Nbrmc", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-01T20:30:54Z", "updated_at": "2022-11-01T20:33:06Z", "author_association": "OWNER", "body": "One idea: add a `/-/debug` page (or `/-/tips` or `/-/checks`) which shows the incoming requests headers and could even detect if there's an `x-forwarded-host` header that isn't being repeated and show a tip on how to fix that.", "reactions": "{\"total_count\": 1, \"+1\": 1, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1432037325, "label": "Make it easier to fix URL proxy problems"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1879#issuecomment-1299102755", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1879", "id": 1299102755, "node_id": "IC_kwDOBm6k_c5Nbrwj", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-01T20:31:37Z", "updated_at": "2022-11-01T20:31:37Z", "author_association": "OWNER", "body": "And some JavaScript that can spot if Datasette thinks it is being served over HTTP when it's actually being served over HTTPS.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1432037325, "label": "Make it easier to fix URL proxy problems"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1879#issuecomment-1299096850", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1879", "id": 1299096850, "node_id": "IC_kwDOBm6k_c5NbqUS", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-01T20:26:12Z", "updated_at": "2022-11-01T20:26:12Z", "author_association": "OWNER", "body": "The other relevant plugin here is https://datasette.io/plugins/datasette-x-forwarded-host\r\n\r\nMaybe that should be rolled into core too?", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1432037325, "label": "Make it easier to fix URL proxy problems"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1879#issuecomment-1299090678", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1879", "id": 1299090678, "node_id": "IC_kwDOBm6k_c5Nboz2", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-01T20:20:28Z", "updated_at": "2022-11-01T20:20:28Z", "author_association": "OWNER", "body": "My first step in debugging these is to install https://datasette.io/plugins/datasette-debug-asgi - but now I'm thinking maybe something like that should be part of core.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1432037325, "label": "Make it easier to fix URL proxy problems"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1862#issuecomment-1299073433", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1862", "id": 1299073433, "node_id": "IC_kwDOBm6k_c5NbkmZ", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-01T20:04:31Z", "updated_at": "2022-11-01T20:04:31Z", "author_association": "OWNER", "body": "It really feels like this should be accompanied by a `/db/-/create` API for creating tables. I had to add that to `sqlite-utils` eventually (initially it only supported creating by passing in an example document):\r\n\r\nhttps://sqlite-utils.datasette.io/en/stable/cli.html#cli-create-table", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1425011030, "label": "Create a new table from one or more records, `sqlite-utils` style"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1878#issuecomment-1299071456", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1878", "id": 1299071456, "node_id": "IC_kwDOBm6k_c5NbkHg", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-01T20:02:43Z", "updated_at": "2022-11-01T20:02:43Z", "author_association": "OWNER", "body": "Note that \"update\" is partially covered by the `replace` option to `/-/insert`, added here:\r\n- https://github.com/simonw/datasette/issues/1873#issuecomment-1298885451\r\n\r\n\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1432013704, "label": "/db/table/-/upsert API"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1873#issuecomment-1298919552", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1873", "id": 1298919552, "node_id": "IC_kwDOBm6k_c5Na_CA", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-01T18:11:27Z", "updated_at": "2022-11-01T18:11:27Z", "author_association": "OWNER", "body": "I forgot to document `ignore` and `replace`. Also I need to add tests that cover:\r\n\r\n- Forgetting to include a primary key on a non-autoincrement table\r\n- Compound primary keys\r\n- Rowid only tables with and without rowid specified\r\n\r\nI think my validation logic here will get caught out by the fact that `rowid` does not show up as a valid column name: https://github.com/simonw/datasette/blob/9bec7c38eb93cde5afb16df9bdd96aea2a5b0459/datasette/views/table.py#L1151-L1160\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1428630253, "label": "Ensure insert API has good tests for rowid and compound primark key tables"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1873#issuecomment-1298905135", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1873", "id": 1298905135, "node_id": "IC_kwDOBm6k_c5Na7gv", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-01T17:59:59Z", "updated_at": "2022-11-01T17:59:59Z", "author_association": "OWNER", "body": "It's a bit surprising that you can send `\"ignore\": true, \"return_rows\": true` and the returned `\"inserted\"` key will list rows that were NOT inserted (since they were ignored).\r\n\r\nThree options:\r\n\r\n1. Ignore that and document it\r\n2. Fix it so `\"inserted\"` only returns rows that were actually inserted (bit tricky)\r\n3. Change the name of `\"inserted\"` to something else\r\n\r\nI'm picking 3 - I'm going to change it to be called `\"rows\"` instead.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1428630253, "label": "Ensure insert API has good tests for rowid and compound primark key tables"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1873#issuecomment-1298885451", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1873", "id": 1298885451, "node_id": "IC_kwDOBm6k_c5Na2tL", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-01T17:42:20Z", "updated_at": "2022-11-01T17:42:20Z", "author_association": "OWNER", "body": "Design decision:\r\n```json\r\n{\r\n \"rows\": [{\"id\": 1, \"title\": \"The title\"}],\r\n \"ignore\": true\r\n}\r\n```\r\nOr `\"replace\": true`.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1428630253, "label": "Ensure insert API has good tests for rowid and compound primark key tables"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/506#issuecomment-1298879701", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/506", "id": 1298879701, "node_id": "IC_kwDOCGYnMM5Na1TV", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-01T17:37:13Z", "updated_at": "2022-11-01T17:37:13Z", "author_association": "OWNER", "body": "The question I was originally trying to answer here was this: how many rows were actually inserted by that call to `.insert_all()`?\r\n\r\nI don't know that `.rowcount` would ever be useful here, since the \"correct\" answer depends on other factors - had I determined to ignore or replace records with a primary key that matches an existing record for example?\r\n\r\nSo I think if people need `rowcount` they can get it by using a `cursor` directly.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1429029604, "label": "Make `cursor.rowcount` accessible (wontfix)"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/506#issuecomment-1298877872", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/506", "id": 1298877872, "node_id": "IC_kwDOCGYnMM5Na02w", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-01T17:35:30Z", "updated_at": "2022-11-01T17:35:30Z", "author_association": "OWNER", "body": "This may not make sense.\r\n\r\nFirst, `.last_rowid` is a property on table - but that doesn't make sense for `rowcount` since it should clearly be a property on the database itself (you can run a query directly using `db.execute()` without going through a `Table` object).\r\n\r\nSo I tried this prototype:\r\n\r\n```diff\r\ndiff --git a/docs/python-api.rst b/docs/python-api.rst\r\nindex 206e5e6..78d3a8d 100644\r\n--- a/docs/python-api.rst\r\n+++ b/docs/python-api.rst\r\n@@ -186,6 +186,15 @@ The ``db.query(sql)`` function executes a SQL query and returns an iterator over\r\n # {'name': 'Cleo'}\r\n # {'name': 'Pancakes'}\r\n \r\n+After executing a query the ``db.rowcount`` property on that database instance will reflect the number of rows affected by any insert, update or delete operations performed by that query:\r\n+\r\n+.. code-block:: python\r\n+\r\n+ db = Database(memory=True)\r\n+ db[\"dogs\"].insert_all([{\"name\": \"Cleo\"}, {\"name\": \"Pancakes\"}])\r\n+ print(db.rowcount)\r\n+ # Outputs: 2\r\n+\r\n .. _python_api_execute:\r\n \r\n db.execute(sql, params)\r\ndiff --git a/sqlite_utils/db.py b/sqlite_utils/db.py\r\nindex a06f4b7..c19c2dd 100644\r\n--- a/sqlite_utils/db.py\r\n+++ b/sqlite_utils/db.py\r\n@@ -294,6 +294,8 @@ class Database:\r\n \r\n _counts_table_name = \"_counts\"\r\n use_counts_table = False\r\n+ # Number of rows inserted, updated or deleted\r\n+ rowcount: Optional[int] = None\r\n \r\n def __init__(\r\n self,\r\n@@ -480,9 +482,11 @@ class Database:\r\n if self._tracer:\r\n self._tracer(sql, parameters)\r\n if parameters is not None:\r\n- return self.conn.execute(sql, parameters)\r\n+ cursor = self.conn.execute(sql, parameters)\r\n else:\r\n- return self.conn.execute(sql)\r\n+ cursor = self.conn.execute(sql)\r\n+ self.rowcount = cursor.rowcount\r\n+ return cursor\r\n \r\n def executescript(self, sql: str) -> sqlite3.Cursor:\r\n \"\"\"\r\n```\r\nBut this happens:\r\n```pycon\r\n>>> from sqlite_utils import Database\r\n>>> db = Database(memory=True)\r\n>>> db[\"dogs\"].insert_all([{\"name\": \"Cleo\"}, {\"name\": \"Pancakes\"}])\r\n
    \r\n>>> db.rowcount\r\n-1\r\n```\r\nTurning on query tracing demonstrates why:\r\n```pycon\r\n>>> db = Database(memory=True, tracer=print)\r\nPRAGMA recursive_triggers=on; None\r\n>>> db[\"dogs\"].insert_all([{\"name\": \"Cleo\"}, {\"name\": \"Pancakes\"}])\r\nselect name from sqlite_master where type = 'view' None\r\nselect name from sqlite_master where type = 'table' None\r\nselect name from sqlite_master where type = 'view' None\r\nCREATE TABLE [dogs] (\r\n [name] TEXT\r\n);\r\n None\r\nselect name from sqlite_master where type = 'view' None\r\nINSERT INTO [dogs] ([name]) VALUES (?), (?); ['Cleo', 'Pancakes']\r\nselect name from sqlite_master where type = 'table' None\r\nselect name from sqlite_master where type = 'table' None\r\nPRAGMA table_info([dogs]) None\r\n
    \r\n>>>\r\n```\r\nThe `.insert_all()` function does a bunch of other queries too, so `.rowcount` is quickly over-ridden by the same result from extra queries that it executed.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1429029604, "label": "Make `cursor.rowcount` accessible (wontfix)"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1876#issuecomment-1298856054", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1876", "id": 1298856054, "node_id": "IC_kwDOBm6k_c5Navh2", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-01T17:16:01Z", "updated_at": "2022-11-01T17:16:01Z", "author_association": "OWNER", "body": "`ta.style.height = ta.scrollHeight + 'px'` is an easy way to do that.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1431786951, "label": "SQL query should wrap on SQL interrupted screen"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1876#issuecomment-1298854321", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1876", "id": 1298854321, "node_id": "IC_kwDOBm6k_c5NavGx", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-11-01T17:14:33Z", "updated_at": "2022-11-01T17:14:33Z", "author_association": "OWNER", "body": "I could use a `textarea` here (would need to figure out a neat pattern to expand it to fit the query):\r\n\r\n\"image\"\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1431786951, "label": "SQL query should wrap on SQL interrupted screen"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/507#issuecomment-1297859539", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/507", "id": 1297859539, "node_id": "IC_kwDOCGYnMM5NW8PT", "user": {"value": 7908073, "label": "chapmanjacobd"}, "created_at": "2022-11-01T00:40:16Z", "updated_at": "2022-11-01T00:40:16Z", "author_association": "CONTRIBUTOR", "body": "Ideally people could fix their data if they run into this issue.\r\n\r\nIf you are using filenames try [convmv](https://linux.die.net/man/1/convmv)\r\n\r\n```\r\nconvmv --preserve-mtimes -f utf8 -t utf8 --notest -i -r .\r\n```\r\n\r\nmaybe this script will also help: \r\n\r\n```py\r\nimport argparse, shutil\r\nfrom pathlib import Path\r\n\r\nimport ftfy\r\n\r\nfrom xklb import utils\r\nfrom xklb.utils import log\r\n\r\n\r\ndef parse_args() -> argparse.Namespace:\r\n parser = argparse.ArgumentParser()\r\n parser.add_argument(\"paths\", nargs='*')\r\n parser.add_argument(\"--verbose\", \"-v\", action=\"count\", default=0)\r\n args = parser.parse_args()\r\n\r\n log.info(utils.dict_filter_bool(args.__dict__))\r\n return args\r\n\r\n\r\ndef rename_invalid_paths() -> None:\r\n args = parse_args()\r\n\r\n for path in args.paths:\r\n log.info(path)\r\n for p in sorted([str(p) for p in Path(path).rglob(\"*\")], key=len):\r\n fixed = ftfy.fix_text(p, uncurl_quotes=False).replace(\"\\r\\n\", \"\\n\").replace(\"\\r\", \"\\n\").replace(\"\\n\", \"\")\r\n if p != fixed:\r\n try:\r\n shutil.move(p, fixed)\r\n except FileNotFoundError:\r\n log.warning(\"FileNotFound. %s\", p)\r\n else:\r\n log.info(fixed)\r\n\r\n\r\nif __name__ == \"__main__\":\r\n rename_invalid_paths()\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1430325103, "label": "conn.execute: UnicodeEncodeError: 'utf-8' codec can't encode character"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/pull/508#issuecomment-1297754631", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/508", "id": 1297754631, "node_id": "IC_kwDOCGYnMM5NWioH", "user": {"value": 22429695, "label": "codecov[bot]"}, "created_at": "2022-10-31T22:14:48Z", "updated_at": "2022-10-31T22:53:59Z", "author_association": "NONE", "body": "# [Codecov](https://codecov.io/gh/simonw/sqlite-utils/pull/508?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report\nBase: **96.25**% // Head: **96.09**% // Decreases project coverage by **`-0.15%`** :warning:\n> Coverage data is based on head [(`2d6a149`)](https://codecov.io/gh/simonw/sqlite-utils/pull/508?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) compared to base [(`529110e`)](https://codecov.io/gh/simonw/sqlite-utils/commit/529110e7d8c4a6b1bbf5fb61f2e29d72aa95a611?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison).\n> Patch coverage: 63.63% of modified lines in pull request are covered.\n\n> :exclamation: Current head 2d6a149 differs from pull request most recent head 43a8c4c. Consider uploading reports for the commit 43a8c4c to get more accurate results\n\n
    Additional details and impacted files\n\n\n```diff\n@@ Coverage Diff @@\n## main #508 +/- ##\n==========================================\n- Coverage 96.25% 96.09% -0.16% \n==========================================\n Files 4 4 \n Lines 2401 2407 +6 \n==========================================\n+ Hits 2311 2313 +2 \n- Misses 90 94 +4 \n```\n\n\n| [Impacted Files](https://codecov.io/gh/simonw/sqlite-utils/pull/508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) | Coverage \u0394 | |\n|---|---|---|\n| [sqlite\\_utils/db.py](https://codecov.io/gh/simonw/sqlite-utils/pull/508/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-c3FsaXRlX3V0aWxzL2RiLnB5) | `96.79% <63.63%> (-0.30%)` | :arrow_down: |\n\nHelp us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison)\n\n
    \n\n[:umbrella: View full report at Codecov](https://codecov.io/gh/simonw/sqlite-utils/pull/508?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). \n:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison).\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1430563092, "label": "Allow surrogates in parameters"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/448#issuecomment-1297703307", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/448", "id": 1297703307, "node_id": "IC_kwDOCGYnMM5NWWGL", "user": {"value": 167893, "label": "mcarpenter"}, "created_at": "2022-10-31T21:23:51Z", "updated_at": "2022-10-31T21:27:32Z", "author_association": "CONTRIBUTOR", "body": "The Windows aspect is a red herring: OP's sample above produces the same error on Linux. (Though I don't know what's going on with the CI).\r\n\r\nThe same error can also be obtained by passing an `io` from a file opened in non-binary mode (`'r'` as opposed to `'rb'`) to `rows_from_file()`. This is how I got here.\r\n\r\nThe fix for my case is easy: open the file in mode `'rb'`. The analagous fix for OP's problem also works: use `BytesIO` in place of `StringIO`.\r\n\r\nMinimal test case (derived from [utils.py](https://github.com/simonw/sqlite-utils/blob/main/sqlite_utils/utils.py#L304)):\r\n\r\n``` python\r\nimport io\r\nfrom typing import cast\r\n\r\n#fp = io.StringIO(\"id,name\\n1,Cleo\") # error\r\nfp = io.BytesIO(bytes(\"id,name\\n1,Cleo\", encoding='utf-8')) # okay\r\nreader = io.BufferedReader(cast(io.RawIOBase, fp))\r\nreader.peek(1) # exception thrown here\r\n```\r\nI see the signature of `rows_from_file()` correctly has `fp: BinaryIO` but I guess you'd need either a runtime type check for that (not all `io`s have `mode()`), or to catch the `AttributeError` on `peek()` to produce a better error for users. Neither option is ideal.\r\n\r\nSome thoughts on testing binary-ness of `io`s in this SO question: https://stackoverflow.com/questions/44584829/how-to-determine-if-file-is-opened-in-binary-or-text-mode", "reactions": "{\"total_count\": 2, \"+1\": 2, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1279144769, "label": "Reading rows from a file => AttributeError: '_io.StringIO' object has no attribute 'readinto'"}, "performed_via_github_app": null} {"html_url": "https://github.com/dogsheep/twitter-to-sqlite/issues/61#issuecomment-1297201971", "issue_url": "https://api.github.com/repos/dogsheep/twitter-to-sqlite/issues/61", "id": 1297201971, "node_id": "IC_kwDODEm0Qs5NUbsz", "user": {"value": 3153638, "label": "Profpatsch"}, "created_at": "2022-10-31T14:47:58Z", "updated_at": "2022-10-31T14:47:58Z", "author_association": "NONE", "body": "There\u2019s also a limit of 3200 tweets. I wonder if that can be circumvented somehow.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1077560091, "label": "Data Pull fails for \"Essential\" level access to the Twitter API (for Documentation)"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1864#issuecomment-1296403316", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1864", "id": 1296403316, "node_id": "IC_kwDOBm6k_c5NRYt0", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-31T00:39:43Z", "updated_at": "2022-10-31T00:39:43Z", "author_association": "OWNER", "body": "It looks like SQLite has features for this already: https://www.sqlite.org/foreignkeys.html#fk_actions\r\n\r\n> Foreign key ON DELETE and ON UPDATE clauses are used to configure actions that take place when deleting rows from the parent table (ON DELETE), or modifying the parent key values of existing rows (ON UPDATE). A single foreign key constraint may have different actions configured for ON DELETE and ON UPDATE. Foreign key actions are similar to triggers in many ways. \r\n\r\nOn that basis, I'm not going to implement anything additional in the `.../-/delete` endpoint relating to foreign keys. Developers who want special treatment of them can do that with a combination of a plugin (maybe I'll build a `datasette-enable-foreign-keys` plugin) and tables created using those `ON DELETE` clauses.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1425029275, "label": "Delete a single record from an existing table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1864#issuecomment-1296402071", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1864", "id": 1296402071, "node_id": "IC_kwDOBm6k_c5NRYaX", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-31T00:37:09Z", "updated_at": "2022-10-31T00:37:09Z", "author_association": "OWNER", "body": "I need to think about what happens if you delete a row that is the target of a foreign key from another row.\r\n\r\nhttps://www.sqlite.org/foreignkeys.html#fk_enable shows that SQLite will only actively enforce these relationships (e.g. throw an error if you try to delete a row that is referenced by another row) if you first run `PRAGMA foreign_keys = ON;` against the connection.\r\n\r\n> Foreign key constraints are disabled by default (for backwards compatibility), so must be enabled separately for each [database connection](https://www.sqlite.org/c3ref/sqlite3.html). (Note, however, that future releases of SQLite might change so that foreign key constraints enabled by default. Careful developers will not make any assumptions about whether or not foreign keys are enabled by default but will instead enable or disable them as necessary.)\r\n\r\nI don't actually believe that the SQLite maintainers will ever make that the default though.\r\n\r\nDatasette doesn't turn these on at the moment, but it could be turned on by a `prepare_connection()` plugin.\r\n\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1425029275, "label": "Delete a single record from an existing table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1864#issuecomment-1296375536", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1864", "id": 1296375536, "node_id": "IC_kwDOBm6k_c5NRR7w", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-30T23:17:11Z", "updated_at": "2022-10-30T23:17:11Z", "author_association": "OWNER", "body": "I'm a bit nervous about calling `.delete()` with the `pk_values` - can I be sure they are in the correct order? https://github.com/simonw/datasette/blob/00632ded30e7cf9f0cf9478680645d1dabe269ae/datasette/views/row.py#L188-L190", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1425029275, "label": "Delete a single record from an existing table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1864#issuecomment-1296375310", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1864", "id": 1296375310, "node_id": "IC_kwDOBm6k_c5NRR4O", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-30T23:16:19Z", "updated_at": "2022-10-30T23:16:19Z", "author_association": "OWNER", "body": "Still needs tests that cover compound primary keys and rowid tables.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1425029275, "label": "Delete a single record from an existing table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1874#issuecomment-1296363981", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1874", "id": 1296363981, "node_id": "IC_kwDOBm6k_c5NRPHN", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-30T22:19:47Z", "updated_at": "2022-10-30T22:19:47Z", "author_association": "OWNER", "body": "Documentation: https://docs.datasette.io/en/1.0-dev/json_api.html#dropping-tables", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1429030341, "label": "API to drop a table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/506#issuecomment-1296358636", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/506", "id": 1296358636, "node_id": "IC_kwDOCGYnMM5NRNzs", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-30T21:52:11Z", "updated_at": "2022-10-30T21:52:11Z", "author_association": "OWNER", "body": "This could work in a similar way to `db.insert(...).last_rowid`.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1429029604, "label": "Make `cursor.rowcount` accessible (wontfix)"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1873#issuecomment-1296343716", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1873", "id": 1296343716, "node_id": "IC_kwDOBm6k_c5NRKKk", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-30T20:24:55Z", "updated_at": "2022-10-30T20:24:55Z", "author_association": "OWNER", "body": "I think the key feature I need here is going to be the equivalent of `ignore=True` and `replace=True` for dealing with primary key collisions, see https://sqlite-utils.datasette.io/en/stable/reference.html#sqlite_utils.db.Table.insert", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1428630253, "label": "Ensure insert API has good tests for rowid and compound primark key tables"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1873#issuecomment-1296343317", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1873", "id": 1296343317, "node_id": "IC_kwDOBm6k_c5NRKEV", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-30T20:22:40Z", "updated_at": "2022-10-30T20:22:40Z", "author_association": "OWNER", "body": "So maybe they're not actually worth worrying about separately, because they are guaranteed to have a primary key set.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1428630253, "label": "Ensure insert API has good tests for rowid and compound primark key tables"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1873#issuecomment-1296343173", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1873", "id": 1296343173, "node_id": "IC_kwDOBm6k_c5NRKCF", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-30T20:21:54Z", "updated_at": "2022-10-30T20:22:20Z", "author_association": "OWNER", "body": "One last case to consider: `WITHOUT ROWID` tables.\r\n\r\nhttps://www.sqlite.org/withoutrowid.html\r\n\r\n> By default, every row in SQLite has a special column, usually called the \"[rowid](https://www.sqlite.org/lang_createtable.html#rowid)\", that uniquely identifies that row within the table. However if the phrase \"WITHOUT ROWID\" is added to the end of a [CREATE TABLE](https://www.sqlite.org/lang_createtable.html) statement, then the special \"rowid\" column is omitted. There are sometimes space and performance advantages to omitting the rowid.\r\n>\r\n> ...\r\n>\r\n> Every WITHOUT ROWID table must have a [PRIMARY KEY](https://www.sqlite.org/lang_createtable.html#primkeyconst). An error is raised if a CREATE TABLE statement with the WITHOUT ROWID clause lacks a PRIMARY KEY.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1428630253, "label": "Ensure insert API has good tests for rowid and compound primark key tables"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1873#issuecomment-1296343014", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1873", "id": 1296343014, "node_id": "IC_kwDOBm6k_c5NRJ_m", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-30T20:21:01Z", "updated_at": "2022-10-30T20:21:01Z", "author_association": "OWNER", "body": "Actually, for simplicity I'm going to say that you can always set the primary key, even for auto-incrementing primary key columns... but you cannot set it on pure `rowid` columns.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1428630253, "label": "Ensure insert API has good tests for rowid and compound primark key tables"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1873#issuecomment-1296342814", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1873", "id": 1296342814, "node_id": "IC_kwDOBm6k_c5NRJ8e", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-30T20:20:05Z", "updated_at": "2022-10-30T20:20:05Z", "author_association": "OWNER", "body": "Some notes on what Datasette does already\r\n\r\nhttps://latest.datasette.io/fixtures/tags.json?_shape=array returns:\r\n```json\r\n[\r\n {\r\n \"tag\": \"canine\"\r\n },\r\n {\r\n \"tag\": \"feline\"\r\n }\r\n]\r\n```\r\nThat table is defined [like this](https://latest.datasette.io/fixtures/tags):\r\n```sql\r\nCREATE TABLE tags (\r\n tag TEXT PRIMARY KEY\r\n);\r\n```\r\nHere's a `rowid` table with no explicit primary key: https://latest.datasette.io/fixtures/binary_data\r\n\r\nhttps://latest.datasette.io/fixtures/binary_data.json?_shape=array\r\n```json\r\n[\r\n {\r\n \"rowid\": 1,\r\n \"data\": {\r\n \"$base64\": true,\r\n \"encoded\": \"FRwCx60F/g==\"\r\n }\r\n },\r\n {\r\n \"rowid\": 2,\r\n \"data\": {\r\n \"$base64\": true,\r\n \"encoded\": \"FRwDx60F/g==\"\r\n }\r\n },\r\n {\r\n \"rowid\": 3,\r\n \"data\": null\r\n }\r\n]\r\n```\r\n```sql\r\nCREATE TABLE binary_data (\r\n data BLOB\r\n);\r\n```\r\nhttps://latest.datasette.io/fixtures/simple_primary_key has a text primary key:\r\n\r\nhttps://latest.datasette.io/fixtures/simple_primary_key.json?_shape=array\r\n```json\r\n[\r\n {\r\n \"id\": \"1\",\r\n \"content\": \"hello\"\r\n },\r\n {\r\n \"id\": \"2\",\r\n \"content\": \"world\"\r\n },\r\n {\r\n \"id\": \"3\",\r\n \"content\": \"\"\r\n },\r\n {\r\n \"id\": \"4\",\r\n \"content\": \"RENDER_CELL_DEMO\"\r\n },\r\n {\r\n \"id\": \"5\",\r\n \"content\": \"RENDER_CELL_ASYNC\"\r\n }\r\n]\r\n```\r\n```sql\r\nCREATE TABLE simple_primary_key (\r\n id varchar(30) primary key,\r\n content text\r\n);\r\n```\r\nhttps://latest.datasette.io/fixtures/compound_primary_key is a compound primary key.\r\n\r\nhttps://latest.datasette.io/fixtures/compound_primary_key.json?_shape=array\r\n```json\r\n[\r\n {\r\n \"pk1\": \"a\",\r\n \"pk2\": \"b\",\r\n \"content\": \"c\"\r\n },\r\n {\r\n \"pk1\": \"a/b\",\r\n \"pk2\": \".c-d\",\r\n \"content\": \"c\"\r\n }\r\n]\r\n```\r\n```sql\r\nCREATE TABLE compound_primary_key (\r\n pk1 varchar(30),\r\n pk2 varchar(30),\r\n content text,\r\n PRIMARY KEY (pk1, pk2)\r\n);\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1428630253, "label": "Ensure insert API has good tests for rowid and compound primark key tables"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1873#issuecomment-1296341469", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1873", "id": 1296341469, "node_id": "IC_kwDOBm6k_c5NRJnd", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-30T20:13:50Z", "updated_at": "2022-10-30T20:13:50Z", "author_association": "OWNER", "body": "I checked and SQLite itself does allow you to set the `rowid` on that kind of table - it then increments from whatever you inserted:\r\n```\r\n% sqlite3 /tmp/t.db\r\nSQLite version 3.39.4 2022-09-07 20:51:41\r\nEnter \".help\" for usage hints.\r\nsqlite> create table docs (title text);\r\nsqlite> insert into docs (title) values ('one');\r\nsqlite> select rowid, title from docs;\r\n1|one\r\nsqlite> insert into docs (rowid, title) values (3, 'three');\r\nsqlite> select rowid, title from docs;\r\n1|one\r\n3|three\r\nsqlite> insert into docs (title) values ('another');\r\nsqlite> select rowid, title from docs;\r\n1|one\r\n3|three\r\n4|another\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1428630253, "label": "Ensure insert API has good tests for rowid and compound primark key tables"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1873#issuecomment-1296341055", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1873", "id": 1296341055, "node_id": "IC_kwDOBm6k_c5NRJg_", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-30T20:11:47Z", "updated_at": "2022-10-30T20:12:30Z", "author_association": "OWNER", "body": "If a table has an auto-incrementing primary key, should you be allowed to insert records with an explicit key into it?\r\n\r\nI'm torn on this one. It's something you can do with direct database access, but it's something I very rarely want to do.\r\n\r\nI'm inclined to disallow it and say that if you want that you can get it using a writable canned query instead.\r\n\r\nLikewise, I'm not going to provide a way to set the `rowid` explicitly on a freshly inserted row.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1428630253, "label": "Ensure insert API has good tests for rowid and compound primark key tables"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1871#issuecomment-1296339386", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1871", "id": 1296339386, "node_id": "IC_kwDOBm6k_c5NRJG6", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-30T20:03:04Z", "updated_at": "2022-10-30T20:03:04Z", "author_association": "OWNER", "body": "I do need to skip CSRF for these API calls. I'm going to start out by doing that using the `skip_csrf()` hook to skip CSRF checks on anything with a `content-type: application/json` request header.\r\n\r\n```python\r\n@hookimpl\r\ndef skip_csrf(scope):\r\n if scope[\"type\"] == \"http\":\r\n headers = scope.get(\"headers\")\r\n if dict(headers).get(b'content-type') == b'application/json':\r\n return True\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1427293909, "label": "API explorer tool"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1871#issuecomment-1296339205", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1871", "id": 1296339205, "node_id": "IC_kwDOBm6k_c5NRJEF", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-30T20:02:05Z", "updated_at": "2022-10-30T20:02:05Z", "author_association": "OWNER", "body": "Realized the API explorer doesn't need the API key piece at all - it can work with standard cookie-based auth.\r\n\r\nThis also reflects how most plugins are likely to use this API, where they'll be adding JavaScript that uses `fetch()` to call the write API directly.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1427293909, "label": "API explorer tool"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1871#issuecomment-1296131872", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1871", "id": 1296131872, "node_id": "IC_kwDOBm6k_c5NQWcg", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-30T06:27:56Z", "updated_at": "2022-10-30T06:27:56Z", "author_association": "OWNER", "body": "Initial prototype API explorer is now live at https://latest-1-0-dev.datasette.io/-/api", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1427293909, "label": "API explorer tool"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1873#issuecomment-1296131681", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1873", "id": 1296131681, "node_id": "IC_kwDOBm6k_c5NQWZh", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-30T06:27:12Z", "updated_at": "2022-10-30T06:27:12Z", "author_association": "OWNER", "body": "Relevant TODO: https://github.com/simonw/datasette/blob/c35859ae3df163406f1a1895ccf9803e933b2d8e/datasette/views/table.py#L1131-L1135", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1428630253, "label": "Ensure insert API has good tests for rowid and compound primark key tables"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1872#issuecomment-1296131343", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1872", "id": 1296131343, "node_id": "IC_kwDOBm6k_c5NQWUP", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-30T06:26:01Z", "updated_at": "2022-10-30T06:26:01Z", "author_association": "OWNER", "body": "Good spot fixing that!\r\n\r\nSorry about this - it was a change in Datasette 0.63 which should have been better called out.\r\n\r\nMy goal for Datasette 1.0 (which I aim to have out by the end of the year) is to introduce a formal process for avoiding problems like this, with very clear documentation when something like this might happen.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1428560020, "label": "SITE-BUSTING ERROR: \"render_template() called before await ds.invoke_startup()\""}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1871#issuecomment-1296130073", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1871", "id": 1296130073, "node_id": "IC_kwDOBm6k_c5NQWAZ", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-30T06:20:56Z", "updated_at": "2022-10-30T06:20:56Z", "author_association": "OWNER", "body": "That initial prototype looks like this:\r\n\r\n\"image\"\r\n\r\nIt currently shows the returned JSON from the API in an `alert()`. Next I should make that part of the page instead.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1427293909, "label": "API explorer tool"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1871#issuecomment-1296126389", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1871", "id": 1296126389, "node_id": "IC_kwDOBm6k_c5NQVG1", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-30T06:04:48Z", "updated_at": "2022-10-30T06:04:48Z", "author_association": "OWNER", "body": "This is even more important now I have pushed:\r\n- #1866", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1427293909, "label": "API explorer tool"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1871#issuecomment-1296114136", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1871", "id": 1296114136, "node_id": "IC_kwDOBm6k_c5NQSHY", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-30T05:15:40Z", "updated_at": "2022-10-30T05:15:40Z", "author_association": "OWNER", "body": "Host it at `/-/api`\r\n\r\nIt's an input box with a path in and a textarea you can put JSON in, plus a submit button to post the request.\r\n\r\nIt lists the API endpoints you can use - click on a link to populate the form field plus a example.\r\n\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1427293909, "label": "API explorer tool"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1872#issuecomment-1296080804", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1872", "id": 1296080804, "node_id": "IC_kwDOBm6k_c5NQJ-k", "user": {"value": 192568, "label": "mroswell"}, "created_at": "2022-10-30T03:06:32Z", "updated_at": "2022-10-30T03:06:32Z", "author_association": "CONTRIBUTOR", "body": "I updated datasette-publish-vercel to 0.14.2 in requirements.txt\r\n\r\nAnd the site is back up!\r\n\r\nIs there a way that we can get some sort of notice when something like this will have critical impact on website function?", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1428560020, "label": "SITE-BUSTING ERROR: \"render_template() called before await ds.invoke_startup()\""}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1872#issuecomment-1296076803", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1872", "id": 1296076803, "node_id": "IC_kwDOBm6k_c5NQJAD", "user": {"value": 192568, "label": "mroswell"}, "created_at": "2022-10-30T02:50:34Z", "updated_at": "2022-10-30T02:50:34Z", "author_association": "CONTRIBUTOR", "body": "should this issue be under https://github.com/simonw/datasette-publish-vercel/issues ?\r\n\r\nPerhaps I just need to update: \r\ndatasette-publish-vercel==0.11\r\nin requirements.txt?\r\n \r\n I'll try that and see what happens...\r\n ", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1428560020, "label": "SITE-BUSTING ERROR: \"render_template() called before await ds.invoke_startup()\""}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/1870#issuecomment-1295667649", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1870", "id": 1295667649, "node_id": "IC_kwDOBm6k_c5NOlHB", "user": {"value": 536941, "label": "fgregg"}, "created_at": "2022-10-29T00:52:43Z", "updated_at": "2022-10-29T00:53:43Z", "author_association": "CONTRIBUTOR", "body": "> Are you saying that I can build a container, but then when I run it and it does `datasette serve -i data.db ...` it will somehow modify the image, or create a new modified filesystem layer in the runtime environment, as a result of running that `serve` command?\r\n\r\nSomehow, `datasette serve -i data.db` will lead to the `data.db` being modified, which will trigger a [copy-on-write](https://docs.docker.com/storage/storagedriver/#the-copy-on-write-cow-strategy) of `data.db` into the read-write layer of the container.\r\n\r\nI don't understand **how** that happens.\r\n\r\nit kind of feels like a bug in sqlite, but i can't quite follow the sqlite code.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1426379903, "label": "don't use immutable=1, only mode=ro"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/1870#issuecomment-1295660092", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1870", "id": 1295660092, "node_id": "IC_kwDOBm6k_c5NOjQ8", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-29T00:25:26Z", "updated_at": "2022-10-29T00:25:26Z", "author_association": "OWNER", "body": "Saw your comment here too: https://github.com/simonw/datasette/issues/1480#issuecomment-1271101072\r\n\r\n> switching from `immutable=1` to `mode=ro` completely addressed this. see https://github.com/simonw/datasette/issues/1836#issuecomment-1271100651 for details.\r\n\r\nSo maybe we need a special case for containers that are intended to be run using Docker - the ones produced by `datasette package` and `datasette publish cloudrun`? Those are cases where the `-i` option should actually be opened in read-only mode, not immutable mode.\r\n\r\nMaybe a `datasette serve --irw data.db` option for opening a file in immutable-but-actually-read-only mode? Bit ugly though.\r\n\r\nI should run some benchmarks to figure out if `immutable` really does offer significant performance benefits.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1426379903, "label": "don't use immutable=1, only mode=ro"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/1870#issuecomment-1295657771", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1870", "id": 1295657771, "node_id": "IC_kwDOBm6k_c5NOisr", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-29T00:19:03Z", "updated_at": "2022-10-29T00:19:03Z", "author_association": "OWNER", "body": "Just saw your comment here: https://github.com/simonw/datasette/issues/1836#issuecomment-1272357976\r\n\r\n> when you are running from docker, you **always** will want to run as `mode=ro` because the same thing that is causing duplication in the inspect layer will cause duplication in the final container read/write layer when `datasette serve` runs.\r\n\r\nI don't understand this. My mental model of how Docker works is that the image itself is created using `docker build`... but then when the image runs later on (`docker run`) the image itself isn't touched at all.\r\n\r\nAre you saying that I can build a container, but then when I run it and it does `datasette serve -i data.db ...` it will somehow modify the image, or create a new modified filesystem layer in the runtime environment, as a result of running that `serve` command?", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1426379903, "label": "don't use immutable=1, only mode=ro"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1866#issuecomment-1295200988", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1866", "id": 1295200988, "node_id": "IC_kwDOBm6k_c5NMzLc", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-28T16:29:55Z", "updated_at": "2022-10-28T16:29:55Z", "author_association": "OWNER", "body": "I wonder if there's something clever I could do here within a transaction?\r\n\r\nStart a transaction. Write out a temporary in-memory table with all of the existing primary keys in the table. Run the bulk insert. Then run `select pk from table where pk not in (select pk from old_pks)` to see what has changed.\r\n\r\nI don't think that's going to work well for large tables.\r\n\r\nI'm going to go with not returning inserted rows by default, unless you pass a special option requesting that.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1426001541, "label": "API for bulk inserting records into a table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/sqlite-utils/issues/496#issuecomment-1294408928", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/496", "id": 1294408928, "node_id": "IC_kwDOCGYnMM5NJxzg", "user": {"value": 39538958, "label": "justmars"}, "created_at": "2022-10-28T03:36:56Z", "updated_at": "2022-10-28T03:37:50Z", "author_association": "NONE", "body": "With respect to the typing of Table class itself, my interim solution:\r\n\r\n```python\r\nfrom sqlite_utils.db import Table\r\ndef tbl(self, table_name: str) -> Table:\r\n tbl = self.db[table_name]\r\n if isinstance(tbl, Table):\r\n return tbl\r\n raise Exception(f\"Missing {table_name=}\")\r\n```\r\n\r\nWith respect to @chapmanjacobd concern on the `DEFAULT` being an empty class, have also been using `# type: ignore`, e.g.\r\n\r\n```python\r\n@classmethod\r\ndef insert_list(cls, areas: list[str]):\r\n return meta.tbl(meta.Areas).insert_all(\r\n ({\"area\": a} for a in areas), ignore=True # type: ignore\r\n )\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1393202060, "label": "devrel/python api: Pylance type hinting"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1866#issuecomment-1294316640", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1866", "id": 1294316640, "node_id": "IC_kwDOBm6k_c5NJbRg", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-28T01:51:40Z", "updated_at": "2022-10-28T01:51:40Z", "author_association": "OWNER", "body": "This needs to support the following:\r\n- Rows do not include a primary key - one is assigned by the database\r\n- Rows provide their own primary key, any clashes are errors\r\n- Rows provide their own primary key, clashes are silently ignored\r\n- Rows provide their own primary key, replacing any existing records", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1426001541, "label": "API for bulk inserting records into a table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1866#issuecomment-1294306071", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1866", "id": 1294306071, "node_id": "IC_kwDOBm6k_c5NJYsX", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-28T01:37:14Z", "updated_at": "2022-10-28T01:37:59Z", "author_association": "OWNER", "body": "Quick crude benchmark:\r\n```python\r\nimport sqlite3\r\n\r\ndb = sqlite3.connect(\":memory:\")\r\n\r\ndef create_table(db, name):\r\n db.execute(f\"create table {name} (id integer primary key, title text)\")\r\n\r\ncreate_table(db, \"single\")\r\ncreate_table(db, \"multi\")\r\ncreate_table(db, \"bulk\")\r\n\r\ndef insert_singles(titles):\r\n inserted = []\r\n for title in titles:\r\n cursor = db.execute(f\"insert into single (title) values (?)\", [title])\r\n inserted.append((cursor.lastrowid, title))\r\n return inserted\r\n\r\n\r\ndef insert_many(titles):\r\n db.executemany(f\"insert into multi (title) values (?)\", ((t,) for t in titles))\r\n\r\n\r\ndef insert_bulk(titles):\r\n db.execute(\"insert into bulk (title) values {}\".format(\r\n \", \".join(\"(?)\" for _ in titles)\r\n ), titles)\r\n\r\ntitles = [\"title {}\".format(i) for i in range(1, 10001)]\r\n```\r\nThen in iPython I ran these:\r\n```\r\nIn [14]: %timeit insert_singles(titles)\r\n23.8 ms \u00b1 535 \u00b5s per loop (mean \u00b1 std. dev. of 7 runs, 10 loops each)\r\n\r\nIn [13]: %timeit insert_many(titles)\r\n12 ms \u00b1 520 \u00b5s per loop (mean \u00b1 std. dev. of 7 runs, 100 loops each)\r\n\r\nIn [12]: %timeit insert_bulk(titles)\r\n2.59 ms \u00b1 25 \u00b5s per loop (mean \u00b1 std. dev. of 7 runs, 100 loops each)\r\n```\r\nSo the bulk insert really is a lot faster - 3ms compared to 24ms for single inserts, so ~8x faster.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1426001541, "label": "API for bulk inserting records into a table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1866#issuecomment-1294296767", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1866", "id": 1294296767, "node_id": "IC_kwDOBm6k_c5NJWa_", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-28T01:22:25Z", "updated_at": "2022-10-28T01:23:09Z", "author_association": "OWNER", "body": "Nasty catch on this one: I wanted to return the IDs of the freshly inserted rows. But... the `insert_all()` method I was planning to use from `sqlite-utils` doesn't appear to have a way of doing that:\r\n\r\nhttps://github.com/simonw/sqlite-utils/blob/529110e7d8c4a6b1bbf5fb61f2e29d72aa95a611/sqlite_utils/db.py#L2813-L2835\r\n\r\nSQLite itself added a `RETURNING` statement which might help, but that is only available from version 3.35 released in March 2021: https://www.sqlite.org/lang_returning.html - which isn't commonly available yet. https://latest.datasette.io/-/versions right now shows 3.34, and https://lite.datasette.io/#/-/versions shows 3.27.2 (from Feb 2019).\r\n\r\nTwo options then:\r\n\r\n1. Even for bulk inserts do one insert at a time so I can use `cursor.lastrowid` to get the ID of the inserted record. This isn't terrible since SQLite is very fast, but it may still be a big performance hit for large inserts.\r\n2. Don't return the list of inserted rows for bulk inserts\r\n3. Default to not returning the list of inserted rows for bulk inserts, but allow the user to request that - in which case we use the slower path\r\n\r\nThat third option might be the way to go here.\r\n\r\nI should benchmark first to figure out how much of a difference this actually makes.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1426001541, "label": "API for bulk inserting records into a table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/1870#issuecomment-1294285471", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1870", "id": 1294285471, "node_id": "IC_kwDOBm6k_c5NJTqf", "user": {"value": 536941, "label": "fgregg"}, "created_at": "2022-10-28T01:06:03Z", "updated_at": "2022-10-28T01:06:03Z", "author_association": "CONTRIBUTOR", "body": "as far as i can tell, [this is where the \"immutable\" argument is used](https://github.com/sqlite/sqlite/blob/c97bb14fab566f6fa8d967c8fd1e90f3702d5b73/src/pager.c#L4926-L4931) in sqlite:\r\n\r\n```c\r\n pPager->noLock = sqlite3_uri_boolean(pPager->zFilename, \"nolock\", 0);\r\n if( (iDc & SQLITE_IOCAP_IMMUTABLE)!=0\r\n || sqlite3_uri_boolean(pPager->zFilename, \"immutable\", 0) ){\r\n vfsFlags |= SQLITE_OPEN_READONLY;\r\n goto act_like_temp_file;\r\n }\r\n```\r\n\r\nso it does set the read only flag, but then has a goto.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1426379903, "label": "don't use immutable=1, only mode=ro"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1866#issuecomment-1294282263", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1866", "id": 1294282263, "node_id": "IC_kwDOBm6k_c5NJS4X", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-28T01:00:42Z", "updated_at": "2022-10-28T01:00:42Z", "author_association": "OWNER", "body": "I'm going to set the limit at 1,000 rows inserted at a time. I'll make this configurable using a new `max_insert_rows` setting (for consistency with `max_returned_rows`).", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1426001541, "label": "API for bulk inserting records into a table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1851#issuecomment-1294281451", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1851", "id": 1294281451, "node_id": "IC_kwDOBm6k_c5NJSrr", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-28T00:59:25Z", "updated_at": "2022-10-28T00:59:25Z", "author_association": "OWNER", "body": "I'm going to use this endpoint for bulk inserts too, so I'm closing this issue and continuing the work here:\r\n- #1866", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1421544654, "label": "API to insert a single record into an existing table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/1870#issuecomment-1294238862", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1870", "id": 1294238862, "node_id": "IC_kwDOBm6k_c5NJISO", "user": {"value": 22429695, "label": "codecov[bot]"}, "created_at": "2022-10-27T23:44:25Z", "updated_at": "2022-10-27T23:44:25Z", "author_association": "NONE", "body": "# [Codecov](https://codecov.io/gh/simonw/datasette/pull/1870?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) Report\nBase: **92.55**% // Head: **92.55**% // No change to project coverage :thumbsup:\n> Coverage data is based on head [(`4faa4fd`)](https://codecov.io/gh/simonw/datasette/pull/1870?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) compared to base [(`bf00b0b`)](https://codecov.io/gh/simonw/datasette/commit/bf00b0b59b6692bdec597ac9db4e0b497c5a47b4?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison).\n> Patch has no changes to coverable lines.\n\n
    Additional details and impacted files\n\n\n```diff\n@@ Coverage Diff @@\n## main #1870 +/- ##\n=======================================\n Coverage 92.55% 92.55% \n=======================================\n Files 35 35 \n Lines 4432 4432 \n=======================================\n Hits 4102 4102 \n Misses 330 330 \n```\n\n\n| [Impacted Files](https://codecov.io/gh/simonw/datasette/pull/1870?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison) | Coverage \u0394 | |\n|---|---|---|\n| [datasette/app.py](https://codecov.io/gh/simonw/datasette/pull/1870/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison#diff-ZGF0YXNldHRlL2FwcC5weQ==) | `94.30% <\u00f8> (\u00f8)` | |\n\nHelp us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison)\n\n
    \n\n[:umbrella: View full report at Codecov](https://codecov.io/gh/simonw/datasette/pull/1870?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison). \n:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Simon+Willison).\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1426379903, "label": "don't use immutable=1, only mode=ro"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/pull/1870#issuecomment-1294237783", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1870", "id": 1294237783, "node_id": "IC_kwDOBm6k_c5NJIBX", "user": {"value": 536941, "label": "fgregg"}, "created_at": "2022-10-27T23:42:18Z", "updated_at": "2022-10-27T23:42:18Z", "author_association": "CONTRIBUTOR", "body": "Relevant sqlite forum thread: https://www.sqlite.org/forum/forumpost/02f7bda329f41e30451472421cf9ce7f715b768ce3db02797db1768e47950d48", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1426379903, "label": "don't use immutable=1, only mode=ro"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1851#issuecomment-1289712350", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1851", "id": 1289712350, "node_id": "IC_kwDOBm6k_c5M33Le", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-24T22:28:39Z", "updated_at": "2022-10-27T23:18:48Z", "author_association": "OWNER", "body": "API design: (**UPDATE: this was [later changed to POST /db/table/-/insert](https://github.com/simonw/datasette/issues/1851#issuecomment-1294224185))\r\n\r\n```\r\nPOST /db/table\r\nAuthorization: Bearer xxx\r\nContent-Type: application/json\r\n{\r\n \"row\": {\r\n \"id\": 1,\r\n \"name\": \"New record\"\r\n }\r\n}\r\n```\r\nReturns:\r\n```\r\n201 Created\r\n{\r\n \"row\": {\r\n \"id\": 1,\r\n \"name\": \"New record\"\r\n }\r\n}\r\n```\r\nYou can omit optional fields in the input, including the ID field. The returned object will always include all fields - and will even include `rowid` if your object doesn't have a primary key of its own.\r\n\r\nI decided to use `\"row\"` as the key in both request and response, to preserve space for other future keys - one that tells you that the table has been created, for example.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1421544654, "label": "API to insert a single record into an existing table"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1869#issuecomment-1294181485", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1869", "id": 1294181485, "node_id": "IC_kwDOBm6k_c5NI6Rt", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-10-27T22:24:37Z", "updated_at": "2022-10-27T22:24:37Z", "author_association": "OWNER", "body": "https://docs.datasette.io/en/stable/changelog.html#v0-63\r\n\r\nAnnotated release notes: https://simonwillison.net/2022/Oct/27/datasette-0-63/", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1426253476, "label": "Release 0.63"}, "performed_via_github_app": null}