... Column, other table, other column to set as a\r\n foreign key\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1855894222, "label": "CLI equivalents to `transform(add_foreign_keys=)`"}, "performed_via_github_app": null}
{"html_url": "https://github.com/simonw/datasette/issues/2145#issuecomment-1684530060", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2145", "id": 1684530060, "node_id": "IC_kwDOBm6k_c5kZ-OM", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-08-18T23:09:03Z", "updated_at": "2023-08-18T23:09:14Z", "author_association": "OWNER", "body": "Ran a quick benchmark on ChatGPT Code Interpreter: https://chat.openai.com/share/8357dc01-a97e-48ae-b35a-f06249935124\r\n\r\nConclusion from there is that this query returns fast no matter how much the table grows:\r\n\r\n```sql\r\nSELECT EXISTS(SELECT 1 FROM \"nasty\" WHERE \"id\" IS NULL)\r\n```\r\nSo detecting if a table contains any null primary keys is definitely feasible without a performance hit.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1857234285, "label": "If a row has a primary key of `null` various things break"}, "performed_via_github_app": null}
{"html_url": "https://github.com/simonw/datasette/issues/2145#issuecomment-1684497000", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2145", "id": 1684497000, "node_id": "IC_kwDOBm6k_c5kZ2Jo", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-08-18T22:31:53Z", "updated_at": "2023-08-18T22:31:53Z", "author_association": "OWNER", "body": "So it sounds like SQLite does ensure that a `rowid` before it allows a primary key to be null.\r\n\r\nSo one solution here would be to detect a null primary key and switch that table over to using `rowid` URLs instead. The key problem we're trying to solve here after all is how to link to a row:\r\n\r\nhttps://latest.datasette.io/fixtures/infinity/1\r\n\r\nBut when would we run that check? And does every row in the table get a new `/rowid/` URL just because someone messed up and inserted a `null` by mistake?", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1857234285, "label": "If a row has a primary key of `null` various things break"}, "performed_via_github_app": null}
{"html_url": "https://github.com/simonw/sqlite-utils/issues/585#issuecomment-1683212074", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/585", "id": 1683212074, "node_id": "IC_kwDOCGYnMM5kU8cq", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-08-18T01:43:54Z", "updated_at": "2023-08-18T01:43:54Z", "author_association": "OWNER", "body": "Some manual testing:\r\n```bash\r\nsqlite-utils create-table /tmp/t.db places id integer name text country integer city integer continent integer --pk id\r\nsqlite-utils schema /tmp/t.db\r\n```\r\n```sql\r\nCREATE TABLE [places] (\r\n [id] INTEGER PRIMARY KEY,\r\n [name] TEXT,\r\n [country] INTEGER,\r\n [city] INTEGER,\r\n [continent] INTEGER\r\n);\r\n```\r\n```bash\r\nsqlite-utils create-table /tmp/t.db country id integer name text\r\nsqlite-utils create-table /tmp/t.db city id integer name text\r\nsqlite-utils create-table /tmp/t.db continent id integer name text\r\nsqlite-utils schema /tmp/t.db\r\n```\r\n```sql\r\nCREATE TABLE [places] (\r\n [id] INTEGER PRIMARY KEY,\r\n [name] TEXT,\r\n [country] INTEGER,\r\n [city] INTEGER,\r\n [continent] INTEGER\r\n);\r\nCREATE TABLE [country] (\r\n [id] INTEGER,\r\n [name] TEXT\r\n);\r\nCREATE TABLE [city] (\r\n [id] INTEGER,\r\n [name] TEXT\r\n);\r\nCREATE TABLE [continent] (\r\n [id] INTEGER,\r\n [name] TEXT\r\n);\r\n```\r\n```bash\r\nsqlite-utils transform /tmp/t.db places --add-foreign-key country country id --add-foreign-key continent continent id\r\nsqlite-utils schema /tmp/t.db\r\n```\r\n```sql\r\nCREATE TABLE [country] (\r\n [id] INTEGER,\r\n [name] TEXT\r\n);\r\nCREATE TABLE [city] (\r\n [id] INTEGER,\r\n [name] TEXT\r\n);\r\nCREATE TABLE [continent] (\r\n [id] INTEGER,\r\n [name] TEXT\r\n);\r\nCREATE TABLE \"places\" (\r\n [id] INTEGER PRIMARY KEY,\r\n [name] TEXT,\r\n [country] INTEGER REFERENCES [country]([id]),\r\n [city] INTEGER,\r\n [continent] INTEGER REFERENCES [continent]([id])\r\n);\r\n```\r\n```bash\r\nsqlite-utils transform /tmp/t.db places --drop-foreign-key country\r\nsqlite-utils schema /tmp/t.db places\r\n```\r\n```sql\r\nCREATE TABLE \"places\" (\r\n [id] INTEGER PRIMARY KEY,\r\n [name] TEXT,\r\n [country] INTEGER,\r\n [city] INTEGER,\r\n [continent] INTEGER REFERENCES [continent]([id])\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": 1855894222, "label": "CLI equivalents to `transform(add_foreign_keys=)`"}, "performed_via_github_app": null}
{"html_url": "https://github.com/simonw/datasette/issues/2145#issuecomment-1684384750", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2145", "id": 1684384750, "node_id": "IC_kwDOBm6k_c5kZavu", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-08-18T20:07:18Z", "updated_at": "2023-08-18T20:07:18Z", "author_association": "OWNER", "body": "The big challenge here is what the URL to that row page should look like. How can I encode a `None` in a form that can be encoded and decoded without clashing with primary keys that are the string `None` or `null`?", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1857234285, "label": "If a row has a primary key of `null` various things break"}, "performed_via_github_app": null}
{"html_url": "https://github.com/simonw/sqlite-utils/pull/584#issuecomment-1683137259", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/584", "id": 1683137259, "node_id": "IC_kwDOCGYnMM5kUqLr", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-08-18T00:06:59Z", "updated_at": "2023-08-18T00:06:59Z", "author_association": "OWNER", "body": "The docs still describe the old trick, I need to update that: https://sqlite-utils.datasette.io/en/3.34/python-api.html#adding-foreign-key-constraints", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1855838223, "label": ".transform() instead of modifying sqlite_master for add_foreign_keys"}, "performed_via_github_app": null}
{"html_url": "https://github.com/simonw/datasette/issues/2145#issuecomment-1684498947", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2145", "id": 1684498947, "node_id": "IC_kwDOBm6k_c5kZ2oD", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-08-18T22:35:04Z", "updated_at": "2023-08-18T22:35:04Z", "author_association": "OWNER", "body": "The most interesting row URL in the fixtures database right now is this one:\r\n\r\nhttps://latest.datasette.io/fixtures/compound_primary_key/a~2Fb,~2Ec-d\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": 1857234285, "label": "If a row has a primary key of `null` various things break"}, "performed_via_github_app": null}
{"html_url": "https://github.com/simonw/sqlite-utils/issues/585#issuecomment-1683198740", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/585", "id": 1683198740, "node_id": "IC_kwDOCGYnMM5kU5MU", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-08-18T01:26:47Z", "updated_at": "2023-08-18T01:26:47Z", "author_association": "OWNER", "body": "The only CLI feature that supports providing just the column name appears to be this:\r\n```bash\r\nsqlite-utils add-foreign-key --help\r\n```\r\n```\r\nUsage: sqlite-utils add-foreign-key [OPTIONS] PATH TABLE COLUMN [OTHER_TABLE]\r\n [OTHER_COLUMN]\r\n\r\n Add a new foreign key constraint to an existing table\r\n\r\n Example:\r\n\r\n sqlite-utils add-foreign-key my.db books author_id authors id\r\n\r\n WARNING: Could corrupt your database! Back up your database file first.\r\n```\r\nI can drop that WARNING now since I'm not writing to `sqlite_master` any more.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1855894222, "label": "CLI equivalents to `transform(add_foreign_keys=)`"}, "performed_via_github_app": null}
{"html_url": "https://github.com/simonw/datasette/issues/2143#issuecomment-1683429959", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2143", "id": 1683429959, "node_id": "IC_kwDOBm6k_c5kVxpH", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-08-18T06:43:33Z", "updated_at": "2023-08-18T15:19:07Z", "author_association": "OWNER", "body": "The single biggest design challenge I've had with metadata relates to how it should or should not be inherited.\r\n\r\nIf you apply a license to a Datasette instance, it feels like that should flow down to cover all of the databases and all of the tables within those databases.\r\n\r\nIf the license is at the database level, it should cover all tables.\r\n\r\nBut... should source do the same thing? I made it behave the same way as license, but it's presumably common for one database to have a single license but multiple different sources of data.\r\n\r\nThen there's title - should that inherit? It feels like title should apply to only one level - you may want a title that applies to the instance, then a different custom title for databases and tables.\r\n\r\nHere's the current state of play for metadata: https://docs.datasette.io/en/1.0a3/metadata.html\r\n\r\nSo there's `title` and `description` - and I'll be honest, I'm not 100% sure even I understand how those should be inherited down by tables/etc.\r\n\r\nThere's `description_html` which over-rides the `description` if it is set. It's a useful customization hack, but a bit surprising.\r\n\r\nThen there are these six:\r\n\r\n- `license`\r\n- `license_url`\r\n- `source`\r\n- `source_url`\r\n- `about`\r\n- `about_url`\r\n\r\nI added `about` later than the others, because I realized that plenty of my own projects needed a link to an article explaining them somewhere - e.g. https://scotrail.datasette.io/\r\n\r\nTables can also have column descriptions - just a string for each column. There's a demo of those here: https://latest.datasette.io/fixtures/roadside_attractions\r\n\r\nAnd then there's all of the other stuff, most of which feels much more like \"settings\" than \"metadata\":\r\n\r\n- `sort: created` - the custom sort order\r\n- `size: 10` for a custom page size for a specific table\r\n- `sortable_columns` to set which columns can be used to sort\r\n- `hidden: true` to hide a table\r\n- `label_column: title` is an interesting one - it lets you hint to Datasette which column should be displayed when there is a foreign key relationship. It's sort-of-metadata and sort-of-a-setting.\r\n- `facets` sets default facets, see https://docs.datasette.io/en/1.0a3/facets.html#facets-in-metadata\r\n- `facet_size` sets the number of facets to display\r\n- `fts_table` and `fts_pk` can be used to configure FTS, especially for views: https://docs.datasette.io/en/1.0a3/full_text_search.html\r\n\r\nAnd the authentication stuff! `allow` and `allow_sql` blocks: https://docs.datasette.io/en/1.0a3/authentication.html#defining-permissions-with-allow-blocks\r\n\r\nAnd the new `permissions` key in the 1.0 alphas: https://docs.datasette.io/en/1.0a3/authentication.html#other-permissions-in-metadata\r\n\r\nI think that might be everything (excluding the `plugins` settings stuff, which is also a bad fit for metadata.)\r\n\r\nAnd to make things even more confusing... I believe you can add arbitrary key/value pairs to your metadata and then use them in your templates! I think I've heard from at least one person who uses that ability.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1855885427, "label": "De-tangling Metadata before Datasette 1.0"}, "performed_via_github_app": null}
{"html_url": "https://github.com/simonw/datasette/issues/2145#issuecomment-1684525943", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2145", "id": 1684525943, "node_id": "IC_kwDOBm6k_c5kZ9N3", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-08-18T23:04:14Z", "updated_at": "2023-08-18T23:04:14Z", "author_association": "OWNER", "body": "This is hard. I tried this:\r\n```python\r\ndef path_from_row_pks(row, pks, use_rowid, quote=True):\r\n \"\"\"Generate an optionally tilde-encoded unique identifier\r\n for a row from its primary keys.\"\"\"\r\n if use_rowid or any(row[pk] is None for pk in pks):\r\n bits = [row[\"rowid\"]]\r\n else:\r\n bits = [\r\n row[pk][\"value\"] if isinstance(row[pk], dict) else row[pk] for pk in pks\r\n ]\r\n if quote:\r\n bits = [tilde_encode(str(bit)) for bit in bits]\r\n else:\r\n bits = [str(bit) for bit in bits]\r\n\r\n return \",\".join(bits)\r\n```\r\nThe ` if use_rowid or any(row[pk] is None for pk in pks)` bit is new.\r\n\r\nBut I got this error on http://127.0.0.1:8003/nulls/nasty :\r\n\r\n```\r\n File \"/Users/simon/Dropbox/Development/datasette/datasette/views/table.py\", line 1364, in run_display_columns_and_rows\r\n display_columns, display_rows = await display_columns_and_rows(\r\n File \"/Users/simon/Dropbox/Development/datasette/datasette/views/table.py\", line 186, in display_columns_and_rows\r\n pk_path = path_from_row_pks(row, pks, not pks, False)\r\n File \"/Users/simon/Dropbox/Development/datasette/datasette/utils/__init__.py\", line 124, in path_from_row_pks\r\n bits = [row[\"rowid\"]]\r\nIndexError: No item with that key\r\n```\r\nBecause the SQL query I ran to populate the page didn't know that it would need to select `rowid` as well.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1857234285, "label": "If a row has a primary key of `null` various things break"}, "performed_via_github_app": null}
{"html_url": "https://github.com/simonw/sqlite-utils/pull/584#issuecomment-1683145819", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/584", "id": 1683145819, "node_id": "IC_kwDOCGYnMM5kUsRb", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-08-18T00:17:26Z", "updated_at": "2023-08-18T00:17:26Z", "author_association": "OWNER", "body": "Updated documentation: https://sqlite-utils--584.org.readthedocs.build/en/584/python-api.html#adding-foreign-key-constraints", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1855838223, "label": ".transform() instead of modifying sqlite_master for add_foreign_keys"}, "performed_via_github_app": null}
{"html_url": "https://github.com/simonw/sqlite-utils/pull/584#issuecomment-1683139304", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/584", "id": 1683139304, "node_id": "IC_kwDOCGYnMM5kUqro", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-08-18T00:09:56Z", "updated_at": "2023-08-18T00:09:56Z", "author_association": "OWNER", "body": "Upgrading `flake8` locally replicated the error:\r\n```\r\npip install -U flake8\r\nflake8\r\n```\r\n```\r\n./tests/test_recipes.py:99:9: F811 redefinition of unused 'fn' from line 96\r\n./tests/test_recipes.py:127:9: F811 redefinition of unused 'fn' from line 124\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1855838223, "label": ".transform() instead of modifying sqlite_master for add_foreign_keys"}, "performed_via_github_app": null}
{"html_url": "https://github.com/simonw/sqlite-utils/pull/584#issuecomment-1683138953", "issue_url": "https://api.github.com/repos/simonw/sqlite-utils/issues/584", "id": 1683138953, "node_id": "IC_kwDOCGYnMM5kUqmJ", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-08-18T00:09:20Z", "updated_at": "2023-08-18T00:09:20Z", "author_association": "OWNER", "body": "Weird, I'm getting a `flake8` problem in CI which doesn't occur on my laptop:\r\n```\r\n./tests/test_recipes.py:99:9: F811 redefinition of unused 'fn' from line 96\r\n./tests/test_recipes.py:127:9: F811 redefinition of unused 'fn' from line 124\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1855838223, "label": ".transform() instead of modifying sqlite_master for add_foreign_keys"}, "performed_via_github_app": null}
{"html_url": "https://github.com/simonw/datasette/issues/2143#issuecomment-1684484426", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2143", "id": 1684484426, "node_id": "IC_kwDOBm6k_c5kZzFK", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-08-18T22:12:52Z", "updated_at": "2023-08-18T22:12:52Z", "author_association": "OWNER", "body": "Yeah, I'm convinced by that. There's not point in having both `settings.json` and `datasette.json`.\r\n\r\nI like `datasette.json` ( / `datasette.yml`) as a name. That can be the file that lives in your config directory too, so if you run `datasette .` in a folder containing `datasette.yml` all of those settings get picked up.\r\n\r\nHere's a thought for how it could look - I'll go with the YAML format because I expect that to be the default most people use, just because it supports multi-line strings better.\r\n\r\nI based this on the big example at https://docs.datasette.io/en/1.0a3/metadata.html#using-yaml-for-metadata - and combined some bits from https://docs.datasette.io/en/1.0a3/authentication.html as well.\r\n\r\n```yaml\r\ntitle: Demonstrating Metadata from YAML\r\ndescription_html: |-\r\n This description includes a long HTML string
\r\n \r\n - YAML is better for embedding HTML strings than JSON!
\r\n
\r\n\r\nsettings:\r\n default_page_size: 10\r\n max_returned_rows: 3000\r\n sql_time_limit_ms\": 8000\r\n\r\ndatabases:\r\n docs:\r\n permissions:\r\n create-table:\r\n id: editor\r\n fixtures:\r\n tables:\r\n no_primary_key:\r\n hidden: true\r\n queries:\r\n neighborhood_search:\r\n sql: |-\r\n select neighborhood, facet_cities.name, state\r\n from facetable join facet_cities on facetable.city_id = facet_cities.id\r\n where neighborhood like '%' || :text || '%' order by neighborhood;\r\n title: Search neighborhoods\r\n description_html: |-\r\n This demonstrates basic LIKE search\r\n\r\npermissions:\r\n debug-menu:\r\n id: '*'\r\n\r\nplugins:\r\n datasette-ripgrep:\r\n path: /usr/local/lib/python3.11/site-packages\r\n```\r\nI'm inclined to say we try to be a super-set of the existing `metadata.yml` format, at least where it makes sense to do so. That way the upgrade path is smooth for people. Also, I don't think the format itself is terrible - it's the name that's the big problem.\r\n\r\nIn this example I've mixed in one extra concept: that `settings:` block with a bunch of settings in it.\r\n\r\nThere are some things in there that look a little bit like metadata - the `title` and `description_html` fields.\r\n\r\nBut _are they_ metadata? The title and description of the overall instance feels like it could be described as general configuration. The stuff for the `query` should live where the query itself is defined.\r\n\r\nNote that queries can be defined by a plugin hook too: https://docs.datasette.io/en/1.0a3/plugin_hooks.html#canned-queries-datasette-database-actor\r\n\r\nWhat do you think? Is this the right direction, or are you thinking there's a more radical redesign that would make sense here?", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1855885427, "label": "De-tangling Metadata before Datasette 1.0"}, "performed_via_github_app": null}
{"html_url": "https://github.com/simonw/datasette/issues/2145#issuecomment-1684504398", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2145", "id": 1684504398, "node_id": "IC_kwDOBm6k_c5kZ39O", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-08-18T22:43:31Z", "updated_at": "2023-08-18T22:43:46Z", "author_association": "OWNER", "body": "`(?P[^/]+?)` could instead be a regex that is restricted to the tilde-encoded set of characters, or `\\.\\d+`.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1857234285, "label": "If a row has a primary key of `null` various things break"}, "performed_via_github_app": null}
{"html_url": "https://github.com/simonw/datasette/issues/2145#issuecomment-1684503189", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2145", "id": 1684503189, "node_id": "IC_kwDOBm6k_c5kZ3qV", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-08-18T22:41:51Z", "updated_at": "2023-08-18T22:41:51Z", "author_association": "OWNER", "body": "```pycon\r\n>>> tilde_encode(\"~\")\r\n'~7E'\r\n>>> tilde_encode(\".\")\r\n'~2E'\r\n>>> tilde_encode(\"-\")\r\n'-'\r\n```\r\nI think `.` might be the way to do this:\r\n\r\n /database/table/.4\r\n\r\nBut... I worry about that colliding with my URL routing code that spots the difference between these:\r\n\r\n /database/table/.4\r\n /database/table/.4.json\r\n /database/table/.4.csv\r\n\r\netc.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1857234285, "label": "If a row has a primary key of `null` various things break"}, "performed_via_github_app": null}
{"html_url": "https://github.com/simonw/datasette/issues/2145#issuecomment-1684495674", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/2145", "id": 1684495674, "node_id": "IC_kwDOBm6k_c5kZ106", "user": {"value": 9599, "label": "simonw"}, "created_at": "2023-08-18T22:29:47Z", "updated_at": "2023-08-18T22:29:47Z", "author_association": "OWNER", "body": "https://www.sqlite.org/lang_createtable.html#the_primary_key says:\r\n\r\n>According to the SQL standard, PRIMARY KEY should always imply NOT NULL. Unfortunately, due to a bug in some early versions, this is not the case in SQLite. Unless the column is an [INTEGER PRIMARY KEY](https://www.sqlite.org/lang_createtable.html#rowid) or the table is a [WITHOUT ROWID](https://www.sqlite.org/withoutrowid.html) table or a [STRICT](https://www.sqlite.org/stricttables.html) table or the column is declared NOT NULL, SQLite allows NULL values in a PRIMARY KEY column. SQLite could be fixed to conform to the standard, but doing so might break legacy applications. Hence, it has been decided to merely document the fact that SQLite allows NULLs in most PRIMARY KEY columns.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 1857234285, "label": "If a row has a primary key of `null` various things break"}, "performed_via_github_app": null}