{"html_url": "https://github.com/simonw/datasette/pull/1574#issuecomment-1105464661", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1574", "id": 1105464661, "node_id": "IC_kwDOBm6k_c5B5A1V", "user": {"value": 208018, "label": "dholth"}, "created_at": "2022-04-21T16:51:24Z", "updated_at": "2022-04-21T16:51:24Z", "author_association": "NONE", "body": "tfw you have more ephemeral storage than upstream bandwidth\r\n\r\n```\r\nFROM python:3.10-slim AS base\r\n\r\nRUN apt update && apt -y install zstd\r\n\r\nENV DATASETTE_SECRET 'sosecret'\r\nRUN --mount=type=cache,target=/root/.cache/pip\r\n pip install -U datasette datasette-pretty-json datasette-graphql\r\n\r\nENV PORT 8080\r\nEXPOSE 8080\r\n\r\nFROM base AS pack\r\n\r\nCOPY . /app\r\nWORKDIR /app\r\n\r\nRUN datasette inspect --inspect-file inspect-data.json\r\nRUN zstd --rm *.db\r\n\r\nFROM base AS unpack\r\n\r\nCOPY --from=pack /app /app\r\nWORKDIR /app\r\n\r\nCMD [\"/bin/bash\", \"-c\", \"shopt -s nullglob && zstd --rm -d *.db.zst && datasette serve --host 0.0.0.0 --cors --inspect-file inspect-data.json --metadata metadata.json --create --port $PORT *.db\"]\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1084193403, "label": "introduce new option for datasette package to use a slim base image"}, "performed_via_github_app": null} {"html_url": "https://github.com/dogsheep/github-to-sqlite/issues/72#issuecomment-1105474232", "issue_url": "https://api.github.com/repos/dogsheep/github-to-sqlite/issues/72", "id": 1105474232, "node_id": "IC_kwDODFdgUs5B5DK4", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-04-21T17:02:15Z", "updated_at": "2022-04-21T17:02:15Z", "author_association": "MEMBER", "body": "That's interesting - yeah it looks like the number of pages can be derived from the `Link` header, which is enough information to show a progress bar, probably using Click just to avoid adding another dependency.\r\n\r\nhttps://docs.github.com/en/rest/guides/traversing-with-pagination", "reactions": "{\"total_count\": 1, \"+1\": 1, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1211283427, "label": "feature: display progress bar when downloading multi-page responses"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1101#issuecomment-1105571003", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1101", "id": 1105571003, "node_id": "IC_kwDOBm6k_c5B5ay7", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-04-21T18:10:38Z", "updated_at": "2022-04-21T18:10:46Z", "author_association": "OWNER", "body": "Maybe the simplest design for this is to add an optional `can_stream` to the contract:\r\n\r\n```python\r\n @hookimpl\r\n def register_output_renderer(datasette):\r\n return {\r\n \"extension\": \"tsv\",\r\n \"render\": render_tsv,\r\n \"can_render\": lambda: True,\r\n \"can_stream\": lambda: True\r\n }\r\n```\r\nWhen streaming, a new parameter could be passed to the render function - maybe `chunks` - which is an iterator/generator over a sequence of chunks of rows.\r\n\r\nOr it could use the existing `rows` parameter but treat that as an iterator?", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 749283032, "label": "register_output_renderer() should support streaming data"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1101#issuecomment-1105588651", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1101", "id": 1105588651, "node_id": "IC_kwDOBm6k_c5B5fGr", "user": {"value": 25778, "label": "eyeseast"}, "created_at": "2022-04-21T18:15:39Z", "updated_at": "2022-04-21T18:15:39Z", "author_association": "CONTRIBUTOR", "body": "What if you split rendering and streaming into two things:\r\n\r\n- `render` is a function that returns a response\r\n- `stream` is a function that sends chunks, or yields chunks passed to an ASGI `send` callback\r\n\r\nThat way current plugins still work, and streaming is purely additive. A `stream` function could get a cursor or iterator of rows, instead of a list, so it could more efficiently handle large queries.\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 749283032, "label": "register_output_renderer() should support streaming data"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1101#issuecomment-1105608964", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1101", "id": 1105608964, "node_id": "IC_kwDOBm6k_c5B5kEE", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-04-21T18:26:29Z", "updated_at": "2022-04-21T18:26:29Z", "author_association": "OWNER", "body": "I'm questioning if the mechanisms should be separate at all now - a single response rendering is really just a case of a streaming response that only pulls the first N records from the iterator.\r\n\r\nIt probably needs to be an `async for` iterator, which I've not worked with much before. Good opportunity to learn.\r\n\r\nThis actually gets a fair bit more complicated due to the work I'm doing right now to improve the default JSON API:\r\n\r\n- #1709\r\n\r\nI want to do things like make faceting results optionally available to custom renderers - which is a separate concern from streaming rows.\r\n\r\nI'm going to poke around with a bunch of prototypes and see what sticks.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 749283032, "label": "register_output_renderer() should support streaming data"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1101#issuecomment-1105615625", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1101", "id": 1105615625, "node_id": "IC_kwDOBm6k_c5B5lsJ", "user": {"value": 9599, "label": "simonw"}, "created_at": "2022-04-21T18:31:41Z", "updated_at": "2022-04-21T18:32:22Z", "author_association": "OWNER", "body": "The `datasette-geojson` plugin is actually an interesting case here, because of the way it converts SpatiaLite geometries into GeoJSON: https://github.com/eyeseast/datasette-geojson/blob/602c4477dc7ddadb1c0a156cbcd2ef6688a5921d/datasette_geojson/__init__.py#L61-L66\r\n\r\n```python\r\n\r\n if isinstance(geometry, bytes):\r\n results = await db.execute(\r\n \"SELECT AsGeoJSON(:geometry)\", {\"geometry\": geometry}\r\n )\r\n return geojson.loads(results.single_value())\r\n```\r\nThat actually seems to work really well as-is, but it does worry me a bit that it ends up having to execute an extra `SELECT` query for every single returned row - especially in streaming mode where it might be asked to return 1m rows at once.\r\n\r\nMy PostgreSQL/MySQL engineering brain says that this would be better handled by doing a chunk of these (maybe 100) at once, to avoid the per-query-overhead - but with SQLite that might not be necessary.\r\n\r\nAt any rate, this is one of the reasons I'm interested in \"iterate over this sequence of chunks of 100 rows at a time\" as a potential option here.\r\n\r\nOf course, a better solution would be for `datasette-geojson` to have a way to influence the SQL query before it is executed, adding a `AsGeoJSON(geometry)` clause to it - so that's something I'm open to as well.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 749283032, "label": "register_output_renderer() should support streaming data"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1101#issuecomment-1105642187", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1101", "id": 1105642187, "node_id": "IC_kwDOBm6k_c5B5sLL", "user": {"value": 25778, "label": "eyeseast"}, "created_at": "2022-04-21T18:59:08Z", "updated_at": "2022-04-21T18:59:08Z", "author_association": "CONTRIBUTOR", "body": "Ha! That was your idea (and a good one).\r\n\r\nBut it's probably worth measuring to see what overhead it adds. It did require both passing in the database and making the whole thing `async`. \r\n\r\nJust timing the queries themselves:\r\n\r\n1. [Using `AsGeoJSON(geometry) as geometry`](https://alltheplaces-datasette.fly.dev/alltheplaces?sql=select%0D%0A++id%2C%0D%0A++properties%2C%0D%0A++AsGeoJSON%28geometry%29+as+geometry%2C%0D%0A++spider%0D%0Afrom%0D%0A++places%0D%0Aorder+by%0D%0A++id%0D%0Alimit%0D%0A++1000) takes 10.235 ms\r\n2. [Leaving as binary](https://alltheplaces-datasette.fly.dev/alltheplaces?sql=select%0D%0A++id%2C%0D%0A++properties%2C%0D%0A++geometry%2C%0D%0A++spider%0D%0Afrom%0D%0A++places%0D%0Aorder+by%0D%0A++id%0D%0Alimit%0D%0A++1000) takes 8.63 ms\r\n\r\nLooking at the network panel:\r\n\r\n1. Takes about 200 ms for the `fetch` request\r\n2. Takes about 300 ms\r\n\r\nI'm not sure how best to time the GeoJSON generation, but it would be interesting to check. Maybe I'll write a plugin to add query times to response headers.\r\n\r\nThe other thing to consider with async streaming is that it might be well-suited for a slower response. When I have to get the whole result and send a response in a fixed amount of time, I need the most efficient query possible. If I can hang onto a connection and get things one chunk at a time, maybe it's ok if there's some overhead.\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 749283032, "label": "register_output_renderer() should support streaming data"}, "performed_via_github_app": null}