<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Jan&#x27;s Blog</title>
    <subtitle>Jan&#x27;s personal blog.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://jflessau.com/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://jflessau.com"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-01-24T00:00:00+00:00</updated>
    <id>https://jflessau.com/atom.xml</id>
    <entry xml:lang="en">
        <title>Retro Cube</title>
        <published>2026-01-24T00:00:00+00:00</published>
        <updated>2026-01-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Jan Flessau
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jflessau.com/projects/retro-cube/"/>
        <id>https://jflessau.com/projects/retro-cube/</id>
        
        <content type="html" xml:base="https://jflessau.com/projects/retro-cube/">&lt;p&gt;A while ago, I became absolutely fascinated with cyberdecks. On my journey to design and build my own, I started small, and this is the result:&lt;&#x2F;p&gt;
&lt;video controls width=&quot;100%&quot; alt=&quot;Rotating the dial knob of the retro-cube to navigate between the views.&quot; autoplay muted loop playsinline&gt;
  &lt;source src=&quot;retro-cube.mp4&quot; type=&quot;video&#x2F;mp4&quot;&gt;
  Your browser does not support the video tag.
&lt;&#x2F;video&gt;
&lt;p&gt;It&#x27;s a 3D-printed housing for a Raspberry Pi Zero W 2, an SSD1309 OLED display, and a rotary encoder for navigation.&lt;&#x2F;p&gt;
&lt;p&gt;Before this, I used Blender to design &lt;a href=&quot;&#x2F;tags&#x2F;3d-printing&quot;&gt;my custom 3D-printed things&lt;&#x2F;a&gt;, simply because I&#x27;m &lt;a href=&quot;&#x2F;projects&#x2F;drawer-inserts&quot;&gt;familiar with Blender&lt;&#x2F;a&gt; and was a little intimidated to learn CAD. But then I discovered &lt;a href=&quot;https:&#x2F;&#x2F;www.freecad.org&#x2F;&quot;&gt;FreeCAD&lt;&#x2F;a&gt; and thought it would be a good time to actually try it. It was so worth it. It took about two days, and I won&#x27;t go back to Blender.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;features&quot;&gt;Features&lt;&#x2F;h3&gt;
&lt;p&gt;The Retro Cube has three views. You can switch between them by rotating the dial. If you press it down, the display turns off completely, which can be useful at night. The three views are:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Clock&lt;&#x2F;li&gt;
&lt;li&gt;Weather dashboard&lt;&#x2F;li&gt;
&lt;li&gt;Message&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;The first two are rather basic, but the last one is the most interesting.
There is a server running at &lt;a href=&quot;https:&#x2F;&#x2F;retro-cube.jflessau.com&quot;&gt;retro-cube.jflessau.com&lt;&#x2F;a&gt; serving a basic-auth protected endpoint that returns a text message.&lt;br &#x2F;&gt;
The Pi fetches that text at a configurable interval and displays it in the respective view. The second endpoint of the server is a web form where users can alter the message.&lt;&#x2F;p&gt;
&lt;p&gt;Soooooooo&lt;&#x2F;p&gt;
&lt;p&gt;If you want to say hi or something else, go to &lt;a href=&quot;https:&#x2F;&#x2F;retro-cube.jflessau.com&#x2F;form&quot;&gt;retro-cube.jflessau.com&lt;&#x2F;a&gt;, log in with&lt;&#x2F;p&gt;
&lt;pre style=&quot;background-color:#212733;color:#ccc9c2;&quot;&gt;&lt;code&gt;&lt;span&gt;user: retro
&lt;&#x2F;span&gt;&lt;span&gt;password: cube
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;and send something :)&lt;&#x2F;p&gt;
&lt;p&gt;And in case you want to build your own Retro Cube, head over to the &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jflessau&#x2F;retro-cube&quot;&gt;repo&lt;&#x2F;a&gt; for the code, STL files for printing, and a list of parts.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Gasoline Price Cycles</title>
        <published>2025-12-23T00:00:00+00:00</published>
        <updated>2025-12-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Jan Flessau
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jflessau.com/projects/gassoline-prices/"/>
        <id>https://jflessau.com/projects/gassoline-prices/</id>
        
        <content type="html" xml:base="https://jflessau.com/projects/gassoline-prices/">&lt;p&gt;Gas stations in Germany are required by law to send their prices to a central database, maintained by the German Cartel Office. Regular people like you and me can apply to access the data for the purpose of building websites or apps to help consumers find cheap gas stations.&lt;&#x2F;p&gt;
&lt;p&gt;A while ago a friend and I did just that and built &lt;a href=&quot;https:&#x2F;&#x2F;mcsprit.de&quot;&gt;https:&#x2F;&#x2F;mcsprit.de&lt;&#x2F;a&gt;.&lt;br &#x2F;&gt;
There are many of those services; the Cartel Office &lt;a href=&quot;https:&#x2F;&#x2F;www.bundeskartellamt.de&#x2F;DE&#x2F;Aufgaben&#x2F;MarkttransparenzstelleFuerKraftstoffe&#x2F;TankApps&#x2F;tankapps_node.html&quot;&gt;lists&lt;&#x2F;a&gt; them on their website.&lt;&#x2F;p&gt;
&lt;p&gt;While working with the data, I found the following:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;projects&#x2F;gassoline-prices&#x2F;gasoline-price-cycles-germany.jpg&quot; alt=&quot;Gasoline Price Cycles&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;A chart of average prices in euros for Diesel, Super E5 and Super E10 of all &lt;code&gt;15075&lt;&#x2F;code&gt; gas stations in Germany from &lt;code&gt;2025-12-21 5:53&lt;&#x2F;code&gt; to &lt;code&gt;2025-12-21 21:47&lt;&#x2F;code&gt; at 10 minute intervals.&lt;&#x2F;p&gt;
&lt;p&gt;There are sudden increases followed by gradual decreases in the prices of all three fuel types, and I wondered why.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;demand&quot;&gt;Demand&lt;&#x2F;h2&gt;
&lt;p&gt;My initial thought was: The first spike is caused by early commuters, starting to work at 7:00, then a second wave starting at 8:00, and so on; elevated demand causing prices to rise.&lt;&#x2F;p&gt;
&lt;p&gt;But I would expect a more wavy pattern then, not these sudden jumps. Also, there are too many spikes. The amount of cars on the road does not really spike, I thought.&lt;&#x2F;p&gt;
&lt;p&gt;To check that, I downloaded a dataset from Hamburg&#x27;s &lt;a href=&quot;https:&#x2F;&#x2F;transparenz.hamburg.de&#x2F;&quot;&gt;Transparency Portal&lt;&#x2F;a&gt;. Hamburg publishes data of infrared traffic sensors with a resolution of 15 minutes. Here is the average amount of cars passing a sensor per 15 minutes from all 52 active sensors plotted for &lt;code&gt;2025-12-21&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;projects&#x2F;gassoline-prices&#x2F;car-volume-hamburg-avg-15-min.png&quot; alt=&quot;Average Car Volume in Hamburg per 15 Minutes&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;I am using the volume of cars on the road as a proxy for demand at gas stations here. The curve does not show many sudden jumps, with an exception at 20:00. To be fair, this is car volume in Hamburg only, not nationwide. But fuel prices in Hamburg alone show the same pattern as the nationwide average. This suggests that, at least in Hamburg, the price patterns are not correlated with demand.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;supply&quot;&gt;Supply&lt;&#x2F;h2&gt;
&lt;p&gt;Gas stations have to buy their fuel at some price, which I thought could vary a lot over the day. The German Cartel Office says:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;After the oil has been processed in refineries, the oil products are sold on wholesale markets &lt;strong&gt;mainly on the basis of long-term supply contracts&lt;&#x2F;strong&gt; (term contracts); the volumes traded on the spot market are much smaller.
&lt;cite&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.bundeskartellamt.de&#x2F;SharedDocs&#x2F;Meldung&#x2F;EN&#x2F;Pressemitteilungen&#x2F;2025&#x2F;02_19_2025_SU_Raffinerien.html&quot;&gt;Press Release on Sector Inquiry on Refineries&lt;&#x2F;a&gt;&lt;&#x2F;cite&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Gas stations mostly buy fuel based on long-term contracts. And those &lt;em&gt;long-term&lt;&#x2F;em&gt; contracts are pretty long:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;In detail, the inquiry found that well over 50 per cent of all volumes traded at the wholesale level, irrespective of the product traded, are based on term contracts, which typically have a term of one year.
&lt;cite&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.bundeskartellamt.de&#x2F;SharedDocs&#x2F;Publikation&#x2F;EN&#x2F;SectorInquiries&#x2F;Sektor_inquiry_Raffinerien_Executive_Summary.pdf?__blob=publicationFile&amp;amp;v=5&quot;&gt;Sector inquiry 2025, Cartel Office&lt;&#x2F;a&gt;&lt;&#x2F;cite&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;The contracts might last a year, but the price gas stations pay for fuel...&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;...is based on the price
assessment provided at the contractually agreed point in time (e.g. the time of supply) or on the
average prices over a specified period (usually the month or week of supply)
&lt;cite&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.bundeskartellamt.de&#x2F;SharedDocs&#x2F;Publikation&#x2F;EN&#x2F;SectorInquiries&#x2F;Sektor_inquiry_Raffinerien_Executive_Summary.pdf?__blob=publicationFile&amp;amp;v=5&quot;&gt;Sector inquiry 2025, Cartel Office&lt;&#x2F;a&gt;&lt;&#x2F;cite&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;So prices change on a monthly or weekly bases. If the price assessment is based on delivery, then the price changes as often as the gas station gets deliveries. Some stations get multiple deliveries per day, but I doubt they are as frequent or &lt;em&gt;synchronized&lt;&#x2F;em&gt; across stations as the price changes in the chart.&lt;&#x2F;p&gt;
&lt;p&gt;From what I can tell, supply prices are unlikely to be the cause of the price pattern either.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;price-cycles&quot;&gt;Price cycles&lt;&#x2F;h2&gt;
&lt;p&gt;If supply and demand are not correlated with the price pattern, then what is?&lt;&#x2F;p&gt;
&lt;p&gt;The best theory I&#x27;ve found to explain this particular pattern is described in the &lt;a href=&quot;https:&#x2F;&#x2F;www.ifo.de&#x2F;DocDL&#x2F;cesifo1_wp11971.pdf&quot;&gt;Competitive price cycles paper&lt;&#x2F;a&gt;. They specifically look at diesel prices in Germany in 2019. It&#x27;s a derivative of the well-known &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Edgeworth_price_cycle&quot;&gt;Edgeworth price cycle&lt;&#x2F;a&gt; theory.
Bottom line, very broadly, is this:&lt;&#x2F;p&gt;
&lt;p&gt;Some customers are price sensitive and buy at stations with lower prices, others are more brandloyal and shop at their preferred station, even if prices are a little higher. From here it goes like this:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;To get more nonloyal customers, ALL stations lower prices&lt;&#x2F;li&gt;
&lt;li&gt;Prices lower further over time, with every station trying to undercut the others&lt;&#x2F;li&gt;
&lt;li&gt;At some point prices have to rise, otherwise stations make no profit&lt;&#x2F;li&gt;
&lt;li&gt;Stations with loyal customers raise prices first and drastically, but not so much that they lose loyal customers&lt;&#x2F;li&gt;
&lt;li&gt;Other stations follow quickly, as their current prices are way too low compared to competitors&lt;&#x2F;li&gt;
&lt;li&gt;Repeat&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The key points here, I think, are that bigger brands with more loyal customers initiate the price increases. There is no point in ramping up slowly, because most other stations will still be cheaper anyway. So they do it quickly and drastically. And they can do so because they can be somewhat sure customers loyal to them will stay.&lt;&#x2F;p&gt;
&lt;p&gt;Other gas stations then follow very quickly, leaving little time for consumers to benefit from the lowest prices. This raises the question...&lt;&#x2F;p&gt;
&lt;h3 id=&quot;where-do-gas-stations-get-their-competitor-s-prices-from&quot;&gt;Where do gas stations get their competitor&#x27;s prices from?&lt;&#x2F;h3&gt;
&lt;p&gt;The theory above assumes that gas stations know their competitors&#x27; prices in (almost) real time.&lt;&#x2F;p&gt;
&lt;p&gt;Consumers are using the fuel price apps to find cheap stations. It would be naive to think that gas station owners don&#x27;t use these services as well. In fact, the central database maintained by the Cartel Office is, in my opinion, the only practical way to get near-real-time prices of competitors.&lt;&#x2F;p&gt;
&lt;p&gt;Earlier, we looked at the average prices across all gas stations in Germany, but the pattern is visible at individual stations as well:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;projects&#x2F;gassoline-prices&#x2F;gasoline-price-cycles-two-gas-stations.gif&quot; alt=&quot;Switching between two chart back and forth to show the similarity of their price changes over the course of a day.&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;This GIF shows the prices of two individual stations on the same day (one frame per station). The stations are on the same road, about 5 km apart. One is a JET station, the other is an OIL! station. As far as I know, JET and OIL! are not part of the same company. There is also no clear line of sight between the two stations. They can&#x27;t see each other&#x27;s price boards. Yet, their prices are pretty much in sync down to just a couple of minutes.&lt;&#x2F;p&gt;
&lt;p&gt;This is not an isolated case. See for yourself by picking pairs of stations in one of the price apps &lt;a href=&quot;https:&#x2F;&#x2F;www.bundeskartellamt.de&#x2F;DE&#x2F;Aufgaben&#x2F;MarkttransparenzstelleFuerKraftstoffe&#x2F;TankApps&#x2F;tankapps_node.html&quot;&gt;listed&lt;&#x2F;a&gt; by the Cartel Office.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;are-gas-stations-allowed-to-use-the-data&quot;&gt;Are gas stations allowed to use the data?&lt;&#x2F;h3&gt;
&lt;p&gt;Disclaimer: I&#x27;m not a lawyer.&lt;&#x2F;p&gt;
&lt;p&gt;To access the real-time data, you have to apply at the Cartel Office. You have to use your access to build some kind of publicly available price app or website for consumers.&lt;&#x2F;p&gt;
&lt;p&gt;The Cartel Office can revoke access to the data for price apps if they find that data is provided to, say, mineral oil companies. Aside from that, there seems to be no kind of punishment for either services providing the data to mineral oil companies or mineral oil companies using the data.&lt;&#x2F;p&gt;
&lt;p&gt;So mineral oil companies cannot directly access the data nor apply for access themselves. But they can get the data from the same price apps consumers are using. Since the apps are required to be public, there is little to prevent gas stations from using them to get competitor prices.&lt;&#x2F;p&gt;
&lt;p&gt;Note that applying for access to the data is completely free, yet a bit of a &lt;a href=&quot;https:&#x2F;&#x2F;www.bundeskartellamt.de&#x2F;SharedDocs&#x2F;Publikation&#x2F;DE&#x2F;Sonstiges&#x2F;MTS-K&#x2F;Hilfestellung_Antragsgestaltung.pdf?__blob=publicationFile&amp;amp;v=10&quot;&gt;bureaucratic hassle&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;To be clear, I strongly believe the Cartel Office has this database for the sake of consumers. Station owners accessing the data is just inevitable.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;are-consumers-getting-more-transparency&quot;&gt;Are consumers getting more transparency?&lt;&#x2F;h2&gt;
&lt;p&gt;Thanks to the central database, prices are very transparent. But by now they change so frequently that it is questionable if consumers really benefit. How useful is it to know the price at a gas station if it might change again before you get there?&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;While prices were changed around four to five times a day in 2014, the number of price changes already reached an average of 18 times a day in early 2024 and is still on the rise.
&lt;cite&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.bundeskartellamt.de&#x2F;SharedDocs&#x2F;Meldung&#x2F;EN&#x2F;Pressemitteilungen&#x2F;2025&#x2F;02_19_2025_SU_Raffinerien.html&quot;&gt;Press Release on Sector Inquiry on Refineries&lt;&#x2F;a&gt;&lt;&#x2F;cite&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;what-should-consumers-do&quot;&gt;What should consumers do?&lt;&#x2F;h2&gt;
&lt;p&gt;Price differences between stations at any given moment are still relevant and differ by several cents per liter. For example, stations near highways tend to be much more expensive.&lt;&#x2F;p&gt;
&lt;p&gt;Using an app to find a station that&#x27;s cheaper &lt;em&gt;right now&lt;&#x2F;em&gt; is still useful. But actually getting the exact price an app shows you is unlikely when refueling during these price cycles. More so when you have to drive a while to get to the gas station.&lt;&#x2F;p&gt;
&lt;p&gt;If you&#x27;re feeling lucky, you can try to time your refueling just before a price spike. If you can wait, refuel later in the day. And if you have time and want to play safe, find a cheap station at night. Most stations are pretty expensive at night, but if you find a cheap one, it&#x27;s also likely to stay cheap for a while:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;projects&#x2F;gassoline-prices&#x2F;gas-station-prices-at-night.png&quot; alt=&quot;Gasoline Prices Late at Night&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The chart shows fuel prices of one station for just over 24 hours. Note that there are no changes at all between &lt;code&gt;2025-12-20 22:00&lt;&#x2F;code&gt; and &lt;code&gt;2025-12-21 05:00&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;regulation&quot;&gt;Regulation&lt;&#x2F;h2&gt;
&lt;p&gt;With a &lt;a href=&quot;https:&#x2F;&#x2F;www.bundesrat.de&#x2F;DE&#x2F;plenum&#x2F;bundesrat-kompakt&#x2F;25&#x2F;1059&#x2F;23.html?view=main%5BDrucken%5D&quot;&gt;resolution passed on 21 November 2025&lt;&#x2F;a&gt;, the German Bundesrat (Federal Council) calls on the federal government to examine how petrol prices can be made more transparent for consumers, specifically by limiting the number of price increases per day.&lt;&#x2F;p&gt;
&lt;p&gt;It is not safe to say that limits on price changes would benefit customers after all. Stations may set prices a bit higher by default to account for less frequent opportunities to increase them later. But customers would at least have a more stable price environment to make decisions in.&lt;&#x2F;p&gt;
&lt;p&gt;Austria &lt;a href=&quot;https:&#x2F;&#x2F;www.wko.at&#x2F;ktn&#x2F;transport-verkehr&#x2F;garagen-tankstellen-serviceunternehmungen&#x2F;spritpreisverordnung&quot;&gt;implemented such limits&lt;&#x2F;a&gt;, allowing for just one price increase per day, exactly at 12:00. Price decreases are allowed at any time. As a result, the best time to refuel in Austria is just before 12:00, while prices after that are probably higher than they would be without the regulation.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h2&gt;
&lt;p&gt;Real-time fuel price data can be used by consumers &lt;em&gt;and&lt;&#x2F;em&gt; gas stations to their advantage. The situation for customers is not ideal as prices change often and in sync with other stations. The price you see in an app might not be the price you pay when you get there. And since most stations&#x27; prices rise and fall together, looking out for alternative stations is less useful, since all are either up or down at the same time.&lt;&#x2F;p&gt;
&lt;p&gt;Putting regulation in place to limit price changes could help, but designing such regulation is not trivial.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;opinion&quot;&gt;Opinion&lt;&#x2F;h2&gt;
&lt;p&gt;Since this is, after all, my personal blog, here is my very personal opinion on the matter:&lt;&#x2F;p&gt;
&lt;p&gt;Introducing the central database for fuel prices was a good idea. I support gathering non-personal data that was very public anyway, just not centralized and therefore hardly useful.&lt;&#x2F;p&gt;
&lt;p&gt;Aforementioned price cycles are a known phenomenon in economics and have been for many decades. Economics is not my strong suit, I still think this outcome could have been predicted to some extent. Politics, also not my strong suit, could have put limits on price changes into the same piece of legislation that introduced the requirement to report prices to the Cartel Office. Arguing for this would have been reasonable back then, when price changes were way less frequent anyway.&lt;&#x2F;p&gt;
&lt;p&gt;The second-best time to push for limiting price changes would be now. We got the data, we see the effects. Prices are transparent, yet of little use to customers if they are altered within minutes. It might be unclear how to exactly design the limits, but other countries have done it, let&#x27;s learn from them. The current situation is clearly not ideal for consumers.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>sqlx-fmt</title>
        <published>2025-11-08T00:00:00+00:00</published>
        <updated>2025-11-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Jan Flessau
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jflessau.com/projects/sqlx-fmt/"/>
        <id>https://jflessau.com/projects/sqlx-fmt/</id>
        
        <content type="html" xml:base="https://jflessau.com/projects/sqlx-fmt/">&lt;p&gt;I&#x27;m a huge fan of &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;launchbadge&#x2F;sqlx&quot;&gt;SQLx&lt;&#x2F;a&gt;, and it has always bothered me that SQL within its macros isn&#x27;t formatted automatically:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&lt;span&gt;sqlx&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span&gt;query&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;!&lt;&#x2F;span&gt;&lt;span&gt;(
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;r&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;#&amp;quot;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;        select *
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;        from users
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;        where verified = true
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;    &amp;quot;#
&lt;&#x2F;span&gt;&lt;span&gt;)
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;So I created &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jflessau&#x2F;sqlx-fmt&quot;&gt;sqlx-fmt&lt;&#x2F;a&gt;, a CLI and GitHub Action that formats SQL within SQLx macros in Rust code. It uses &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;quarylabs&#x2F;sqruff&quot;&gt;sqruff&lt;&#x2F;a&gt; as a formatter under the hood. &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;tree-sitter&#x2F;tree-sitter&quot;&gt;Tree-sitter&lt;&#x2F;a&gt; is used to parse the Rust code and extract the (raw) string literals within SQLx macros. I had used regexes initially, but that required a lot of edge case handling, which tree-sitter solves elegantly.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;installation&quot;&gt;Installation&lt;&#x2F;h2&gt;
&lt;p&gt;Clone the &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jflessau&#x2F;sqlx-fmt&quot;&gt;repo&lt;&#x2F;a&gt; and install sqlx-fmt using cargo:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;# install sqlx-fmt
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffd580;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span&gt; clone https:&#x2F;&#x2F;github.com&#x2F;jflessau&#x2F;sqlx-fmt.git
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f28779;&quot;&gt;cd&lt;&#x2F;span&gt;&lt;span&gt; sqlx-fmt
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffd580;&quot;&gt;cargo&lt;&#x2F;span&gt;&lt;span&gt; install&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt; --path&lt;&#x2F;span&gt;&lt;span&gt; .
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If you haven&#x27;t already, install &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;quarylabs&#x2F;sqruff&quot;&gt;sqruff&lt;&#x2F;a&gt;, which can also be installed via cargo:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;color:#ffd580;&quot;&gt;cargo&lt;&#x2F;span&gt;&lt;span&gt; install sqruff
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;&#x2F;h2&gt;
&lt;p&gt;This CLI has two commands: &lt;code&gt;format&lt;&#x2F;code&gt; and &lt;code&gt;check&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;# format code
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffd580;&quot;&gt;sqlx-fmt&lt;&#x2F;span&gt;&lt;span&gt; format&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt; --path&lt;&#x2F;span&gt;&lt;span&gt; path_to_files
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;# check if code is formatted
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffd580;&quot;&gt;sqlx-fmt&lt;&#x2F;span&gt;&lt;span&gt; check&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt; --path&lt;&#x2F;span&gt;&lt;span&gt; path_to_files
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;configuration&quot;&gt;Configuration&lt;&#x2F;h3&gt;
&lt;p&gt;Optionally, specify a &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;quarylabs&#x2F;sqruff&#x2F;blob&#x2F;main&#x2F;crates&#x2F;lib&#x2F;src&#x2F;core&#x2F;default_config.cfg&quot;&gt;sqruff config file&lt;&#x2F;a&gt; with &lt;code&gt;--config .sqruff&lt;&#x2F;code&gt;. The config file allows you to specify things like SQL dialect, rules to apply, and indentation style. Here&#x27;s an example &lt;code&gt;.sqruff&lt;&#x2F;code&gt; config file:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;toml&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-toml &quot;&gt;&lt;code class=&quot;language-toml&quot; data-lang=&quot;toml&quot;&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;sqruff&lt;&#x2F;span&gt;&lt;span&gt;]
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;dialect &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ff3333;&quot;&gt;postgres
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;rules &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ff3333;&quot;&gt;ambiguous,capitalisation,convention,layout,references
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;sqruff&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ff3333;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;indentation&lt;&#x2F;span&gt;&lt;span&gt;]
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;indent_unit &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ff3333;&quot;&gt;space
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;tab_space_size &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;4
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;indented_joins &lt;&#x2F;span&gt;&lt;span&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ff3333;&quot;&gt;True
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h2 id=&quot;github-action&quot;&gt;GitHub Action&lt;&#x2F;h2&gt;
&lt;p&gt;You can also use it &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;marketplace&#x2F;actions&#x2F;sqlx-fmt&quot;&gt;as a GitHub Action&lt;&#x2F;a&gt; to automatically run format checks in CI:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;steps&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;  - &lt;&#x2F;span&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;checkout code
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;uses&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;actions&#x2F;checkout@v4
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;  - &lt;&#x2F;span&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;run format checker
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;uses&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;jflessau&#x2F;sqlx-fmt@main
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;with&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;context&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;quot;.&#x2F;code_to_format&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;config-file&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;quot;.&#x2F;code_to_format&#x2F;.sqruff&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;fail-on-unformatted&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;quot;true&amp;quot;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Retweet 2 Win</title>
        <published>2025-10-05T00:00:00+00:00</published>
        <updated>2025-10-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Jan Flessau
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jflessau.com/projects/retweet-2-win/"/>
        <id>https://jflessau.com/projects/retweet-2-win/</id>
        
        <content type="html" xml:base="https://jflessau.com/projects/retweet-2-win/">&lt;p&gt;Back when I was studying, around 2015, I saw &lt;a href=&quot;https:&#x2F;&#x2F;www.hscott.net&#x2F;twitter-contest-winning-as-a-service&quot;&gt;this post&lt;&#x2F;a&gt; from a guy winning stuff on Twitter with a self-made bot. I couldn’t find the code, so I wrote my own.&lt;&#x2F;p&gt;
&lt;p&gt;On Twitter, at least at the time, people and companies used giveaways to promote their accounts. They asked for retweets and, in return, promised to randomly select a winner from all the retweeters to receive some kind of prize.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;hurdles&quot;&gt;Hurdles&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;Twitter enforced rate limits, so searches and retweets were limited.&lt;&#x2F;li&gt;
&lt;li&gt;Some giveaways required you to also follow the account, and following too many people too quickly would get your account blocked.&lt;&#x2F;li&gt;
&lt;li&gt;Surprisingly, there were other bots tweeting tweets with keywords like &lt;code&gt;#retweet2win&lt;&#x2F;code&gt; without actually offering a prize.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;The first two were relatively easy to solve: just add delays and unfollow accounts after some time. That made the whole process more time-consuming, but since the number of giveaways was also limited and spread out over time, that was fine.&lt;&#x2F;p&gt;
&lt;p&gt;One day I found that one of those anti-giveaway-bot bots had my bot&#x27;s name plus some suffix as its own name. So someone must have seen my bot and created an account to interfere with its mission.&lt;&#x2F;p&gt;
&lt;p&gt;To avoid wasting time retweeting those bots, I made a blacklist of accounts not to interact with. After a few days, I found most of these bots and also some scammy giveaway accounts I had no interest in retweeting.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;scams&quot;&gt;Scams&lt;&#x2F;h2&gt;
&lt;p&gt;Every other day, I would check my DMs to see if any giveaway accounts had selected me as the winner. Pretty straightforward, but, of course, most of these alleged wins were things like:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;“You won the PS4! I only need you to send me 20 € on PayPal for shipping, then I’ll send it to you.”&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;To get real, physical prizes sent to my door, I needed to disclose my address to the giveaway creator. So I spent quite some time figuring out if the whole giveaway was a scam or not. That was a bit annoying.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;prizes&quot;&gt;Prizes&lt;&#x2F;h2&gt;
&lt;p&gt;I won random stuff like:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;tickets to expos I’d never heard of&lt;&#x2F;li&gt;
&lt;li&gt;gift cards for online shops&lt;&#x2F;li&gt;
&lt;li&gt;an educational poster for geography classes&lt;&#x2F;li&gt;
&lt;li&gt;self-published books&lt;&#x2F;li&gt;
&lt;li&gt;plushies&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;My favorite to this day is a &lt;em&gt;Corgi certificate&lt;&#x2F;em&gt;: a piece of relatively thick paper with a corgi on it, surrounded by the phrase “100% woof”. I don’t own a dog and have no clue what this certificate certifies. ¯\&lt;em&gt;(ツ)&lt;&#x2F;em&gt;&#x2F;¯&lt;&#x2F;p&gt;
&lt;p&gt;For stuff I couldn’t use myself (tickets to concerts in other countries, etc.), I found people on Reddit who were interested so the tickets wouldn&#x27;t be wasted.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;customer-support&quot;&gt;Customer Support&lt;&#x2F;h2&gt;
&lt;p&gt;I made the bot open source and, after some days, had a few people emailing me asking for instructions to set it up or reporting bugs. That felt nice, finally seeing other people take interest in what I’d created. But it turned out to be quite time-consuming. And I also didn’t want armies of my bots polluting Twitter. Just, you know, mine and maybe a handful of others.&lt;&#x2F;p&gt;
&lt;p&gt;This taught me a valuable lesson: If you put some code into the world, you basically lose control over it.&lt;&#x2F;p&gt;
&lt;p&gt;After a few weeks, requests for support ended and I eventually archived the GitHub repo.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;regrets&quot;&gt;Regrets&lt;&#x2F;h2&gt;
&lt;p&gt;Once a few physical wins arrived at my doorstep, I told a friend about my bot. He wanted to try it, and in an afternoon, we set it up on his Raspberry Pi.&lt;&#x2F;p&gt;
&lt;p&gt;Within a month, he won a 3D printer, surpassing pretty much everything I had won up to that time and, in hindsight, ever. Envy is an ugly emotion and it happened almost a decade ago, but it still stings somehow.&lt;&#x2F;p&gt;
&lt;p&gt;Looking back at this today, I wouldn&#x27;t do something like this again. Who really likes bots on their beloved social media platform? And I won giveaways other people, potentially much more interested in the prizes, could have won. So, yeah, regret.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Ritmo</title>
        <published>2025-10-04T00:00:00+00:00</published>
        <updated>2025-10-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Jan Flessau
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jflessau.com/projects/ritmo/"/>
        <id>https://jflessau.com/projects/ritmo/</id>
        
        <content type="html" xml:base="https://jflessau.com/projects/ritmo/">&lt;p&gt;Yes, yes, another habit tracker. But this one is different. Well, not really. But it&#x27;s mine.&lt;&#x2F;p&gt;
&lt;p&gt;Out of frustration with previous attempts at using other trackers, I decided to build my own. Here are its key features:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;No login required&lt;&#x2F;li&gt;
&lt;li&gt;Works offline&lt;&#x2F;li&gt;
&lt;li&gt;Import &amp;amp; export data&lt;&#x2F;li&gt;
&lt;li&gt;PWA-ready&lt;&#x2F;li&gt;
&lt;li&gt;MIT licensed&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;GitHub Repo: &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jflessau&#x2F;ritmo&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;jflessau&#x2F;ritmo&lt;&#x2F;a&gt;&lt;br &#x2F;&gt;
Live Demo: &lt;a href=&quot;https:&#x2F;&#x2F;ritmo.jflessau.com&quot;&gt;https:&#x2F;&#x2F;ritmo.jflessau.com&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;ve been dogfooding Ritmo for half a year now as a PWA on iOS and have never looked back.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;screenrecording&quot;&gt;Screenrecording&lt;&#x2F;h2&gt;
&lt;video controls width=&quot;100%&quot; alt=&quot;Screen video in portrait mode. The first view (list view) shows a list of habits (&amp;#x27;Drink water,&amp;#x27; &amp;#x27;Go for a walk&amp;#x27; etc.) as rows and the dates of the past five days as columns. A checkmark indicates habits completed for that day. The second view shows the detail page of a habit with its name and options to: 1) go back to the list view, 2) rename the habit, and 3) delete the habit. Below that is a grid of dates (one week per row). Days on which the habit has been completed are highlighted. The third view shows a text box with some JSON (a format for text data) that represents the state of all habits (their names and the dates they have been completed). Users can copy and paste this text to export their data. The fourth view shows an empty text box. Users can paste exported data here to import it.&quot; autoplay muted loop playsinline&gt;
  &lt;source src=&quot;ritmo.mp4&quot; type=&quot;video&#x2F;mp4&quot;&gt;
  Your browser does not support the video tag.
&lt;&#x2F;video&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Sitcom as a Service</title>
        <published>2025-10-04T00:00:00+00:00</published>
        <updated>2025-10-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Jan Flessau
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jflessau.com/projects/sitcom-as-a-service/"/>
        <id>https://jflessau.com/projects/sitcom-as-a-service/</id>
        
        <content type="html" xml:base="https://jflessau.com/projects/sitcom-as-a-service/">&lt;p&gt;A while back, I created a web app that fills the silence in conversations with canned laughter using machine learning. This, along with my &lt;a href=&quot;&#x2F;projects&#x2F;bs-bingo&#x2F;&quot;&gt;multiplayer bullshit bingo&lt;&#x2F;a&gt;, turned out to be slightly more fun than I expected while building it.&lt;&#x2F;p&gt;
&lt;p&gt;You might know the rush of building something and how convinced you are that it will be great, only to find out later that it is not. Not this time, so I’m keeping it around for people to try: &lt;a href=&quot;https:&#x2F;&#x2F;canned-laughter.jflessau.com&#x2F;&quot;&gt;https:&#x2F;&#x2F;canned-laughter.jflessau.com&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Hint: It won&#x27;t work in mobile Safari for reasons I&#x27;m too lazy to investigate.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;projects&#x2F;sitcom-as-a-service&#x2F;saas.png&quot; alt=&quot;Screenshot of a web app showing a slider (1-100 %) and this&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Coding with AI</title>
        <published>2025-08-26T00:00:00+00:00</published>
        <updated>2025-08-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Jan Flessau
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jflessau.com/dev/ai-coding/"/>
        <id>https://jflessau.com/dev/ai-coding/</id>
        
        <content type="html" xml:base="https://jflessau.com/dev/ai-coding/">&lt;p&gt;I code with AI. Autocomplete is, in my opinion, the most useful AI coding tool. Typing the first two characters of a line of code and having the rest filled in for you is a game changer. Even if it only works 50% of the time, it saves a substantial amount of keystrokes and time. But I admit, it&#x27;s a distraction in the typing flow. Because of that, my typing skills have suffered. Even more concerning: When I’m on the train or at any place without internet, having no access to copilot, I feel much slower than I was before using it at all.&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s worth it, at least for now. It saves me countless hours of looking up the same things in docs I keep forgetting (how to parse json from a file, how to configure a reqwest client, how to set up tracing, how to start postgres with testcontainers, etc.).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;agentic-editing&quot;&gt;Agentic Editing&lt;&#x2F;h2&gt;
&lt;p&gt;Agentic editing is another story entirely. Witnessing how the Claude CLI burns through tokens and creates a functional little web app that does not look too bad is mesmerizing. Heavily using git and branching is a good idea here and makes this whole process feel even faster, creating branches left and right, discarding some and progressing with others.&lt;&#x2F;p&gt;
&lt;p&gt;At some point, I found, it breaks apart. Prompts do not hit the mark, bugs stay or reappear, and the code structure gets messy. By then, the amount of code produced is already substantial. And the hard part is now reading and understanding it in order to carry it over the finish line. For some smaller projects, AI gets it over there alone, but if not, there is a lot of work to do.&lt;&#x2F;p&gt;
&lt;p&gt;The work is mostly reading and understanding the code, then fixing not only the parts that do not work but also the structure so that it fits your mental model more. So that it becomes more like &lt;em&gt;your&lt;&#x2F;em&gt; code.&lt;&#x2F;p&gt;
&lt;p&gt;Agentic editing still has some use cases in my workflow. I mainly use it to speed up setups and confined tasks that require context from multiple files. Here are some examples:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Looking at multiple files and an error message to help me debug&lt;&#x2F;li&gt;
&lt;li&gt;Setting up a node project, pretty much eliminating the need for templates. I just say I want, for example: TypeScript, Tailwind, SolidJS, unplugin icons with autoimport, and Bun.&lt;&#x2F;li&gt;
&lt;li&gt;Writing a down-migration file for postgres based on an up-migration or generating structs for each table.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Aside from generating code, I use the agentic features in my editor of choice for getting to know a new codebase by asking questions about it and getting mostly accurate responses with references to files and lines in the code at hand.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h2&gt;
&lt;p&gt;Coding with AI can make developers faster and reduce frustration caused by constantly googling for docs and references. I think the hard limit on how fast we can be is actually understanding the codebase. Don’t push to prod until you do. AI can help you understand, but at some point, the code has to pass through &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Wetware_(brain)&quot;&gt;wetware&lt;&#x2F;a&gt;. Therefore, I find the (mostly) line-by-line autocomplete much more useful than agentic editing. It does work for you in chunks you can follow and understand while saving time on typing.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>InkDay</title>
        <published>2025-05-17T00:00:00+00:00</published>
        <updated>2025-05-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Jan Flessau
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jflessau.com/projects/inkday/"/>
        <id>https://jflessau.com/projects/inkday/</id>
        
        <content type="html" xml:base="https://jflessau.com/projects/inkday/">&lt;p&gt;I made a 3D printable frame for a 7.5&quot; e-ink display and a website to control its content.&lt;&#x2F;p&gt;
&lt;p&gt;On the website, you can pick one image for each day via a calendar interface. A Raspberry Pi connected to the display fetches the current day&#x27;s image from the website and displays it on the e-ink screen.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;demo&quot;&gt;Demo&lt;&#x2F;h2&gt;
&lt;p&gt;The frame on my desk is controlled by a public version of this website. You can control what I see here: &lt;a href=&quot;https:&#x2F;&#x2F;inkday.jflessau.com&quot;&gt;https:&#x2F;&#x2F;inkday.jflessau.com&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;everything-you-need-to-build-it&quot;&gt;Everything You Need to Build It&lt;&#x2F;h2&gt;
&lt;p&gt;You can find the code, 3D file, list of the parts used, and instructions for assembling them in this GitHub repo:&lt;&#x2F;p&gt;
&lt;p&gt;👉 &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jflessau&#x2F;inkday&quot;&gt;InkDay GitHub Repo&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;more-pics&quot;&gt;More Pics&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;back&quot;&gt;Back&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;projects&#x2F;inkday&#x2F;back.jpg&quot; alt=&quot;Backside of the frame with a Raspberry Pi mounted close to the e-ink display.&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;website-screenshot&quot;&gt;Website Screenshot&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;projects&#x2F;inkday&#x2F;website-screenshot.png&quot; alt=&quot;Screenshot of a website with a date and a file input. Below that is a list of the next ten dates that don&amp;#39;t have an image yet.&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Beam Base</title>
        <published>2025-05-07T00:00:00+00:00</published>
        <updated>2025-05-07T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Jan Flessau
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jflessau.com/projects/beam-base/"/>
        <id>https://jflessau.com/projects/beam-base/</id>
        
        <content type="html" xml:base="https://jflessau.com/projects/beam-base/">&lt;p&gt;This cylindrical base for RPG minis has a built-in &lt;strong&gt;field of view&lt;&#x2F;strong&gt; (FOV) cone that can be adjusted to different angles. The FOV is projected onto the table, making it easy to see what the mini can see.&lt;&#x2F;p&gt;
&lt;p&gt;I find the FOV features of tabletop simulators like Roll20 useful, so I wanted to create something similar for real-life use.&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s made of three printable parts and a light source. Here are the STL files:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;projects&#x2F;beam-base&#x2F;beam-base-base.stl&quot;&gt;Base&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;projects&#x2F;beam-base&#x2F;beam-base-slider.stl&quot;&gt;Slider&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;projects&#x2F;beam-base&#x2F;beam-base-top.stl&quot;&gt;Top&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I used these &lt;a href=&quot;https:&#x2F;&#x2F;www.amazon.de&#x2F;dp&#x2F;B07L8XHGW7&quot;&gt;balloon lights&lt;&#x2F;a&gt; as light sources. You can use something else as long as it fits into the rails of the bottom part (w: 10 mm, h: 10 mm, d: 40 mm) leaving some space in the depth direction to allow the adjustable beam angle to function properly. You might need some adhesive to glue the light to the slider. Sticky Tack, poster putty, or Patafix should do the trick.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pictures&quot;&gt;Pictures&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;individual-parts&quot;&gt;Individual Parts&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;projects&#x2F;beam-base&#x2F;beam-base-parts.jpg&quot; alt=&quot;Individual parts of the Beam Base: the base, slider, and top, shown alongside a light source and an RPG miniature.&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;narrow-beam&quot;&gt;Narrow Beam&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;projects&#x2F;beam-base&#x2F;beam-base-narrow.jpg&quot; alt=&quot;3D printed Beam Base projecting a narrow field of view cone onto a tabletop, with an RPG miniature placed on top.&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;wide-beam&quot;&gt;Wide Beam&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;projects&#x2F;beam-base&#x2F;beam-base-wide.jpg&quot; alt=&quot;3D printed Beam Base projecting a wide field of view cone onto a tabletop, with an RPG miniature placed on top.&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Postgres Ultras T-Shirt</title>
        <published>2025-02-18T00:00:00+00:00</published>
        <updated>2025-02-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Jan Flessau
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jflessau.com/designs/postgres-shirt/"/>
        <id>https://jflessau.com/designs/postgres-shirt/</id>
        
        <content type="html" xml:base="https://jflessau.com/designs/postgres-shirt/">&lt;p&gt;I looked around at all the known sites selling shirts but cound&#x27;t find one for Postgres that I liked. So I made my own.&lt;&#x2F;p&gt;
&lt;p&gt;Download a high-res PNG below from &lt;a href=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;designs&#x2F;postgres-shirt&#x2F;pg_ultras_transparent.png&quot;&gt;here&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;designs&#x2F;postgres-shirt&#x2F;mosaic.jpg&quot; alt=&quot;Postgres Ultras fan shirt design featuring a muscular elephant framed from head to shoulders, with the text &amp;quot;POSTGRES ULTRAS&amp;quot; below it.&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>SurrealDB</title>
        <published>2025-01-02T00:00:00+00:00</published>
        <updated>2025-01-02T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Jan Flessau
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jflessau.com/dev/surrealdb/"/>
        <id>https://jflessau.com/dev/surrealdb/</id>
        
        <content type="html" xml:base="https://jflessau.com/dev/surrealdb/">&lt;p&gt;Are you the relational type when it comes to databases? Do you like Postgres, have heard of &lt;a href=&quot;https:&#x2F;&#x2F;surrealdb.com&#x2F;&quot;&gt;SurrealDB&lt;&#x2F;a&gt;, and want to know if it&#x27;s worth a shot? This post is for you :)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-is-surrealdb&quot;&gt;What is SurrealDB?&lt;&#x2F;h2&gt;
&lt;p&gt;SurrealDB is a multi-modal database. You can store relational data, graphs, documents, and time series in it. It supports full-text search, vector search, has its own SQL-like query language, and lets you store and execute ML models. You can have fine-grained access control (down to fields, aka columns), which leads to their claim of being a backend as a service (think &lt;a href=&quot;https:&#x2F;&#x2F;firebase.google.com&#x2F;&quot;&gt;Firebase&lt;&#x2F;a&gt;).&lt;&#x2F;p&gt;
&lt;p&gt;That&#x27;s quite a lot cover. I’ll focus on relational data storage, consistency, querying, performance, and DX. Here we go.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;data-consistency&quot;&gt;Data Consistency&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;strict-mode&quot;&gt;Strict Mode&lt;&#x2F;h3&gt;
&lt;p&gt;You can start SurrealDB either with or without the &lt;code&gt;strict&lt;&#x2F;code&gt; flag. &lt;strong&gt;Without&lt;&#x2F;strong&gt;, you can create records (in Postgres, these would be rows) without defining their schema. That might be cool for prototyping, but it&#x27;s not my cup of tea. Note that strict mode is either on or off for the entire database instance.&lt;br &#x2F;&gt;
Here I create a &lt;code&gt;dog&lt;&#x2F;code&gt; record without defining a schema for it:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;surql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-surql &quot;&gt;&lt;code class=&quot;language-surql&quot; data-lang=&quot;surql&quot;&gt;&lt;span&gt;-- insert record
&lt;&#x2F;span&gt;&lt;span&gt;create dog content {
&lt;&#x2F;span&gt;&lt;span&gt;    name: &amp;quot;cookie&amp;quot;,
&lt;&#x2F;span&gt;&lt;span&gt;    good_dog: true
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;With&lt;&#x2F;strong&gt; strict mode, you need to define &lt;em&gt;what&lt;&#x2F;em&gt; records exist (e.g. &lt;code&gt;dog&lt;&#x2F;code&gt;), but not necessarily how they look:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;surql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-surql &quot;&gt;&lt;code class=&quot;language-surql&quot; data-lang=&quot;surql&quot;&gt;&lt;span&gt;-- define table
&lt;&#x2F;span&gt;&lt;span&gt;define table dog schemaless;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;-- insert record
&lt;&#x2F;span&gt;&lt;span&gt;create dog content {
&lt;&#x2F;span&gt;&lt;span&gt;    name: &amp;quot;cookie&amp;quot;,
&lt;&#x2F;span&gt;&lt;span&gt;    good_dog: true
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;schemafull-schemaless&quot;&gt;Schemafull &amp;amp; Schemaless&lt;&#x2F;h3&gt;
&lt;p&gt;We defined a schemaless table for &lt;code&gt;dog&lt;&#x2F;code&gt;, and aside from actually putting records in that specific table, they can have pretty much any shape or form we want.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;surql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-surql &quot;&gt;&lt;code class=&quot;language-surql&quot; data-lang=&quot;surql&quot;&gt;&lt;span&gt;create dog content {
&lt;&#x2F;span&gt;&lt;span&gt;    title: &amp;quot;sir doggo&amp;quot;,
&lt;&#x2F;span&gt;&lt;span&gt;    age_years: 3
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;By the way, all records have IDs, which are generated automatically if not explicitly specified.&lt;&#x2F;p&gt;
&lt;p&gt;We &lt;em&gt;can&lt;&#x2F;em&gt; define fields for schemaless tables. Inserting with extra fields is acceptable (and they become part of the stored record), but inserting with missing fields returns an error:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;surql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-surql &quot;&gt;&lt;code class=&quot;language-surql&quot; data-lang=&quot;surql&quot;&gt;&lt;span&gt;define table dog schemaless;
&lt;&#x2F;span&gt;&lt;span&gt;define field name on dog type string;
&lt;&#x2F;span&gt;&lt;span&gt;define field age_years on dog type int assert $value &amp;gt;= 0;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;-- this insert works!
&lt;&#x2F;span&gt;&lt;span&gt;create dog content {
&lt;&#x2F;span&gt;&lt;span&gt;    name: &amp;quot;cookie&amp;quot;,
&lt;&#x2F;span&gt;&lt;span&gt;    age_years: 2,
&lt;&#x2F;span&gt;&lt;span&gt;    extra_field: &amp;quot;good dog&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;-- doesn&amp;#39;t work, because &amp;quot;age_years&amp;quot; is missing
&lt;&#x2F;span&gt;&lt;span&gt;create dog content {
&lt;&#x2F;span&gt;&lt;span&gt;    name: &amp;quot;cookie&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now we&#x27;re done prototyping and know how the schema should look. We discard the old table and define it again, this time as &lt;strong&gt;schemafull&lt;&#x2F;strong&gt;. Trying to insert records with missing or extra fields will return an error for schemafull tables.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;surql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-surql &quot;&gt;&lt;code class=&quot;language-surql&quot; data-lang=&quot;surql&quot;&gt;&lt;span&gt;remove table dog;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;define table dog schemafull;
&lt;&#x2F;span&gt;&lt;span&gt;define field name on dog type string;
&lt;&#x2F;span&gt;&lt;span&gt;define field age_years on dog type int assert $value &amp;gt;= 0;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;create dog content {
&lt;&#x2F;span&gt;&lt;span&gt;    name: &amp;quot;cookie&amp;quot;,
&lt;&#x2F;span&gt;&lt;span&gt;    age_years: 2
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;altering-the-schema&quot;&gt;Altering the Schema&lt;&#x2F;h3&gt;
&lt;p&gt;One thing I really don&#x27;t like about SurrealDB: If we change, say, a field&#x27;s type from &lt;code&gt;int&lt;&#x2F;code&gt; to &lt;code&gt;string&lt;&#x2F;code&gt;, I would expect all records in that table to conform to the new schema. However, they don&#x27;t.&lt;&#x2F;p&gt;
&lt;p&gt;Here, I use the &lt;code&gt;overwrite&lt;&#x2F;code&gt; keyword (there is an &lt;code&gt;alter&lt;&#x2F;code&gt; keyword, but it works for tables only, at least for now) to change the field type of &lt;code&gt;zip_code&lt;&#x2F;code&gt; from &lt;code&gt;int&lt;&#x2F;code&gt; to &lt;code&gt;string&lt;&#x2F;code&gt;. However, existing records still have the old type for that field. You must manually update the records to fit the new schema, even in strict mode for schemafull tables 👎&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;surql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-surql &quot;&gt;&lt;code class=&quot;language-surql&quot; data-lang=&quot;surql&quot;&gt;&lt;span&gt;-- define table
&lt;&#x2F;span&gt;&lt;span&gt;define table address schemafull;
&lt;&#x2F;span&gt;&lt;span&gt;define field street on table address type string;
&lt;&#x2F;span&gt;&lt;span&gt;define field zip_code on table address type int;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;-- insert record
&lt;&#x2F;span&gt;&lt;span&gt;create address content {
&lt;&#x2F;span&gt;&lt;span&gt;    street: &amp;quot;Second Avenue 2&amp;quot;,
&lt;&#x2F;span&gt;&lt;span&gt;    zip_code: 1234
&lt;&#x2F;span&gt;&lt;span&gt;};
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;-- change field type
&lt;&#x2F;span&gt;&lt;span&gt;define field overwrite zip_code on table address type string;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;-- add field
&lt;&#x2F;span&gt;&lt;span&gt;define field test on table address type string;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;-- remove field
&lt;&#x2F;span&gt;&lt;span&gt;remove field street on table address;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;-- query records
&lt;&#x2F;span&gt;&lt;span&gt;select * from address;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Querying the addresses with &lt;code&gt;select * from address;&lt;&#x2F;code&gt; returns:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;surql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-surql &quot;&gt;&lt;code class=&quot;language-surql&quot; data-lang=&quot;surql&quot;&gt;&lt;span&gt;[
&lt;&#x2F;span&gt;&lt;span&gt;	{
&lt;&#x2F;span&gt;&lt;span&gt;		id: address:o5fabxa3eir08n731b5o,
&lt;&#x2F;span&gt;&lt;span&gt;		street: &amp;#39;Second Avenue 2&amp;#39;,
&lt;&#x2F;span&gt;&lt;span&gt;		zip_code: 1234
&lt;&#x2F;span&gt;&lt;span&gt;	}
&lt;&#x2F;span&gt;&lt;span&gt;]
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Nothing changed. Both &lt;code&gt;street&lt;&#x2F;code&gt; and &lt;code&gt;zip_code&lt;&#x2F;code&gt; (as string) are still there and the new field &lt;code&gt;test&lt;&#x2F;code&gt; is missing as well.&lt;&#x2F;p&gt;
&lt;p&gt;Altering the schema doesn&#x27;t alter old data. New records will have the new schema, though.&lt;&#x2F;p&gt;
&lt;p&gt;This is especially dangerous, I think, when you&#x27;ve just added a non-optional field to a table that your backend code relies on to be neither null nor undefined. Older records won&#x27;t have that field until you write a query to update them. That was a footgun, at least for me.&lt;&#x2F;p&gt;
&lt;p&gt;Let&#x27;s transition to something more harmonious: Relations!&lt;&#x2F;p&gt;
&lt;h3 id=&quot;record-links&quot;&gt;Record Links&lt;&#x2F;h3&gt;
&lt;p&gt;There are two main ways to create relations between records. Let&#x27;s explore the simplest one: &lt;strong&gt;Record Links&lt;&#x2F;strong&gt;. First, we need another table: &lt;code&gt;human&lt;&#x2F;code&gt;. Then we define a field of type &lt;code&gt;record&amp;lt;dog&amp;gt;&lt;&#x2F;code&gt;. I find that pretty neat.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;surql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-surql &quot;&gt;&lt;code class=&quot;language-surql&quot; data-lang=&quot;surql&quot;&gt;&lt;span&gt;define table human schemafull;
&lt;&#x2F;span&gt;&lt;span&gt;define field name on human type string;
&lt;&#x2F;span&gt;&lt;span&gt;define field loves on human type record&amp;lt;dog&amp;gt;;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;But there is no guarantee that the dog referenced in the human table actually exists. Hm...&lt;&#x2F;p&gt;
&lt;h3 id=&quot;relations&quot;&gt;Relations&lt;&#x2F;h3&gt;
&lt;p&gt;We could create custom events that fire on create&#x2F;update&#x2F;delete, but who wants that? Another way to relate two records is through a &lt;strong&gt;Relation&lt;&#x2F;strong&gt;. Relations are basically tables, but with two default fields: &lt;code&gt;in&lt;&#x2F;code&gt; and &lt;code&gt;out&lt;&#x2F;code&gt;, both holding a record ID. You can add more fields to describe the relation.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;surql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-surql &quot;&gt;&lt;code class=&quot;language-surql&quot; data-lang=&quot;surql&quot;&gt;&lt;span&gt;-- create records for human and dog
&lt;&#x2F;span&gt;&lt;span&gt;create human:human_1 content { name: &amp;quot;alex&amp;quot;, loves: dog:dog_1 };
&lt;&#x2F;span&gt;&lt;span&gt;create dog:dog_1 content { name: &amp;quot;cookie&amp;quot;, age_years: 2 };
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;-- define relation between human and dog
&lt;&#x2F;span&gt;&lt;span&gt;define table cares_for type relation in human out dog enforced;
&lt;&#x2F;span&gt;&lt;span&gt;define field since on cares_for type datetime;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;-- create relation
&lt;&#x2F;span&gt;&lt;span&gt;relate human:human_1 -&amp;gt; cares_for -&amp;gt; dog:dog_1 content { since: time::now()}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Relations also won&#x27;t guarantee that their related records exist—not by default. The &lt;code&gt;enforced&lt;&#x2F;code&gt; keyword used in the relation&#x27;s definition will prevent the insertion of relations with missing records in referenced tables. If a referenced record is deleted, the relation will be deleted as well.&lt;&#x2F;p&gt;
&lt;p&gt;You would need to build custom events to achieve the &quot;on delete cascade&quot; feature that Postgres offers. However, a &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;surrealdb&#x2F;surrealdb&#x2F;pull&#x2F;5300&quot;&gt;PR&lt;&#x2F;a&gt; to go in that direction was opened less than a week ago. Fingers crossed 🤞&lt;&#x2F;p&gt;
&lt;h2 id=&quot;data-storage&quot;&gt;Data Storage&lt;&#x2F;h2&gt;
&lt;p&gt;SurrealDB stores data in key-value stores. This is &lt;strong&gt;very&lt;&#x2F;strong&gt; important to know, I think. You can choose between RocksDB, FoundationDB, IndexedDB, SurrealKV, TiKV, or an in-memory KV. SurrealKV is still in beta, though.&lt;br &#x2F;&gt;
&lt;a href=&quot;https:&#x2F;&#x2F;tikv.org&#x2F;&quot;&gt;TiKV&lt;&#x2F;a&gt;, for instance, is horizontally scalable while being ACID compliant.&lt;&#x2F;p&gt;
&lt;p&gt;Record IDs follow the format &lt;code&gt;table_name:id&lt;&#x2F;code&gt;. Record IDs are used to retrieve records from KV stores quickly. SurrealDB strongly encourages you to use these record IDs for filtering whenever possible because they are indexed by default. And you really better do that, or else...&lt;&#x2F;p&gt;
&lt;h2 id=&quot;performance&quot;&gt;Performance&lt;&#x2F;h2&gt;
&lt;p&gt;Performance can be a problem. As far as I know, SurrealDB hasn&#x27;t published any benchmarks, which is sus. So I measured some things myself.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;test-query-table-with-1-mio-records&quot;&gt;Test: Query Table with 1 Mio. Records&lt;&#x2F;h3&gt;
&lt;p&gt;I populated a &lt;code&gt;user&lt;&#x2F;code&gt; table with one million rows in both SurrealDB and Postgres for some query benchmarks. I&#x27;m using SurrealDB 2.1.2 with the in-memory KV store and Postgres 17.0, both running in Docker on an M1 Mac.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;surrealdb&quot;&gt;SurrealDB&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;surql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-surql &quot;&gt;&lt;code class=&quot;language-surql&quot; data-lang=&quot;surql&quot;&gt;&lt;span&gt;-- define table
&lt;&#x2F;span&gt;&lt;span&gt;define table user schemafull;
&lt;&#x2F;span&gt;&lt;span&gt;define field id on table user type string;
&lt;&#x2F;span&gt;&lt;span&gt;define field username on table user type string;
&lt;&#x2F;span&gt;&lt;span&gt;define field email on table user type string;
&lt;&#x2F;span&gt;&lt;span&gt;define field password_hash on table user type string;
&lt;&#x2F;span&gt;&lt;span&gt;define field coins on table user type int;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;-- populate table (took 65 s)
&lt;&#x2F;span&gt;&lt;span&gt;FOR $i IN 1..=1000000 {
&lt;&#x2F;span&gt;&lt;span&gt;    create user content {
&lt;&#x2F;span&gt;&lt;span&gt;        username: string::concat(&amp;#39;username&amp;#39; + return &amp;lt;string&amp;gt; $i),
&lt;&#x2F;span&gt;&lt;span&gt;        email: string::concat(&amp;#39;email&amp;#39; + return &amp;lt;string&amp;gt; $i),
&lt;&#x2F;span&gt;&lt;span&gt;        password_hash: string::concat(&amp;#39;id&amp;#39; + return &amp;lt;string&amp;gt; $i),
&lt;&#x2F;span&gt;&lt;span&gt;        coins: rand::int(0, 1000000)
&lt;&#x2F;span&gt;&lt;span&gt;    };
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;postgres&quot;&gt;Postgres&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- define table
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;create table &lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffd580;&quot;&gt;user&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot; (
&lt;&#x2F;span&gt;&lt;span&gt;	id &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;text primary key&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;	username &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;text &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;not &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;null &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;check&lt;&#x2F;span&gt;&lt;span&gt; (length(username) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;) unique,
&lt;&#x2F;span&gt;&lt;span&gt;	email &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;text &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;not &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;null&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;	password_hash &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;text &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;not &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;null&lt;&#x2F;span&gt;&lt;span&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;	coins &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;int &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;not &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;null
&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- populate table (took 10 s)
&lt;&#x2F;span&gt;&lt;span&gt;DO $$
&lt;&#x2F;span&gt;&lt;span&gt;DECLARE
&lt;&#x2F;span&gt;&lt;span&gt;    id &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;text&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    username &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;TEXT&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    email &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;TEXT&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    password_hash &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;TEXT&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    coins &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;INT&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;BEGIN
&lt;&#x2F;span&gt;&lt;span&gt;    FOR i &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;IN &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;..&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;1000000&lt;&#x2F;span&gt;&lt;span&gt; LOOP
&lt;&#x2F;span&gt;&lt;span&gt;    	id :&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;id&amp;#39; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;||&lt;&#x2F;span&gt;&lt;span&gt; i;
&lt;&#x2F;span&gt;&lt;span&gt;        username :&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;user&amp;#39; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;||&lt;&#x2F;span&gt;&lt;span&gt; i;
&lt;&#x2F;span&gt;&lt;span&gt;        email :&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;user&amp;#39; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;||&lt;&#x2F;span&gt;&lt;span&gt; i &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;|| &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;@example.com&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;        password_hash :&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;hash&amp;#39; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;||&lt;&#x2F;span&gt;&lt;span&gt; i;
&lt;&#x2F;span&gt;&lt;span&gt;        coins :&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; (RANDOM() &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5ccfe6;&quot;&gt;* &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;1000 &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;+ &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;)::&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;INT&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;INSERT INTO &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;quot;user&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt; (id, username, email, password_hash, coins)
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;VALUES&lt;&#x2F;span&gt;&lt;span&gt; (id, username, email, password_hash, coins);
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;END&lt;&#x2F;span&gt;&lt;span&gt; LOOP;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;END&lt;&#x2F;span&gt;&lt;span&gt; $$;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;I then queried both databases for:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Number of users&lt;&#x2F;li&gt;
&lt;li&gt;Ten users with the most coins&lt;&#x2F;li&gt;
&lt;li&gt;Sum of all coins of all users&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;My measurements aren&#x27;t very scientific or exact, but they&#x27;re still worth a look, I think.
Keep in mind: Postgres has been around for a long time. SurrealDB is relatively young, and performance optimizations could be coming. They &lt;em&gt;could&lt;&#x2F;em&gt; be huge. Or not. I don&#x27;t know.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;results-surrealdb&quot;&gt;Results SurrealDb&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;surql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-surql &quot;&gt;&lt;code class=&quot;language-surql&quot; data-lang=&quot;surql&quot;&gt;&lt;span&gt;-- count all users ≈ 6 s
&lt;&#x2F;span&gt;&lt;span&gt;select count() from user group all;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;-- get ten users with most coins ≈ 8 s
&lt;&#x2F;span&gt;&lt;span&gt;select * from user order by coins desc limit 10;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;-- sum of all coins from all users ≈ 8 s
&lt;&#x2F;span&gt;&lt;span&gt;select math::sum(coins) from user group all;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h4 id=&quot;results-postgres&quot;&gt;Results Postgres&lt;&#x2F;h4&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- count all users ≈ 38 ms
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;select &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f28779;&quot;&gt;count&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5ccfe6;&quot;&gt;*&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;quot;user&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- get ten users with most coins ≈ 30 ms
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;select &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5ccfe6;&quot;&gt;* &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;quot;user&amp;quot; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;order by&lt;&#x2F;span&gt;&lt;span&gt; coins &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;desc limit &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;10&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- sum of all coins from all users ≈ 25 ms
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;select &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f28779;&quot;&gt;sum&lt;&#x2F;span&gt;&lt;span&gt;(coins) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;from &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;quot;user&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;As you can see, Postgres is orders of magnitude faster than SurrealDB for these queries.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;indexes-to-the-rescue&quot;&gt;Indexes to the Rescue?&lt;&#x2F;h3&gt;
&lt;p&gt;What about indexes? Can&#x27;t they help with, say, ordering by coin amount? Creating an index for the &lt;code&gt;coins&lt;&#x2F;code&gt; field and re-running the query for the ten richest users reduced the time for that query by ... nothing?. It still takes about &lt;code&gt;8 seconds&lt;&#x2F;code&gt; in SurrealDB. I&#x27;m not sure if I did something wrong, but I expected a little speedup:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;surql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-surql &quot;&gt;&lt;code class=&quot;language-surql&quot; data-lang=&quot;surql&quot;&gt;&lt;span&gt;-- define index ≈ 26 s
&lt;&#x2F;span&gt;&lt;span&gt;define index coins_index on table user columns coins;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;-- get ten users with most coins ≈ 8 s
&lt;&#x2F;span&gt;&lt;span&gt;select * from user order by coins desc limit 10;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Adding an index for &lt;code&gt;email&lt;&#x2F;code&gt; and querying for two users with an email containing &lt;code&gt;email9999&lt;&#x2F;code&gt; drops the time from approximately &lt;code&gt;8 seconds&lt;&#x2F;code&gt; to about &lt;code&gt;50 milliseconds&lt;&#x2F;code&gt; when using an index. So indexes &lt;em&gt;can&lt;&#x2F;em&gt; help you:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;surql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-surql &quot;&gt;&lt;code class=&quot;language-surql&quot; data-lang=&quot;surql&quot;&gt;&lt;span&gt;-- select without index ≈ 8 s
&lt;&#x2F;span&gt;&lt;span&gt;select email
&lt;&#x2F;span&gt;&lt;span&gt;from user
&lt;&#x2F;span&gt;&lt;span&gt;where email contains &amp;quot;email9999&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;order by email desc
&lt;&#x2F;span&gt;&lt;span&gt;limit 2;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;-- define index ≈ 25 s
&lt;&#x2F;span&gt;&lt;span&gt;define index email on table user columns email;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;-- select with index ≈ 50 ms
&lt;&#x2F;span&gt;&lt;span&gt;select email
&lt;&#x2F;span&gt;&lt;span&gt;from user
&lt;&#x2F;span&gt;&lt;span&gt;where email contains &amp;quot;email9999&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;order by email desc
&lt;&#x2F;span&gt;&lt;span&gt;limit 2;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;local-vs-cloud&quot;&gt;Local vs. Cloud&lt;&#x2F;h3&gt;
&lt;p&gt;And for good measure, I did the exact same measurements on a DB instance in SurrealDB&#x27;s cloud. Just in case my way of running the DB locally was the bottleneck. Here is a summary of all results&lt;&#x2F;p&gt;
&lt;h4 id=&quot;postgres-local&quot;&gt;Postgres local&lt;&#x2F;h4&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Query&lt;&#x2F;th&gt;&lt;th&gt;Time&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;populate table&lt;&#x2F;td&gt;&lt;td&gt;10 s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;count all users&lt;&#x2F;td&gt;&lt;td&gt;38 ms&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;get ten users with most coins&lt;&#x2F;td&gt;&lt;td&gt;30 ms&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;sum of all coins from all users&lt;&#x2F;td&gt;&lt;td&gt;25 ms&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h4 id=&quot;surrealdb-local&quot;&gt;SurrealDB local&lt;&#x2F;h4&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Query&lt;&#x2F;th&gt;&lt;th&gt;Time&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;populate table&lt;&#x2F;td&gt;&lt;td&gt;65 s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;count all users&lt;&#x2F;td&gt;&lt;td&gt;6 s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;get ten users with most coins&lt;&#x2F;td&gt;&lt;td&gt;8 s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;sum of all coins from all users&lt;&#x2F;td&gt;&lt;td&gt;8 s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h4 id=&quot;surrealdb-cloud-free-tier&quot;&gt;SurrealDB cloud (free tier)&lt;&#x2F;h4&gt;
&lt;p&gt;Populating the table worked, but i got &lt;code&gt;The engine reported the connection to SurrealDB has dropped&lt;&#x2F;code&gt; for the rest of the queries. Every time. It might be that the free tier is too slow for these queries.&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Query&lt;&#x2F;th&gt;&lt;th&gt;Time&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;populate table&lt;&#x2F;td&gt;&lt;td&gt;339 s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;count all users&lt;&#x2F;td&gt;&lt;td&gt;?? s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;get ten users with most coins&lt;&#x2F;td&gt;&lt;td&gt;?? s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;sum of all coins from all users&lt;&#x2F;td&gt;&lt;td&gt;?? s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h4 id=&quot;surrealdb-cloud-paid-large-instance&quot;&gt;SurrealDB cloud (paid &quot;large&quot; instance)&lt;&#x2F;h4&gt;
&lt;p&gt;This instance has &lt;code&gt;2 vCPUs&lt;&#x2F;code&gt;, &lt;code&gt;8 GB RAM&lt;&#x2F;code&gt;, and &lt;code&gt;64 GB storage&lt;&#x2F;code&gt; and costs &lt;code&gt;$0.301&#x2F;hour&lt;&#x2F;code&gt; or about &lt;code&gt;$215&#x2F;month&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Query&lt;&#x2F;th&gt;&lt;th&gt;Time&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;populate table&lt;&#x2F;td&gt;&lt;td&gt;48 s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;count all users&lt;&#x2F;td&gt;&lt;td&gt;3 s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;get ten users with most coins&lt;&#x2F;td&gt;&lt;td&gt;6 s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;sum of all coins from all users&lt;&#x2F;td&gt;&lt;td&gt;4 s&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;bugs&quot;&gt;Bugs&lt;&#x2F;h2&gt;
&lt;p&gt;If you want to use SurrealDB in production, I recommend first skimming through their &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;surrealdb&#x2F;surrealdb&#x2F;issues?q=is%3Aopen+is%3Aissue+label%3Abug&quot;&gt;GitHub issues labeled with &quot;bug&quot;&lt;&#x2F;a&gt;. That might sober you up before jumping on the hype train too quickly.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;surrealdb&#x2F;surrealdb&#x2F;issues&#x2F;5062&quot;&gt;This one&lt;&#x2F;a&gt; kept me busy for a while: &lt;code&gt;SELECT * FROM users WHERE email CONTAINS &quot;@example.com&quot;&lt;&#x2F;code&gt; returned no results if &lt;code&gt;email&lt;&#x2F;code&gt; had an index.&lt;&#x2F;p&gt;
&lt;p&gt;Another, similar &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;surrealdb&#x2F;surrealdb&#x2F;issues&#x2F;3178#issuecomment-1863037508&quot;&gt;issue&lt;&#x2F;a&gt; I ran into also altered a SELECT query’s result if an index was present versus not. But it got resolved a few days after I published this post. Thanks to @mithridates on Bluesky for letting me know :)&lt;&#x2F;p&gt;
&lt;p&gt;I have little doubt that the SurrealDB team will fix these bugs and others like them. But for now, they are there and you should be aware of them.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;developer-experience&quot;&gt;Developer Experience&lt;&#x2F;h2&gt;
&lt;p&gt;As demonstrated in this post, &lt;a href=&quot;https:&#x2F;&#x2F;gohugo.io&quot;&gt;Hugo&lt;&#x2F;a&gt;, my SSG of choice, has syntax highlighting for SQL but not for SurrealQL. At least for now.&lt;&#x2F;p&gt;
&lt;p&gt;But there are syntax highlighting extensions for VS Code, Vim and &lt;a href=&quot;https:&#x2F;&#x2F;zed.dev&#x2F;&quot;&gt;Zed&lt;&#x2F;a&gt;, the editors I&#x27;m working with.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sdks&quot;&gt;SDKs&lt;&#x2F;h3&gt;
&lt;p&gt;SurrealDB has quite a few &lt;a href=&quot;https:&#x2F;&#x2F;surrealdb.com&#x2F;docs&#x2F;surrealdb&#x2F;integration&#x2F;sdks&quot;&gt;SDKs&lt;&#x2F;a&gt; for different languages. I&#x27;ve worked with the ones in Rust and Typescript. The one in Rust is fine, as is the one in Typescript.&lt;&#x2F;p&gt;
&lt;p&gt;But using the latter revealed a rather unpleasant side effect of record IDs in SurrealDB: As mentioned, they are composed of the table&#x27;s name and the record&#x27;s ID (&lt;code&gt;table_name:record_id&lt;&#x2F;code&gt;), e.g. &lt;code&gt;user:1234&lt;&#x2F;code&gt;. When querying the DB, it returns an object where the IDs are of class &lt;code&gt;RecordID&lt;&#x2F;code&gt;. Additionally, that class cannot be serialized to JSON. So you have to use the utility method &lt;code&gt;jsonify()&lt;&#x2F;code&gt; on on objects retrieved from the DB before you can, for example, send them as JSON to a client, who called your REST API.&lt;&#x2F;p&gt;
&lt;p&gt;The client gets &lt;code&gt;user:1234&lt;&#x2F;code&gt; as a string and needs to work with it. Suppose it wants to generate a link to the profile page of a user. Unless you manually remove the table part from the record ID, a link would look like this: &lt;code&gt;&#x2F;users&#x2F;user:1234&#x2F;profile&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;As far as I know, their SDK doesn&#x27;t offer a method to convert a string back to a &lt;code&gt;RecordID&lt;&#x2F;code&gt;, which has methods for safely extracting the table and ID parts. Doing that manually might be trivial for record IDs like &lt;code&gt;user:1234&lt;&#x2F;code&gt;, but the ID part could also be an &lt;a href=&quot;https:&#x2F;&#x2F;surrealdb.com&#x2F;docs&#x2F;surrealql&#x2F;datamodel&#x2F;ids#object-based-record-ids&quot;&gt;array or object&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;surrealist&quot;&gt;Surrealist&lt;&#x2F;h2&gt;
&lt;p&gt;Surrealist is the GUI for SurrealDB, and it is neat. It lets you manage connections to many DBs, query them, visualize their content, and it even paints you a pretty view of your relational data model. Here you can see the Record Link &lt;code&gt;loves&lt;&#x2F;code&gt; and the Relation &lt;code&gt;cares_for&lt;&#x2F;code&gt; between &lt;code&gt;human&lt;&#x2F;code&gt; and &lt;code&gt;dog&lt;&#x2F;code&gt; that we created earlier:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;dev&#x2F;surrealdb&#x2F;surrealist.png&quot; alt=&quot;Surrealist Designer View&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h2&gt;
&lt;p&gt;I&#x27;m excited about this relatively new piece of DB technology!
The feature list is genuinely impressive. Here are three more that I find particularly interesting:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Live queries actively pushing data to the client when new records pop up&lt;&#x2F;li&gt;
&lt;li&gt;Make HTTP calls from within the query language&lt;&#x2F;li&gt;
&lt;li&gt;Run SurrealDB in WASM in the browser&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Having one DB to store all your data (graph, relational, time series, and more) is compelling. However, I wouldn&#x27;t use it in production until it gets quite a bit faster, resolves pressing index issues, and offers more convenience for data consistency, like &lt;code&gt;on delete cascade&lt;&#x2F;code&gt;, and an &lt;code&gt;alter&lt;&#x2F;code&gt; keyword that also alters data, not just the schema.&lt;&#x2F;p&gt;
&lt;p&gt;But it&#x27;s fine for side projects like these I&#x27;ve built with it so far:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jflessau&#x2F;botto&quot;&gt;Botto&lt;&#x2F;a&gt; - a general-purpose chat bot for the Matrix protocol&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jflessau&#x2F;recipe-robot&quot;&gt;Recipe Robot&lt;&#x2F;a&gt; - an AI-powered web app that extracts ingredients from recipes and matches them with actual products from a grocery store, with prices and everything&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I&#x27;m working on two other projects powered by SurrealDB, and I will keep this blog post updated in case there are any more findings along the way.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;your-turn&quot;&gt;Your Turn&lt;&#x2F;h2&gt;
&lt;p&gt;If you want to get hands-on with SurrealDB, I recommend downloading &lt;a href=&quot;https:&#x2F;&#x2F;surrealdb.com&#x2F;surrealist&quot;&gt;Surrealist&lt;&#x2F;a&gt; and setting up a local DB to play around with. You don&#x27;t even have to learn the query language first to create tables, records, record links and relations. Surrealist has you covered for that.&lt;&#x2F;p&gt;
&lt;p&gt;Use this docker compose file to start a local SurrealDB instance:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;yaml&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-yaml &quot;&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;version&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;quot;3.8&amp;quot;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;services&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;surreal&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;image&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;surrealdb&#x2F;surrealdb
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;ports&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;      - &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;quot;8000:8000&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;volumes&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;      - &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;.&#x2F;db_data&#x2F;content:&#x2F;data&#x2F;
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;environment&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;      - &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;SURREAL_NAMESPACE=default
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#73d0ff;&quot;&gt;command&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;:
&lt;&#x2F;span&gt;&lt;span&gt;      [
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;quot;start&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;quot;--username&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;quot;test&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;quot;--password&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;quot;test&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;quot;--strict&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;quot;file:&#x2F;data&#x2F;database&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;,
&lt;&#x2F;span&gt;&lt;span&gt;      ]
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Drawer Inserts</title>
        <published>2024-02-12T00:00:00+00:00</published>
        <updated>2024-02-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Jan Flessau
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jflessau.com/projects/drawer-inserts/"/>
        <id>https://jflessau.com/projects/drawer-inserts/</id>
        
        <content type="html" xml:base="https://jflessau.com/projects/drawer-inserts/">&lt;p&gt;A few weeks ago I found &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;node-dojo&#x2F;dojo-recursive-bins&quot;&gt;this project&lt;&#x2F;a&gt; from node-dojo on GitHub and it used a feature of &lt;a href=&quot;https:&#x2F;&#x2F;www.blender.org&#x2F;&quot;&gt;Blender&lt;&#x2F;a&gt; that I didn’t know about.&lt;&#x2F;p&gt;
&lt;p&gt;node-dojo uses &lt;strong&gt;geometry nodes&lt;&#x2F;strong&gt; to automatically fill a 3D box with other boxes, which could then be 3D printed to serve as drawer inserts. I measured a drawer and tried it, but the &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Bin_packing_problem&quot;&gt;bin-packing&lt;&#x2F;a&gt; node-dojo implemented wasn’t quite what I needed for my drawers.&lt;&#x2F;p&gt;
&lt;p&gt;So I built &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jflessau&#x2F;drawer-inserts&quot;&gt;my own thing&lt;&#x2F;a&gt; and explored what geometry nodes could actually do.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;working-with-geometry-nodes&quot;&gt;Working with Geometry Nodes&lt;&#x2F;h2&gt;
&lt;p&gt;Geometry nodes allow you to edit geometry by configuring and connecting nodes. While you could achieve the same and more by writing Python code, these geometry nodes looked so fun that I had to try them.&lt;&#x2F;p&gt;
&lt;p&gt;It turns out they can do a lot, and the learning curve is okay if you&#x27;re already a little familiar with Blender.&lt;&#x2F;p&gt;
&lt;p&gt;Don’t get me wrong; it was still a challenge, but a fun one. If you&#x27;re not careful, your nodes will get messy pretty quickly. While text editors offer automatic formatting, with these nodes, you&#x27;re on your own. This &lt;a href=&quot;https:&#x2F;&#x2F;docs.blender.org&#x2F;manual&#x2F;en&#x2F;4.0&#x2F;addons&#x2F;node&#x2F;node_arrange.html&quot;&gt;addon for node arrangement&lt;&#x2F;a&gt; provides some level of automatic formatting, but I found that manual arrangement led to much more readable layouts.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;projects&#x2F;drawer-inserts&#x2F;blender-geometry-node-view-screenshot.jpg&quot; alt=&quot;Screenshot of Blender&amp;#39;s geometry nodes view showing a bunch of nodes connected with lines. Nothing particularly interesting here, just to illustrate how messy it can look.&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Geometry nodes work similarly to coding in text: you have access to arithmetic and boolean operations. You can group and collapse nodes and reuse networks with fixed sets of inputs and outputs, much like functions in a programming language.&lt;&#x2F;p&gt;
&lt;p&gt;Though it felt more difficult to achieve my desired outcome compared to coding in text. I found it hard to get a quick overview of what&#x27;s happening in a specific area. Frames (those dark boxes you see in the image above) helped with grouping and naming things. I wished I had discovered them sooner.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;create-your-custom-drawer-inserts&quot;&gt;Create your custom drawer inserts&lt;&#x2F;h2&gt;
&lt;p&gt;Download the Blender file from here: &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jflessau&#x2F;drawer-inserts&quot;&gt;https:&#x2F;&#x2F;github.com&#x2F;jflessau&#x2F;drawer-inserts&lt;&#x2F;a&gt; and open it.&lt;br &#x2F;&gt;
Now, you can experiment with the configuration options and generate your drawer inserts. The repository&#x27;s README contains a table explaining all configuration options.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;results&quot;&gt;Results&lt;&#x2F;h2&gt;
&lt;p&gt;Here is my first drawer with 3D printed inserts:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;projects&#x2F;drawer-inserts&#x2F;3d-printed-drawer-inserts.jpg&quot; alt=&quot;Photo of a drawer with 3D printed inserts.&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>BS Bingo</title>
        <published>2024-02-10T00:00:00+00:00</published>
        <updated>2024-02-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Jan Flessau
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jflessau.com/projects/bs-bingo/"/>
        <id>https://jflessau.com/projects/bs-bingo/</id>
        
        <content type="html" xml:base="https://jflessau.com/projects/bs-bingo/">&lt;p&gt;It’s fair to say that there is no shortage of bullshit. Sometimes it may even be hard to escape it. You should still try though.&lt;&#x2F;p&gt;
&lt;p&gt;I built a real-time multiplayer bullshit bingo web app for when there is no way around it. It&#x27;s a chance to still have some fun, even while knee-deep in it.&lt;&#x2F;p&gt;
&lt;p&gt;You can play it right here, no login required: &lt;a href=&quot;https:&#x2F;&#x2F;bingo.jflessau.com&#x2F;&quot;&gt;bingo.jflessau.com&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-it-works&quot;&gt;How it works&lt;&#x2F;h2&gt;
&lt;p&gt;Choose a public template consisting of 25+ words and phrases or create your own, then hit &lt;code&gt;Play&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Bullshit bingo, if you don&#x27;t already know it, works just like regular bingo. You get your card with a (usually 5x5) grid of words or phrases. For a marketing meeting, your card could look like this:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;projects&#x2F;bs-bingo&#x2F;marketing-buzzword-bingo-game.jpg&quot; alt=&quot;Marketing meeting bullshit bingo card with various marketing Buzzwords like &amp;quot;Viral&amp;quot;. Below that a list of players currently in the game with their username and a small representation of their bingo card showing which fileds were hit, but not the actual phrases of on the cards. There is also a count for how many bingos each player has.&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;When you hear the buzzword, click on it. Once you get a full row, horizontally, vertically, or diagonally, you&#x27;ve got a bingo!&lt;&#x2F;p&gt;
&lt;p&gt;Below your card is a list of all players and a representation of their cards. Cards may contain the same buzzwords, but they are randomly shuffled for each player.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;use-cases&quot;&gt;Use cases&lt;&#x2F;h2&gt;
&lt;p&gt;The obvious ones are presentations, meetings, and other occasions where you expect a ton of buzzwords. But that&#x27;s just the tip of the turd. Bullshit is to be found everywhere.&lt;&#x2F;p&gt;
&lt;p&gt;My favorite use case so far is public transport. Don’t get me wrong, I’m all for it. And playing a game of whose commute was more &lt;em&gt;interesting&lt;&#x2F;em&gt; makes it a lot more enjoyable:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;projects&#x2F;bs-bingo&#x2F;public-transport-bingo-card.jpg&quot; alt=&quot;5x5 bingo card with phrases like &amp;quot;Drunk passenger&amp;quot;, &amp;quot;Broken door&amp;quot;, &amp;quot;Connection canceled&amp;quot; on it.&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Games have no time limit so you and your friends can play one over the course of a year or more.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tech&quot;&gt;Tech&lt;&#x2F;h2&gt;
&lt;p&gt;The web app is built with &lt;a href=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;&quot;&gt;Svelte&lt;&#x2F;a&gt;, which might not be as mature as react, but feels less verbose.&lt;&#x2F;p&gt;
&lt;p&gt;The API is built in Rust. Apart from the boring CRUD stuff, there is a WebSocket connection for sending and receiving game updates in real-time. Postgres holds the game’s state and card templates. The Rust service subscribes to Postgres notifications, which is quite convenient when using &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;launchbadge&#x2F;sqlx&quot;&gt;sqlx&lt;&#x2F;a&gt; as your DB client:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;let mut&lt;&#x2F;span&gt;&lt;span&gt; listener &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span&gt;PgListener&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span&gt;connect_with(pool)&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span&gt;await&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;?&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;listener
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f28779;&quot;&gt;listen_all&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f28779;&quot;&gt;vec!&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;quot;fields_update&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;quot;players_update&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;])
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span&gt;await&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;?&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;loop &lt;&#x2F;span&gt;&lt;span&gt;{
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;let&lt;&#x2F;span&gt;&lt;span&gt; notification &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; listener&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f28779;&quot;&gt;recv&lt;&#x2F;span&gt;&lt;span&gt;()&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span&gt;await&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;?&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;let&lt;&#x2F;span&gt;&lt;span&gt; game_update&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span&gt; PgGameUpdateNotification &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span&gt;serde_json&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span&gt;from_str(notification&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f28779;&quot;&gt;payload&lt;&#x2F;span&gt;&lt;span&gt;())&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;?&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ccc9c2cc;&quot;&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;&#x2F;&#x2F; do something with game_update
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The corresponding part in SQL that sets up the notifier looks like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;create or replace function &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffd580;&quot;&gt;game_update_notification &lt;&#x2F;span&gt;&lt;span&gt;()
&lt;&#x2F;span&gt;&lt;span&gt;  returns trigger
&lt;&#x2F;span&gt;&lt;span&gt;  language plpgsql
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;as&lt;&#x2F;span&gt;&lt;span&gt; $$
&lt;&#x2F;span&gt;&lt;span&gt;  declare
&lt;&#x2F;span&gt;&lt;span&gt;    channel &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;text&lt;&#x2F;span&gt;&lt;span&gt; :&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; tg_argv[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;];
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;begin
&lt;&#x2F;span&gt;&lt;span&gt;    perform (
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;with&lt;&#x2F;span&gt;&lt;span&gt; payload(game_id) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;as&lt;&#x2F;span&gt;&lt;span&gt; (
&lt;&#x2F;span&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;select &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;new&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;game_id
&lt;&#x2F;span&gt;&lt;span&gt;      )
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;select
&lt;&#x2F;span&gt;&lt;span&gt;        pg_notify(channel,
&lt;&#x2F;span&gt;&lt;span&gt;        row_to_json(payload)::&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;text&lt;&#x2F;span&gt;&lt;span&gt;)
&lt;&#x2F;span&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;from
&lt;&#x2F;span&gt;&lt;span&gt;        payload
&lt;&#x2F;span&gt;&lt;span&gt;    );
&lt;&#x2F;span&gt;&lt;span&gt;    return &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;null&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;end&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;$$;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;create trigger &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffd580;&quot;&gt;fields_update
&lt;&#x2F;span&gt;&lt;span&gt;after &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;update&lt;&#x2F;span&gt;&lt;span&gt; on &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;bingo&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;fields
&lt;&#x2F;span&gt;&lt;span&gt;for each row execute procedure game_update_notification(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;fields_update&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;create trigger &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffd580;&quot;&gt;players_update
&lt;&#x2F;span&gt;&lt;span&gt;after &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;update&lt;&#x2F;span&gt;&lt;span&gt; on &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;bingo&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;players
&lt;&#x2F;span&gt;&lt;span&gt;for each row execute procedure game_update_notification(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;players_update&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In this case, the notification&#x27;s payload contains just the game’s ID. It’s up to the Rust service to then load more info about that game and send it to the players. But you could query as much data as you want within a notification trigger.&lt;&#x2F;p&gt;
&lt;p&gt;Shoutout to the makers of the &lt;a href=&quot;https:&#x2F;&#x2F;crates.io&#x2F;crates&#x2F;sqlx&quot;&gt;sqlx crate&lt;&#x2F;a&gt;! It is by far the most ergonomic way to interact with a DB in Rust and pretty much every other language I use. ORMs can be nice, until you debug whatever complicated query they’ve constructed for you.&lt;&#x2F;p&gt;
&lt;p&gt;ORMs offer typesafety you might say, but so does sqlx! You can write good old SQL with it and its macros will ensure that the types line up nicely. Works like magic.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;demo&quot;&gt;Demo&lt;&#x2F;h2&gt;
&lt;p&gt;Here is a demo of it: &lt;a href=&quot;https:&#x2F;&#x2F;bingo.jflessau.com&#x2F;&quot;&gt;bingo.jflessau.com&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;If you find a bug, have a feature request, or just want to host it yourself, here is the MIT licensed GitHub repo: &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jflessau&#x2F;bs-bingo&quot;&gt;github.com&#x2F;jflessau&#x2F;bs-bingo&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Catenary</title>
        <published>2024-01-11T00:00:00+00:00</published>
        <updated>2024-01-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Jan Flessau
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jflessau.com/projects/catenary/"/>
        <id>https://jflessau.com/projects/catenary/</id>
        
        <content type="html" xml:base="https://jflessau.com/projects/catenary/">&lt;p&gt;A while back, I spent a couple of hours on a train and pondered how cool it would be to have a chatroom for the entire train.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;problem&quot;&gt;Problem&lt;&#x2F;h2&gt;
&lt;p&gt;Being on a train is not as enjoyable as it could be. On longer journeys, you have two options: Keep to yourself and be bored, or attempt to strike up a conversation with someone nearby. The latter is high risk, high reward: You might have a fun talk with a complete stranger, or you could end up with someone less interesting or more annoying.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;solution&quot;&gt;Solution&lt;&#x2F;h2&gt;
&lt;p&gt;After many months since that train trip, I&#x27;m finally bringing this idea to life. Here&#x27;s how it works:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;User enters the site.&lt;&#x2F;li&gt;
&lt;li&gt;The site tracks the user&#x27;s location for a few seconds.&lt;&#x2F;li&gt;
&lt;li&gt;The site finds other users traveling with roughly the same speed, direction, and current location.&lt;&#x2F;li&gt;
&lt;li&gt;The site displays messages from matching other users.&lt;&#x2F;li&gt;
&lt;li&gt;The site allows users to send and up- or downvote messages.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Now, you can talk to people who share your (un)fortunate fate of being on the same vehicle.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;nasty-details&quot;&gt;Nasty Details&lt;&#x2F;h2&gt;
&lt;p&gt;With this small set of rules, people traveling in the same train, bus, etc., should see each other&#x27;s messages. Tuning the parameters for matching users is the challenging part. Buses are short and change direction often, while trains are long, fast, and less likely to change direction drastically. I guess this part is learning by dogfooding.&lt;&#x2F;p&gt;
&lt;p&gt;The obvious drawback is that you can&#x27;t really use the app if you are not moving. However, this is part of the appeal.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tech&quot;&gt;Tech&lt;&#x2F;h2&gt;
&lt;p&gt;It not only requires a few real users, but they also need to be moving in the same direction with the same speed at the same time. I have (almost) zero hopes that more than a few dozen people will ever see, let alone try it. So, building it for the sake of it being used is not enough to start building it.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;m encouraging myself to do it by choosing a somewhat new tech stack: &lt;a href=&quot;https:&#x2F;&#x2F;leptos.dev&#x2F;&quot;&gt;Leptos&lt;&#x2F;a&gt;. Leptos is a frontend framework for Rust with hydration, SSR, and the usual promises of being (blazingly) fast. And so far, I like it.&lt;&#x2F;p&gt;
&lt;p&gt;Chats and their messages are ephemeral; nothing is stored long-term, no database, no login, everyone stays anonymous. I&#x27;m imagining active users having a digital fishnet, capturing only nearby messages if they are in the right spot at the right time. Messages are deleted from the server a few minutes after they are sent. This is not only a privacy feature but also a way to keep the server load low. Win-win.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;demo&quot;&gt;Demo&lt;&#x2F;h2&gt;
&lt;p&gt;In case you want to try a demo, here you go:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;catenary.jflessau.com&#x2F;&quot;&gt;catenary.jflessau.de&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Remember to nag me by opening issues on GitHub if you even whiff a bug or have a feature request: &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jflessau&#x2F;catenary&quot;&gt;github.com&#x2F;jflessau&#x2F;catenary&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Scheduled Postgres Backups to S3</title>
        <published>2022-03-16T00:00:00+00:00</published>
        <updated>2022-03-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Jan Flessau
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jflessau.com/dev/postgres-dump-s3/"/>
        <id>https://jflessau.com/dev/postgres-dump-s3/</id>
        
        <content type="html" xml:base="https://jflessau.com/dev/postgres-dump-s3/">&lt;p&gt;A managed RDS is not exactly cheap. And I like my pet projects to be cheap. That‘s how I ran into the problem of making scheduled backups of a Postgres DB. I can‘t be the only one, so here is one way of solving it with a docker image.&lt;&#x2F;p&gt;
&lt;p&gt;👉 &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jflessau&#x2F;pg-dump-s3&quot;&gt;Link to the repository&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;inside-the-docker-container&quot;&gt;Inside the Docker Container&lt;&#x2F;h2&gt;
&lt;p&gt;Within the running container this happens:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pg_dump&lt;&#x2F;code&gt; creates a dump&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;aws-cli&lt;&#x2F;code&gt; uploads the dump to s3&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;cron&lt;&#x2F;code&gt; schedules a job with the two steps above to run every x minutes&#x2F;houres&#x2F;etc.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Pretty straight forward. But there is a catch.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;version-mismatch&quot;&gt;Version Mismatch&lt;&#x2F;h3&gt;
&lt;p&gt;The &lt;code&gt;pg_dump&lt;&#x2F;code&gt; command may not work across Postgres versions. Meaning you would want the &lt;code&gt;pg_dump&lt;&#x2F;code&gt; command to match the version of the Postgres database you pull a dump from.&lt;&#x2F;p&gt;
&lt;p&gt;Therefore building this image requires the build arg &lt;code&gt;POSTGRES_IMAGE_TAG&lt;&#x2F;code&gt;.&lt;br &#x2F;&gt;
Here is an example of how building the image could look like for Postgres v13.5:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;color:#ffd580;&quot;&gt;docker&lt;&#x2F;span&gt;&lt;span&gt; build&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt; --build-arg &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;quot;POSTGRES_IMAGE_TAG=13.5&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt; -t&lt;&#x2F;span&gt;&lt;span&gt; pg_dump_s3 .
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Internally it pulls the official Postgres image matching the provided tag. Here is a list of available &lt;a href=&quot;https:&#x2F;&#x2F;hub.docker.com&#x2F;_&#x2F;postgres&quot;&gt;tags for official Postgres images&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Note that this docker image does not work for all of them. E.g. &lt;code&gt;*-alpine&lt;&#x2F;code&gt; images don‘t work. But there is at least one that does for every major version from v9 to v14. Have a look at this &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jflessau&#x2F;pg-dump-s3#tested-postgres-images&quot;&gt;list of tested tags&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;See the &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jflessau&#x2F;pg-dump-s3&quot;&gt;README&lt;&#x2F;a&gt; for detailed information on how to build the image and run the container.&lt;&#x2F;p&gt;
&lt;p&gt;Feel free to open PRs or issues 🙂&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Postgres Transaction Isolation</title>
        <published>2021-11-25T00:00:00+00:00</published>
        <updated>2021-11-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Jan Flessau
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jflessau.com/dev/postgres-transaction-isolation-levels/"/>
        <id>https://jflessau.com/dev/postgres-transaction-isolation-levels/</id>
        
        <content type="html" xml:base="https://jflessau.com/dev/postgres-transaction-isolation-levels/">&lt;p&gt;In this post we‘ll have a look at transaction isolation levels in Postgres. But first, let‘s have a short recap about transactions and why they‘re needed.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-is-a-transaction&quot;&gt;What is a Transaction?&lt;&#x2F;h2&gt;
&lt;p&gt;A transaction combines multiple read and write steps. If a transaction is started but couldn&#x27;t be completed, it will be rolled back. A rolled back transaction has no effects on the data in the database. It&#x27;s an all-or-nothing operation.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;example&quot;&gt;Example&lt;&#x2F;h2&gt;
&lt;p&gt;Let&#x27;s say you have got a table for users of an online game:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;owner&lt;&#x2F;th&gt;&lt;th&gt;balance&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Lisa&lt;&#x2F;td&gt;&lt;td&gt;2000&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Users can spend digital coins on stuff in the game.&lt;&#x2F;p&gt;
&lt;p&gt;You want the balance to be correct at any time. No user should be able to spend more than they have and no one should be billed twice for an item.&lt;&#x2F;p&gt;
&lt;p&gt;When Lisa logs in and buys an item for 1000 coins the sql for it could look like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- check the balance to see if Lisa has enough coins
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;select&lt;&#x2F;span&gt;&lt;span&gt; balance &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; accounts &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;where&lt;&#x2F;span&gt;&lt;span&gt; owner &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;Lisa&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- in your business logic between the queries: check if that&amp;#39;s enough and
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- calculate the new balance (1000)
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- update Lisa&amp;#39;s balance
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;update&lt;&#x2F;span&gt;&lt;span&gt; accounts &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;set&lt;&#x2F;span&gt;&lt;span&gt; balance &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;1000 &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;where&lt;&#x2F;span&gt;&lt;span&gt; owner &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;Lisa&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That works totally fine and Lisa&#x27;s new balance is correct.&lt;&#x2F;p&gt;
&lt;p&gt;But what if Lisa is really clever and opens two sessions, one on a phone and one on her laptop?&lt;&#x2F;p&gt;
&lt;p&gt;In this case, she could click the buy-button for two items almost simultaneously:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;dev&#x2F;postgres-transaction-isolation-levels&#x2F;no-transaction.png&quot; alt=&quot;Example of two concurrent sessions without using transactions in Postgres&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;In session A Lisa buys an item for &lt;code&gt;1000&lt;&#x2F;code&gt; coins, so the new balance would be &lt;code&gt;1000&lt;&#x2F;code&gt;. In Session B she purchases an item for &lt;code&gt;1250&lt;&#x2F;code&gt; coins, so the new balance would be &lt;code&gt;750&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The problem is that both sessions read Lisa&#x27;s initial balance (&lt;code&gt;2000&lt;&#x2F;code&gt; coins) and therefore allow the respective purchases. The latest update query sets her balance to &lt;code&gt;750&lt;&#x2F;code&gt; coins when she really should have &lt;code&gt;-250&lt;&#x2F;code&gt;. She effectively stole &lt;code&gt;1000&lt;&#x2F;code&gt; coins 🙀&lt;&#x2F;p&gt;
&lt;p&gt;Transactions can be used to avoid this total nightmare. Wrap the queries of a purchase in a transaction and Postgres ensures that the balances are correct, even when purchases happen concurrently:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- begin transaction
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;begin&lt;&#x2F;span&gt;&lt;span&gt; transaction isolation level repeatable read;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- check the balance to see if Lisa has enough coins
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;select&lt;&#x2F;span&gt;&lt;span&gt; balance &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; accounts &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;where&lt;&#x2F;span&gt;&lt;span&gt; owner &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;Lisa&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- in your business logic between the queries: check if that&amp;#39;s enough and
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- calculate the new balance (1000)
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- update Lisa&amp;#39;s balance
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;update&lt;&#x2F;span&gt;&lt;span&gt; accounts &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;set&lt;&#x2F;span&gt;&lt;span&gt; balance &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;1000 &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;where&lt;&#x2F;span&gt;&lt;span&gt; owner &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;Lisa&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- commit the transaction
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;commit&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Let&#x27;s see what happens when we replay the scenario of two concurrent purchases. To simulate this situation we create and populate the table &lt;code&gt;accounts&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;create table &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffd580;&quot;&gt;accounts&lt;&#x2F;span&gt;&lt;span&gt; (owner &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;text primary key&lt;&#x2F;span&gt;&lt;span&gt;, balance &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;integer &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;not &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;null&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;insert into&lt;&#x2F;span&gt;&lt;span&gt; accounts &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;values&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;Lisa&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;2000&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;From now on we need two terminals to simulate Lisa&#x27;s two user sessions, terminal A and terminal B:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- TERMINAL A
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- begin transaction
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;begin&lt;&#x2F;span&gt;&lt;span&gt; transaction isolation level repeatable read;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- check the balance to see if Lisa has enough coins
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;select&lt;&#x2F;span&gt;&lt;span&gt; balance &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; accounts &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;where&lt;&#x2F;span&gt;&lt;span&gt; owner &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;Lisa&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- update Lisa&amp;#39;s balance
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;update&lt;&#x2F;span&gt;&lt;span&gt; accounts &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;set&lt;&#x2F;span&gt;&lt;span&gt; balance &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;1000 &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;where&lt;&#x2F;span&gt;&lt;span&gt; owner &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;Lisa&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Note that we did not commit the transaction yet, because we want to see what happens when we got two transactions running at the same time. So here comes the second one in terminal B:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- TERMINAL B
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- begin transaction
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;begin&lt;&#x2F;span&gt;&lt;span&gt; transaction isolation level repeatable read;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- check the balance to see if Lisa has enough coins
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;select&lt;&#x2F;span&gt;&lt;span&gt; balance &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; accounts &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;where&lt;&#x2F;span&gt;&lt;span&gt; owner &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;Lisa&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- update Lisa&amp;#39;s balance
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;update&lt;&#x2F;span&gt;&lt;span&gt; accounts &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;set&lt;&#x2F;span&gt;&lt;span&gt; balance &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;750 &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;where&lt;&#x2F;span&gt;&lt;span&gt; owner &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;Lisa&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Lisa starts with a balance of &lt;code&gt;2000&lt;&#x2F;code&gt; coins. In Terminal A we&#x27;re processing a purchase of an item worth &lt;code&gt;1000&lt;&#x2F;code&gt; coins, so we&#x27;re setting the new balance to &lt;code&gt;1000&lt;&#x2F;code&gt; coins. In terminal B we are processing a purchase worth &lt;code&gt;1250&lt;&#x2F;code&gt; coins and setting the balance to &lt;code&gt;750&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Without wrapping the respective queries in transactions this could actually work; Lisa would get both items and a final balance of either &lt;code&gt;1000&lt;&#x2F;code&gt; or &lt;code&gt;750&lt;&#x2F;code&gt;, depending on which update query is faster.&lt;&#x2F;p&gt;
&lt;p&gt;Now we commit the transaction in terminal A:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- TERMINAL A
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- commit the transaction
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;commit&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That should work just fine. But now we see this in terminal B:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- TERMINAL B
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;ERROR:  could not serialize access due to concurrent &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;update
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;and if we try to commit the transaction in terminal B with &lt;code&gt;commit;&lt;&#x2F;code&gt; we get&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- TERMINAL B
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;ROLLBACK
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Postgres throws an error because it wasn&#x27;t safe to update the same row that was updated in the transaction of terminal A.&lt;&#x2F;p&gt;
&lt;p&gt;Only the transaction in terminal A was successful and Lisa&#x27;s new balance is now &lt;code&gt;1000&lt;&#x2F;code&gt;. Cool! Postgres saved us and Lisa&#x27;s trick didn&#x27;t work this time.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;three-isolation-levels-of-postgres&quot;&gt;Three Isolation Levels of Postgres&lt;&#x2F;h2&gt;
&lt;p&gt;You probably noticed that we initiated the transactions in the example with the transaction isolation level &lt;code&gt;repeatable read&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- begin transaction
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;begin&lt;&#x2F;span&gt;&lt;span&gt; transaction isolation level repeatable read;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Postgres has three transaction isolation levels:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;read committed&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;repeatable read&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;serializable&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;Read committed&lt;&#x2F;code&gt; is the default option in Postgres and it would NOT throw an error in our example. That&#x27;s why we choose &lt;code&gt;repeatable read&lt;&#x2F;code&gt; and that&#x27;s why I think it&#x27;s a good idea check what those isolation levels actually do for us.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;four-things-to-worry-about&quot;&gt;Four Things to Worry About&lt;&#x2F;h2&gt;
&lt;p&gt;There are four things to worry about in concurrent transactions:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Dirty read&lt;&#x2F;li&gt;
&lt;li&gt;Repeatable read&lt;&#x2F;li&gt;
&lt;li&gt;Phantom read&lt;&#x2F;li&gt;
&lt;li&gt;Serialization anomaly&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;It&#x27;s kind of a mismatch: Three isolation levels but four things to worry about. Other database systems have one more isolation level: &lt;code&gt;read uncommitted&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;You can actually set the isolation level of a transaction to &lt;code&gt;read uncommited&lt;&#x2F;code&gt;, but internally there is no difference between &lt;code&gt;read committed&lt;&#x2F;code&gt; and &lt;code&gt;read uncommited&lt;&#x2F;code&gt; in Postgres.&lt;&#x2F;p&gt;
&lt;p&gt;Here is what the isolation levels in Postgres allow and what they prevent:&lt;&#x2F;p&gt;
&lt;div class=&quot;scrollwrapper&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Isolation Level&lt;&#x2F;th&gt;&lt;th&gt;Dirty Read&lt;&#x2F;th&gt;&lt;th&gt;Non repeatable Read&lt;&#x2F;th&gt;&lt;th&gt;Phantom Read&lt;&#x2F;th&gt;&lt;th&gt;Serialization Anomaly&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Read uncommitted&lt;&#x2F;td&gt;&lt;td&gt;🚫&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Read committed (default)&lt;&#x2F;td&gt;&lt;td&gt;🚫&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Repeatable read&lt;&#x2F;td&gt;&lt;td&gt;🚫&lt;&#x2F;td&gt;&lt;td&gt;🚫&lt;&#x2F;td&gt;&lt;td&gt;🚫&lt;&#x2F;td&gt;&lt;td&gt;✅&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Serializable&lt;&#x2F;td&gt;&lt;td&gt;🚫&lt;&#x2F;td&gt;&lt;td&gt;🚫&lt;&#x2F;td&gt;&lt;td&gt;🚫&lt;&#x2F;td&gt;&lt;td&gt;🚫&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;As you can see, &lt;code&gt;read uncommited&lt;&#x2F;code&gt; does the exact same things as &lt;code&gt;read commited&lt;&#x2F;code&gt; so we&#x27;ll ignore &lt;code&gt;read uncommited&lt;&#x2F;code&gt; from now on.&lt;&#x2F;p&gt;
&lt;p&gt;To understand what the different isolation levels do, we have to understand the four things to worry about in transactions.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;dirty-read&quot;&gt;Dirty Read&lt;&#x2F;h3&gt;
&lt;p&gt;A dirty read is a read within a transaction that reads data from another, uncommitted transaction. Dirty reads are never allowed in Postgres transactions, regardless of the isolation level. If you try to create a dirty read within a transaction, this happens:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;dev&#x2F;postgres-transaction-isolation-levels&#x2F;dirty-read.png&quot; alt=&quot;Example of a dirty read in Postgres with two concurrent transactions&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;You would expect that the read in transaction T2 gets a balance of 1000, but that would be a dirty read, because transaction T1 didn&#x27;t commit yet.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;non-repeatable-read&quot;&gt;Non Repeatable Read&lt;&#x2F;h3&gt;
&lt;p&gt;Non repeatable reads are the kind of phenomenon we tried to avoid in the example at the beginning of this article. We used the transaction level &lt;code&gt;repeatable read&lt;&#x2F;code&gt; to ensure that Lisa&#x27;s balance is correct, even when purchases are happening concurrently.&lt;&#x2F;p&gt;
&lt;p&gt;In the example we had two concurrent transactions that looked like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- begin transaction
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;begin&lt;&#x2F;span&gt;&lt;span&gt; transaction isolation level repeatable read;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- check the balance to see if Lisa has enough coins
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;select&lt;&#x2F;span&gt;&lt;span&gt; balance &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; accounts &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;where&lt;&#x2F;span&gt;&lt;span&gt; owner &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;Lisa&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- update Lisa&amp;#39;s balance
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;update&lt;&#x2F;span&gt;&lt;span&gt; accounts &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;set&lt;&#x2F;span&gt;&lt;span&gt; balance &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;1000 &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;where&lt;&#x2F;span&gt;&lt;span&gt; owner &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;Lisa&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- commit the transaction
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;commit&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;When running two of these transactions concurrently with the isolation level &lt;code&gt;repeatable read&lt;&#x2F;code&gt; you&#x27;ll get an error in transaction T2 once you commit T1. And when you then try to commit T2, T2 will perform a &lt;code&gt;rollback&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;dev&#x2F;postgres-transaction-isolation-levels&#x2F;non-repeatable-read.png&quot; alt=&quot;A timeline of two repeatable read transactions.&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;And even if you leave out the select statements...&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- begin transaction
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;begin&lt;&#x2F;span&gt;&lt;span&gt; transaction isolation level repeatable read;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- update Lisa&amp;#39;s balance
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;update&lt;&#x2F;span&gt;&lt;span&gt; accounts &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;set&lt;&#x2F;span&gt;&lt;span&gt; balance &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;500 &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;where&lt;&#x2F;span&gt;&lt;span&gt; owner &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;Lisa&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- commit the transaction
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;commit&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;... you&#x27;ll get the same error (&lt;code&gt;could not serialize access due to concurrent update&lt;&#x2F;code&gt;) when trying to run two of these transactions concurrently.&lt;&#x2F;p&gt;
&lt;p&gt;But without the select statements, are we still doing a non repeatable &lt;strong&gt;read&lt;&#x2F;strong&gt;? Yes! According to the &lt;a href=&quot;https:&#x2F;&#x2F;www.postgresql.org&#x2F;docs&#x2F;current&#x2F;transaction-iso.html#XACT-REPEATABLE-READ&quot;&gt;Postgres docs&lt;&#x2F;a&gt;:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;UPDATE&lt;&#x2F;code&gt;, &lt;code&gt;DELETE&lt;&#x2F;code&gt;, &lt;code&gt;SELECT FOR UPDATE&lt;&#x2F;code&gt;, and &lt;code&gt;SELECT FOR SHARE&lt;&#x2F;code&gt; commands behave the same as &lt;code&gt;SELECT&lt;&#x2F;code&gt; in terms of searching for target rows [...]&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;phantom-read&quot;&gt;Phantom Read&lt;&#x2F;h3&gt;
&lt;p&gt;A phantom read occurs when transaction T1 reads some rows from a table, then another, concurrent transaction T2 adds rows that would be in the selection of T1 and commits, finally T1 performs the same select again, but gets more rows returned because of T2&#x27;s insertion.&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s super complicated as a sentence, but much simpler in a picture:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;dev&#x2F;postgres-transaction-isolation-levels&#x2F;phantom-read-1.png&quot; alt=&quot;Example of a phantom read with two concurrent transactions&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Let&#x27;s try that in our terminal:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- (re-) create the accounts table from the first example
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;create table &lt;&#x2F;span&gt;&lt;span&gt;if not exists &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffd580;&quot;&gt;accounts&lt;&#x2F;span&gt;&lt;span&gt; (owner &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;text primary key&lt;&#x2F;span&gt;&lt;span&gt;, balance &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;integer &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;not &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;null&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- wipe the table
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;delete from&lt;&#x2F;span&gt;&lt;span&gt; accounts;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- insert the first user
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;insert into&lt;&#x2F;span&gt;&lt;span&gt; accounts &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;values&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;Lisa&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;2000&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;From now on we&#x27;re again working with two separate terminals (A &amp;amp; B) so that we can create concurrent transactions.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- TERMINAL A
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- start transaction that prevents phantom reads
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;begin&lt;&#x2F;span&gt;&lt;span&gt; transaction isolation level repeatable read;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- select rows from the accounts table
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;select &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5ccfe6;&quot;&gt;* &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; accounts &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;where&lt;&#x2F;span&gt;&lt;span&gt; balance &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;500&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- it returns:
&lt;&#x2F;span&gt;&lt;span&gt;owner | balance
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-------+---------
&lt;&#x2F;span&gt;&lt;span&gt; Lisa  |    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;2000
&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt; row)
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now we switch to terminal B, start a transaction, insert a new row and commit:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- TERMINAL B
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- start transaction
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;begin&lt;&#x2F;span&gt;&lt;span&gt; transaction isolation level repeatable read;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- insert a new row
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;insert into&lt;&#x2F;span&gt;&lt;span&gt; accounts &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;values&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;John&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;1250&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- commit the transaction
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;commit&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now back to terminal A and let&#x27;s see what the same select statement returns:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- TERMINAL A
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- select rows from the accounts table
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;select &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5ccfe6;&quot;&gt;* &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; accounts &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;where&lt;&#x2F;span&gt;&lt;span&gt; balance &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;500&lt;&#x2F;span&gt;&lt;span&gt;;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- it returns:
&lt;&#x2F;span&gt;&lt;span&gt;owner | balance
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-------+---------
&lt;&#x2F;span&gt;&lt;span&gt; Lisa  |    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;2000
&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt; row)
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Ha! We got just Lisa&#x27;s row. If the select query also returned John&#x27;s row, that would be a phantom read. But the isolation level we chose (&lt;code&gt;repeatable read&lt;&#x2F;code&gt;) does not allow that, so only Lisa&#x27;s row is returned.&lt;&#x2F;p&gt;
&lt;p&gt;If you did the same steps with the lower isolation level &lt;code&gt;read committed&lt;&#x2F;code&gt; you would also get John&#x27;s row, because &lt;code&gt;read committed&lt;&#x2F;code&gt; allows phantom reads:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;dev&#x2F;postgres-transaction-isolation-levels&#x2F;phantom-read-2.png&quot; alt=&quot;Timeline of two transactions with read committed isolation&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;serialization-anomaly&quot;&gt;Serialization Anomaly&lt;&#x2F;h3&gt;
&lt;p&gt;Serializable is the most restrictive isolation level in Postgres. When using this level, Postgres will prevent serialization anomaly. Meaning it ensures that concurrent transactions are only allowed if the result is the same, regardless of the order in which transactions are processed.&lt;&#x2F;p&gt;
&lt;p&gt;Let&#x27;s use the &lt;code&gt;accounts&lt;&#x2F;code&gt; table one last time to see how that looks in sql. First we prepare our setup:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- (re-) create the accounts table from the first example
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;create table &lt;&#x2F;span&gt;&lt;span&gt;if not exists &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffd580;&quot;&gt;accounts&lt;&#x2F;span&gt;&lt;span&gt; (owner &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;text primary key&lt;&#x2F;span&gt;&lt;span&gt;, balance &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;integer &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;not &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;null&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- wipe the table
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;delete from&lt;&#x2F;span&gt;&lt;span&gt; accounts;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- insert the first user
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;insert into&lt;&#x2F;span&gt;&lt;span&gt; accounts &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;values&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;Lisa&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;2000&lt;&#x2F;span&gt;&lt;span&gt;);
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now we want to perform two transactions that produce different results, depending on the order of their execution. Both transactions will try to insert a new row into the table. The balance value will be equal to the sum of all balances in the table.&lt;&#x2F;p&gt;
&lt;p&gt;Again, we&#x27;re working with two terminals (A &amp;amp; B) to be able to start two concurrent transactions:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- TERMINAL A
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- start transaction
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;begin&lt;&#x2F;span&gt;&lt;span&gt; transaction isolation level serializable;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- select rows from the accounts table
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;insert into&lt;&#x2F;span&gt;&lt;span&gt; accounts &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;select &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;transaction T1&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f28779;&quot;&gt;sum&lt;&#x2F;span&gt;&lt;span&gt;(balance) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; accounts;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- TERMINAL B
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- start transaction
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;begin&lt;&#x2F;span&gt;&lt;span&gt; transaction isolation level serializable;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- insert a new row
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;insert into&lt;&#x2F;span&gt;&lt;span&gt; accounts &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;select &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;transaction T2&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f28779;&quot;&gt;sum&lt;&#x2F;span&gt;&lt;span&gt;(balance) &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; accounts;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Depending on which transaction is faster, we expect one of these results:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- if T1 commits before T2
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;select &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5ccfe6;&quot;&gt;* &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; accounts;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;owner             | balance
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;------------------+---------
&lt;&#x2F;span&gt;&lt;span&gt; Lisa             |    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;2000
&lt;&#x2F;span&gt;&lt;span&gt; transaction T1   |    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;2000
&lt;&#x2F;span&gt;&lt;span&gt; transaction T2   |    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;4000
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;-- if T2 commits before T1
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;select &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5ccfe6;&quot;&gt;* &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; accounts;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;owner             | balance
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;------------------+---------
&lt;&#x2F;span&gt;&lt;span&gt; Lisa             |    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;2000
&lt;&#x2F;span&gt;&lt;span&gt; transaction T1   |    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;4000
&lt;&#x2F;span&gt;&lt;span&gt; transaction T2   |    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;2000
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If T1 commits before T2, the sum for T1 should be &lt;code&gt;2000&lt;&#x2F;code&gt;. By the time T2 commits, T2 should get &lt;code&gt;4000&lt;&#x2F;code&gt; as the sum of all balances (&lt;code&gt;2000&lt;&#x2F;code&gt; for each &lt;code&gt;Lisa&lt;&#x2F;code&gt; and &lt;code&gt;transaction T1&lt;&#x2F;code&gt;).&lt;&#x2F;p&gt;
&lt;p&gt;But the transaction level &lt;code&gt;serializable&lt;&#x2F;code&gt; does not allow both transactions to commit. The first transaction that commits will be fine, but for the second one you commit you will get this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span&gt;ERROR:  could not serialize access due to read&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;&#x2F;&lt;&#x2F;span&gt;&lt;span&gt;write dependencies among transactions
&lt;&#x2F;span&gt;&lt;span&gt;DETAIL:  Reason code: Canceled on identification &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;as&lt;&#x2F;span&gt;&lt;span&gt; a pivot, during &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;commit&lt;&#x2F;span&gt;&lt;span&gt; attempt.
&lt;&#x2F;span&gt;&lt;span&gt;HINT:  The transaction might succeed if retried.
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Here is a timeline of the transactions:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;jflessau.com&#x2F;dev&#x2F;postgres-transaction-isolation-levels&#x2F;serialization-anomaly.png&quot; alt=&quot;Timeline of two transactions with serializable isolation&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;If you did the same transactions with one of the other two isolation levels, both transactions would be able to commit and the result would be:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;sql&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-sql &quot;&gt;&lt;code class=&quot;language-sql&quot; data-lang=&quot;sql&quot;&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;select &lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5ccfe6;&quot;&gt;* &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffa759;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; accounts;
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;owner           | balance
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#5c6773;&quot;&gt;----------------+---------
&lt;&#x2F;span&gt;&lt;span&gt; Lisa           |    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;2000
&lt;&#x2F;span&gt;&lt;span&gt; transaction T1 |    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;2000
&lt;&#x2F;span&gt;&lt;span&gt; transaction T2 |    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt;2000
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And this is not what we expected. Either the balance of &lt;code&gt;transaction T1&lt;&#x2F;code&gt; or &lt;code&gt;transaction T1&lt;&#x2F;code&gt; should be &lt;code&gt;4000&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Because the order of execution matters for the outcome of these two transactions, the isolation level &lt;code&gt;serializable&lt;&#x2F;code&gt; will only allow one of them to commit.&lt;&#x2F;p&gt;
&lt;p&gt;Postgres again prevented us from ending up with the wrong balance.&lt;&#x2F;p&gt;
&lt;p&gt;We can now retry the failed transaction and given that no other interfering transaction is running, it will commit just fine 🚀&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h2&gt;
&lt;p&gt;Transaction isolation is a powerful concept. It helps you to avoid inconsistent data without using a lot more business logic code.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Searchable PDFs</title>
        <published>2021-10-13T00:00:00+00:00</published>
        <updated>2021-10-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Jan Flessau
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jflessau.com/dev/searchable-pdfs/"/>
        <id>https://jflessau.com/dev/searchable-pdfs/</id>
        
        <content type="html" xml:base="https://jflessau.com/dev/searchable-pdfs/">&lt;p&gt;Welcome to my first post on this new blog!&lt;br &#x2F;&gt;
In this article I&#x27;ll explain how I make my ever growing pile of PDFs searchable.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;motivation&quot;&gt;Motivation&lt;&#x2F;h2&gt;
&lt;p&gt;Recently I ran into some situations where I needed to find a specific document. It drove me nuts to manually open and close PDFs to find the one I was looking for.&lt;&#x2F;p&gt;
&lt;p&gt;So I made all my PDFs searchable and here is how that worked:&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tl-dr&quot;&gt;TL;DR&lt;&#x2F;h2&gt;
&lt;p&gt;Throw all your PDF files into a folder, run &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jbarlow83&#x2F;OCRmyPDF&quot;&gt;ocrmypdf&lt;&#x2F;a&gt; to make them searchable:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;color:#ffd580;&quot;&gt;find&lt;&#x2F;span&gt;&lt;span&gt; .&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;*&lt;&#x2F;span&gt;&lt;span&gt;.pdf&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt; -type&lt;&#x2F;span&gt;&lt;span&gt; f&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt; -name &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;*.pdf&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt; -not -name &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;*_ocr.pdf&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt; -exec&lt;&#x2F;span&gt;&lt;span&gt; sh&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt; -c &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;ocrmypdf &amp;quot;${0%.*}.pdf&amp;quot; &amp;quot;${0%.*}_ocr.pdf&amp;quot;&amp;#39; &lt;&#x2F;span&gt;&lt;span&gt;{} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#95e6cb;&quot;&gt;\;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;then use &lt;a href=&quot;https:&#x2F;&#x2F;pdfgrep.org&#x2F;&quot;&gt;pdfgrep&lt;&#x2F;a&gt; to search for things:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;color:#ffd580;&quot;&gt;pdfgrep&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt; -i --cache &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;taxpayer identification number&amp;#39; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;*&lt;&#x2F;span&gt;&lt;span&gt;.pdf
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;and never waste time on tagging, renaming or categorizing PDFs again.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;using-tesseract-pdfgrep&quot;&gt;Using tesseract &amp;amp; pdfgrep&lt;&#x2F;h2&gt;
&lt;p&gt;There are some really powerful tools out there to get us out of this dilemma. And it&#x27;s super simple:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Put all PDFs in one place&lt;&#x2F;li&gt;
&lt;li&gt;Let tesseract take care of OCR&lt;&#x2F;li&gt;
&lt;li&gt;Use pdfgrep for search&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;The first step is my favorite. No manual categorizing, no file tags, no problems!&lt;&#x2F;p&gt;
&lt;h3 id=&quot;tesseract&quot;&gt;Tesseract&lt;&#x2F;h3&gt;
&lt;p&gt;Tesseract is an open source tool for optical character recognition (OCR). It takes an image and finds text in it:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;color:#ffd580;&quot;&gt;tesseract&lt;&#x2F;span&gt;&lt;span&gt; input.png output&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt; -l&lt;&#x2F;span&gt;&lt;span&gt; eng
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Cool! Now we have a way to read text from images. However, this post is all about PDFs. So how can we feed a PDF to tesseract?&lt;&#x2F;p&gt;
&lt;p&gt;Unfortunately, tesseract only works with image formats like PNG, JPEG and some others. You could convert your PDFs to images like this:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;color:#ffd580;&quot;&gt;convert&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt; -density&lt;&#x2F;span&gt;&lt;span&gt; 300 input.pdf&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt; -depth&lt;&#x2F;span&gt;&lt;span&gt; 8 output.png
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;and then use tesseract on the generated images. But there is a simpler way to do this: &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jbarlow83&#x2F;OCRmyPDF&quot;&gt;OCRmyPDF&lt;&#x2F;a&gt;. This is another CLI that does the conversion for you automatically.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;color:#ffd580;&quot;&gt;ocrmypdf&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt; -l&lt;&#x2F;span&gt;&lt;span&gt; eng input.pdf output.pdf
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That command works for one file. To run OCR on all PDF files in a folder, use this one:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;color:#ffd580;&quot;&gt;find&lt;&#x2F;span&gt;&lt;span&gt; .&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;*&lt;&#x2F;span&gt;&lt;span&gt;.pdf&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt; -type&lt;&#x2F;span&gt;&lt;span&gt; f&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt; -name &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;*.pdf&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt; -not -name &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;*_ocr.pdf&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt; -exec&lt;&#x2F;span&gt;&lt;span&gt; sh&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt; -c &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;ocrmypdf &amp;quot;${0%.*}.pdf&amp;quot; &amp;quot;${0%.*}_ocr.pdf&amp;quot;&amp;#39; &lt;&#x2F;span&gt;&lt;span&gt;{} &lt;&#x2F;span&gt;&lt;span style=&quot;color:#95e6cb;&quot;&gt;\;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This will keep the original files and creates copies of those with &quot;_ocr&quot; attached to their filenames. Whenever you add more PDFs to your folder and need to OCR them, just re-run this command and it will try to OCR files without the &quot;_ocr&quot; suffix.&lt;&#x2F;p&gt;
&lt;p&gt;You can either keep the old files or delete all files without the &quot;_ocr&quot; suffix in the current directory by using this command:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;color:#ffd580;&quot;&gt;find &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;*&lt;&#x2F;span&gt;&lt;span&gt;.pdf !&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt; -name &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;*_ocr.pdf&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt; -delete
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Only searchable PDFs are left now. Whenever you throw in more PDFs you will instantly know which need to be treated with ocrmypdf: All those without the &lt;code&gt;_ocr&lt;&#x2F;code&gt; suffix.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pdfgrep&quot;&gt;pdfgrep&lt;&#x2F;h3&gt;
&lt;p&gt;Now that all your PDFs are searchable, let&#x27;s try pdfgrep:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;bash&quot; style=&quot;background-color:#212733;color:#ccc9c2;&quot; class=&quot;language-bash &quot;&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span style=&quot;color:#ffd580;&quot;&gt;pdfgrep&lt;&#x2F;span&gt;&lt;span style=&quot;color:#ffcc66;&quot;&gt; -i --cache &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bae67e;&quot;&gt;&amp;#39;invoice&amp;#39; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#f29e74;&quot;&gt;*&lt;&#x2F;span&gt;&lt;span&gt;.pdf
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This will give you a list of filenames as well as some characters before&#x2F;after the matching phrase and the page number.&lt;&#x2F;p&gt;
&lt;p&gt;Note that depending on the amount of PDFs, the first run could take a while. By using the &lt;code&gt;--cache&lt;&#x2F;code&gt; flag the next search will be much quicker!&lt;&#x2F;p&gt;
&lt;h2 id=&quot;using-docker&quot;&gt;Using Docker&lt;&#x2F;h2&gt;
&lt;p&gt;If you are familiar with docker, you can also try &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;cmccambridge&#x2F;ocrmypdf-auto&quot;&gt;this docker image&lt;&#x2F;a&gt;. It let&#x27;s you specify an input and and output directory. You put all your PDFs into the input directory and the running docker container will perform OCR on them and outputs the searchable PDFs to the specified output directory.&lt;&#x2F;p&gt;
&lt;p&gt;Have a look at the &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;cmccambridge&#x2F;ocrmypdf-auto&quot;&gt;Readme&lt;&#x2F;a&gt; for more instructions on how to use and configure it.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Legal Notice</title>
        <published>2021-03-06T00:00:00+00:00</published>
        <updated>2021-03-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Jan Flessau
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jflessau.com/info/legal-notice/"/>
        <id>https://jflessau.com/info/legal-notice/</id>
        
        <content type="html" xml:base="https://jflessau.com/info/legal-notice/">&lt;h2 id=&quot;site-notice&quot;&gt;Site Notice&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;information-pursuant-to-sect-5-german-telemedia-act-tmg&quot;&gt;Information pursuant to Sect. 5 German Telemedia Act (TMG)&lt;&#x2F;h3&gt;
&lt;p&gt;Jan Flessau&lt;br &#x2F;&gt;
Meiendorfer Straße 56b&lt;br &#x2F;&gt;
22145 Hamburg&lt;&#x2F;p&gt;
&lt;h3 id=&quot;contact&quot;&gt;Contact&lt;&#x2F;h3&gt;
&lt;p&gt;Phone: 0175 5950528&lt;br &#x2F;&gt;
E-mail: jan.jf@me.com&lt;&#x2F;p&gt;
&lt;h3 id=&quot;vat-id&quot;&gt;VAT ID&lt;&#x2F;h3&gt;
&lt;p&gt;Sales tax identification number according to Sect. 27 a of the Sales Tax Law:
DE323212188&lt;&#x2F;p&gt;
&lt;h3 id=&quot;responsible-for-the-content-according-to-sect-55-paragraph-2-rstv&quot;&gt;Responsible for the content according to Sect. 55, paragraph 2 RStV&lt;&#x2F;h3&gt;
&lt;p&gt;Jan Flessau&lt;br &#x2F;&gt;
Meiendorfer Straße 56b&lt;br &#x2F;&gt;
22145 Hamburg&lt;&#x2F;p&gt;
&lt;h3 id=&quot;eu-dispute-resolution&quot;&gt;EU dispute resolution&lt;&#x2F;h3&gt;
&lt;p&gt;The European Commission provides a platform for online dispute resolution (ODR): &lt;a href=&quot;https:&#x2F;&#x2F;ec.europa.eu&#x2F;consumers&#x2F;odr&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https:&#x2F;&#x2F;ec.europa.eu&#x2F;consumers&#x2F;odr&lt;&#x2F;a&gt;. Our e-mail address can be found above in the site notice.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;dispute-resolution-proceedings-in-front-of-a-consumer-arbitration-board&quot;&gt;Dispute resolution proceedings in front of a consumer arbitration board&lt;&#x2F;h3&gt;
&lt;p&gt;We are not willing or obliged to participate in dispute resolution proceedings in front of a consumer arbitration board.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;liability-for-contents&quot;&gt;Liability for Contents&lt;&#x2F;h3&gt;
&lt;p&gt;As service providers, we are liable for own contents of these websites according to Paragraph 7, Sect. 1 German Telemedia Act (TMG). However, according to Paragraphs 8 to 10 German Telemedia Act (TMG), service providers are not obligated to permanently monitor submitted or stored information or to search for evidences that indicate illegal activities. Legal obligations to removing information or to blocking the use of information remain unchallenged. In this case, liability is only possible at the time of knowledge about a specific violation of law. Illegal contents will be removed immediately at the time we get knowledge of them.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;liability-for-links&quot;&gt;Liability for Links&lt;&#x2F;h3&gt;
&lt;p&gt;Our offer includes links to external third party websites. We have no influence on the contents of those websites, therefore we cannot guarantee for those contents. Providers or administrators of linked websites are always responsible for their own contents. The linked websites had been checked for possible violations of law at the time of the establishment of the link. Illegal contents were not detected at the time of the linking. A permanent monitoring of the contents of linked websites cannot be imposed without reasonable indications that there has been a violation of law. Illegal links will be removed immediately at the time we get knowledge of them.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;copyright&quot;&gt;Copyright&lt;&#x2F;h3&gt;
&lt;p&gt;Contents and compilations published on these websites by the providers are subject to German copyright laws. Reproduction, editing, distribution as well as the use of any kind outside the scope of the copyright law require a written permission of the author or originator. Downloads and copies of these websites are permitted for private use only. The commercial use of our contents without permission of the originator is prohibited. Copyright laws of third parties are respected as long as the contents on these websites do not originate from the provider. Contributions of third parties on this site are indicated as such. However, if you notice any violations of copyright law, please inform us. Such contents will be removed immediately.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Privacy Policy</title>
        <published>2021-03-06T00:00:00+00:00</published>
        <updated>2021-03-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Jan Flessau
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://jflessau.com/info/privacy-policy/"/>
        <id>https://jflessau.com/info/privacy-policy/</id>
        
        <content type="html" xml:base="https://jflessau.com/info/privacy-policy/">&lt;h2 id=&quot;1-an-overview-of-data-protection&quot;&gt;1. An overview of data protection&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;general-information&quot;&gt;General information&lt;&#x2F;h3&gt;
&lt;p&gt;The following information will provide you with an easy to navigate overview of what will happen with your personal data when you visit this website. The term “personal data” comprises all data that can be used to personally identify you. For detailed information about the subject matter of data protection, please consult our Data Protection Declaration, which we have included beneath this copy.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;data-recording-on-this-website&quot;&gt;Data recording on this website&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;who-is-the-responsible-party-for-the-recording-of-data-on-this-website-i-e-the-controller&quot;&gt;Who is the responsible party for the recording of data on this website (i.e. the “controller”)?&lt;&#x2F;h4&gt;
&lt;p&gt;The data on this website is processed by the operator of the website, whose contact information is available under section “Information Required by Law” on this website.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;how-do-we-record-your-data&quot;&gt;How do we record your data?&lt;&#x2F;h4&gt;
&lt;p&gt;We collect your data as a result of your sharing of your data with us. This may, for instance be information you enter into our contact form.&lt;&#x2F;p&gt;
&lt;p&gt;Other data shall be recorded by our IT systems automatically or after you consent to its recording during your website visit. This data comprises primarily technical information (e.g. web browser, operating system or time the site was accessed). This information is recorded automatically when you access this website.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;what-are-the-purposes-we-use-your-data-for&quot;&gt;What are the purposes we use your data for?&lt;&#x2F;h4&gt;
&lt;p&gt;A portion of the information is generated to guarantee the error free provision of the website. Other data may be used to analyze your user patterns.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;what-rights-do-you-have-as-far-as-your-information-is-concerned&quot;&gt;What rights do you have as far as your information is concerned?&lt;&#x2F;h4&gt;
&lt;p&gt;You have the right to receive information about the source, recipients and purposes of your archived personal data at any time without having to pay a fee for such disclosures. You also have the right to demand that your data are rectified or eradicated. If you have consented to data processing, you have the option to revoke this consent at any time, which shall affect all future data processing. Moreover, you have the right to demand that the processing of your data be restricted under certain circumstances. Furthermore, you have the right to log a complaint with the competent supervising agency.&lt;&#x2F;p&gt;
&lt;p&gt;Please do not hesitate to contact us at any time under the address disclosed in section “Information Required by Law” on this website if you have questions about this or any other data protection related issues.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;analysis-tools-and-tools-provided-by-third-parties&quot;&gt;Analysis tools and tools provided by third parties&lt;&#x2F;h3&gt;
&lt;p&gt;There is a possibility that your browsing patterns will be statistically analyzed when your visit this website. Such analyses are performed primarily with what we refer to as analysis programs.&lt;&#x2F;p&gt;
&lt;p&gt;For detailed information about these analysis programs please consult our Data Protection Declaration below.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2-hosting-and-content-delivery-networks-cdn&quot;&gt;2. Hosting and Content Delivery Networks (CDN)&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;external-hosting&quot;&gt;External Hosting&lt;&#x2F;h3&gt;
&lt;p&gt;This website is hosted by an external service provider (host). Personal data collected on this website are stored on the servers of the host. These may include, but are not limited to, IP addresses, contact requests, metadata and communications, contract information, contact information, names, web page access, and other data generated through a web site.
The host is used for the purpose of fulfilling the contract with our potential and existing customers (Art. 6 para. 1 lit. b GDPR) and in the interest of secure, fast and efficient provision of our online services by a professional provider (Art. 6 para. 1 lit. f GDPR).
Our host will only process your data to the extent necessary to fulfil its performance obligations and to follow our instructions with respect to such data.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;execution-of-a-contract-data-processing-agreement&quot;&gt;Execution of a contract data processing agreement&lt;&#x2F;h4&gt;
&lt;p&gt;In order to guarantee processing in compliance with data protection regulations, we have concluded an order processing contract with our host.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;3-general-information-and-mandatory-information&quot;&gt;3. General information and mandatory information&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;data-protection&quot;&gt;Data protection&lt;&#x2F;h3&gt;
&lt;p&gt;The operators of this website and its pages take the protection of your personal data very seriously. Hence, we handle your personal data as confidential information and in compliance with the statutory data protection regulations and this Data Protection Declaration.&lt;&#x2F;p&gt;
&lt;p&gt;Whenever you use this website, a variety of personal information will be collected. Personal data comprises data that can be used to personally identify you. This Data Protection Declaration explains which data we collect as well as the purposes we use this data for. It also explains how, and for which purpose the information is collected.&lt;&#x2F;p&gt;
&lt;p&gt;We herewith advise you that the transmission of data via the Internet (i.e. through e-mail communications) may be prone to security gaps. It is not possible to completely protect data against third party access.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;information-about-the-responsible-party-referred-to-as-the-controller-in-the-gdpr&quot;&gt;Information about the responsible party (referred to as the “controller” in the GDPR)&lt;&#x2F;h3&gt;
&lt;p&gt;The data processing controller on this website is:&lt;br &#x2F;&gt;
Jan Flessau&lt;br &#x2F;&gt;
Meiendorfer Straße 56b&lt;br &#x2F;&gt;
22145 Hamburg&lt;br &#x2F;&gt;
Phone: 0175 5950528&lt;br &#x2F;&gt;
E-mail: jan.jf@me.com&lt;&#x2F;p&gt;
&lt;p&gt;The controller is the natural person or legal entity that single-handedly or jointly with others makes decisions as to the purposes of and resources for the processing of personal data (e.g. names, e-mail addresses, etc.).&lt;&#x2F;p&gt;
&lt;h3 id=&quot;revocation-of-your-consent-to-the-processing-of-data&quot;&gt;Revocation of your consent to the processing of data&lt;&#x2F;h3&gt;
&lt;p&gt;A wide range of data processing transactions are possible only subject to your express consent. You can also revoke at any time any consent you have already given us. This shall be without prejudice to the lawfulness of any data collection that occurred prior to your revocation.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;right-to-object-to-the-collection-of-data-in-special-cases-right-to-object-to-direct-advertising-art-21-gdpr&quot;&gt;Right to object to the collection of data in special cases; right to object to direct advertising (Art. 21 GDPR)&lt;&#x2F;h3&gt;
&lt;p&gt;IN THE EVENT THAT DATA ARE PROCESSED ON THE BASIS OF ART. 6 SECT. 1 LIT. E OR F GDPR, YOU HAVE THE RIGHT TO AT ANY TIME OBJECT TO THE PROCESSING OF YOUR PERSONAL DATA BASED ON GROUNDS ARISING FROM YOUR UNIQUE SITUATION. THIS ALSO APPLIES TO ANY PROFILING BASED ON THESE PROVISIONS. TO DETERMINE THE LEGAL BASIS, ON WHICH ANY PROCESSING OF DATA IS BASED, PLEASE CONSULT THIS DATA PROTECTION DECLARATION. IF YOU LOG AN OBJECTION, WE WILL NO LONGER PROCESS YOUR AFFECTED PERSONAL DATA, UNLESS WE ARE IN A POSITION TO PRESENT COMPELLING PROTECTION WORTHY GROUNDS FOR THE PROCESSING OF YOUR DATA, THAT OUTWEIGH YOUR INTERESTS, RIGHTS AND FREEDOMS OR IF THE PURPOSE OF THE PROCESSING IS THE CLAIMING, EXERCISING OR DEFENCE OF LEGAL ENTITLEMENTS (OBJECTION PURSUANT TO ART. 21 SECT. 1 GDPR).
IF YOUR PERSONAL DATA IS BEING PROCESSED IN ORDER TO ENGAGE IN DIRECT ADVERTISING, YOU HAVE THE RIGHT TO AT ANY TIME OBJECT TO THE PROCESSING OF YOUR AFFECTED PERSONAL DATA FOR THE PURPOSES OF SUCH ADVERTISING. THIS ALSO APPLIES TO PROFILING TO THE EXTENT THAT IT IS AFFILIATED WITH SUCH DIRECT ADVERTISING. IF YOU OBJECT, YOUR PERSONAL DATA WILL SUBSEQUENTLY NO LONGER BE USED FOR DIRECT ADVERTISING PURPOSES (OBJECTION PURSUANT TO ART. 21 SECT. 2 GDPR).&lt;&#x2F;p&gt;
&lt;h3 id=&quot;right-to-log-a-complaint-with-the-competent-supervisory-agency&quot;&gt;Right to log a complaint with the competent supervisory agency&lt;&#x2F;h3&gt;
&lt;p&gt;In the event of violations of the GDPR, data subjects are entitled to log a complaint with a supervisory agency, in particular in the member state where they usually maintain their domicile, place of work or at the place where the alleged violation occurred. The right to log a complaint is in effect regardless of any other administrative or court proceedings available as legal recourses.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;right-to-data-portability&quot;&gt;Right to data portability&lt;&#x2F;h3&gt;
&lt;p&gt;You have the right to demand that we hand over any data we automatically process on the basis of your consent or in order to fulfil a contract be handed over to you or a third party in a commonly used, machine readable format. If you should demand the direct transfer of the data to another controller, this will be done only if it is technically feasible.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ssl-and-or-tls-encryption&quot;&gt;SSL and&#x2F;or TLS encryption&lt;&#x2F;h3&gt;
&lt;p&gt;For security reasons and to protect the transmission of confidential content, such as purchase orders or inquiries you submit to us as the website operator, this website uses either an SSL or a TLS encryption program. You can recognize an encrypted connection by checking whether the address line of the browser switches from “http:&#x2F;&#x2F;” to “https:&#x2F;&#x2F;” and also by the appearance of the lock icon in the browser line.&lt;&#x2F;p&gt;
&lt;p&gt;If the SSL or TLS encryption is activated, data you transmit to us cannot be read by third parties.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;information-about-rectification-and-eradication-of-data&quot;&gt;Information about, rectification and eradication of data&lt;&#x2F;h3&gt;
&lt;p&gt;Within the scope of the applicable statutory provisions, you have the right to at any time demand information about your archived personal data, their source and recipients as well as the purpose of the processing of your data. You may also have a right to have your data rectified or eradicated. If you have questions about this subject matter or any other questions about personal data, please do not hesitate to contact us at any time at the address provided in section “Information Required by Law.”&lt;&#x2F;p&gt;
&lt;h3 id=&quot;right-to-demand-processing-restrictions&quot;&gt;Right to demand processing restrictions&lt;&#x2F;h3&gt;
&lt;p&gt;You have the right to demand the imposition of restrictions as far as the processing of your personal data is concerned. To do so, you may contact us at any time at the address provided in section “Information Required by Law.” The right to demand restriction of processing applies in the following cases:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;In the event that you should dispute the correctness of your data archived by us, we will usually need some time to verify this claim. During the time that this investigation is ongoing, you have the right to demand that we restrict the processing of your personal data.&lt;&#x2F;li&gt;
&lt;li&gt;If the processing of your personal data was&#x2F;is conducted in an unlawful manner, you have the option to demand the restriction of the processing of your data in lieu of demanding the eradication of this data.&lt;&#x2F;li&gt;
&lt;li&gt;If we do not need your personal data any longer and you need it to exercise, defend or claim legal entitlements, you have the right to demand the restriction of the processing of your personal data instead of its eradication.&lt;&#x2F;li&gt;
&lt;li&gt;If you have raised an objection pursuant to Art. 21 Sect. 1 GDPR, your rights and our rights will have to be weighed against each other. As long as it has not been determined whose interests prevail, you have the right to demand a restriction of the processing of your personal data.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;If you have restricted the processing of your personal data, these data – with the exception of their archiving – may be processed only subject to your consent or to claim, exercise or defend legal entitlements or to protect the rights of other natural persons or legal entities or for important public interest reasons cited by the European Union or a member state of the EU.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;rejection-of-unsolicited-e-mails&quot;&gt;Rejection of unsolicited e-mails&lt;&#x2F;h3&gt;
&lt;p&gt;We herewith object to the use of contact information published in conjunction with the mandatory information to be provided in section “Information Required by Law” to send us promotional and information material that we have not expressly requested. The operators of this website and its pages reserve the express right to take legal action in the event of the unsolicited sending of promotional information, for instance via SPAM messages.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;4-recording-of-data-on-this-website&quot;&gt;4. Recording of data on this website&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;cookies&quot;&gt;Cookies&lt;&#x2F;h3&gt;
&lt;p&gt;Our websites and pages use what the industry refers to as “cookies.” Cookies are small text files that do not cause any damage to your device. They are either stored temporarily for the duration of a session (session cookies) or they are permanently archived on your device (permanent cookies). Session cookies are automatically deleted once you terminate your visit. Permanent cookies remain archived on your device until you actively delete them or they are automatically eradicated by your web browser.
In some cases it is possible that third party cookies are stored on your device once you enter our site (third party cookies). These cookies enable you or us to take advantage of certain services offered by the third party (e.g. cookies for the processing of payment services).
Cookies have a variety of functions. Many cookies are technically essential since certain website functions would not work in the absence of the cookies (e.g. the shopping cart function or the display of videos). The purpose of other cookies may be the analysis of user patterns or the display of promotional messages.
Cookies, which are required for the performance of electronic communication transactions (required cookies) or for the provision of certain functions you want to use (functional cookies, e.g. for the shopping cart function) or those that are necessary for the optimization of the website (e.g. cookies that provide measurable insights into the web audience), shall be stored on the basis of Art. 6 Sect. 1 lit. f GDPR, unless a different legal basis is cited. The operator of the website has a legitimate interest in the storage of cookies to ensure the technically error free and optimized provision of the operator’s services. If your consent to the storage of the cookies has been requested, the respective cookies are stored exclusively on the basis of the consent obtained (Art. 6 Sect. 1 lit. a GDPR); this consent may be revoked at any time.&lt;&#x2F;p&gt;
&lt;p&gt;You have the option to set up your browser in such a manner that you will be notified any time cookies are placed and to permit the acceptance of cookies only in specific cases. You may also exclude the acceptance of cookies in certain cases or in general or activate the delete function for the automatic eradication of cookies when the browser closes. If cookies are deactivated, the functions of this website may be limited.
In the event that third party cookies are used or if cookies are used for analytical purposes, we will separately notify you in conjunction with this Data Protection Policy and, if applicable, ask for your consent.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;server-log-files&quot;&gt;Server log files&lt;&#x2F;h3&gt;
&lt;p&gt;The provider of this website and its pages automatically collects and stores information in so-called server log files, which your browser communicates to us automatically. The information comprises:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;The type and version of browser used&lt;&#x2F;li&gt;
&lt;li&gt;The used operating system&lt;&#x2F;li&gt;
&lt;li&gt;Referrer URL&lt;&#x2F;li&gt;
&lt;li&gt;The hostname of the accessing computer&lt;&#x2F;li&gt;
&lt;li&gt;The time of the server inquiry&lt;&#x2F;li&gt;
&lt;li&gt;The IP address&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;This data is not merged with other data sources.&lt;&#x2F;p&gt;
&lt;p&gt;This data is recorded on the basis of Art. 6 Sect. 1 lit. f GDPR. The operator of the website has a legitimate interest in the technically error free depiction and the optimization of the operator’s website. In order to achieve this, server log files must be recorded.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;request-by-e-mail-telephone-or-fax&quot;&gt;Request by e-mail, telephone or fax&lt;&#x2F;h3&gt;
&lt;p&gt;If you contact us by e-mail, telephone or fax, your request, including all resulting personal data (name, request) will be stored and processed by us for the purpose of processing your request. We do not pass these data on without your consent.
These data are processed on the basis of Art. 6 Sect. 1 lit. b GDPR if your inquiry is related to the fulfillment of a contract or is required for the performance of pre-contractual measures. In all other cases, the data are processed on the basis of our legitimate interest in the effective handling of inquiries submitted to us (Art. 6 Sect. 1 lit. f GDPR) or on the basis of your consent (Art. 6 Sect. 1 lit. a GDPR) if it has been obtained.
The data sent by you to us via contact requests remain with us until you request us to delete, revoke your consent to the storage or the purpose for the data storage lapses (e.g. after completion of your request). Mandatory statutory provisions - in particular statutory retention periods - remain unaffected.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;5-newsletter&quot;&gt;5. Newsletter&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;newsletter-data&quot;&gt;Newsletter data&lt;&#x2F;h3&gt;
&lt;p&gt;If you would like to subscribe to the newsletter offered on this website, we will need from you an e-mail address as well as information that allow us to verify that you are the owner of the e-mail address provided and consent to the receipt of the newsletter. No further data shall be collected or shall be collected only on a voluntary basis. We shall use such data only for the sending of the requested information and shall not share such data with any third parties.&lt;&#x2F;p&gt;
&lt;p&gt;The processing of the information entered into the newsletter subscription form shall occur exclusively on the basis of your consent (Art. 6 Sect. 1 lit. a GDPR). You may revoke the consent you have given to the archiving of data, the e-mail address and the use of this information for the sending of the newsletter at any time, for instance by clicking on the “Unsubscribe” link in the newsletter. This shall be without prejudice to the lawfulness of any data processing transactions that have taken place to date.&lt;&#x2F;p&gt;
&lt;p&gt;The data deposited with us for the purpose of subscribing to the newsletter will be stored by us until you unsubscribe from the newsletter or the newsletter service provider and deleted from the newsletter distribution list after you unsubscribe from the newsletter. Data stored for other purposes with us remain unaffected.&lt;&#x2F;p&gt;
&lt;p&gt;After you unsubscribe from the newsletter distribution list, your e-mail address may be stored by us or the newsletter service provider in a blacklist to prevent future mailings. The data from the blacklist is used only for this purpose and not merged with other data. This serves both your interest and our interest in complying with the legal requirements when sending newsletters (legitimate interest within the meaning of Art. 6 para. 1 lit. f GDPR). The storage in the blacklist is indefinite. You may object to the storage if your interests outweigh our legitimate interest.&lt;&#x2F;p&gt;
</content>
        
    </entry>
</feed>
