<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mark Burggraf</title>
    <description>The latest articles on DEV Community by Mark Burggraf (@burggraf).</description>
    <link>https://dev.to/burggraf</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F870481%2Fa6f17ee8-a038-4b85-9675-a678130283fa.png</url>
      <title>DEV Community: Mark Burggraf</title>
      <link>https://dev.to/burggraf</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/burggraf"/>
    <language>en</language>
    <item>
      <title>How to Get PostgreSQL Explain / Analyze Statistics from the Supabase Javascript Client</title>
      <dc:creator>Mark Burggraf</dc:creator>
      <pubDate>Fri, 03 Feb 2023 18:09:27 +0000</pubDate>
      <link>https://dev.to/burggraf/how-to-get-postgresql-explain-analyze-statistics-from-the-supabase-javascript-client-3nm</link>
      <guid>https://dev.to/burggraf/how-to-get-postgresql-explain-analyze-statistics-from-the-supabase-javascript-client-3nm</guid>
      <description>&lt;h2&gt;
  
  
  Please Explain
&lt;/h2&gt;

&lt;p&gt;PostgreSQL has this great tool for analyzing complex queries.  You can use it to see what parts of your query are "costing" you CPU time, and use that information to build the right indexes to make your queries run lightning fast:&lt;/p&gt;

&lt;p&gt;Postgres &lt;code&gt;EXPLAIN&lt;/code&gt; — show the execution plan of a statement&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.postgresql.org/docs/current/sql-explain.html" rel="noopener noreferrer"&gt;https://www.postgresql.org/docs/current/sql-explain.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is great when you're writing SQL, but what if you're using the Supabase Javascript client?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
    name,
    teams (
      name
    )
  `&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Maybe you tried using &lt;code&gt;select query from pg_stat_statements&lt;/code&gt; to find the query that your client code is sending to PostgreSQL.  But then you see all sorts of parameters in the query such as &lt;code&gt;$1&lt;/code&gt;, &lt;code&gt;$2&lt;/code&gt;, &lt;code&gt;$3&lt;/code&gt;.  Now how do you analyze this?&lt;/p&gt;

&lt;p&gt;There's an easier way -- you can get this data right from your client in debug mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step One: Turn on &lt;code&gt;db_plan_enabled&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;To be able to debug from the client side, you'll need to enable it first by running these commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;role&lt;/span&gt; &lt;span class="n"&gt;authenticator&lt;/span&gt; 
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;pgrst&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_plan_enabled&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;notify&lt;/span&gt; &lt;span class="n"&gt;pgrst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'reload config'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step Two: Debug your client queries
&lt;/h2&gt;

&lt;p&gt;Now that you have &lt;code&gt;db_plan_enabled&lt;/code&gt; turned on you can do this in your client code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
    name,
    teams (
      name
    )
  `&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;explain&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
      &lt;span class="na"&gt;verbose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
      &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will return valuable information about the query execution plan that you can use to tune your database for better performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step Three: Turn it off
&lt;/h2&gt;

&lt;p&gt;When you're done debugging, just turn &lt;code&gt;db_plan_enabled&lt;/code&gt; back off with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;role&lt;/span&gt; &lt;span class="n"&gt;authenticator&lt;/span&gt; 
&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;pgrst&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_plan_enabled&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;notify&lt;/span&gt; &lt;span class="n"&gt;pgrst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'reload config'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This makes it easier to see exactly what PostgREST (the PostgreSQL REST interface used by the Supabase Javascript library) is doing behind the scenes.  Once you know where the big costs are in your query, you can build the necessary indexes and you should see better performance on those operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Note
&lt;/h2&gt;

&lt;p&gt;You'll need to be on PostgREST 10.1.1 or higher to get this capability.  &lt;/p&gt;

&lt;p&gt;To check your version, you can run this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s1"&gt;'&amp;lt;SUPABASE_URL&amp;gt;/rest/v1/'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"apikey: &amp;lt;SUPABASE_ANON_KEY&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;SUPABASE_ANON_KEY&amp;gt;"&lt;/span&gt; | more
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see the PostgREST version near the very beginning of the output.&lt;/p&gt;

</description>
      <category>accountabilibuddies</category>
      <category>career</category>
      <category>productivity</category>
      <category>discuss</category>
    </item>
    <item>
      <title>SQL or NoSQL? Why not use both (with PostgreSQL)?</title>
      <dc:creator>Mark Burggraf</dc:creator>
      <pubDate>Mon, 21 Nov 2022 23:11:59 +0000</pubDate>
      <link>https://dev.to/burggraf/sql-or-nosql-why-not-use-both-with-postgresql-1df8</link>
      <guid>https://dev.to/burggraf/sql-or-nosql-why-not-use-both-with-postgresql-1df8</guid>
      <description>&lt;p&gt;It's a tough decision for any developer starting a new project.  Should you store your data in a standard, time-tested SQL database, or go with one of the newer NoSQL document-based databases?  This seemingly simple decision can literally make or break your project down the line.  Choose correctly and structure your data well, and you may sail smoothly into production and watch your app take off.  Make the wrong choice and you could be headed for nightmares (and maybe even some major re-writes) before your app ever makes it out the door.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simplicity vs Power
&lt;/h3&gt;

&lt;p&gt;There are tradeoffs with both SQL and NoSQL solutions.  Typically it's easier to get started with NoSQL data structures, especially when the data is complex or hierarchical.  You can just take a JSON data object from your front-end code and throw it in the database and be done with it.  But later when you need to access that data to answer some basic business questions, it's much more difficult.  A SQL solution makes it easier to gather data and draw conclusions down the line.  Let's look at an example:&lt;/p&gt;

&lt;p&gt;Each day I track the food I eat, along with the number of calories in each item:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Day&lt;/th&gt;
&lt;th&gt;Food Item&lt;/th&gt;
&lt;th&gt;Calories&lt;/th&gt;
&lt;th&gt;Meal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;01 Jan&lt;/td&gt;
&lt;td&gt;Apple&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;Breakfast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;01 Jan&lt;/td&gt;
&lt;td&gt;Oatmeal&lt;/td&gt;
&lt;td&gt;146&lt;/td&gt;
&lt;td&gt;Breakfast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;01 Jan&lt;/td&gt;
&lt;td&gt;Sandwich&lt;/td&gt;
&lt;td&gt;445&lt;/td&gt;
&lt;td&gt;Lunch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;01 Jan&lt;/td&gt;
&lt;td&gt;Chips&lt;/td&gt;
&lt;td&gt;280&lt;/td&gt;
&lt;td&gt;Lunch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;01 Jan&lt;/td&gt;
&lt;td&gt;Cookie&lt;/td&gt;
&lt;td&gt;108&lt;/td&gt;
&lt;td&gt;Lunch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;01 Jan&lt;/td&gt;
&lt;td&gt;Mixed Nuts&lt;/td&gt;
&lt;td&gt;175&lt;/td&gt;
&lt;td&gt;Snack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;01 Jan&lt;/td&gt;
&lt;td&gt;Pasta/Sauce&lt;/td&gt;
&lt;td&gt;380&lt;/td&gt;
&lt;td&gt;Dinner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;01 Jan&lt;/td&gt;
&lt;td&gt;Garlic Bread&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;Dinner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;01 Jan&lt;/td&gt;
&lt;td&gt;Broccoli&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;Dinner&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I also track the number of cups of water I drink and when I drink them:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Day&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Cups&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jan 01&lt;/td&gt;
&lt;td&gt;08:15&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jan 01&lt;/td&gt;
&lt;td&gt;09:31&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jan 01&lt;/td&gt;
&lt;td&gt;10:42&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jan 01&lt;/td&gt;
&lt;td&gt;12:07&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jan 01&lt;/td&gt;
&lt;td&gt;14:58&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jan 01&lt;/td&gt;
&lt;td&gt;17:15&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jan 01&lt;/td&gt;
&lt;td&gt;18:40&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jan 01&lt;/td&gt;
&lt;td&gt;19:05&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And finally, I track my exercise:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Day&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Duration&lt;/th&gt;
&lt;th&gt;Exercise&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jan 01&lt;/td&gt;
&lt;td&gt;11:02&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;td&gt;Walking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jan 02&lt;/td&gt;
&lt;td&gt;09:44&lt;/td&gt;
&lt;td&gt;0.75&lt;/td&gt;
&lt;td&gt;Bicycling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jan 02&lt;/td&gt;
&lt;td&gt;17:00&lt;/td&gt;
&lt;td&gt;0.25&lt;/td&gt;
&lt;td&gt;Walking&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For each day I also track my current weight along with any notes for the day:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Day&lt;/th&gt;
&lt;th&gt;Weight&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jan 01&lt;/td&gt;
&lt;td&gt;172.6&lt;/td&gt;
&lt;td&gt;This new diet is awesome!&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jan 14&lt;/td&gt;
&lt;td&gt;170.2&lt;/td&gt;
&lt;td&gt;Not sure all this is worth it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jan 22&lt;/td&gt;
&lt;td&gt;169.8&lt;/td&gt;
&lt;td&gt;Jogged past a McDonald's today. It was hard.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feb 01&lt;/td&gt;
&lt;td&gt;168.0&lt;/td&gt;
&lt;td&gt;I feel better, but sure miss all that greasy food.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Gathering All That Data
&lt;/h3&gt;

&lt;p&gt;That's a lot of different data that needs to be gathered, stored, retrieved, and later analyzed.  It's organized simply and easily, but the number of records varies from day to day.  On any given day I may have zero or more entries for food, water, and exercise, and I may have zero or one entry for weight &amp;amp; notes.&lt;/p&gt;

&lt;p&gt;In my app, I gather all the data for a single day on one page, to make it easier for my users.  So, I get a JSON object for each day that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2022-01-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
  &lt;/span&gt;&lt;span class="nl"&gt;"weight"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;172.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
  &lt;/span&gt;&lt;span class="nl"&gt;"notes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"This new diet is awesome!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
  &lt;/span&gt;&lt;span class="nl"&gt;"food"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Apple"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"calories"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"meal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Breakfast"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Oatmeal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"calories"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;146&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"meal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Breakfast"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sandwich"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"calories"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;445&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"meal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Lunch"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Chips"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"calories"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"meal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Lunch"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cookie"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"calories"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;108&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"meal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Lunch"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mixed Nuts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"calories"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;175&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"meal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Snack"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pasta/Sauce"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"calories"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;380&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"meal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dinner"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Garlic Bread"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"calories"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"meal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dinner"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Broccoli"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"calories"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"meal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dinner"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"water"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"08:15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"09:31"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10:42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10:42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12:07"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"14:58"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"17:15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"18:40"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"19:05"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exercise"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"11:02"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Walking"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Saving the Data
&lt;/h3&gt;

&lt;p&gt;Once we've gathered all the data for a day, we need to store it in our database.  In a NoSQL database, this can be a pretty easy process, as we can just create a record (document) for a specific user for a specific date and throw document into a collection and we're done.  With SQL, we have some structure we have to work within, and in this case it looks like 4 separate tables: food, water, exercise, and notes.  We'd want to do 4 separate inserts here, one for each table.  If we don't have data for a specific table (say no exercise was recorded today) then we can skip that table.&lt;/p&gt;

&lt;p&gt;If you're using SQL to store this data, you might want to save each table's data as it's entered in your data entry form (and not wait until all the data is entered.)  Or you might want to create a database function that takes all the JSON data, parses it, and writes it to all the related tables in a single transaction.  There's a lot of ways to handle this, but suffice it to say this: it's a bit more complicated than saving the data in a NoSQL database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retrieving the Data
&lt;/h3&gt;

&lt;p&gt;If we want to display all the data for a single day, it's pretty much the same.  With NoSQL you can grab the data for the user's day and then use it in your application.  Nice!  With SQL we need to query 4 tables to get all the data (or we could use a function to get it all in a single call.)  Of course, when displaying the data, we'd need to first break up our JSON data into pieces that are needed by each section of our dashboard screen, and you could argue that it's simpler to map each SQL table with the dashboard section on the screen, but that's a trivial point.&lt;/p&gt;

&lt;h3&gt;
  
  
  Analyzing the Data
&lt;/h3&gt;

&lt;p&gt;Now that we've saved the data and we can retrieve it and display it, let's use it for some analysis.  &lt;em&gt;Let's display a graph of how many total calories I've eaten over the past month.&lt;/em&gt;  With SQL, this is a simple task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; 
   &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;calories&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;total_calories&lt;/span&gt; 
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;food_log&lt;/span&gt; 
&lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt; 
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'xyz'&lt;/span&gt; 
&lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt; &lt;span class="k"&gt;between&lt;/span&gt; &lt;span class="s1"&gt;'2022-01-01'&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="s1"&gt;'2022-01-31'&lt;/span&gt; 
&lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bam!  Done!  Now we can send those results to our graphing library and make a nice pretty picture of my eating habits.&lt;/p&gt;

&lt;p&gt;But if we've stored this data in NoSQL, it gets a little more complicated.  We'll need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;grab all the data for the user for the month&lt;/li&gt;
&lt;li&gt;parse each day's data to get the food log information&lt;/li&gt;
&lt;li&gt;loop through each day and total the calories&lt;/li&gt;
&lt;li&gt;send the aggregate data to our graphing module&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this is something we're going to do regularly, it makes sense to calculate the total calories for each day and store it in the day's document so we can get at that data faster.  But that requires more work up front, and we still need to pull the data for each day and parse out that calorie total first.  And if we update the data we still need to recalculate things and update that total.  Eventually we'll want to do that with the water and exercise totals as well.  The code will eventually start to get longer and more complex.&lt;/p&gt;

&lt;h3&gt;
  
  
  SQL and NoSQL Together - FTW
&lt;/h3&gt;

&lt;p&gt;Let's see how we can use the power of SQL together with the ease-of-use of NoSQL in the same database to make this all a bit easier.  We'll create a table for each day of data (for each user) and store the basic fields such as &lt;code&gt;weight&lt;/code&gt; and &lt;code&gt;notes&lt;/code&gt; first.  Then we'll just throw the &lt;code&gt;food_log&lt;/code&gt;, &lt;code&gt;water_log&lt;/code&gt;, and &lt;code&gt;exercise_log&lt;/code&gt; fields in a &lt;code&gt;JSONB&lt;/code&gt; field.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;uuid_generate_v4&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;weight&lt;/span&gt; &lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;food_log&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;water_log&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;exercise_log&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- (Optional) - create a foreign key relationship for the user_id field &lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt;
    &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;calendar_user_id_fkey&lt;/span&gt; &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let's insert some data into the table.  PostgreSQL offers both JSON and JSONB fields, and since the latter are more optimized by the database and much faster for query processing, we’ll almost always want to use JSONB.  We’ll use JSONB fields for &lt;code&gt;food_log&lt;/code&gt;, &lt;code&gt;water_log&lt;/code&gt;, and &lt;code&gt;exercise_log&lt;/code&gt; and just dump the data we got from our app right into those fields as a string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;food_log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;water_log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exercise_log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
   &lt;span class="s1"&gt;'2022-01-01'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
   &lt;span class="s1"&gt;'xyz'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
   &lt;span class="mi"&gt;172&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
   &lt;span class="s1"&gt;'This new diet is awesome!'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="s1"&gt;'[
      { "title": "Apple", "calories": 72, "meal": "Breakfast"},
      { "title": "Oatmeal", "calories": 146, "meal": "Breakfast"},
      { "title": "Sandwich", "calories": 445, "meal": "Lunch"},
      { "title": "Chips", "calories": 280, "meal": "Lunch"},
      { "title": "Cookie", "calories": 108, "meal": "Lunch"},
      { "title": "Mixed Nuts", "calories": 175, "meal": "Snack"},
      { "title": "Pasta/Sauce", "calories": 380, "meal": "Dinner"},
      { "title": "Garlic Bread", "calories": 200, "meal": "Dinner"},
      { "title": "Broccoli", "calories": 32, "meal": "Dinner"}
     ]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="s1"&gt;'[
      {"time": "08:15", "qty": 1},
      {"time": "09:31", "qty": 1},
      {"time": "10:42", "qty": 2},
      {"time": "10:42", "qty": 2},
      {"time": "12:07", "qty": 1},
      {"time": "14:58", "qty": 1},
      {"time": "17:15", "qty": 1},
      {"time": "18:40", "qty": 1},
      {"time": "19:05", "qty": 1}
    ]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="s1"&gt;'[
      {"time": "11:02", "duration": 0.5, "type": "Walking"}
    ]'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While that's a big insert statement, it sure beats doing inserts on 4 separate tables.  With all those food entries and water log entries, we would have had to made 1 entry in the main table, then 9 food_log entries, 9 water_log entries, and one exercise_log entry for a total of 20 database records.  We've wrapped that into a single record.&lt;/p&gt;

&lt;h3&gt;
  
  
  But How Do We Query This Data?
&lt;/h3&gt;

&lt;p&gt;Great, we're collecting the data now, and it's easy to insert the data into the database.  Editing the data isn't too bad either because we're just downloading the data to the client, updating the JSON field(s) as needed, and throwing them back into the database.  Not too hard.  But how can I query this data?  What about that task from before? &lt;em&gt;Let's display a graph of how many total calories I've eaten over the past month.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In this case, that data is stored inside the &lt;code&gt;food_log&lt;/code&gt; field inside the &lt;code&gt;calendar&lt;/code&gt; table.  If only PostgreSQL had a way of converting JSONB arrays into individual database records (recordsets).  Well, it does!  The &lt;code&gt;jsonb_array_elements&lt;/code&gt; function will do this for us, allowing to create a simple table we can use to calculate our caloric intake.&lt;/p&gt;

&lt;p&gt;Here's some SQL to turn that &lt;code&gt;food_log&lt;/code&gt; array into individual output records:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; 
  &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;jsonb_array_elements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;food_log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;jsonb_array_elements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;food_log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'calories'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;calories&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;jsonb_array_elements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;food_log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'meal'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;meal&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt; 
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'xyz'&lt;/span&gt; 
   &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;between&lt;/span&gt; &lt;span class="s1"&gt;'2022-01-01'&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="s1"&gt;'2022-01-31'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returns a table that looks like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;date&lt;/th&gt;
&lt;th&gt;title&lt;/th&gt;
&lt;th&gt;calories&lt;/th&gt;
&lt;th&gt;meal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2022-01-01&lt;/td&gt;
&lt;td&gt;Apple&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;Breakfast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2022-01-01&lt;/td&gt;
&lt;td&gt;Oatmeal&lt;/td&gt;
&lt;td&gt;146&lt;/td&gt;
&lt;td&gt;Breakfast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2022-01-01&lt;/td&gt;
&lt;td&gt;Sandwich&lt;/td&gt;
&lt;td&gt;445&lt;/td&gt;
&lt;td&gt;Lunch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2022-01-01&lt;/td&gt;
&lt;td&gt;Chips&lt;/td&gt;
&lt;td&gt;280&lt;/td&gt;
&lt;td&gt;Lunch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2022-01-01&lt;/td&gt;
&lt;td&gt;Cookie&lt;/td&gt;
&lt;td&gt;108&lt;/td&gt;
&lt;td&gt;Lunch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2022-01-01&lt;/td&gt;
&lt;td&gt;Mixed Nuts&lt;/td&gt;
&lt;td&gt;175&lt;/td&gt;
&lt;td&gt;Snack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2022-01-01&lt;/td&gt;
&lt;td&gt;Pasta/Sauce&lt;/td&gt;
&lt;td&gt;380&lt;/td&gt;
&lt;td&gt;Dinner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2022-01-01&lt;/td&gt;
&lt;td&gt;Garlic Bread&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;Dinner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2022-01-01&lt;/td&gt;
&lt;td&gt;Broccoli&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;Dinner&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A couple things to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;jsonb_array_elements(food_log)-&amp;gt;&amp;gt;'title' as title&lt;/code&gt; this returns a text field, since the &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt; operator returns TEXT&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jsonb_array_elements(food_log)-&amp;gt;'calories' as calories&lt;/code&gt; this returns a JSON object, since the &lt;code&gt;-&amp;gt;&lt;/code&gt; operator return JSON&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If we want to &lt;code&gt;sum&lt;/code&gt; the calories to get some totals, we can't have a JSON object, so we need to cast that to something more useful, like an &lt;code&gt;INTEGER&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;(jsonb_array_elements(food_log)-&amp;gt;'calories')::INTEGER as calories&lt;/code&gt; this returns an INTEGER&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now we can't just throw the &lt;code&gt;sum&lt;/code&gt; operator on this to get the total calories by day.  If we try this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; 
  &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;jsonb_array_elements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;food_log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'calories'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;total_calories&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'xyz'&lt;/span&gt; 
      &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;between&lt;/span&gt; &lt;span class="s1"&gt;'2022-01-01'&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="s1"&gt;'2022-01-31'&lt;/span&gt;
&lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we get an error back from PostgreSQL: &lt;strong&gt;Failed to run sql query: aggregate function calls cannot contain set-returning function calls&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead, we need to think of this as a set of building blocks, where our first SQL statement returns a table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; 
  &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jsonb_array_elements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;food_log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'calories'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;calories&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'xyz'&lt;/span&gt; 
      &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;between&lt;/span&gt; &lt;span class="s1"&gt;'2022-01-01'&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="s1"&gt;'2022-01-31'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can take that "table" statement, throw some (parenthesis) around it, and query &lt;strong&gt;it&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt;
   &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;select&lt;/span&gt; 
        &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jsonb_array_elements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;food_log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'calories'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;calories&lt;/span&gt;
      &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt; 
      &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'xyz'&lt;/span&gt; 
         &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;between&lt;/span&gt; &lt;span class="s1"&gt;'2022-01-01'&lt;/span&gt; 
&lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="s1"&gt;'2022-01-31'&lt;/span&gt;
   &lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;calories&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; 
   &lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives us exactly what we want:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;date&lt;/th&gt;
&lt;th&gt;sum&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2022-01-01&lt;/td&gt;
&lt;td&gt;1838&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If we add more data for the rest of the days of the month, we'll have all the data we need for a beautiful graph.&lt;/p&gt;

&lt;h3&gt;
  
  
  Searching the Data
&lt;/h3&gt;

&lt;p&gt;What if we want to answer the question: &lt;em&gt;How many calories were in the garlic bread I ate last month?&lt;/em&gt;  This data is stored inside the &lt;code&gt;food_log&lt;/code&gt; field in the &lt;code&gt;calendar&lt;/code&gt; table.  We can use the same type of query we used before to "flatten" the &lt;code&gt;food_log&lt;/code&gt; data so we can search it.&lt;/p&gt;

&lt;p&gt;To get every item I ate during the month of January, we can use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; 
  &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;jsonb_array_elements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;food_log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jsonb_array_elements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;food_log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'calories'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;calories&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt; 
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'xyz'&lt;/span&gt; 
   &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;between&lt;/span&gt; &lt;span class="s1"&gt;'2022-01-01'&lt;/span&gt; 
&lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="s1"&gt;'2022-01-31'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now to search for the &lt;strong&gt;garlic bread&lt;/strong&gt; we can just put (parenthesis) around this to make a "table" and then search for the item we want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;my_food&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; 
&lt;span class="p"&gt;(&lt;/span&gt;
   &lt;span class="k"&gt;select&lt;/span&gt; 
     &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="n"&gt;jsonb_array_elements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;food_log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jsonb_array_elements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;food_log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'calories'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;calories&lt;/span&gt;
   &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt; 
   &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'xyz'&lt;/span&gt; 
   &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;between&lt;/span&gt; &lt;span class="s1"&gt;'2022-01-01'&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="s1"&gt;'2022-01-31'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;calories&lt;/span&gt; 
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;my_food&lt;/span&gt; 
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Garlic Bread'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which gives us:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;title&lt;/th&gt;
&lt;th&gt;calories&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Garlic Bread&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;If we take a little time to study the &lt;a href="https://www.postgresql.org/docs/9.5/functions-json.html"&gt;JSON Functions and Operators&lt;/a&gt; that PostgreSQL offers, we can turn Postgres into an easy-to-use NoSQL database that still retains all the power of SQL.  This gives us a super easy way to store our complex JSON data coming from our application code in our database.  Then we can use powerful SQL capabilities to analyze and present that data in our application.  It's the best of both worlds!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Hacking the PostgREST Headers: Oh, the Things You Can Do!</title>
      <dc:creator>Mark Burggraf</dc:creator>
      <pubDate>Mon, 17 Oct 2022 22:30:25 +0000</pubDate>
      <link>https://dev.to/burggraf/hacking-the-postgrest-headers-oh-the-things-you-can-do-ck2</link>
      <guid>https://dev.to/burggraf/hacking-the-postgrest-headers-oh-the-things-you-can-do-ck2</guid>
      <description>&lt;p&gt;When using the Supabase Javascript Client, reading the PostgREST headers gives us all sorts of useful information for logging and security purposes.&lt;/p&gt;

&lt;p&gt;Luke Bechtel from &lt;a href="https://revaly.com"&gt;Revaly&lt;/a&gt; recently asked "is there any way to get things like &lt;strong&gt;Browser Type&lt;/strong&gt; or &lt;strong&gt;Host Name&lt;/strong&gt; from inside a PostgreSQL trigger or RLS (Row Level Security) Policy?"  Well, since the Supabase client uses PostgREST, and PostgREST is a web tool, then it should be able to access to the server's request object.  And indeed, it does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Examples
&lt;/h3&gt;

&lt;p&gt;See &lt;a href="https://github.com/supabase-community/pg_headerkit"&gt;pg_headerkit: PostgREST Header Kit&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Interesting Use Cases
&lt;/h3&gt;

&lt;p&gt;Why is this useful or important?  Imagine these use cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Allow-listing IPs: only allow users to select, insert, update, or delete if they're coming from a pre-defined list of IP addresses.&lt;/li&gt;
&lt;li&gt;Origin Restrictions: allow a feature only during development (when the request is coming from &lt;strong&gt;localhost&lt;/strong&gt; but not your production domain).&lt;/li&gt;
&lt;li&gt;Platform Checking: only allow users from mobile platforms to use your application (no desktop browsers).&lt;/li&gt;
&lt;li&gt;Logging: Log the user's IP address and browser User Agent in your database with their anonymous request data.&lt;/li&gt;
&lt;li&gt;Version Requirements: only allow clients coming from the most recent version of the Supabase Javascript Client Library.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of course, if the user is logged into our app, we can also get their id using &lt;code&gt;auth.uid()&lt;/code&gt;, or their &lt;a href="https://supabase.com/docs/guides/auth/row-level-security#authemail"&gt;email&lt;/a&gt; using &lt;code&gt;auth.jwt() -&amp;gt;&amp;gt; 'email'&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting Access to the Request Headers
&lt;/h3&gt;

&lt;p&gt;How can we get access to all this useful information?  By using the PostgreSQL &lt;code&gt;current_setting&lt;/code&gt; function, we can access the &lt;code&gt;request.headers&lt;/code&gt; liks this: &lt;code&gt;current_setting('request.headers', true)&lt;/code&gt;.  So, to put that into a useful function, we get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;get_headers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;
    &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="k"&gt;sql&lt;/span&gt; &lt;span class="k"&gt;STABLE&lt;/span&gt;
    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'request.headers'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function returns a &lt;strong&gt;JSON&lt;/strong&gt; object with all the header information from the request.  We get things like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;accept-encoding: "gzip"
accept-language: "en-US,en;q=0.9"
host: "localhost:3000"
origin: "http://localhost:8100"
referer: "http://localhost:8100/"
user-agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"
x-client-info: "supabase-js/1.35.7"
x-consumer-username: "anon-key"
x-forwarded-for: "142.251.46.206, 20.112.52.29"
x-forwarded-host: "xxxxxxxxxxxxxxxx.supabase.co"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we want to get the data from a specific header, we can create this function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;get_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;
    &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="k"&gt;sql&lt;/span&gt; &lt;span class="k"&gt;STABLE&lt;/span&gt;
    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'request.headers'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows us to get the text value for any specified header, such as: &lt;code&gt;get_header('user-agent')&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using the Results in a RLS (Row Level Security) Policy
&lt;/h3&gt;

&lt;p&gt;Let's say we want to only allow records to be inserted into our table &lt;code&gt;beta_tests&lt;/code&gt; if the request is coming from a server running on &lt;code&gt;localhost&lt;/code&gt;, port &lt;code&gt;3000&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"only allow inserts on public.beta_tests from localhost:3000"&lt;/span&gt; 
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;beta_tests&lt;/span&gt; 
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; 
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'host'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'localhost:3000'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For security purposes, we can restict the usage for a table based on a whitelisted set of IPs.  First, we need to get the user's IP address, which is found in &lt;code&gt;x-forwarded-for&lt;/code&gt;, but that has 2 IP addresses separated by commas, and we only want the first one.  So we can use the PostgreSQL &lt;code&gt;SPLIT_PART&lt;/code&gt; function, which is similar to the Javascript &lt;code&gt;split&lt;/code&gt; function: &lt;code&gt;SPLIT_PART(get_header('x-forwarded-for') || ',', ',', 1)&lt;/code&gt;.  Note how we concatenate a comma to the &lt;code&gt;x-forwarded-for&lt;/code&gt; header (&lt;code&gt;get_header('x-forwarded-for') || ','&lt;/code&gt;), just in case there's an empty string there?&lt;/p&gt;

&lt;p&gt;Now that we have the user's IP address, we can test to see if it's in our whitelist set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"only allow access to table_for_internal_use_only from a set of IPs"&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="nv"&gt;"public"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"table_for_internal_use_only"&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;PERMISSIVE&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
&lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SPLIT_PART&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'x-forwarded-for'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;ANY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'123.44.152.151'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'203.44.11.22'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'11.4.102.33'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SPLIT_PART&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'x-forwarded-for'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;ANY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'123.44.152.151'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'203.44.11.22'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'11.4.102.33'&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You could extend this by creating a table of IP addresses and check against that table (&lt;code&gt;(SELECT count(*) from my_whitelist_table where ip = SPLIT_PART(get_header('x-forwarded-for') || ',', ',', 1)) &amp;gt; 0&lt;/code&gt;), but be careful, this adds an extra lookup to another table, and this slows down your RLS policy considerably and could lead to scaling problems down the road.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using the Results in a PostgreSQL Trigger
&lt;/h3&gt;

&lt;p&gt;Let's create a log table caled &lt;code&gt;log_table&lt;/code&gt;, and then for every record inserted into our &lt;code&gt;test_table&lt;/code&gt;, we'll log a record there with the user's &lt;code&gt;user_agent&lt;/code&gt;, &lt;code&gt;host&lt;/code&gt;, &lt;code&gt;origin&lt;/code&gt;, &lt;code&gt;referer&lt;/code&gt;, and &lt;code&gt;ip&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;log_table&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;serial&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;user_agent&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;host&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;origin&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;referer&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;log_user_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;log_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;referer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;VALUES&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TG_TABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'user-agent'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;get_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'host'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;get_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'origin'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;get_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'referer'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;SPLIT_PART&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'x-forwarded-for'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="s1"&gt;'plpgsql'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;test_trigger&lt;/span&gt;
  &lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;test_table&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;EACH&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
  &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;PROCEDURE&lt;/span&gt; &lt;span class="n"&gt;log_user_data&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Things of note here:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;TG_TABLE_NAME::regclass::text&lt;/code&gt; returns the current table name in our trigger (so we can re-use this trigger on other tables!)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NEW.id::text&lt;/code&gt; converts the &lt;code&gt;id&lt;/code&gt; field of the current table to text (a string).  I use a &lt;code&gt;UUID&lt;/code&gt; as the &lt;code&gt;id&lt;/code&gt; field for almost every table I create, so this should work just fine.  If you use a different convention or primary key type, you may have to alter this.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SPLIT_PART(get_header('x-forwarded-for') || ',', ',', 1)&lt;/code&gt;, as mentioned earlier, grabs the first &lt;code&gt;ip&lt;/code&gt; found in the &lt;code&gt;x-forwarded-for&lt;/code&gt; header.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Other Interesting Tidbits from the User-Agent
&lt;/h3&gt;

&lt;p&gt;We can parse the &lt;code&gt;user-agent&lt;/code&gt; header to get relevant information, such as:&lt;/p&gt;

&lt;p&gt;Is the user running on a Windows platform:&lt;br&gt;
&lt;code&gt;get_header('user-agent') LIKE '%Windows%'&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Or Mac:&lt;br&gt;
&lt;code&gt;get_header('user-agent') LIKE '%Mac OS X%'&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Is the user on a Mobile device:&lt;br&gt;
&lt;code&gt;get_header('user-agent') LIKE 'Mobile/%'&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Check iOS Major Version:&lt;br&gt;
&lt;code&gt;get_header('user-agent') LIKE '%iPhone OS 16%'&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;user-agent&lt;/code&gt; isn't the most accurate way to get this information, though, and &lt;code&gt;user-agent&lt;/code&gt;s are always subject to change (and can be forged) so be careful with this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other Caveats and Warnings
&lt;/h3&gt;

&lt;p&gt;You may find additional headers beyond the ones listed here available to you when testing this, but it's best not to rely on them, as they:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;may not be available on every platform or device (some headers exist on desktop systems but not mobile systems, for instance)&lt;/li&gt;
&lt;li&gt;may change or go away completely based on infrastructure changes or changes to PostgREST or the Supabase Client Libraries.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;PostgREST exposes some really useful request headers that give PostgreSQL functions the power to do some things that previously required a separate middleware tier.  Moving this functionality into the database eliminates the need for that extra tier and might also speed up your application by reducing extra network round-trips.  It also allows you to add an extra security layer at the database level, so you can allow or restrict access based on IP address, host name, client type, Javascript client version, and more!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Postgres WASM by Snaplet and Supabase</title>
      <dc:creator>Mark Burggraf</dc:creator>
      <pubDate>Mon, 10 Oct 2022 23:13:41 +0000</pubDate>
      <link>https://dev.to/burggraf/postgres-wasm-by-snaplet-and-supabase-55fo</link>
      <guid>https://dev.to/burggraf/postgres-wasm-by-snaplet-and-supabase-55fo</guid>
      <description>&lt;p&gt;Today we're open sourcing &lt;a href="https://github.com/snaplet/postgres-wasm"&gt;&lt;code&gt;postgres-wasm&lt;/code&gt;&lt;/a&gt; with our friends at &lt;a href="https://www.snaplet.dev/"&gt;Snaplet&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;postgres-wasm&lt;/code&gt; is a PostgreSQL server that runs inside a browser. It provides a full suite of features, including persisting state to browser, restoring from pg_dump, and logical replication from a remote database.&lt;/p&gt;

&lt;p&gt;We're not the first to run Postgres in the browser - that title belongs to the team at &lt;a href="https://www.crunchydata.com/"&gt;Crunchy Data&lt;/a&gt; who shared their version of this &lt;a href="https://news.ycombinator.com/item?id=32498435"&gt;on HN&lt;/a&gt; a month ago. It's awesome, and we wanted an &lt;a href="https://staltz.com/time-till-open-source-alternative.html"&gt;open source version&lt;/a&gt;, so we teamed up with Snaplet build it. Let's explore how it's built, and some extra features we've added.&lt;/p&gt;

&lt;p&gt;🐈 Who's Snaplet?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Snaplet gives developers production-accurate data and preview databases that they can code against, so they can focus on shipping.&lt;/p&gt;

&lt;p&gt;Checkout out how you can &lt;a href="https://docs.snaplet.dev/tutorials/supabase-clone-environments"&gt;use Snaplet to clone Supabase environments&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Before we get into the technical details, try it out yourself (warning: it will download ~30MB):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://wasm.supabase.com"&gt;wasm.supabase.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--sPdfRWHT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wy0jpufvlp4muwrnzkyk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--sPdfRWHT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wy0jpufvlp4muwrnzkyk.png" alt="Postgres WASM" width="880" height="645"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To run it locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# From Snaplet Repo&lt;/span&gt;
git clone git@github.com:snaplet/postgres-wasm.git
&lt;span class="nb"&gt;cd &lt;/span&gt;postgres-browser/packages/pg-browser
npx serve

&lt;span class="c"&gt;# From Supabase Fork&lt;/span&gt;
git clone git@github.com:supabase-community/postgres-wasm.git
&lt;span class="nb"&gt;cd &lt;/span&gt;postgres-wasm
git checkout web
&lt;span class="nb"&gt;cd &lt;/span&gt;packages/supabrowser
npx serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And open a browser at &lt;a href="http://localhost:3000"&gt;localhost:3000&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;p&gt;Our demo version has a few neat features!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Postgres 14.5, psql, pg_dump, etc.&lt;/li&gt;
&lt;li&gt;Save &amp;amp; restore state to/from a file.&lt;/li&gt;
&lt;li&gt;Save &amp;amp; restore Postgres state to/from the browser storage (IndexedDB).&lt;/li&gt;
&lt;li&gt;Quick start from a state file or fully reboot the emulator.&lt;/li&gt;
&lt;li&gt;Memory configuration options from 128MB to 1024MB.&lt;/li&gt;
&lt;li&gt;Adjust the font size for the terminal.&lt;/li&gt;
&lt;li&gt;Upload files to the emulator (including database dumps and CSVs).&lt;/li&gt;
&lt;li&gt;Download files from the emulator.&lt;/li&gt;
&lt;li&gt;Outgoing network connectivity from the emulator to the internet.&lt;/li&gt;
&lt;li&gt;Incoming network tunnel to postgres port 5432 inside the emulator.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why?
&lt;/h2&gt;

&lt;p&gt;That's a good question. &lt;code&gt;postgres-wasm&lt;/code&gt; is currently about 30mb. So at this stage, running Postgres in the browser isn't great for general use-cases. It has a lot of potential though. Some ideas we'll be playing with over the next few months:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Documentation:&lt;/strong&gt; for tutorials and demos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline data:&lt;/strong&gt; running it in the browser for an offline cache, similar to &lt;a href="https://sql.js.org/#/"&gt;sql.js&lt;/a&gt; or &lt;a href="https://github.com/jlongster/absurd-sql"&gt;absurd-sql&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline data analysis:&lt;/strong&gt; using it in a dashboard for offline data analysis and charts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing:&lt;/strong&gt; testing PostgresSQL functions, triggers, data modeling, logical replication, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev environments:&lt;/strong&gt; use it as a development environment — pull data from production or push new data, functions, triggers, views up to production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshots:&lt;/strong&gt; create a test version of your database with sample data, then take a snapshot to send to other developers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support:&lt;/strong&gt; send snapshots of your database to support personnel to demonstrate an issue you're having.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Repository Overview
&lt;/h2&gt;

&lt;p&gt;We've divided in our repository into three folders: a virtual machine, a web application, and a network proxy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Virtual Machine (VM)
&lt;/h3&gt;

&lt;p&gt;We create an embeddable Virtual Machine (VM) using &lt;a href="https://en.wikipedia.org/wiki/Buildroot"&gt;Buildroot&lt;/a&gt;. Our VM is a stripped-down Linux build with Postgres installed.&lt;/p&gt;

&lt;p&gt;See &lt;a href="https://github.com/snaplet/postgres-browser/tree/main/packages/buildroot"&gt;source code&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Web application
&lt;/h3&gt;

&lt;p&gt;Next, we need to run the the VM inside our browser. How? &lt;a href="https://en.wikipedia.org/wiki/WebAssembly"&gt;WASM&lt;/a&gt;. We use &lt;a href="https://github.com/copy/v86"&gt;v86&lt;/a&gt; to run our VM inside the browser. Our demo application is very simple - plain HTML and some basic styling.&lt;/p&gt;

&lt;p&gt;See &lt;a href="https://github.com/snaplet/postgres-browser/tree/main/packages/pg-browser"&gt;source code&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Network proxy
&lt;/h3&gt;

&lt;p&gt;Running Postgres in a browser is great, but connecting to it with PgAdmin is even better. Unfortunately browsers block TCP network access to the VM. To circumvent this, we proxy the traffic through websockets. We run a fork of &lt;a href="https://github.com/benjamincburns/websockproxy"&gt;Websockproxy&lt;/a&gt; which allows the emulator to talk to the internet by converting data sent over a websocket port into TCP packets. Our fork of Websockproxy adds the ability to tunnel into the Postgres server.&lt;/p&gt;

&lt;p&gt;See &lt;a href="https://github.com/snaplet/postgres-browser/tree/main/packages/websockproxy"&gt;source code&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical deep-dive
&lt;/h2&gt;

&lt;p&gt;There were a lot of hurdles that we discovered throughout the development of postgres-wasm.&lt;/p&gt;

&lt;h3&gt;
  
  
  WASM
&lt;/h3&gt;

&lt;p&gt;The first thing to point out is that our implementation isn't &lt;em&gt;pure&lt;/em&gt; WASM. We attempted to compile Postgres for WASM directly from source, but it was more complicated that we anticipated.&lt;/p&gt;

&lt;p&gt;Crunchy's HN post provided some hints about the approach they took, which was to virtualize a machine in the browser. We pursued this strategy too, settling on v86 which emulates an x86-compatible CPU and hardware in the browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  PostgreSQL 14 segfault errors
&lt;/h3&gt;

&lt;p&gt;We quickly had PostgreSQL 13.3 running using an outdated version of Buildroot. But version PG14+ wouldn't start, giving a segfault during initialization. We tried:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;manually copying build files for PG14 into the older version(s) of Buildroot&lt;/li&gt;
&lt;li&gt;building with (many) newer copies of Buildroot&lt;/li&gt;
&lt;li&gt;adjusting kernel parameters and environment settings such as the amount of memory allocated to the emulator, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eventually, Fabian, the creator of v86 suggested we turn off JIT compilation for v86 and that solved the issue. He narrowed it down to a bug in v86 and is pushing an update that will fix it. Switching Postgres from posix to sysv memory management also solved the issue for the current release of V86.&lt;/p&gt;

&lt;h3&gt;
  
  
  Optimizing startup time and image size
&lt;/h3&gt;

&lt;p&gt;With PG14 running in the emulator, we shifted our focus to performance. The image size for the emulator was too big for a browser-based tool. Even with our best efforts, a compressed snapshot was over 30mb - a fairly large payload to download before you can see any interaction.&lt;/p&gt;

&lt;p&gt;We solved this by booting only a minimal Linux image and then dynamically loading the rest of the VM over HTTPS after initialization.&lt;/p&gt;

&lt;p&gt;We achieve this by mounting a compressed &lt;a href="https://en.wikipedia.org/wiki/9P_(protocol)"&gt;9P filesystem&lt;/a&gt; in the VM. 9P provides a &lt;a href="https://github.com/supabase-community/postgres-wasm/tree/main/packages/buildroot/tools"&gt;Python script&lt;/a&gt; which takes a filesystem folder,renames every file an 8-character name and produces a &lt;code&gt;filesystem.json&lt;/code&gt; file representing a nested structure with files, original file names, sizes, etc.&lt;/p&gt;

&lt;p&gt;We then &lt;a href="https://github.com/supabase-community/postgres-wasm/blob/main/packages/buildroot/config/board/pg-browser/post-image.sh"&gt;copy this compressed output&lt;/a&gt; to the VM.&lt;br&gt;
We modified the kernel command line and v86 boot parameters to boot directly from the 9P filesystem, and even put our kernel file into the p9 filesystem. All non-essential files are loaded asynchronously over HTTPS in the browser as needed.&lt;/p&gt;

&lt;p&gt;The initial state was smaller, but still 15-20mb in size. We discussed this with Fabian who pointed us towards the &lt;code&gt;page_poison=on&lt;/code&gt; kernel parameter. This parameter allowed us to clear caches before creating the snapshots, by forcing Linux to write arbitrary bytes on freed memory instead of random bytes, so unused memory is compressed much more efficiently.&lt;/p&gt;

&lt;p&gt;The end result of all these changes? The compressed initial state file is about 12mb - including a &lt;em&gt;running network state and Postgres 14.4 running with psql loaded.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We experimented without Postgres started too - this produced an even smaller initial state but the time to initialize Postgres was usually longer than the time to download a few extra megabytes. We opted for a faster startup time.&lt;/p&gt;

&lt;p&gt;Additionally, without a running state you'd hit a performance penalty every time you refreshed the page, since Postgres would need to be initialized each time. In our current version, after the initial state is downloaded and cached by the browser the VM feels almost instant on refresh.&lt;/p&gt;

&lt;h3&gt;
  
  
  Networking
&lt;/h3&gt;

&lt;p&gt;Networking was particularly difficult to solve. For security reasons, browsers only support access to a nominal set of ports. In short, browsers speak “80/443” and Postgres speaks “5432” so there was no way for our Postgres to communicate with the outside world.&lt;/p&gt;

&lt;p&gt;WASM isn't much help here - a WebAssembly module running in the browser is generally not able to do anything outside itself, except for calling functions exposed from JavaScript.&lt;/p&gt;

&lt;p&gt;Browsers provide another connection option for us however: Websockets. The nice thing about websocket connections is that they are &lt;em&gt;persistent&lt;/em&gt;, so all we needed was to figure out how to proxy the virtual machine traffic through a websocket connection.&lt;/p&gt;

&lt;p&gt;The VM includes an emulated network card (&lt;code&gt;ne2k-pci&lt;/code&gt;) and a &lt;code&gt;proxy_url&lt;/code&gt; startup parameter which points to an external server running a websocket proxy (i.e. on the internet). The websocket proxy establishes connections via a websocket port, then accepts raw ethernet packets. It turns those raw packets into TCP/IP packets and routes them between the Internet and the VM.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3-qc8rBn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iennknd4ed67rnecqb1v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3-qc8rBn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iennknd4ed67rnecqb1v.png" alt="Postgres WASM to the Proxy" width="880" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With our tunnel established, we could now send network traffic to the outside world. For example, try “starting” the network on the VM, "exiting" out of psql (&lt;code&gt;cmd&lt;/code&gt;+&lt;code&gt;D&lt;/code&gt;), and then run &lt;code&gt;ping 1.1.1.1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--e6Ri3oXJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0jagaw4enpabo9nezdmm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--e6Ri3oXJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0jagaw4enpabo9nezdmm.png" alt="Postgres WASM to the internet" width="880" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most v86 VM's use an open proxy at &lt;code&gt;wss://relay.widgetry.org/&lt;/code&gt; based on &lt;a href="https://github.com/benjamincburns/websockproxy"&gt;websockproxy&lt;/a&gt;. This proxy allows the VM to communicate with the outside world. It can handle all the basic use-cases like loading data from other PostgreSQL databases using &lt;code&gt;pg_dump&lt;/code&gt; and &lt;code&gt;psql&lt;/code&gt;, or operating as read-only replica of another database.&lt;/p&gt;

&lt;p&gt;However, this proxy doesn't allow incoming traffic to be routed into a VM running locally on a laptop.  For instance, you can't connect a local &lt;code&gt;PgAdmin4&lt;/code&gt; to the PostgreSQL instance running inside your browser. So we &lt;a href="https://github.com/snaplet/postgres-wasm/tree/main/packages/websockproxy"&gt;forked&lt;/a&gt; the [proxy(&lt;a href="https://github.com/benjamincburns/websockproxy"&gt;https://github.com/benjamincburns/websockproxy&lt;/a&gt;) and added nginx to provide a reverse proxy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Rrn7lIbD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m17ghec6xix982wopjp7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Rrn7lIbD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m17ghec6xix982wopjp7.png" alt="Postgres WASM with psql" width="880" height="581"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The reverse proxy listens on a range of ports, mapping each port to the VM at a specific private address you want to reach. The simple solution was to take the last 2 segments of the private IP and map it to a port number. So if your private IP is 10.5.6.123, you connect to the proxy on port 6123 and that points you to PostgreSQL (running on port 5432) on private IP 10.5.6.123. Shortened addresses are padded, so 10.5.6.2 maps to port 6002 and 10.5.6.44 to port 6044.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--dlHHN2UH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aowayjg0s93v7rsmp2sn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--dlHHN2UH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aowayjg0s93v7rsmp2sn.png" alt="Postgres WASM with remote logins" width="880" height="758"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Sending commands to the emulator from the browser UI
&lt;/h3&gt;

&lt;p&gt;Once the emulator is running, in the words of Fabian, “it runs as a black box process”, and the only way to communicate between the UI and the operating system running in the emulator is through &lt;code&gt;serial0_send&lt;/code&gt; to send keystrokes over the emulated serial port, or through the &lt;code&gt;create_file&lt;/code&gt; function which uploads a file to the running emulator.&lt;/p&gt;

&lt;p&gt;To restart the network after restoring a state file, we need to run a short script to unbind and re-bind the emulated network card (so it gets a new mac address and thus a unique private IP address at the proxy), and then a standard network restart command.&lt;/p&gt;

&lt;p&gt;We could run this script by sending keystrokes to the emulator, but that's clumsy. What if the user is at a &lt;code&gt;psql&lt;/code&gt; prompt? Then we'd send &lt;code&gt;\! script_command&lt;/code&gt;. But if they're at an OS prompt, that won't work.&lt;/p&gt;

&lt;p&gt;We solved the issue by creating a folder called &lt;code&gt;\inbox&lt;/code&gt; and added a listener to the folder. When files arrive there, if the file ends in &lt;code&gt;.sh&lt;/code&gt; (a shell script), we &lt;code&gt;chmod +x&lt;/code&gt; that file, execute it, then delete it. That allows us to run commands “in the background” without upsetting the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Just for fun
&lt;/h2&gt;

&lt;p&gt;If you want to do something very cool, you can connect to another user's Postgres instance that they are running in their browser.&lt;/p&gt;

&lt;p&gt;Here are the steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find a friend (sometimes the first step is the hardest)&lt;/li&gt;
&lt;li&gt;Tell them to go to &lt;a href="https://wasm.supabase.com/"&gt;wasm.supabase.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Tell them to “Start Network” and get their Network URL&lt;/li&gt;
&lt;li&gt;Have them set a password on their database &lt;code&gt;ALTER ROLE postgres WITH PASSWORD 'my_password';&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;On your own machine, open a terminal and run
&lt;code&gt;psql postgres://postgres:my_password@proxy.wasm.supabase.com:&amp;lt;PORT&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;And you're in! You're now connecting to a remote Postgres instance, inside a VM, inside a browser, from a terminal on your own computer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Database Replication&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even more mind-blowing - it's possible to replicate data from an online PostgreSQL database (for example, a Supabase project) to PostgreSQL running inside your browser,&lt;br&gt;
and vice-versa using Postgres's logical replication. The steps are no different than any other Postgres database - just use the proxy URL as the target.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;For now, this is very experimental - but it has a lot of potential. If you want to get involved, please reach out to us or the team at [Snaplet(&lt;a href="https://www.snaplet.dev/"&gt;https://www.snaplet.dev/&lt;/a&gt;). The work they're doing over at Snaplet is incredible, and we've had a blast collaborating with them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Snaplet repo: &lt;a href="https://github.com/snaplet/postgres-wasm"&gt;https://github.com/snaplet/postgres-wasm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Supabase fork: &lt;a href="https://github.com/supabase-community/postgres-wasm"&gt;https://github.com/supabase-community/postgres-wasm&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Speeding Up Bulk Loading in PostgreSQL</title>
      <dc:creator>Mark Burggraf</dc:creator>
      <pubDate>Tue, 05 Jul 2022 15:06:47 +0000</pubDate>
      <link>https://dev.to/supabase/speeding-up-bulk-loading-in-postgresql-41g5</link>
      <guid>https://dev.to/supabase/speeding-up-bulk-loading-in-postgresql-41g5</guid>
      <description>&lt;h1&gt;
  
  
  Speeding Up Bulk Loading in PostgreSQL
&lt;/h1&gt;

&lt;p&gt;Testing 4 ways to bulk load data into PostgreSQL&lt;/p&gt;

&lt;h2&gt;
  
  
  The Need For Speed
&lt;/h2&gt;

&lt;p&gt;If you only need to load a few hundred records into your database, you probably aren't too concerned about efficiency.  But what happens when try to insert thousands, or even millions of records?  Now, data-loading efficiency can mean the difference between success and failure for your project, or at the very least the difference between a project that’s delivered timely and one that’s woefully overdue.&lt;/p&gt;

&lt;p&gt;PostgreSQL has a great &lt;strong&gt;copy&lt;/strong&gt; command that’s optimized for this task: &lt;a href="https://www.postgresql.org/docs/current/sql-copy.html" rel="noopener noreferrer"&gt;https://www.postgresql.org/docs/current/sql-copy.html&lt;/a&gt;.  But that’s only a good solution if your data is specifically in a CSV (or Binary) file.  But what if you need to load data from pure SQL? Then what’s the fastest way?&lt;/p&gt;

&lt;h2&gt;
  
  
  Four Ways to Insert Data
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Basic Insert Commands
&lt;/h3&gt;

&lt;p&gt;Let’s look at the structure for some basic SQL insert commands:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;firstname&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lastname&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;firstname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lastname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'George'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Washington'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;firstname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lastname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'John'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Adams'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;firstname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lastname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Thomas'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Jefferson'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now we have some basic SQL for inserting records into our user table. This will get the data into our table, alright, but it's the slowest way to get data into our table.  Let's look at some ways we can speed things up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transactions
&lt;/h3&gt;

&lt;p&gt;A quick and easy way to speed things up is simply to put large batches of insert statements inside a transaction:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;

&lt;span class="k"&gt;begin&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;firstname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lastname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'George'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Washington'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;firstname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lastname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'John'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Adams'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;firstname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lastname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Thomas'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Jefferson'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;In my Windows test, this doubled the speed of the insert of 100k user records.  Under MacOS, the speed tripled.  While you can technically create batches with billions of records in them, you'll probably want to experiment with batch sizes of, say 1000, 10000, 100000, or something like that to see what works best based on your hardware, bandwidth, and record size.&lt;/p&gt;

&lt;h3&gt;
  
  
  Batched Inserts
&lt;/h3&gt;

&lt;p&gt;Another way to speed things up is to use the SQL batch insert syntax when doing your insert.  For example:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;

&lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;firstname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lastname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;values&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'George'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Washington'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'John'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Adams'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Thomas'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Jefferson'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This method speeds things up considerably.  In my tests, it was about 6 times faster.  The same rules apply to batch sizes as with transactions -- you'll want to test different batch sizes to optimize things.  I generally tend to start with a batch of around 10000 records for most applications and if that works well enough, I leave it there.&lt;/p&gt;

&lt;h3&gt;
  
  
  What About Both?
&lt;/h3&gt;

&lt;p&gt;Can you combine transactions and batch insert statements for even more speed?  Well, yes, and no.  You certainly can combine them, but the speed increase is negligible (or in my Windows test case it even slowed things down just a bit.)&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;

&lt;span class="k"&gt;begin&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;firstname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lastname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;values&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'George'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Washington'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'John'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Adams'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Thomas'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Jefferson'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;So, while using both techniques here is perfectly valid, it may not be the fastest way to load data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Downsides
&lt;/h2&gt;

&lt;p&gt;What are the potential downsides of using transactions or batched inserts?  Error handling is the main one.  If any one of the records in your batch fails, the entire batch will fail and no data will be inserted into your table from that batch.  So you'll need to make sure your data is valid or else have some way to break up and fix failed batches.&lt;/p&gt;

&lt;p&gt;If the failure is caused by a unique constraint, you can use the &lt;a href="https://www.postgresql.org/docs/current/sql-insert.html#SQL-ON-CONFLICT" rel="noopener noreferrer"&gt;&lt;code&gt;on conflict&lt;/code&gt;&lt;/a&gt; clause in your insert statement, but if your insert fails for any other reason, it'll throw out the whole batch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other Speed Considerations
&lt;/h2&gt;

&lt;p&gt;There are many other factors that can affect your data insert speed and ways you can make things even faster.  Removing indexes until after inserting data, creating non-logged tables, and avoiding unnecessary unique keys are just a few of these.  These other optimizations will improve performance, but probably not nearly as dramatically as the basic techniques described here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;If you need to deal with large amounts of data, it pays to plan ahead when you're writing your SQL insert code.  A few small changes can potentially save you hours (or sometimes even days) of processing time.&lt;/p&gt;

&lt;h4&gt;
  
  
  Appendix: Sample Test Results
&lt;/h4&gt;

&lt;p&gt;See my GitHub Repo &lt;a href="https://github.com/burggraf/postgresql-bulk-load-tests" rel="noopener noreferrer"&gt;postgresql-bulk-load-tests&lt;/a&gt; for some code to test these methods.  My test run results are listed below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faed16v2x2k8pihjrzpxq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faed16v2x2k8pihjrzpxq.png" alt="Results"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

===========================
Windows VM (UTM Windows 11)
===========================
create 100k users with individual insert statements
30.0 seconds
create 100k users with individual insert statements in a transaction
14.0 seconds
create 100k users with batch insert statement
4.3 seconds
create 100k users with batch insert statement in a transaction
4.6 seconds

====================
MacBook Pro (M1 Max)
====================
create 100k users with individual insert statements

real    0m9.112s
user    0m0.509s
sys     0m0.337s

create 100k users with individual insert statements in a transaction

real    0m2.540s
user    0m0.457s
sys     0m0.325s

create 100k users with batch insert statement

real    0m1.360s
user    0m0.179s
sys     0m0.042s

create 100k users with batch insert statement in a transaction

real    0m1.154s
user    0m0.189s
sys     0m0.041s


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>postgres</category>
      <category>supabase</category>
      <category>sql</category>
      <category>database</category>
    </item>
    <item>
      <title>Supabase Custom Claims</title>
      <dc:creator>Mark Burggraf</dc:creator>
      <pubDate>Tue, 31 May 2022 13:45:57 +0000</pubDate>
      <link>https://dev.to/supabase/supabase-custom-claims-34l2</link>
      <guid>https://dev.to/supabase/supabase-custom-claims-34l2</guid>
      <description>&lt;p&gt;I've put together a [GitHub Repository (&lt;a href="https://github.com/supabase-community/supabase-custom-claims"&gt;https://github.com/supabase-community/supabase-custom-claims&lt;/a&gt;) that makes it easy to implement Custom Claims for your application based on &lt;a href="https://supabase.com"&gt;Supabase&lt;/a&gt;.  (Supabase is an open-source backend service that uses PostgreSQL as its database and GoTrue for authentication.  It does plenty of other cool things, too, but those items are out of the scope of this article.)&lt;/p&gt;

&lt;p&gt;This is just one way to implement &lt;code&gt;custom claims&lt;/code&gt; for a Supabase project.  The goal here is simply to add JSON data to the &lt;a href="https://jwt.io"&gt;JWT&lt;/a&gt; access token that an authenticated user receives when logging into your application.  That token (and thus the &lt;code&gt;custom claims&lt;/code&gt; contained in that token) can be read and used by both your application and by your PostgreSQL database server.  These &lt;code&gt;custom claims&lt;/code&gt; are stored in the &lt;code&gt;raw_app_meta_data&lt;/code&gt; field of the &lt;code&gt;users&lt;/code&gt; table in the &lt;code&gt;auth&lt;/code&gt; schema.  (&lt;code&gt;auth.users.raw_app_meta_data&lt;/code&gt;)&lt;/p&gt;

&lt;p&gt;In order to make this technique as portable as possible, I've implemented it using a set of PostgreSQL functions, which can be called from any &lt;a href="https://supabase.com/docs/reference/javascript/supabase-client"&gt;Supabase Client Language&lt;/a&gt; that can call a PostgreSQL function.  (That covers a lot of programming languages!)&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What are custom claims?
&lt;/h3&gt;

&lt;p&gt;Custom Claims are special attributes attached to a user that you can use to control access to portions of your application.  &lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;plan: "TRIAL"
user_level: 100
group_name: "Super Guild!"
joined_on: "2022-05-20T14:28:18.217Z"
group_manager: false
items: ["toothpick", "string", "ring"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What type of data can I store in a custom claim?
&lt;/h3&gt;

&lt;p&gt;Any valid JSON data can be stored in a claim.  You can store a string, number, boolean, date (as a string), array, or even a complex, nested, complete JSON object.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where are these custom claims stored?
&lt;/h3&gt;

&lt;p&gt;Custom claims are stored in the &lt;code&gt;auth.users&lt;/code&gt; table, in the &lt;code&gt;raw_app_meta_data&lt;/code&gt; column for a user.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are there any naming restrictions?
&lt;/h3&gt;

&lt;p&gt;The Supabase Auth System (GoTrue) currently uses the following custom claims: &lt;code&gt;provider&lt;/code&gt; and &lt;code&gt;providers&lt;/code&gt;, so DO NOT use these.  Any other valid string should be ok as the name for your custom claim(s), though.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why use custom claims instead of just creating a table?
&lt;/h3&gt;

&lt;p&gt;Performance, mostly.  Custom claims are stored in the security token a user receives when logging in, and these claims are made available to the PostgreSQL database as a configuration parameter, i.e. &lt;code&gt;current_setting('request.jwt.claims', true)&lt;/code&gt;.  So the database has access to these values immediately without needing to do any disk i/o.&lt;/p&gt;

&lt;p&gt;This may sound trivial, but this could have a significant effect on scalability if you use claims in an RLS (Row Level Security) Policy, as it could potentially eliminate thousands (or even millions) of database calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  What are the drawbacks to using custom claims?
&lt;/h3&gt;

&lt;p&gt;One drawback is that claims don't get updated automatically, so if you assign a user a new custom claim, they may need to log out and log back in to have the new claim available to them.  The same goes for deleting or changing a claim.  So this is not a good tool for storing data that changes frequently.&lt;/p&gt;

&lt;p&gt;You can force a refresh of the current session token by calling &lt;code&gt;supabase.auth.update({})&lt;/code&gt; on the client, but if a claim is changed by a server process or by a claims administrator manually, there's no easy way to notify the user that their claims have changed.  You can provide a "refresh" button or a refresh function inside your app to update the claims at any time, though.&lt;/p&gt;

&lt;h3&gt;
  
  
  How can I write a query to find all the users who have a specific custom claim set?
&lt;/h3&gt;

&lt;h4&gt;
  
  
  examples
&lt;/h4&gt;

&lt;h5&gt;
  
  
  find all users who have &lt;code&gt;claims_admin&lt;/code&gt; set to &lt;code&gt;true&lt;/code&gt;
&lt;/h5&gt;

&lt;p&gt;&lt;code&gt;select * from auth.users where (auth.users.raw_app_meta_data-&amp;gt;'claims_admin')::bool = true;&lt;/code&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  find all users who have a &lt;code&gt;userlevel&lt;/code&gt; over 100
&lt;/h5&gt;

&lt;p&gt;&lt;code&gt;select * from auth.users where (auth.users.raw_app_meta_data-&amp;gt;'userleval')::numeric &amp;gt; 100;&lt;/code&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  find all users whose &lt;code&gt;userrole&lt;/code&gt; is set to &lt;code&gt;"MANAGER"&lt;/code&gt;
&lt;/h5&gt;

&lt;p&gt;(note for strings you need to add double-quotes becuase data is data is stored as JSONB)&lt;br&gt;
&lt;code&gt;select * from auth.users where (auth.users.raw_app_meta_data-&amp;gt;'userrole')::text = '"MANAGER"';&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  What's the difference between &lt;code&gt;auth.users.raw_app_meta_data&lt;/code&gt; and &lt;code&gt;auth.users.raw_user_meta_data&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;auth.users&lt;/code&gt; table used by Supabase Auth (GoTrue) has both &lt;code&gt;raw_app_meta_data&lt;/code&gt; and a &lt;code&gt;raw_user_meta_data&lt;/code&gt; fields.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;raw_user_meta_data&lt;/code&gt; is designed for profile data and can be created and modified by a user.  For example, this data can be set when a user signs up: &lt;a href="https://supabase.com/docs/reference/javascript/auth-signup#sign-up-with-additional-user-meta-data"&gt;sign-up-with-additional-user-meta-data&lt;/a&gt; or this data can be modified by a user with &lt;a href="https://supabase.com/docs/reference/javascript/auth-update"&gt;auth-update&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;raw_app_meta_data&lt;/code&gt; is designed for use by the application layer and is used by GoTrue to handle authentication (For example, the &lt;code&gt;provider&lt;/code&gt; and &lt;code&gt;providers&lt;/code&gt; claims are used by GoTrue to track authentication providers.)  &lt;code&gt;raw_app_meta_data&lt;/code&gt; is not accessible to the user by default.&lt;/p&gt;
&lt;h2&gt;
  
  
  Security Considerations
&lt;/h2&gt;

&lt;p&gt;If you want to tighten security so that custom claims can only be set or deleted from inside the query editor or inside your PostgreSQL functions or triggers, edit the function &lt;code&gt;is_claims_admin()&lt;/code&gt; to disallow usage by app users (no usage through the API / Postgrest).  Instructions are included in the &lt;a href="https://github.com/supabase-community/supabase-custom-claims/blob/main/install.sql"&gt;function&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;By default, usage is allowed through your API, but the ability to set or delete claims is restricted to only users who have the &lt;code&gt;claims_admin&lt;/code&gt; custom claim set to &lt;code&gt;true&lt;/code&gt;.  This allows you to create an &lt;strong&gt;"admin"&lt;/strong&gt; section of your app that allows designated users to modify custom claims for other users of your app.&lt;/p&gt;
&lt;h3&gt;
  
  
  Bootstrapping
&lt;/h3&gt;

&lt;p&gt;If the only way to set or delete claims requires the &lt;code&gt;claims_admin&lt;/code&gt; claim to be set to &lt;code&gt;true&lt;/code&gt; and no users have that claim, how can I edit custom claims from within my app?&lt;/p&gt;

&lt;p&gt;The answer is to &lt;strong&gt;"bootstrap"&lt;/strong&gt; a user by running the following command inside your &lt;a href="https://app.supabase.io/project/_/sql"&gt;Supabase Query Editor&lt;/a&gt; window:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;select set_claim('03acaa13-7989-45c1-8dfb-6eeb7cf0b92e', 'claims_admin', 'true');&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;where &lt;code&gt;03acaa13-7989-45c1-8dfb-6eeb7cf0b92e&lt;/code&gt; is the &lt;code&gt;id&lt;/code&gt; of your admin user found in &lt;code&gt;auth.users&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Using the functions
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Inside the Query Editor
&lt;/h3&gt;

&lt;p&gt;You can get, set, and delete claims for any user based on the user's &lt;code&gt;id&lt;/code&gt; (uuid) with the following functions:&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;code&gt;get_claims(uid uuid)&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;This returns a JSON object containing all the custom claims for a user.&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;code&gt;get_claim(uid uuid, claim text)&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;This returns a JSON object for a single claim for a user.&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;code&gt;set_claim(uid uuid, claim text, value jsonb)&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;This sets a specific claim for a user.  You can send any valid JSON data here, including numbers, text, boolean, arrays, or complete complex nested JSON objects.&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;code&gt;delete_claim(uid uuid, claim text)&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;This will delete a custom claim for a user.&lt;/p&gt;
&lt;h3&gt;
  
  
  Inside PostgreSQL Functions and Triggers
&lt;/h3&gt;

&lt;p&gt;When using custom claims from inside a PostgreSQL function or trigger, you can use any of the functions shown in the section above: &lt;code&gt;Inside the Query Editor&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In addition, you can use the following functions that are specific to the currently logged-in user:&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;code&gt;is_claims_admin()&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;This returns &lt;code&gt;true&lt;/code&gt; if the &lt;code&gt;claims_admin&lt;/code&gt; claim is set to &lt;code&gt;true&lt;/code&gt;.  This means the user is allowed to execute the functions above (&lt;code&gt;set_claim&lt;/code&gt;, etc.)&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;code&gt;get_my_claims()&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;This returns a JSON object containing all the custom claims for the current user.&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;code&gt;get_my_claim(claim TEXT)&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;This returns a JSON object for a single claim for the current user.&lt;/p&gt;
&lt;h3&gt;
  
  
  Inside an RLS (Row Level Security) Policy
&lt;/h3&gt;

&lt;p&gt;To use custom claims in an RLS Policy, you'll normally use the &lt;code&gt;get_my_claim&lt;/code&gt; to check a specific claim for the currently logged in user.  For example, you could check to see if the user is a subscriber to your app before allowing access to a table.  Something like this:&lt;br&gt;
&lt;code&gt;get_my_claim('userrole') = '"SUBSCRIBER"'&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The syntax is slightly odd here because the &lt;code&gt;get_my_claim&lt;/code&gt; function returns a JSON object, so if you're checking for a string you need to enclose it in quotes.  You can also check for other types (numeric, boolean, etc.)&lt;/p&gt;
&lt;h3&gt;
  
  
  Getting Claims Data from Local Session Data
&lt;/h3&gt;

&lt;p&gt;You can extract claims information from the &lt;code&gt;session&lt;/code&gt; object you get when the user is logged in.  For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onAuthStateChange&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;app_metadata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// show custom claims&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If any claims have changed since your last log in, you may need to log out and back in to see these changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Claims Data From Your Application (using &lt;code&gt;.rpc()&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;The following functions can only be used by a &lt;strong&gt;"claims admin"&lt;/strong&gt;, that is, a user who has the &lt;code&gt;claims_admin&lt;/code&gt; custom claim set to &lt;code&gt;true&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;(Note: these functions allow you to view, set, and delete claims for any user of your application, so these would be appropriate for an &lt;strong&gt;administrative&lt;/strong&gt; branch of your application to be used only by high-level users with the proper security rights (i.e. &lt;code&gt;claims_admin&lt;/code&gt; level users.))&lt;/p&gt;

&lt;p&gt;TypeScript Examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kr"&gt;public&lt;/span&gt; &lt;span class="nx"&gt;get_claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_claims&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;uid&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kr"&gt;public&lt;/span&gt; &lt;span class="nx"&gt;get_claim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_claim&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;claim&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kr"&gt;public&lt;/span&gt; &lt;span class="nx"&gt;set_claim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;set_claim&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kr"&gt;public&lt;/span&gt; &lt;span class="nx"&gt;delete_claim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delete_claim&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;claim&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Warning
&lt;/h2&gt;

&lt;p&gt;Be sure to watch for &lt;strong&gt;reserved&lt;/strong&gt; claims in your particular development environment.  For example, the claims &lt;code&gt;exp&lt;/code&gt; and &lt;code&gt;role&lt;/code&gt; are reserved by the Supabase Realtime system and can cause problems if you try use these names.  To avoid these potential problems, it's good practice to use a custom identifier in your custom claims, such as &lt;code&gt;MY_COMPANY_item1&lt;/code&gt;, &lt;code&gt;MY_COMPANY_item2&lt;/code&gt;, etc.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I hope you've found this article helpful as you develop your application with Supabase.  This is just my personal approach to handling claims, so feel free to use this as a starting point for your custom approach.  I'm looking forward to seeing what cool things you come up with in your applications!&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>sql</category>
      <category>jwt</category>
    </item>
  </channel>
</rss>
