A place to cache linked articles (think custom and personal wayback machine)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.html 37KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693
  1. <!doctype html><!-- This is a valid HTML5 document. -->
  2. <!-- Screen readers, SEO, extensions and so on. -->
  3. <html lang=fr>
  4. <!-- Has to be within the first 1024 bytes, hence before the <title>
  5. See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
  6. <meta charset=utf-8>
  7. <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
  8. <!-- The viewport meta is quite crowded and we are responsible for that.
  9. See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
  10. <meta name=viewport content="width=device-width,minimum-scale=1,initial-scale=1,shrink-to-fit=no">
  11. <!-- Required to make a valid HTML5 document. -->
  12. <title>The interesting ideas in Datasette (archive) — David Larlet</title>
  13. <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
  14. <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons/apple-touch-icon.png">
  15. <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons/favicon-32x32.png">
  16. <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons/favicon-16x16.png">
  17. <link rel="manifest" href="/manifest.json">
  18. <link rel="mask-icon" href="/static/david/icons/safari-pinned-tab.svg" color="#5bbad5">
  19. <link rel="shortcut icon" href="/static/david/icons/favicon.ico">
  20. <meta name="apple-mobile-web-app-title" content="David Larlet">
  21. <meta name="application-name" content="David Larlet">
  22. <meta name="msapplication-TileColor" content="#da532c">
  23. <meta name="msapplication-config" content="/static/david/icons/browserconfig.xml">
  24. <meta name="theme-color" content="#f0f0ea">
  25. <!-- That good ol' feed, subscribe :p. -->
  26. <link rel=alternate type="application/atom+xml" title=Feed href="/david/log/">
  27. <meta name="robots" content="noindex, nofollow">
  28. <meta content="origin-when-cross-origin" name="referrer">
  29. <!-- Canonical URL for SEO purposes -->
  30. <link rel="canonical" href="https://simonwillison.net/2018/Oct/4/datasette-ideas/">
  31. <style>
  32. /* http://meyerweb.com/eric/tools/css/reset/ */
  33. html, body, div, span,
  34. h1, h2, h3, h4, h5, h6, p, blockquote, pre,
  35. a, abbr, address, big, cite, code,
  36. del, dfn, em, img, ins,
  37. small, strike, strong, tt, var,
  38. dl, dt, dd, ol, ul, li,
  39. fieldset, form, label, legend,
  40. table, caption, tbody, tfoot, thead, tr, th, td,
  41. article, aside, canvas, details, embed,
  42. figure, figcaption, footer, header, hgroup,
  43. menu, nav, output, ruby, section, summary,
  44. time, mark, audio, video {
  45. margin: 0;
  46. padding: 0;
  47. border: 0;
  48. font-size: 100%;
  49. font: inherit;
  50. vertical-align: baseline;
  51. }
  52. /* HTML5 display-role reset for older browsers */
  53. article, aside, details, figcaption, figure,
  54. footer, header, hgroup, menu, nav, section { display: block; }
  55. body { line-height: 1; }
  56. blockquote, q { quotes: none; }
  57. blockquote:before, blockquote:after,
  58. q:before, q:after {
  59. content: '';
  60. content: none;
  61. }
  62. table {
  63. border-collapse: collapse;
  64. border-spacing: 0;
  65. }
  66. /* http://practicaltypography.com/equity.html */
  67. /* https://calendar.perfplanet.com/2016/no-font-face-bulletproof-syntax/ */
  68. /* https://www.filamentgroup.com/lab/js-web-fonts.html */
  69. @font-face {
  70. font-family: 'EquityTextB';
  71. src: url('/static/david/css/fonts/Equity-Text-B-Regular-webfont.woff2') format('woff2'),
  72. url('/static/david/css/fonts/Equity-Text-B-Regular-webfont.woff') format('woff');
  73. font-weight: 300;
  74. font-style: normal;
  75. font-display: swap;
  76. }
  77. @font-face {
  78. font-family: 'EquityTextB';
  79. src: url('/static/david/css/fonts/Equity-Text-B-Italic-webfont.woff2') format('woff2'),
  80. url('/static/david/css/fonts/Equity-Text-B-Italic-webfont.woff') format('woff');
  81. font-weight: 300;
  82. font-style: italic;
  83. font-display: swap;
  84. }
  85. @font-face {
  86. font-family: 'EquityTextB';
  87. src: url('/static/david/css/fonts/Equity-Text-B-Bold-webfont.woff2') format('woff2'),
  88. url('/static/david/css/fonts/Equity-Text-B-Bold-webfont.woff') format('woff');
  89. font-weight: 700;
  90. font-style: normal;
  91. font-display: swap;
  92. }
  93. @font-face {
  94. font-family: 'ConcourseT3';
  95. src: url('/static/david/css/fonts/concourse_t3_regular-webfont-20190806.woff2') format('woff2'),
  96. url('/static/david/css/fonts/concourse_t3_regular-webfont-20190806.woff') format('woff');
  97. font-weight: 300;
  98. font-style: normal;
  99. font-display: swap;
  100. }
  101. /* http://practice.typekit.com/lesson/caring-about-opentype-features/ */
  102. body {
  103. /* http://www.cssfontstack.com/ Palatino 99% Win 86% Mac */
  104. font-family: "EquityTextB", Palatino, serif;
  105. background-color: #f0f0ea;
  106. color: #07486c;
  107. font-kerning: normal;
  108. -moz-osx-font-smoothing: grayscale;
  109. -webkit-font-smoothing: subpixel-antialiased;
  110. text-rendering: optimizeLegibility;
  111. font-variant-ligatures: common-ligatures contextual;
  112. font-feature-settings: "kern", "liga", "clig", "calt";
  113. }
  114. pre, code, kbd, samp, var, tt {
  115. font-family: 'TriplicateT4c', monospace;
  116. }
  117. em {
  118. font-style: italic;
  119. color: #323a45;
  120. }
  121. strong {
  122. font-weight: bold;
  123. color: black;
  124. }
  125. nav {
  126. background-color: #323a45;
  127. color: #f0f0ea;
  128. display: flex;
  129. justify-content: space-around;
  130. padding: 1rem .5rem;
  131. }
  132. nav:last-child {
  133. border-bottom: 1vh solid #2d7474;
  134. }
  135. nav a {
  136. color: #f0f0ea;
  137. }
  138. nav abbr {
  139. border-bottom: 1px dotted white;
  140. }
  141. h1 {
  142. border-top: 1vh solid #2d7474;
  143. border-bottom: .2vh dotted #2d7474;
  144. background-color: #e3e1e1;
  145. color: #323a45;
  146. text-align: center;
  147. padding: 5rem 0 4rem 0;
  148. width: 100%;
  149. font-family: 'ConcourseT3';
  150. display: flex;
  151. flex-direction: column;
  152. }
  153. h1.single {
  154. padding-bottom: 10rem;
  155. }
  156. h1 span {
  157. position: absolute;
  158. top: 1vh;
  159. left: 20%;
  160. line-height: 0;
  161. }
  162. h1 span a {
  163. line-height: 1.7;
  164. padding: 1rem 1.2rem .6rem 1.2rem;
  165. border-radius: 0 0 6% 6%;
  166. background: #2d7474;
  167. font-size: 1.3rem;
  168. color: white;
  169. text-decoration: none;
  170. }
  171. h2 {
  172. margin: 4rem 0 1rem;
  173. border-top: .2vh solid #2d7474;
  174. padding-top: 1vh;
  175. }
  176. h3 {
  177. text-align: center;
  178. margin: 3rem 0 .75em;
  179. }
  180. hr {
  181. height: .4rem;
  182. width: .4rem;
  183. border-radius: .4rem;
  184. background: #07486c;
  185. margin: 2.5rem auto;
  186. }
  187. time {
  188. display: bloc;
  189. margin-left: 0 !important;
  190. }
  191. ul, ol {
  192. margin: 2rem;
  193. }
  194. ul {
  195. list-style-type: square;
  196. }
  197. a {
  198. text-decoration-skip-ink: auto;
  199. text-decoration-thickness: 0.05em;
  200. text-underline-offset: 0.09em;
  201. }
  202. article {
  203. max-width: 50rem;
  204. display: flex;
  205. flex-direction: column;
  206. margin: 2rem auto;
  207. }
  208. article.single {
  209. border-top: .2vh dotted #2d7474;
  210. margin: -6rem auto 1rem auto;
  211. background: #f0f0ea;
  212. padding: 2rem;
  213. }
  214. article p:last-child {
  215. margin-bottom: 1rem;
  216. }
  217. p {
  218. padding: 0 .5rem;
  219. margin-left: 3rem;
  220. }
  221. p + p,
  222. figure + p {
  223. margin-top: 2rem;
  224. }
  225. blockquote {
  226. background-color: #e3e1e1;
  227. border-left: .5vw solid #2d7474;
  228. display: flex;
  229. flex-direction: column;
  230. align-items: center;
  231. padding: 1rem;
  232. margin: 1.5rem;
  233. }
  234. blockquote cite {
  235. font-style: italic;
  236. }
  237. blockquote p {
  238. margin-left: 0;
  239. }
  240. figure {
  241. border-top: .2vh solid #2d7474;
  242. background-color: #e3e1e1;
  243. text-align: center;
  244. padding: 1.5rem 0;
  245. margin: 1rem 0 0;
  246. font-size: 1.5rem;
  247. width: 100%;
  248. }
  249. figure img {
  250. max-width: 250px;
  251. max-height: 250px;
  252. border: .5vw solid #323a45;
  253. padding: 1px;
  254. }
  255. figcaption {
  256. padding: 1rem;
  257. line-height: 1.4;
  258. }
  259. aside {
  260. display: flex;
  261. flex-direction: column;
  262. background-color: #e3e1e1;
  263. padding: 1rem 0;
  264. border-bottom: .2vh solid #07486c;
  265. }
  266. aside p {
  267. max-width: 50rem;
  268. margin: 0 auto;
  269. }
  270. /* https://fvsch.com/code/css-locks/ */
  271. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  272. font-size: 1rem;
  273. line-height: calc( 1.5em + 0.2 * 1rem );
  274. }
  275. h1 {
  276. font-size: 1.9rem;
  277. line-height: calc( 1.2em + 0.2 * 1rem );
  278. }
  279. h2 {
  280. font-size: 1.6rem;
  281. line-height: calc( 1.3em + 0.2 * 1rem );
  282. }
  283. h3 {
  284. font-size: 1.35rem;
  285. line-height: calc( 1.4em + 0.2 * 1rem );
  286. }
  287. @media (min-width: 20em) {
  288. /* The (100vw - 20rem) / (50 - 20) part
  289. resolves to 0-1rem, depending on the
  290. viewport width (between 20em and 50em). */
  291. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  292. font-size: calc( 1rem + .6 * (100vw - 20rem) / (50 - 20) );
  293. line-height: calc( 1.5em + 0.2 * (100vw - 50rem) / (20 - 50) );
  294. margin-left: 0;
  295. }
  296. h1 {
  297. font-size: calc( 1.9rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  298. line-height: calc( 1.2em + 0.2 * (100vw - 50rem) / (20 - 50) );
  299. }
  300. h2 {
  301. font-size: calc( 1.5rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  302. line-height: calc( 1.3em + 0.2 * (100vw - 50rem) / (20 - 50) );
  303. }
  304. h3 {
  305. font-size: calc( 1.35rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  306. line-height: calc( 1.4em + 0.2 * (100vw - 50rem) / (20 - 50) );
  307. }
  308. }
  309. @media (min-width: 50em) {
  310. /* The right part of the addition *must* be a
  311. rem value. In this example we *could* change
  312. the whole declaration to font-size:2.5rem,
  313. but if our baseline value was not expressed
  314. in rem we would have to use calc. */
  315. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  316. font-size: calc( 1rem + .6 * 1rem );
  317. line-height: 1.5em;
  318. }
  319. p, li, pre, details {
  320. margin-left: 3rem;
  321. }
  322. h1 {
  323. font-size: calc( 1.9rem + 1.5 * 1rem );
  324. line-height: 1.2em;
  325. }
  326. h2 {
  327. font-size: calc( 1.5rem + 1.5 * 1rem );
  328. line-height: 1.3em;
  329. }
  330. h3 {
  331. font-size: calc( 1.35rem + 1.5 * 1rem );
  332. line-height: 1.4em;
  333. }
  334. figure img {
  335. max-width: 500px;
  336. max-height: 500px;
  337. }
  338. }
  339. figure.unsquared {
  340. margin-bottom: 1.5rem;
  341. }
  342. figure.unsquared img {
  343. height: inherit;
  344. }
  345. @media print {
  346. body { font-size: 100%; }
  347. a:after { content: " (" attr(href) ")"; }
  348. a, a:link, a:visited, a:after {
  349. text-decoration: underline;
  350. text-shadow: none !important;
  351. background-image: none !important;
  352. background: white;
  353. color: black;
  354. }
  355. abbr[title] { border-bottom: 0; }
  356. abbr[title]:after { content: " (" attr(title) ")"; }
  357. img { page-break-inside: avoid; }
  358. @page { margin: 2cm .5cm; }
  359. h1, h2, h3 { page-break-after: avoid; }
  360. p3 { orphans: 3; widows: 3; }
  361. img {
  362. max-width: 250px !important;
  363. max-height: 250px !important;
  364. }
  365. nav, aside { display: none; }
  366. }
  367. ul.with_columns {
  368. column-count: 1;
  369. }
  370. @media (min-width: 20em) {
  371. ul.with_columns {
  372. column-count: 2;
  373. }
  374. }
  375. @media (min-width: 50em) {
  376. ul.with_columns {
  377. column-count: 3;
  378. }
  379. }
  380. ul.with_two_columns {
  381. column-count: 1;
  382. }
  383. @media (min-width: 20em) {
  384. ul.with_two_columns {
  385. column-count: 1;
  386. }
  387. }
  388. @media (min-width: 50em) {
  389. ul.with_two_columns {
  390. column-count: 2;
  391. }
  392. }
  393. .gallery {
  394. display: flex;
  395. flex-wrap: wrap;
  396. justify-content: space-around;
  397. }
  398. .gallery figure img {
  399. margin-left: 1rem;
  400. margin-right: 1rem;
  401. }
  402. .gallery figure figcaption {
  403. font-family: 'ConcourseT3'
  404. }
  405. footer {
  406. font-family: 'ConcourseT3';
  407. display: flex;
  408. flex-direction: column;
  409. border-top: 3px solid white;
  410. padding: 4rem 0;
  411. background-color: #07486c;
  412. color: white;
  413. }
  414. footer > * {
  415. max-width: 50rem;
  416. margin: 0 auto;
  417. }
  418. footer a {
  419. color: #f1c40f;
  420. }
  421. footer .avatar {
  422. width: 200px;
  423. height: 200px;
  424. border-radius: 50%;
  425. float: left;
  426. -webkit-shape-outside: circle();
  427. shape-outside: circle();
  428. margin-right: 2rem;
  429. padding: 2px 5px 5px 2px;
  430. background: white;
  431. border-left: 1px solid #f1c40f;
  432. border-top: 1px solid #f1c40f;
  433. border-right: 5px solid #f1c40f;
  434. border-bottom: 5px solid #f1c40f;
  435. }
  436. </style>
  437. <h1>
  438. <span><a id="jumper" href="#jumpto" title="Un peu perdu ?">?</a></span>
  439. The interesting ideas in Datasette (archive)
  440. <time>Pour la pérennité des contenus liés. Non-indexé, retrait sur simple email.</time>
  441. </h1>
  442. <section>
  443. <article>
  444. <h3><a href="https://simonwillison.net/2018/Oct/4/datasette-ideas/">Source originale du contenu</a></h3>
  445. <p><a href="https://github.com/simonw/datasette">Datasette</a> (<a href="https://simonwillison.net/tags/datasette/">previously</a>) is my open source tool for exploring and publishing structured data. There are a lot of ideas embedded in Datasette. I realized that I haven’t put many of them into writing.</p>
  446. <p>
  447. <a href="#Publishing_readonly_data">Publishing read-only data</a><br/>
  448. <a href="#Bundling_the_data_with_the_code">Bundling the data with the code</a><br/>
  449. <a href="#SQLite_as_the_underlying_data_engine">SQLite as the underlying data engine</a><br/>
  450. <a href="#Farfuture_cache_expiration">Far-future cache expiration</a><br/>
  451. <a href="#Publishing_as_a_core_feature">Publishing as a core feature</a><br/>
  452. <a href="#License_and_source_metadata">License and source metadata</a><br/>
  453. <a href="#Facet_everything">Facet everything</a><br/>
  454. <a href="#Respect_for_CSV">Respect for CSV</a><br/>
  455. <a href="#SQL_as_an_API_language">SQL as an API language</a><br/>
  456. <a href="#Optimistic_query_execution_with_time_limits">Optimistic query execution with time limits</a><br/>
  457. <a href="#Keyset_pagination">Keyset pagination</a><br/>
  458. <a href="#Interactive_demos_based_on_the_unit_tests">Interactive demos based on the unit tests</a><br/>
  459. <a href="#Documentation_unit_tests">Documentation unit tests</a></p>
  460. <p>Datasette provides a read-only API to your data. It makes no attempt to deal with writes. Avoiding writes entirely is fundamental to a plethora of interesting properties, many of which are expanded on further below. In brief:</p>
  461. <ul>
  462. <li>Hosting web applications with no read/write persistence requirements is incredibly cheap in 2018—often free (both <a href="https://zeit.co/now">ZEIT Now</a> and a <a href="https://www.heroku.com/">Heroku</a> have generous free tiers). This is a big deal: even having to pay a few dollars a month is enough to dicentivise sharing data, since now you have to figure out who will pay and ensure the payments don’t expire in the future.</li>
  463. <li>Being read-only makes it trivial to scale: just add more instances, each with their own copy of the data. All of the hard problems in scaling web applications that relate to writable data stores can be skipped entirely.</li>
  464. <li>Since the database file is opened using SQLite’s <a href="https://www.sqlite.org/uri.html#uriimmutable">immutable mode</a>, we can accept arbitrary SQL queries with no risk of them corrupting the data.</li>
  465. </ul>
  466. <p>Any time your data changes, you need to publish a brand new copy of the whole database. With the right hosting this is easy: deploy a brand new copy of your data and application in parallel to your existing live deployment, then switch over incoming HTTP traffic to your API at the load balancer level. Heroku and Zeit Now both support this strategy out of the box.</p>
  467. <p>Since the data is read-only and is encapsulated in a single binary SQLite database file, we can bundle the data as part of the app. This means we can trivially create and publish Docker images that provide both the data and the API and UI for accessing it. We can also publish to any hosting provider that will allow us to run a Python application, without also needing to provision a mutable database.</p>
  468. <p>The <a href="https://datasette.readthedocs.io/en/stable/publish.html#datasette-package">datasette package</a> command takes one or more SQLite databases and bundles them together with the Datasette application in a single Docker image, ready to be deployed anywhere that can run Docker containers.</p>
  469. <p>Datasette encourages people to use <a href="https://www.sqlite.org/">SQLite</a> as a standard format for publishing data.</p>
  470. <p>Relational database are great: once you know how to use them, you can represent any data you can imagine using a carefully designed schema.</p>
  471. <p>What about data that’s too unstructured to fit a relational schema? SQLite includes excellent <a href="https://www.sqlite.org/json1.html">support for JSON data</a>—so if you can’t shape your data to fit a table schema you can instead store it as text blobs of JSON—and use SQLite’s JSON functions to filter by or extract specific fields.</p>
  472. <p>What about binary data? Even that’s covered: SQLite will happily store binary blobs. My <a href="https://github.com/simonw/datasette-render-images">datasette-render-images plugin</a> (<a href="https://datasette-render-images-demo.datasette.io/favicons-6a27915/favicons">live demo here</a>) is one example of a tool that works with binary image data stored in SQLite blobs.</p>
  473. <p>What if my data is too big? Datasette is not a “big data” tool, but if your definition of big data is something that won’t fit in RAM that threshold is growing all the time (2TB of RAM on a single AWS instance <a href="https://aws.amazon.com/about-aws/whats-new/2016/05/now-available-x1-instances-the-largest-amazon-ec2-memory-optimized-instance-with-2-tb-of-memory/">now costs less than $4/hour</a>).</p>
  474. <p>I’ve personally had great results from multiple GB SQLite databases and Datasette. The theoretical maximum size of a single SQLite database is <a href="https://www.sqlite.org/limits.html">around 140TB</a>.</p>
  475. <p>SQLite also has built-in support for <a href="https://www.sqlite.org/fts5.html">surprisingly good full-text search</a>, and thanks to being extensible via modules has excellent geospatial functionality in the form of the <a href="https://www.gaia-gis.it/fossil/libspatialite/index">SpatiaLite extension</a>. Datasette benefits enormously from this wider ecosystem.</p>
  476. <p>The reason most developers avoid SQLite for production web applications is that it doesn’t deal brilliantly with large volumes of concurrent writes. Since Datasette is read-only we can entirely ignore this limitation.</p>
  477. <p>Since the data in a Datasette instance never changes, why not cache calls to it forever?</p>
  478. <p>Datasette sends a far future HTTP cache expiry header with every API response. This means that browsers will only ever fetch data the first time a specific URL is accessed, and if you host Datasette behind a CDN such as <a href="https://www.fastly.com/">Fastly</a> or <a href="https://www.cloudflare.com/">Cloudflare</a> each unique API call will hit Datasette just once and then be cached essentially forever by the CDN.</p>
  479. <p>This means it’s safe to deploy a JavaScript app using an inexpensively hosted Datasette-backed API to the front page of even a high traffic site—the CDN will easily take the load.</p>
  480. <p>Zeit added Cloudflare to every deployment (even their free tier) <a href="https://zeit.co/blog/now-cdn">back in July</a>, so if you are hosted there you get this CDN benefit for free.</p>
  481. <p>What if you re-publish an updated copy of your data? Datasette has that covered too. You may have noticed that every Datasette database gets a hashed suffix automatically when it is deployed:</p>
  482. <p><a href="https://fivethirtyeight.datasettes.com/fivethirtyeight-c9e67c4">https://fivethirtyeight.datasettes.com/fivethirtyeight-c9e67c4</a></p>
  483. <p>This suffix is based on the SHA256 hash of the entire database file contents—so any change to the data will result in new URLs. If you query a previous suffix Datasette will notice and redirect you to the new one.</p>
  484. <p>If you know you’ll be changing your data, you can build your application against the non-suffixed URL. This will not be cached and will always 302 redirect to the correct version (and these redirects are extremely fast).</p>
  485. <p><a href="https://fivethirtyeight.datasettes.com/fivethirtyeight/alcohol-consumption%2Fdrinks.json">https://fivethirtyeight.datasettes.com/fivethirtyeight/alcohol-consumption%2Fdrinks.json</a></p>
  486. <p>The redirect sends an HTTP/2 push header such that if you are running behind a CDN that understands push (<a href="https://blog.cloudflare.com/announcing-support-for-http-2-server-push-2/">such as Cloudflare</a>) your browser won’t have to make two requests to follow the redirect. You can use the Chrome DevTools to see this in action:</p>
  487. <p><img alt="Chrome DevTools showing a redirect initiated by an HTTP/2 push" src="https://static.simonwillison.net/static/2018/http2-push.png"/></p>
  488. <p>And finally, if you need to opt out of HTTP caching for some reason you can disable it on a per-request basis by including <code>?_ttl=0</code> <a href="https://datasette.readthedocs.io/en/stable/json_api.html#special-json-arguments">in the URL query string</a>. —for example, if you want to return a random member of the Avengers it doesn’t make sense to cache the response:</p>
  489. <p><a href="https://fivethirtyeight.datasettes.com/fivethirtyeight?sql=select+*+from+%5Bavengers%2Favengers%5D+order+by+random()+limit+1&amp;_ttl=0">https://fivethirtyeight.datasettes.com/fivethirtyeight?sql=select+*+from+[avengers%2Favengers]+order+by+random()+limit+1&amp;_ttl=0</a></p>
  490. <p>Datasette aims to reduce the friction for publishing interesting data online as much as possible.</p>
  491. <p>To this end, Datasette includes <a href="https://datasette.readthedocs.io/en/stable/publish.html">a “publish” subcommand</a>:</p>
  492. <pre><code># deploy to Heroku
  493. datasette publish heroku mydatabase.db
  494. # Or deploy to Zeit Now
  495. datasette publish now mydatabase.db
  496. </code></pre>
  497. <p>These commands take one or more SQLite databases, upload them to a hosting provider, configure a Datasette instance to serve them and return the public URL of the newly deployed application.</p>
  498. <p>Out of the box, Datasette can publish to either Heroku or to Zeit Now. The <a href="https://datasette.readthedocs.io/en/stable/plugins.html#publish-subcommand-publish">publish_subcommand plugin hook</a> means other providers can be supported by writing plugins.</p>
  499. <p>Datasette believes that data should be accompanied by source information and a license, whenever possible. The <a href="https://datasette.readthedocs.io/en/stable/metadata.html">metadata.json file</a> that can be bundled with your data supports these. You can also provide source and license information when you run <code>datasette publish</code>:</p>
  500. <pre><code>datasette publish fivethirtyeight.db \
  501. --source="FiveThirtyEight" \
  502. --source_url="https://github.com/fivethirtyeight/data" \
  503. --license="CC BY 4.0" \
  504. --license_url="https://creativecommons.org/licenses/by/4.0/"
  505. </code></pre>
  506. <p>When you use these options Datasette will create the corresponding <code>metadata.json</code> file for you as part of the deployment.</p>
  507. <p>I really love faceted search: it’s the first tool I turn to whenever I want to start understanding a collection of data. I’ve built faceted search engines on top of Solr, Elasticsearch and PostgreSQL and many of my favourite tools (like Splunk and Datadog) have it as a core feature.</p>
  508. <p>Datasette automatically attempts to calculate facets against every table. You can read <a href="https://simonwillison.net/2018/May/20/datasette-facets/">more about the Datasette Facets feature here</a>—as a huge faceted search fan it’s one of my all-time favourite features of the project. Now I can add SQLite to the list of technologies I’ve used to build faceted search!</p>
  509. <p>CSV is by far the most common format for sharing and publishing data online. Almost every useful data tool has the ability to export to it, and it remains the lingua franca of spreadsheet import and export.</p>
  510. <p>It has many flaws: it can’t easily represent nested data structures, escaping rules for values containing commas are inconsistently implemented and it doesn’t have a standard way of representing character encoding.</p>
  511. <p>Datasette aims to promote SQLite as a much better default format for publishing data. I would much rather download a .db file full of pre-structured data than download a .csv and then have to re-structure it as a separate piece of work.</p>
  512. <p>But interacting well with the enormous CSV ecosystem is essential. Datasette has <a href="https://datasette.readthedocs.io/en/stable/csv_export.html">deep CSV export functionality</a>: any data you can see, you can export—including the results of arbitrary SQL queries. If your query can be paginated Datasette can stream down every page in a single CSV file for you.</p>
  513. <p>Datasette’s sister-tool <a href="https://github.com/simonw/csvs-to-sqlite">csvs-to-sqlite</a> handles the other side of the equation: importing data from CSV into SQLite tables. And the <a href="https://simonwillison.net/2018/Jan/17/datasette-publish/">Datasette Publish web application</a> allows users to upload their CSVs and have them deployed directly to their own fresh Datasette instance—no command line required.</p>
  514. <p>A lot of people these days are excited about <a href="https://graphql.org/">GraphQL</a>, because it allows API clients to request exactly the data they need, including traversing into related objects in a single query.</p>
  515. <p>Guess what? SQL has been able to do that since the 1970s!</p>
  516. <p>There are a number of reasons most APIs don’t allow people to pass them arbitrary SQL queries:</p>
  517. <ul>
  518. <li>Security: we don’t want people messing up our data</li>
  519. <li>Performance: what if someone sends an accidental (or deliberate) expensive query that exhausts our resources?</li>
  520. <li>Hiding implementation details: if people write SQL against our API we can never change the structure of our database tables</li>
  521. </ul>
  522. <p>Datasette has answers to all three.</p>
  523. <p>On security: the data is read-only, using SQLite’s immutable mode. You can’t damage it with a query—INSERT and UPDATEs will simply throw harmless errors.</p>
  524. <p>On performance: SQLite has a mechanism for canceling queries that take longer than a certain threshold. Datasette sets this to one second by default, though you can <a href="https://datasette.readthedocs.io/en/stable/config.html#sql-time-limit-ms">alter that configuration</a> if you need to (I often bump it up to ten seconds when exploring multi-GB data on my laptop).</p>
  525. <p>On hidden implementation details: since we are publishing static data rather than maintaining an evolving API, we can mostly ignore this issue. If you are really worried about it you can take advantage of <a href="https://datasette.readthedocs.io/en/stable/sql_queries.html#canned-queries">canned queries</a> and <a href="https://datasette.readthedocs.io/en/stable/sql_queries.html#views">SQL view definitions</a> to expose a carefully selected forward-compatible view into your data.</p>
  526. <p>I mentioned Datasette’s SQL time limits above. These aren’t just there to avoid malicious queries: the idea of “optimistic SQL evaluation” is baked into some of Datasette’s core features.</p>
  527. <p>Consider <a href="https://datasette.readthedocs.io/en/stable/facets.html#suggested-facets">suggested facets</a>—where Datasette inspects any table you view and tries to suggest columns that are worth faceting against.</p>
  528. <p>The way this works is Datasette loops over <em>every</em> column in the table and runs a query to see if there are less than 20 unique values for that column. On a large table this could take a prohibitive amount of time, so Datasette sets an aggressive timeout on those queries: <a href="https://datasette.readthedocs.io/en/stable/config.html#facet-suggest-time-limit-ms">just 50ms</a>. If the query fails to run in that time it is silently dropped and the column is not listed as a suggested facet.</p>
  529. <p>Datasette’s JSON API provides a mechanism for JavaScript applications to use that same pattern. If you add <code>?_timelimit=20</code> to any Datasette API call, the underlying query will only get 20ms to run. If it goes over you’ll get a very fast error response from the API. This means you can design your own features that attempt to optimistically run expensive queries without damaging the performance of your app.</p>
  530. <h3/>
  531. <p>SQL pagination using OFFSET/LIMIT has a fatal flaw: if you request page number 300 at 20 per page the underlying SQL engine needs to calculate and sort all 6,000 preceding rows before it can return the 20 you have requested.</p>
  532. <p>This does not scale at all well.</p>
  533. <p><a href="https://use-the-index-luke.com/sql/partial-results/fetch-next-page">Keyset pagination</a> (often known by other names, including cursor-based pagination) is a far more efficient way to paginate through data. It works against ordered data. Each page is returned with a token representing the last record you saw, then when you request the next page the engine merely has to filter for records that are greater than that tokenized value and scan through the next 20 of them.</p>
  534. <p>(Actually, it scans through 21. By requesting one more record than you intend to display you can detect if another page of results exists—if you ask for 21 but get back 20 or less you know you are on the last page.)</p>
  535. <p>Datasette’s table view includes a sophisticated implementation of keyset pagination.</p>
  536. <p>Datasette defaults to sorting by primary key (or SQLite rowid). This is perfect for efficient pagination: running a select against the primary key column for values greater than X is one of the fastest range scan queries any database can support. This allows users to paginate as deep as they like without paying the offset/limit performance penalty.</p>
  537. <p>This is also how the “export all rows as CSV” option works: when you select that option, Datasette opens a stream to your browser and internally starts keyset-pagination over the entire table. This keeps resource usage in check even while streaming back millions of rows.</p>
  538. <p>Here’s where Datasette gets fancy: it handles keyset pagination for any other sort order as well. If you sort by any column and click “next” you’ll be requesting the next set of rows after the last value you saw. And this even works for columns containing duplicate values: If you sort by such a column, Datasette actually sorts by that column combined with the primary key. The “next” pagination token it generates encodes both the sorted value and the primary key, allowing it to correctly serve you the next page when you click the link.</p>
  539. <p>Try clicking “next” <a href="https://latest.datasette.io/fixtures-dd88475/sortable?_sort_desc=sortable">on this page</a> to see keyset pagination against a sorted column in action.</p>
  540. <p>I love interactive demos. I decided it would be useful if every single release of Datasette had a permanent interactive demo illustrating its features.</p>
  541. <p>Thanks to Zeit Now, this was pretty easy to set up. I’ve actually taken it a step further: every successful push to master on GitHub is also deployed to a permanent URL.</p>
  542. <p>Some examples:</p>
  543. <p>The database that is used for this demo is the exact same database that is created by Datasette’s <a href="https://github.com/simonw/datasette/blob/master/tests/fixtures.py">unit test fixtures</a>. The unit tests are already designed to exercise every feature, so reusing them for a live demo makes a lot of sense.</p>
  544. <p>You can view this test database on your own machine by checking out the full Datasette repository from GitHub and running the following:</p>
  545. <pre><code>python tests/fixtures.py fixtures.db metadata.json
  546. datasette fixtures.db -m metadata.json
  547. </code></pre>
  548. <p>Here’s <a href="https://github.com/simonw/datasette/blob/96af802352e49e35751e295e9846aa39c5e22311/.travis.yml#L23-L42">the code in the Datasette Travis CI configuration</a> that deploys a live demo for every commit and every released tag.</p>
  549. <p>I wrote about the <a href="https://simonwillison.net/2018/Jul/28/documentation-unit-tests/">Documentation unit tests</a> pattern back in July.</p>
  550. <p>Datasette’s unit tests <a href="https://github.com/simonw/datasette/blob/master/tests/test_docs.py">include some assertions</a> that ensure that every plugin hook, configuration setting and underlying view class is mentioned in the documentation. A commit or pull request that adds or modifies these without also updating the documentation (or at least ensuring there is a corresponding heading in the docs) will fail its tests.</p>
  551. <p>Datasette’s <a href="http://datasette.readthedocs.io/">documentation</a> is in pretty good shape now, and <a href="https://datasette.readthedocs.io/en/stable/changelog.html">the changelog</a> provides a detailed overview of new features that I’ve added to the project. I presented Datasette at the PyBay conference in August and I’ve published <a href="https://static.simonwillison.net/static/2018/pybay-datasette/">my annotated slides</a> from that talk. I was <a href="https://changelog.com/podcast/296#t=00:54:45">interviewed about Datasette</a> for the Changelog podcast in May and <a href="https://simonwillison.net/2018/May/9/changelog/">my notes from that conversation</a> include some of my favourite demos.</p>
  552. <p>Datasette now has an official Twitter account—you can follow <a href="https://twitter.com/datasetteproj">@datasetteproj</a> there for updates about the project.</p>
  553. </article>
  554. </section>
  555. <nav id="jumpto">
  556. <p>
  557. <a href="/david/blog/">Accueil du blog</a> |
  558. <a href="https://simonwillison.net/2018/Oct/4/datasette-ideas/">Source originale</a> |
  559. <a href="/david/stream/2019/">Accueil du flux</a>
  560. </p>
  561. </nav>
  562. <footer>
  563. <div>
  564. <img src="/static/david/david-larlet-avatar.jpg" loading="lazy" class="avatar" width="200" height="200">
  565. <p>
  566. Bonjour/Hi!
  567. Je suis <a href="/david/" title="Profil public">David&nbsp;Larlet</a>, je vis actuellement à Montréal et j’alimente cet espace depuis 15 ans. <br>
  568. Si tu as apprécié cette lecture, n’hésite pas à poursuivre ton exploration. Par exemple via les <a href="/david/blog/" title="Expériences bienveillantes">réflexions bimestrielles</a>, la <a href="/david/stream/2019/" title="Pensées (dés)articulées">veille hebdomadaire</a> ou en t’abonnant au <a href="/david/log/" title="S’abonner aux publications via RSS">flux RSS</a> (<a href="/david/blog/2019/flux-rss/" title="Tiens c’est quoi un flux RSS ?">so 2005</a>).
  569. </p>
  570. <p>
  571. Je m’intéresse à la place que je peux avoir dans ce monde. En tant qu’humain, en tant que membre d’une famille et en tant qu’associé d’une coopérative. De temps en temps, je fais aussi des <a href="https://github.com/davidbgk" title="Principalement sur Github mais aussi ailleurs">trucs techniques</a>. Et encore plus rarement, <a href="/david/talks/" title="En ce moment je laisse plutôt la place aux autres">j’en parle</a>.
  572. </p>
  573. <p>
  574. Voici quelques articles choisis :
  575. <a href="/david/blog/2019/faire-equipe/" title="Accéder à l’article complet">Faire équipe</a>,
  576. <a href="/david/blog/2018/bivouac-automnal/" title="Accéder à l’article complet">Bivouac automnal</a>,
  577. <a href="/david/blog/2018/commodite-effondrement/" title="Accéder à l’article complet">Commodité et effondrement</a>,
  578. <a href="/david/blog/2017/donnees-communs/" title="Accéder à l’article complet">Des données aux communs</a>,
  579. <a href="/david/blog/2016/accompagner-enfant/" title="Accéder à l’article complet">Accompagner un enfant</a>,
  580. <a href="/david/blog/2016/senior-developer/" title="Accéder à l’article complet">Senior developer</a>,
  581. <a href="/david/blog/2016/illusion-sociale/" title="Accéder à l’article complet">L’illusion sociale</a>,
  582. <a href="/david/blog/2016/instantane-scopyleft/" title="Accéder à l’article complet">Instantané Scopyleft</a>,
  583. <a href="/david/blog/2016/enseigner-web/" title="Accéder à l’article complet">Enseigner le Web</a>,
  584. <a href="/david/blog/2016/simplicite-defaut/" title="Accéder à l’article complet">Simplicité par défaut</a>,
  585. <a href="/david/blog/2016/minimalisme-esthetique/" title="Accéder à l’article complet">Minimalisme et esthétique</a>,
  586. <a href="/david/blog/2014/un-web-omni-present/" title="Accéder à l’article complet">Un web omni-présent</a>,
  587. <a href="/david/blog/2014/manifeste-developpeur/" title="Accéder à l’article complet">Manifeste de développeur</a>,
  588. <a href="/david/blog/2013/confort-convivialite/" title="Accéder à l’article complet">Confort et convivialité</a>,
  589. <a href="/david/blog/2013/testament-numerique/" title="Accéder à l’article complet">Testament numérique</a>,
  590. et <a href="/david/blog/" title="Accéder aux archives">bien d’autres…</a>
  591. </p>
  592. <p>
  593. On peut <a href="mailto:david%40larlet.fr" title="Envoyer un courriel">échanger par courriel</a>. Si éventuellement tu souhaites que l’on travaille ensemble, tu devrais commencer par consulter le <a href="http://larlet.com">profil dédié à mon activité professionnelle</a> et/ou contacter directement <a href="http://scopyleft.fr/">scopyleft</a>, la <abbr title="Société coopérative et participative">SCOP</abbr> dont je fais partie depuis six ans. Je recommande au préalable de lire <a href="/david/blog/2018/cout-site/" title="Attention ce qui va suivre peut vous choquer">combien coûte un site</a> et pourquoi je suis plutôt favorable à une <a href="/david/pro/devis/" title="Discutons-en !">non-demande de devis</a>.
  594. </p>
  595. <p>
  596. Je ne traque pas ta navigation mais mon
  597. <abbr title="Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33.184162340">hébergeur</abbr>
  598. conserve des logs d’accès.
  599. </p>
  600. </div>
  601. </footer>
  602. <script type="text/javascript">
  603. ;(_ => {
  604. const jumper = document.getElementById('jumper')
  605. jumper.addEventListener('click', e => {
  606. e.preventDefault()
  607. const anchor = e.target.getAttribute('href')
  608. const targetEl = document.getElementById(anchor.substring(1))
  609. targetEl.scrollIntoView({behavior: 'smooth'})
  610. })
  611. })()
  612. </script>