<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Geeky Tidbits</title>
    <description>Tidbits on software development, technology, and other geeky stuff</description>
    <link>https://www.geekytidbits.com</link>
    <atom:link href="https://www.geekytidbits.com/rss.xml" rel="self" type="application/rss+xml" />
    <lastBuildDate>Thu, 25 Sep 2025 15:05:49 +0000</lastBuildDate>
    <item>
      <title>SQL Join Strategies and Performance in PostgreSQL</title>
      <description>
        <![CDATA[<p>When you write a SQL query that joins two or more tables, PostgreSQL has to decide how to execute that join. The strategy it chooses can have a big impact on performance. Understanding the different join strategies can help you write more efficient queries and optimize your database.</p>
<p>Let’s take an example. We have two tables: <code>users</code> and <code>posts</code>. We want to find all posts made by a user with the username ‘alice’.</p>

<pre class="language-sql"><code class="language-sql"><span class="token keyword">EXPLAIN</span>​
<span class="token keyword">SELECT</span> p<span class="token punctuation">.</span>id​
<span class="token keyword">FROM</span> posts p <span class="token keyword">INNER</span> <span class="token keyword">JOIN</span> users u <span class="token keyword">ON</span> u<span class="token punctuation">.</span>id <span class="token operator">=</span> p<span class="token punctuation">.</span>user_id​
<span class="token keyword">WHERE</span> u<span class="token punctuation">.</span>username <span class="token operator">=</span> <span class="token string">'alice'</span><span class="token punctuation">;</span>​
                                          QUERY <span class="token keyword">PLAN</span>​
<span class="token comment">----------------------------------------------------------------------------------------------</span>​
 <span class="token keyword">Hash</span> <span class="token keyword">Join</span>  <span class="token punctuation">(</span>cost<span class="token operator">=</span><span class="token number">8.17</span><span class="token punctuation">.</span><span class="token number">.20</span><span class="token number">.21</span> <span class="token keyword">rows</span><span class="token operator">=</span><span class="token number">1</span> width<span class="token operator">=</span><span class="token number">4</span><span class="token punctuation">)</span>​
   <span class="token keyword">Hash</span> Cond: <span class="token punctuation">(</span>p<span class="token punctuation">.</span>user_id <span class="token operator">=</span> u<span class="token punctuation">.</span>id<span class="token punctuation">)</span>​
   <span class="token operator">-</span><span class="token operator">></span>  Seq Scan <span class="token keyword">on</span> posts p  <span class="token punctuation">(</span>cost<span class="token operator">=</span><span class="token number">0.00</span><span class="token punctuation">.</span><span class="token number">.11</span><span class="token number">.60</span> <span class="token keyword">rows</span><span class="token operator">=</span><span class="token number">160</span> width<span class="token operator">=</span><span class="token number">8</span><span class="token punctuation">)</span>​
   <span class="token operator">-</span><span class="token operator">></span>  <span class="token keyword">Hash</span>  <span class="token punctuation">(</span>cost<span class="token operator">=</span><span class="token number">8.16</span><span class="token punctuation">.</span><span class="token number">.8</span><span class="token number">.16</span> <span class="token keyword">rows</span><span class="token operator">=</span><span class="token number">1</span> width<span class="token operator">=</span><span class="token number">4</span><span class="token punctuation">)</span>​
         <span class="token operator">-</span><span class="token operator">></span>  <span class="token keyword">Index</span> Scan <span class="token keyword">using</span> users_username_key <span class="token keyword">on</span> users u  <span class="token punctuation">(</span>cost<span class="token operator">=</span><span class="token number">0.14</span><span class="token punctuation">.</span><span class="token number">.8</span><span class="token number">.16</span> <span class="token keyword">rows</span><span class="token operator">=</span><span class="token number">1</span> width<span class="token operator">=</span><span class="token number">4</span><span class="token punctuation">)</span>​
               <span class="token keyword">Index</span> Cond: <span class="token punctuation">(</span><span class="token punctuation">(</span>username<span class="token punctuation">)</span>::<span class="token keyword">text</span> <span class="token operator">=</span> <span class="token string">'alice'</span>::<span class="token keyword">text</span><span class="token punctuation">)</span>​
</code></pre>
<p>In the example above, PostgreSQL has chosen to use a Hash Join strategy.  Why did it choose this strategy?  How does that strategy work?  Is it the best choice for this query and if not is there something I can do to help it choose a better strategy?</p>
<p>Let’s explore the three main join strategies PostgreSQL uses: Nested Loop Join, Hash Map Join, and Merge Join.</p>
<h2 id="nested-loop-join">Nested Loop Join</h2>
<p>The Nested Loop join is the simplest and most intuitive join strategy. It works by iterating over each row in the outer table and for each row, it looks up matching rows in the inner table. If there is an index on the join column of the inner table, it can use that index to speed up the lookups.  You can think of it like a double for-loop in programming.  It is a good choice when the outer table is small or very selective, and/or the inner table has a good index on the join column.  In fact, it can be the very fastest strategy when the outer table is small and there is a hash index on the inner table. However, if both tables are large and there are no helpful indexes, this strategy can be very slow.  Interesting side-note: This is the ‘default’ strategy when others cannot or will not be be used in PostgreSQL.</p>
<h3 id="time-complexity">Time Complexity</h3>
<ul>
<li>Typical: <code>O(m * log n)</code> (B-tree index)</li>
<li>Worst case: <code>O(m × n)</code> (no indexes)</li>
<li>Best case: <code>O(m)</code> (hash index)</li>
</ul>
<h3 id="visualization">Visualization</h3>
<p><img src="/sql-join-strategies-and-performance/nest-loops-sorted-50fps-2.gif" alt="Nested Loop Join Visualization"></p>
<h3 id="performance-tips">Performance Tips</h3>
<p>To ensure a Nested Loop join is as efficient as possible:</p>
<ul>
<li>Increase selectivity on the outer table side</li>
<li>Ensure inner table join column is indexed properly</li>
<li>Select only columns within indexes</li>
<li>Use INCLUDE in index to allow index-only scans</li>
</ul>
<h2 id="hash-map-join">Hash Map Join</h2>
<p>The Hash Map join strategy works by building a hash table in memory for one of the input tables (usually the smaller one) based on the join key. Then, it scans the other input table and for each row, it “probes” the hash table to find matching rows. This strategy is very efficient for large, unsorted tables and can handle large datasets well, as long as there is enough memory to hold the hash table. However, if the hash table is too large to fit in memory, PostgreSQL may need to write it to disk (called a ‘spill’), which can slow down performance. It is a very flexible strategy and can be thought of as a workhorse as it is often used in practice.</p>
<p>The Hash Map join requires an equality condition (e.g., <code>ON a.id = b.a_id</code>) and is not suitable for non-equality joins.</p>
<h3 id="time-complexity-1">Time Complexity</h3>
<ul>
<li>Best case: <code>O(m + n)</code></li>
<li>Worst case: <code>O(m × n)</code> (when many hash collisions occur and/or when ‘spilling’ to disk because there is not enough <code>work_mem</code> to hold the hash table)</li>
</ul>
<h3 id="visualization-1">Visualization</h3>
<p><img src="/sql-join-strategies-and-performance/Hash-Match-Join-Looping-1.gif" alt="Hash Map Join Visualization"></p>
<h3 id="performance-tips-1">Performance Tips</h3>
<p>To ensure a Hash Map join is as efficient as possible:</p>
<ul>
<li>Reduce row width (select fewer columns) to prevent disk ‘spills’</li>
<li>Filter more data early to reduce burden of hashing step<ul>
<li>Expand WHERE clause</li>
<li>Add another join condition</li>
<li>Use a sub-query or CTE (Common Table Expression)</li>
</ul>
</li>
</ul>
<h2 id="merge-join">Merge Join</h2>
<p>The Merge Join strategy works by first sorting both input tables on the join key, and then merging them together in a single pass. This strategy is very efficient when both input tables are already sorted on the join key, or when sorting is needed for other reasons (e.g., an <code>ORDER BY</code> clause).  B-tree indexes are inherently sorted and can be leveraged to avoid the sorting step. However, if the input tables are large and unsorted, the sorting step can be expensive and may make this strategy less efficient than others. It is often the preferred strategy when conditions are right.</p>
<h3 id="time-complexity-2">Time Complexity</h3>
<ul>
<li>Best case: <code>O(m + n)</code> (if both inputs are already sorted or sorted B-tree indexes can be used)</li>
<li>Worst case: <code>O(m log m + n log n)</code> (both inputs need to be sorted first)</li>
</ul>
<h3 id="visualization-2">Visualization</h3>
<p><img src="/sql-join-strategies-and-performance/Merge-Join-1.gif" alt="Merge Join Visualization"></p>
<h3 id="performance-tips-2">Performance Tips</h3>
<p>To ensure a Merge Join is as efficient as possible:</p>
<ul>
<li>Prefer B-tree indexes (vs. hash index) to ensure they are sorted</li>
<li>Reduce row width (select fewer columns)</li>
<li>Select only columns within indexes</li>
<li>Match collation &amp; data types on both sides to keep scans orderable</li>
<li>Use multi-column indexes in same order as join conditions</li>
</ul>
<h2 id="general-tips">General Tips</h2>
<p>Postgres choses a join strategy based on various factors including table sizes, available indexes, data distribution, query structure, and more.  To ensure you are giving Postgres the best chance to choose the most efficient join strategy, consider the following general tips.</p>
<h3 id="ensure-table-statistics-are-accurate">Ensure table statistics are accurate</h3>
<p>Make sure your table statistics are up-to-date so the query planner can make informed decisions about join strategies.  You can manually run <code>ANALYZE</code> on your tables and/or ensure autovacuum is running properly to keep statistics current.  Statistics play a big role in the query planner’s decision on join strategy!</p>
<h3 id="index-available-on-join-columns">Index available on join column(s)</h3>
<p>This one is probably obvious but it is worth mentioning.  Having an index on the join column(s) can significantly speed up join operations, especially on the “inner input” side when Nested Loop join is being used.</p>
<h3 id="strive-for-simple-equality-join-conditions">Strive for simple equality join conditions</h3>
<p>If possible, use simple equality conditions in your join clauses (ON a.id = b.a_id) and avoid operations such as <code>&lt;, &lt;=, &gt;, &gt;=, !=, LIKE</code> .  Complex expressions or functions can prevent the use of the Hash Map join strategy where it might otherwise be the best choice.</p>
<h3 id="avoid-functionscasts-on-join-conditions">Avoid functions/casts on join conditions</h3>
<p>Using functions or type casts on join conditions can prevent the use of indexes and lead to less efficient join strategies.  If you must use a function or cast, consider creating a generated column that pre-computes the value and indexing that column.</p>
<h3 id="use-multi-column-indexes-in-same-order-as-join-conditions">Use multi-column indexes in same order as join conditions</h3>
<p>If your join condition involves multiple columns, consider creating a multi-column index that matches the order of the columns in the join condition.  This can help the query planner choose a more efficient join strategy.</p>
<h3 id="be-careful-with-select-columns">Be careful with <code>SELECT</code> columns</h3>
<p>Selecting only the columns you need and/or are included in indexes can impact performance more than you might expect.  This is especially true when using the Hash Map join strategy where reducing row width can help prevent ‘spilling’ to disk.  Also, if you select only columns that are included in an index, Postgres can use an index-only scan which can be significantly faster than a regular scan.</p>
<h3 id="perturbing-the-join-strategy">Perturbing the Join Strategy</h3>
<p>If your query is still slow even after ensuring you are giving Postgres everything possible to chose the most efficient join strategy, you can perturb the join strategy by disabling certain strategies (for the current session) using the following commands:</p>

<pre class="language-sql"><code class="language-sql"><span class="token keyword">SET</span> enable_hashjoin <span class="token operator">=</span> <span class="token keyword">off</span><span class="token punctuation">;</span>​
<span class="token keyword">SET</span> enable_mergejoin <span class="token operator">=</span> <span class="token keyword">off</span><span class="token punctuation">;</span>​
<span class="token keyword">SET</span> enable_nestloop <span class="token operator">=</span> <span class="token keyword">off</span><span class="token punctuation">;</span></code></pre>
<p>By inspecting the <code>EXPLAIN</code> output after disabling specific strategies, you can see how the query planner’s choice changes and what factors are impacting its decision to choose a particular join strategy.  This can help you identify potential optimizations for your query.</p>
]]>
      </description>
      <pubDate>Wed, 24 Sep 2025 00:00:00 +0000</pubDate>
      <link>https://www.geekytidbits.com/sql-join-strategies-and-performance/</link>
      <guid isPermaLink="true">https://www.geekytidbits.com/sql-join-strategies-and-performance/</guid>
    </item>
    <item>
      <title>Posting to Bluesky from Zapier (for free)</title>
      <description>
        <![CDATA[<p>Zapier recently added a <a href="https://zapier.com/apps/bluesky/integrations">Bluesky app</a> but unfortunately it is designated as Premium. This means that you need to pay for a Zapier subscription to use it. However, you can still post to Bluesky using the <a href="https://zapier.com/apps/code/integrations">Code by Zapier app</a>, which is free to use.</p>
<p>Here’s how to do it!</p>
<ol>
<li>First of all, you’ll need to create a Bluesky app password to use.  Go to <a href="https://bsky.app/settings/app-passwords">https://bsky.app/settings/app-passwords</a> and generate a password for use later in these steps.</li>
<li><strong>Create a new Zap</strong>: Go to your Zapier dashboard, select “Zaps” in sidebar, and then click “+ Create &gt; New Zap”.</li>
<li><strong>Choose a Trigger</strong>: Select the app and event that will trigger the Zap in the first step of the Zap. For example, you can use “RSS by Zapier” to post new items from an RSS feed.  Edit the details for this trigger per your specific needs.</li>
<li><strong>Choose an Action</strong>: In the second step, click “Action” and select “Code” (Code by Zapier).  Then, do the following steps:<ol>
<li><p>Choose “Run JavaScript” as the action event then click “Continue”.</p>
</li>
<li><p>Add 3 items for “Input Data” and provide the values.</p>
<ul>
<li><code>username</code>: Your Bluesky username (without the @ symbol; for example “bradymholt.bsky.social” ).</li>
<li><code>password</code>: Your Bluesky password.</li>
<li><code>post_text</code>: The text you want to post to Bluesky.  You’ll likely want to use a variable from the trigger step here.</li>
</ul>
</li>
<li><p>Paste in the following code:</p>

<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> sessionResponse <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token string">"https://bsky.social/xrpc/com.atproto.server.createSession"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span>​
    <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">'POST'</span><span class="token punctuation">,</span>​
    <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span><span class="token punctuation">{</span>​
        <span class="token literal-property property">identifier</span><span class="token operator">:</span> inputData<span class="token punctuation">[</span><span class="token string">'username'</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token literal-property property">password</span><span class="token operator">:</span> inputData<span class="token punctuation">[</span><span class="token string">'password'</span><span class="token punctuation">]</span>​
    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span>​
    <span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span>​
        <span class="token string-property property">'Content-Type'</span><span class="token operator">:</span> <span class="token string">'application/json'</span>​
    <span class="token punctuation">}</span><span class="token punctuation">,</span>​
    <span class="token literal-property property">redirect</span><span class="token operator">:</span> <span class="token string">'manual'</span>​
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>​
​
<span class="token comment">// Check if the response status is OK</span>​
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>sessionResponse<span class="token punctuation">.</span>ok<span class="token punctuation">)</span> <span class="token punctuation">{</span>​
    console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token string">'Failed request'</span><span class="token punctuation">,</span> <span class="token keyword">await</span> sessionResponse<span class="token punctuation">.</span><span class="token function">text</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>​
    <span class="token keyword">return</span> <span class="token keyword">null</span><span class="token punctuation">;</span>​
<span class="token punctuation">}</span>​
​
<span class="token keyword">const</span> responseBody <span class="token operator">=</span> <span class="token keyword">await</span> sessionResponse<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>​
​
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>responseBody<span class="token punctuation">.</span>accessJwt<span class="token punctuation">)</span> <span class="token punctuation">{</span>​
    console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token string">'accessJwt not found in the response'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>​
    <span class="token keyword">return</span> <span class="token keyword">null</span><span class="token punctuation">;</span>​
<span class="token punctuation">}</span>​
​
<span class="token keyword">const</span> postText <span class="token operator">=</span> inputData<span class="token punctuation">[</span><span class="token string">'post_text'</span><span class="token punctuation">]</span><span class="token punctuation">}</span><span class="token punctuation">;</span>​
​
<span class="token keyword">const</span> recordData <span class="token operator">=</span> <span class="token punctuation">{</span>​
    <span class="token string-property property">"$type"</span><span class="token operator">:</span> <span class="token string">"app.bsky.feed.post"</span><span class="token punctuation">,</span>​
    <span class="token string-property property">"text"</span><span class="token operator">:</span> postText<span class="token punctuation">,</span>​
    <span class="token string-property property">"createdAt"</span><span class="token operator">:</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toISOString</span><span class="token punctuation">(</span><span class="token punctuation">)</span>​
<span class="token punctuation">}</span><span class="token punctuation">;</span>​
​
<span class="token keyword">const</span> postData <span class="token operator">=</span> <span class="token punctuation">{</span>​
    <span class="token literal-property property">repo</span><span class="token operator">:</span> inputData<span class="token punctuation">[</span><span class="token string">'username'</span><span class="token punctuation">]</span><span class="token punctuation">,</span>​
    <span class="token literal-property property">collection</span><span class="token operator">:</span> <span class="token string">'app.bsky.feed.post'</span><span class="token punctuation">,</span>​
    <span class="token literal-property property">record</span><span class="token operator">:</span> recordData​
<span class="token punctuation">}</span><span class="token punctuation">;</span>​
​
<span class="token keyword">const</span> postResponse <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token string">"https://bsky.social/xrpc/com.atproto.repo.createRecord"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span>​
    <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">'POST'</span><span class="token punctuation">,</span>​
    <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>postData<span class="token punctuation">)</span><span class="token punctuation">,</span>​
    <span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span>​
        <span class="token string-property property">'Content-Type'</span><span class="token operator">:</span> <span class="token string">'application/json'</span><span class="token punctuation">,</span>​
        <span class="token string-property property">'Authorization'</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Bearer </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>responseBody<span class="token punctuation">.</span>accessJwt<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span>​
    <span class="token punctuation">}</span>​
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>​
​
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>postResponse<span class="token punctuation">.</span>ok<span class="token punctuation">)</span> <span class="token punctuation">{</span>​
    console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token string">'Failed request'</span><span class="token punctuation">,</span> <span class="token keyword">await</span> postResponse<span class="token punctuation">.</span><span class="token function">text</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>​
    output <span class="token operator">=</span> <span class="token punctuation">{</span>​
        <span class="token literal-property property">success</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>​
        <span class="token literal-property property">message</span><span class="token operator">:</span> <span class="token string">"Failed to create record"</span>​
    <span class="token punctuation">}</span><span class="token punctuation">;</span>​
    <span class="token keyword">return</span><span class="token punctuation">;</span>​
<span class="token punctuation">}</span>​
​
<span class="token keyword">const</span> postResponseBody <span class="token operator">=</span> <span class="token keyword">await</span> postResponse<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>​
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>postResponseBody<span class="token punctuation">.</span>success<span class="token punctuation">)</span> <span class="token punctuation">{</span>​
    console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token string">'Failed to create record'</span><span class="token punctuation">,</span> postResponseBody<span class="token punctuation">.</span>message<span class="token punctuation">)</span><span class="token punctuation">;</span>​
    output <span class="token operator">=</span> <span class="token punctuation">{</span>​
        <span class="token literal-property property">success</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>​
        <span class="token literal-property property">message</span><span class="token operator">:</span> postResponseBody<span class="token punctuation">.</span>message​
    <span class="token punctuation">}</span><span class="token punctuation">;</span>​
    <span class="token keyword">return</span><span class="token punctuation">;</span>​
<span class="token punctuation">}</span>​
​
output <span class="token operator">=</span> <span class="token punctuation">{</span>​
    <span class="token literal-property property">success</span><span class="token operator">:</span> <span class="token boolean">true</span>​
<span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
</li>
</ol>
</li>
<li>Click “Continue” to test the action and complete the setup.</li>
</ol>
<p>That’s it!  Enjoy.</p>
]]>
      </description>
      <pubDate>Wed, 26 Mar 2025 00:00:00 +0000</pubDate>
      <link>https://www.geekytidbits.com/posting-to-bluesky-from-zapier/</link>
      <guid isPermaLink="true">https://www.geekytidbits.com/posting-to-bluesky-from-zapier/</guid>
    </item>
    <item>
      <title>SvelteKit with Drizzle and Cloudflare D1</title>
      <description>
        <![CDATA[<p>I recently started a little side project for my wife and decided to use <a href="https://svelte.dev/">SvelteKit</a> to build it and <a href="https://www.cloudflare.com/">Cloudflare</a> to host it. Along the way I decided to use <a href="https://orm.drizzle.team/">Drizzle ORM</a> along with <a href="https://developers.cloudflare.com/d1/">Cloudflare D1</a> to store the data. I was really happy with how everything worked but I did run into a few issues: using a local SQLite database for development and applying migrations to both dev and production environments.  After some fiddling I got things working nicely and wanted to document my setup here.</p>
<h2 id="instructions">Instructions</h2>
<p>First off, you’ll want to follow the <a href="https://orm.drizzle.team/docs/get-started/sqlite-new">Get Started with Drizzle and SQLite</a> docs to get a local SQLite database setup and working within SvelteKit. This will involve installing some npm packages and creating a few files including the <code>drizzle.config.ts</code> file and the <code>schema.ts</code> file. Be sure to run <code>npx drizzle-kit generate &amp;&amp; npx drizzle-kit migrate</code> to create and run the initial migration file from <code>./drizzle</code> folder. The guide <a href="https://fullstacksveltekit.com/blog/sveltekit-sqlite-drizzle">SvelteKit with SQLite and Drizzle</a> might be helpful here.</p>
<p><strong>Before proceeding with the instructions below, your app should be setup to work with a local SQLite database using Drizzle.</strong></p>
<p>Now, you’ll want to get your app configured to work with Cloudflare Pages.  Follow the SvelteKit <a href="https://svelte.dev/docs/kit/adapter-cloudflare">Cloudflare Pages</a> instructions.  You will basically install a npm package and and modify the <code>svelte.config.js</code> file.</p>
<h4 id="svelteconfigjs-changes">svelte.config.js changes</h4>
<p>I did find that the instructions for <code>svelte.config.js</code> needed to be changed slightly. <code>platformProxy.persist</code> needs to be set to <code>true</code> in order to persist the local SQLite database.  Also, change <code>configPath</code> to <code>&quot;wrangler.json&quot;</code> (from <code>&quot;wrangler.toml&quot;</code>).</p>

<pre class="language-js"><code class="language-js"><span class="token literal-property property">platformProxy</span><span class="token operator">:</span> <span class="token punctuation">{</span>​
  <span class="token comment">// Other config ...</span>​
  <span class="token literal-property property">configPath</span><span class="token operator">:</span> <span class="token string">"wrangler.json"</span><span class="token punctuation">;</span>​
  <span class="token literal-property property">persist</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">;</span>​
<span class="token punctuation">}</span></code></pre>
<h3 id="install-and-configure-wrangler">Install and configure wrangler</h3>
<p>Install the <code>wrangler</code> CLI tool with <code>npm install wrangler --save-dev</code>.</p>
<p>Then, create a <code>wrangler.json</code> file with the following contents in the root of your project.</p>

<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>​
  <span class="token property">"$schema"</span><span class="token operator">:</span> <span class="token string">"node_modules/wrangler/config-schema.json"</span><span class="token punctuation">,</span>​
  <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"my_project_name"</span><span class="token punctuation">,</span>​
  <span class="token property">"compatibility_date"</span><span class="token operator">:</span> <span class="token string">"2025-02-04"</span><span class="token punctuation">,</span>​
  <span class="token property">"pages_build_output_dir"</span><span class="token operator">:</span> <span class="token string">".svelte-kit/cloudflare"</span><span class="token punctuation">,</span>​
  <span class="token property">"d1_databases"</span><span class="token operator">:</span> <span class="token punctuation">[</span>​
    <span class="token punctuation">{</span>​
      <span class="token property">"migrations_dir"</span><span class="token operator">:</span> <span class="token string">"./drizzle"</span><span class="token punctuation">,</span>​
      <span class="token property">"binding"</span><span class="token operator">:</span> <span class="token string">"DB"</span><span class="token punctuation">,</span>​
      <span class="token property">"database_name"</span><span class="token operator">:</span> <span class="token string">"my-d1-database-name"</span><span class="token punctuation">,</span>​
      <span class="token property">"database_id"</span><span class="token operator">:</span> <span class="token string">"database_id_TBD"</span>​
    <span class="token punctuation">}</span>​
  <span class="token punctuation">]</span>​
<span class="token punctuation">}</span></code></pre>
<h3 id="create-remote-d1-database">Create remote D1 database</h3>
<p>Now, you’ll create your D1 database on Cloudflare using the <code>wrangler</code> CLI.</p>
<p>Run the following:</p>

<pre class="language-shell"><code class="language-shell">npx wrangler d1 create my-d1-database-name</code></pre>
<p>The output of this command will provide you with JSON config for the <code>wrangler.json</code> file.  Just copy paste the value of <code>database_id</code> and update your <code>wrangler.json</code> file, replacing the <code>&quot;database_id_TBD&quot;</code> value.</p>
<h3 id="create-local-d1-sqlite-database">Create local “D1” (SQLite) database</h3>
<p>To create a local SQLite database used for development, you will first need to find the name of the initial Drizzle migration file.  This file should be named in format <code>0000_[migration_file].sql</code> and be located in the <code>./drizzle</code> directory.</p>
<p>Now, run the following command, specifying the specific migration file you located.</p>

<pre class="language-shell"><code class="language-shell">npx wrangler d1 execute my-d1-database-name --local --file<span class="token operator">=</span>./drizzle/0000_<span class="token punctuation">[</span>migration_file<span class="token punctuation">]</span>.sql</code></pre>
<p>A .sqlite file will be created in <code>.wrangler/state/v3/d1/miniflare-D1DatabaseObject/</code>.  You should find this file and note its name.</p>
<h3 id="drizzleconfigts">drizzle.config.ts</h3>
<p>The <code>drizzle.config.ts</code> file is used by Drizzle when working with migrations. You need to change the <code>dbCredentials.url</code> to the location of the SQLite database created by wrangler in the previous step.</p>
<p>Your <code>drizzle.config.ts</code> file will look something like this:</p>

<pre class="language-ts"><code class="language-ts">import { defineConfig } from "drizzle-kit";​
​
export default defineConfig({​
  schema: "./src/lib/server/schema",​
  dialect: "sqlite",​
  out: "./drizzle",​
  dbCredentials: {​
    url: ".wrangler/state/v3/d1/miniflare-D1DatabaseObject/e7352547963de7050bd7d94658afc4fe78b61811b7815da12d90be8e863abf4d.sqlite",​
  },​
});</code></pre>
<h3 id="sveltekit-modifications">SvelteKit modifications</h3>
<p>Update your <code>app.d.ts</code> file to include the <code>env: Env</code> field.  Cloudflare (and wrangler locally) will inject the <code>DB</code> variable binding into the <code>env</code> object which will be used to connect to D1.</p>

<pre class="language-ts"><code class="language-ts">declare global {​
    namespace App {​
        interface Platform {​
            env: Env;​
        }​
    }​
}​
​
export {};</code></pre>
<p>Now, anywhere you want to interact with D1 in your SvelteKit code, you’ll want to <code>import { drizzle } from &#39;drizzle-orm/d1&#39;;</code> and instantiate a db object by passing in the <code>DB</code> binding like this: <code>const db = drizzle(platform.env.DB);</code>.</p>
<p>Here is an example of how you would set things up and select from a <code>users</code> table:</p>

<pre class="language-markup"><code class="language-markup">import { drizzle } from 'drizzle-orm/d1';​
​
export const actions = {​
  fetchUsers: async ({ request, platform }) => {​
    const db = drizzle(platform.env.DB);​
    const users = await db.select().from("users");​
    ...​
  }​
} satisfies Actions;​
</code></pre>
<h3 id="packagejson">package.json</h3>
<p>Finally, add the following scripts to <code>package.json</code> to make it easier to work with the database and deploy the site.</p>
<ul>
<li><code>db:migrate:local</code> - Migrate the local SQLite database</li>
<li><code>db:migrate:prod</code> - Migrate the Cloudflare D1 database</li>
<li><code>deploy</code> - Build the site, migrate the production Cloudflare D1 database, and deploy the site</li>
</ul>

<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>​
  <span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span>​
    ...​
    <span class="token property">"db:migrate:local"</span><span class="token operator">:</span> <span class="token string">"drizzle-kit migrate"</span><span class="token punctuation">,</span>​
    <span class="token property">"db:migrate:prod"</span><span class="token operator">:</span> <span class="token string">"wrangler d1 migrations apply DB --remote"</span><span class="token punctuation">,</span>​
    <span class="token property">"deploy"</span><span class="token operator">:</span> <span class="token string">"npm run build &amp;&amp; npm run db:migrate:prod &amp;&amp; wrangler pages deploy"</span>​
  <span class="token punctuation">}</span>​
<span class="token punctuation">}</span></code></pre>
<p>Note: for <code>db:migrate:prod</code>, we call <code>wrangler d1 migrations apply</code> which uses the <code>wrangler</code> CLI (rather than <code>drizzle-kit</code>!) to apply the migrations to D1. <code>wrangler</code> is able to read and work with the migrations created by Drizzle. I found ways for <code>drizzle-kit</code> to do this directly, but it was more complicated and I found this to be the easiest way to do it.</p>
<h3 id="starting-up-local-development">Starting up local development</h3>
<p>Run <code>npm run db:migrate:local</code> to run any pending migrations you may have for the local database.  Then run <code>npm run dev</code> to start up the local development server.  All interactions with Drizzle should now be targeting the local SQLite database, using the <code>DB</code> binding.</p>
<h3 id="deploying-to-cloudflare">Deploying to Cloudflare</h3>
<p>Run <code>npm run deploy</code> to build the site, run any pending migrations on Cloudflare D1, and deploy the site to Cloudflare.</p>
]]>
      </description>
      <pubDate>Thu, 13 Feb 2025 00:00:00 +0000</pubDate>
      <link>https://www.geekytidbits.com/sveltekit-with-drizzle-and-cloudflare-d1/</link>
      <guid isPermaLink="true">https://www.geekytidbits.com/sveltekit-with-drizzle-and-cloudflare-d1/</guid>
    </item>
    <item>
      <title>Batch Deletes in Postgres</title>
      <description>
        <![CDATA[<p>If you need to delete many, many rows in a Postgres table, issuing a single <code>DELETE</code> statement can be problematic for a few reasons:</p>
<ul>
<li>It can take a very long time to complete</li>
<li>It can lock rows for the duration of the delete leading to blocked queries</li>
<li>It can cause the the transaction log to become huge because the delete transaction is so large</li>
</ul>
<p>To work around these issues, you can delete rows in batches.  There are various ways to do this but I prefer the following approach, using a <code>DELETE ... WHERE ... IN (SELECT ... LIMIT)</code> query with a <a href="https://www.postgresql.org/docs/current/app-psql.htm#APP-PSQL-META-COMMAND-WATCH">\watch command in psql</a>.</p>
<p>First, construct a delete query using a <code>WHERE .. IN</code> statement and a <code>LIMIT</code> to ensure only a batch of records are deleted.  Here’s an example that deletes 1,000 rows from a table named <code>my_table</code> where the <code>created_at</code> column is older than 30 days:</p>

<pre class="language-sql"><code class="language-sql"><span class="token keyword">DELETE</span> <span class="token keyword">FROM</span> my_table​
<span class="token keyword">WHERE</span> id <span class="token operator">IN</span> <span class="token punctuation">(</span>​
  <span class="token keyword">SELECT</span> id​
  <span class="token keyword">FROM</span> my_table​
  <span class="token keyword">WHERE</span> created_at <span class="token operator">&lt;</span> <span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token keyword">interval</span> <span class="token string">'30 days'</span>​
  <span class="token comment">-- Delete only 1000 rows at a time</span>​
  <span class="token keyword">LIMIT</span> <span class="token number">1000</span>​
<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Execute this query within <code>psql</code> and ensure it completes without error.</p>
<p>Now, run <code>\watch 1</code> within <code>psql</code>.  This will re-run the last query every second and display the number of rows deleted.</p>
<p>After a few seconds, you should see output like this:</p>

<pre class="language-markup"><code class="language-markup">DELETE 1000​
DELETE 1000​
DELETE 1000</code></pre>
<p>This indicates that 1,000 rows are being deleted every second.</p>
<p>When all rows have been deleted, you will see output like this:</p>

<pre class="language-markup"><code class="language-markup">DELETE 0​
DELETE 0​
DELETE 0</code></pre>
<p>Since all rows have been deleted at this point, you can stop the watch command with <code>CTRL+C</code> in <code>psql</code>.</p>
]]>
      </description>
      <pubDate>Thu, 16 May 2024 00:00:00 +0000</pubDate>
      <link>https://www.geekytidbits.com/batch-deletes-in-postgres/</link>
      <guid isPermaLink="true">https://www.geekytidbits.com/batch-deletes-in-postgres/</guid>
    </item>
    <item>
      <title>Automatic installation of recommended VS Code Extensions</title>
      <description>
        <![CDATA[<p><a href="https://code.visualstudio.com/docs/editor/extension-marketplace#_recommended-extensions">Recommended extensions</a> in VS Code are a useful way to keep a development team on the same page.  You can recommend extensions that perform formatting when saving files, show lint warning, and many other things that are useful when collaborating together on a codebase.</p>
<p>If extensions are recommended, VS Code users should see the following popup when opening the workspace folder:</p>
<p><img src="/automatic-installation-of-recommended-vs-code-extensions/extension-recommendations.png" alt="Extension recommendations popup"></p>
<p>There is a problem though: if a user dismisses this popup, it will not be shown again for the workspace.  Also, if they have <code>extensions.ignoreRecommendations</code> set to <code>true</code>, they will never see this popup in the first place.</p>
<p>I found a way to install them automatically when opening a workspace folder in VS Code.  With a <code>&quot;runOn&quot;: &quot;folderOpen&quot;</code> VS Code task, we can run some code when the workspace is opened.  Using a shell script, we can parse the <code>extensions.json</code> file and then run <code>code --install-extension</code> for each extension to install it.</p>
<p>In <code>.vscode/tasks.json</code>, you can create the install tasks like this:</p>

<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>​
  <span class="token property">"version"</span><span class="token operator">:</span> <span class="token string">"2.0.0"</span><span class="token punctuation">,</span>​
  <span class="token property">"tasks"</span><span class="token operator">:</span> <span class="token punctuation">[</span>​
    <span class="token punctuation">{</span>​
      <span class="token property">"label"</span><span class="token operator">:</span> <span class="token string">"Install All Recommended Extensions"</span><span class="token punctuation">,</span>​
      <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"shell"</span><span class="token punctuation">,</span>​
      <span class="token property">"windows"</span><span class="token operator">:</span> <span class="token punctuation">{</span>​
        <span class="token property">"command"</span><span class="token operator">:</span> <span class="token string">"echo 'Not Supported on Windows'"</span>​
      <span class="token punctuation">}</span><span class="token punctuation">,</span>​
      <span class="token property">"command"</span><span class="token operator">:</span> <span class="token string">"node -e \"console.log(JSON.parse(require('fs').readFileSync('./.vscode/extensions.json')).recommendations.join('\\n'))\" | xargs -L 1 code --install-extension"</span><span class="token punctuation">,</span>​
      <span class="token property">"runOptions"</span><span class="token operator">:</span> <span class="token punctuation">{</span>​
        <span class="token property">"runOn"</span><span class="token operator">:</span> <span class="token string">"folderOpen"</span>​
      <span class="token punctuation">}</span><span class="token punctuation">,</span>​
      <span class="token property">"presentation"</span><span class="token operator">:</span> <span class="token punctuation">{</span>​
        <span class="token property">"reveal"</span><span class="token operator">:</span> <span class="token string">"silent"</span>​
      <span class="token punctuation">}</span>​
    <span class="token punctuation">}</span>​
  <span class="token punctuation">]</span>​
<span class="token punctuation">}</span></code></pre>
<p>Note: The task assumes <code>node</code> (Node.js) and <code>code</code> CLI commands are available.  Instructions for installing the VS Code CLI <code>code</code> can be found <a href="https://code.visualstudio.com/docs/editor/command-line">here</a>.</p>
<p>With that task in place, extensions will be installed automatically!  It looks like this when you open the workspace folder in VS Code:
<video width="700" controls="controls" autoplay muted loop>
  <source src="/automatic-installation-of-recommended-vs-code-extensions/extension-recommendations.mp4" type="video/mp4">
</video></p>
]]>
      </description>
      <pubDate>Mon, 25 Sep 2023 00:00:00 +0000</pubDate>
      <link>https://www.geekytidbits.com/automatic-installation-of-recommended-vs-code-extensions/</link>
      <guid isPermaLink="true">https://www.geekytidbits.com/automatic-installation-of-recommended-vs-code-extensions/</guid>
    </item>
    <item>
      <title>Dealing with Burnout</title>
      <description>
        <![CDATA[<p>This year marks 20 years since I graduated from university and entered the real world as a software developer.  Throughout that time I have dealt with burnout many times and have learned a few things about working through it.</p>
<p>How do you even describe or define “burnout”, anyway?  For me, it’s when I feel like I simply cannot go on.  I am <em>exhausted</em> and either depressed or seemingly on the verge of depression.</p>
<p>According to <a href="https://www.webmd.com/mental-health/burnout-symptoms-signs">WebMD</a>:</p>
<blockquote>
<p>Burnout is a form of exhaustion caused by constantly feeling swamped. It’s a result of excessive and prolonged emotional, physical, and mental stress. In many cases, burnout is related to one’s job.</p>
</blockquote>
<p>There are times when I am simply unmotivated or feeling a bit passionless about my work but this doesn’t mean I am burned out.  Burnout is more of an intense feeling of <em>exhaustion</em> and it usually follows a very intense period of work; at least this is what I have experienced.</p>
<p>Here are four things I have learned about dealing with burnout:</p>
<ol>
<li>Don’t panic</li>
</ol>
<p>It’s temping to freak out when I start to get burned out.  “What does this mean?”  “I can’t go on…what am I going to do?”  The thing is, burnout passes so if you’re patient you’ll get through it.</p>
<ol start="2">
<li>Make a change</li>
</ol>
<p>Although my situation has many times been out my control I still have the control over other areas of my time and am able to make one or more changes.  Simply making a change in my daily routines and habits has helped me.  In the past I’ve implemented changes like exercising more, picking up a new hobby, avoiding television, taking a few days off and going on a trip.  Yes, sometimes changing jobs is the right change to make, especially if you are in an environment that is perpetually exhausting and demanding.</p>
<ol start="3">
<li>Do something you enjoy</li>
</ol>
<p>Making a change is good but doing something I really enjoy is important.  For me, it means picking up the guitar, going on nature walks, or spending more time with my family.</p>
<ol start="4">
<li>Be around people</li>
</ol>
<p>Forcing myself to be around people more has helped me in the past.  This one is not intuitive to me because my inclination is to isolate when I’m exhausted.  But, rubbing shoulders and socializing is helpful for me.</p>
<p>As a bit of an aside, my tell-tale sign that I am burned out is when I have a desire to quit my job and take up a manual labor job such as delivering packages or working in a warehouse.  There is nothing wrong with those respectable jobs, of course.  I don’t understand the psychology behind it but it’s my own indicator I need to act.</p>
]]>
      </description>
      <pubDate>Mon, 06 Feb 2023 00:00:00 +0000</pubDate>
      <link>https://www.geekytidbits.com/dealing-with-burnout/</link>
      <guid isPermaLink="true">https://www.geekytidbits.com/dealing-with-burnout/</guid>
    </item>
    <item>
      <title>Syndication Fetcher</title>
      <description>
        <![CDATA[<p>In the past few days I have been working on a tool to fetch new entries from RSS and Atom feeds and send them to myself via an email. I tried a few npm packages for fetching and parsing the feeds but never came across one that worked the way I wanted it to. So, I decided to write and publish my own.</p>
<p>The library package is named <code>syndication-fetcher</code>. The repository is located here: <a href="https://github.com/bradymholt/syndication-fetcher">https://github.com/bradymholt/syndication-fetcher</a> and it can be installed with <code>npm install syndication-fetcher</code>.</p>
<h3 id="usage-example">Usage example</h3>

<pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> <span class="token punctuation">{</span> fetchFeed <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"syndication-fetcher"</span><span class="token punctuation">;</span>​
<span class="token keyword">const</span> feed <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetchFeed</span><span class="token punctuation">(</span><span class="token string">"https://www.geekytidbits.com/rss.xml"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>​
​
<span class="token comment">/* `feed` object looks like this:​
{​
  title: string;​
  description: string;​
  link: string;​
  items: [​
    {​
      id: string;​
      title: string;​
      description: string;​
      link: string;​
      pubDate: Date | null;​
      content: string;​
    }​
  ]​
}​
*/</span></code></pre>
]]>
      </description>
      <pubDate>Tue, 31 Jan 2023 00:00:00 +0000</pubDate>
      <link>https://www.geekytidbits.com/syndication-fetcher/</link>
      <guid isPermaLink="true">https://www.geekytidbits.com/syndication-fetcher/</guid>
    </item>
    <item>
      <title>Playing with SvelteKit and Cloudflare Pages</title>
      <description>
        <![CDATA[<p>SvelteKit is a web framework I’ve been keeping my eye on. Sure, it builds upon Svelte, but there is a lot more to it. It uses <a href="https://vitejs.dev/config/">Vite</a> for build tooling, has a simple file-based routing system, and allows you to build “Transitional Web Apps”, as Rich Harris calls them in his <a href="https://www.youtube.com/watch?v=860d8usGC0o">Have Single-Page Apps Ruined the Web?</a> talk from Jamstack Conf 2021. I think at first glace it looks like a great framework for building web apps.</p>
<p>Since it just hit <a href="https://svelte.dev/blog/announcing-sveltekit-1.0">v1.0</a> I thought now would be a good time to play with it. I’ve also been learning more about Cloudflare Workers, Pages, KV, etc. and decided I would like to build a simple web app using SvelteKit and deploy it to Cloudflare Pages.</p>
<p>The result of my playing is simple app template that implements user authentication.  You can register a new user, login with a new registration, initiate a Forgot Password flow including sending a reset email.  You can see the GitHub repo here: <a href="https://github.com/bradymholt/sveltekit-auth-template">https://github.com/bradymholt/sveltekit-auth-template</a> and the live site here: <a href="https://sveltekit-auth-template.pages.dev/">https://sveltekit-auth-template.pages.dev/</a>.</p>
<p><img src="./template.png" alt="SvelteKit Auth Template"></p>
<p>Things I learned and worked with while building this template:</p>
<ul>
<li>How to create JWTs on Cloudflare Workers</li>
<li>How to use <a href="https://developers.cloudflare.com/workers/learning/how-kv-works/">KV</a>, Cloudflare’s key value store</li>
<li>Sending emails with <a href="https://blog.cloudflare.com/sending-email-from-workers-with-mailchannels/">MailChannels</a></li>
<li>Validation with <a href="https://github.com/colinhacks/zod">zod</a></li>
<li>TailwindCSS - although I initially used Tailwind I changed my mind and removed it.  But, I took it for a spin and learned a bit about it.</li>
<li>Self-hosting web fonts</li>
<li>PostCSS - I haven’t done much with PostCSS before but I was able to play with it and in particular, use the “postcss-nested” plugin to get nested CSS working without having to use Sass or Less.</li>
</ul>
]]>
      </description>
      <pubDate>Tue, 03 Jan 2023 00:00:00 +0000</pubDate>
      <link>https://www.geekytidbits.com/playing-with-sveltekit-on-cloudflare-pages/</link>
      <guid isPermaLink="true">https://www.geekytidbits.com/playing-with-sveltekit-on-cloudflare-pages/</guid>
    </item>
    <item>
      <title>Daily WTF JavaScript</title>
      <description>
        <![CDATA[<p>I have been itching to build something recently.  In the not so distant past when I’ve had this desire I ended up building a project template as a way to learn some new things and create a scaffold for a project I might tackle in the future.  But, this time I decided I want to build something that I could actually publish.</p>
<p>What I ended up building is silly and trivial but it was fun: <a href="https://dailywtfjs.geekytidbits.com/">Daily WTF JavaScript</a> - a site that displays a daily rotating snippet of JavaScript that makes you say “WTF!”.  Yes, there are no shortage of examples.</p>
<p><img src="/daily-wtf-javascript/wtfjs.png" alt="Daily WTF JavaScript"></p>
<p>As always when building things, I learned plenty:</p>
<ul>
<li><a href="https://vitejs.dev/">Vite</a> - I used Vite as a build tool and wow was I impressed.  This felt so much better than using webpack or one of its derivatives.</li>
<li><code>place-items: center;</code> for centering an HTML element horizontally and vertically</li>
<li>GitHub Actions with GitHub Pages - My <a href="https://github.com/bradymholt/daily-wtf-js/blob/main/.github/workflows/gh-pages.yml">workflow</a> to publish the site to GitHub Pages uses GitHub Actions, which is the new generation method for working with pages.  I can now use this template as a baseline for other projects.</li>
<li>Although I’ve used <a href="https://github.com/jsdom/jsdom">jsdom</a> before, it’s been awhile and I was able to reintroduce myself to its usefulness to parse HTML on the server side.</li>
<li>I was exposed to the wonderful world of <a href="https://cssgradient.io/swatches/">CSS Gradient Swatches</a>.</li>
<li>I learned more about <a href="https://postcss.org/">PostCSS</a> and want to continue learning and using it further.</li>
</ul>
]]>
      </description>
      <pubDate>Thu, 15 Dec 2022 00:00:00 +0000</pubDate>
      <link>https://www.geekytidbits.com/daily-wtf-javascript/</link>
      <guid isPermaLink="true">https://www.geekytidbits.com/daily-wtf-javascript/</guid>
    </item>
    <item>
      <title>I switched to an iPhone</title>
      <description>
        <![CDATA[<p>For the past 10 years I’ve been an Android user.  And, I’ve exclusively had Google phones, starting with the <a href="https://en.wikipedia.org/wiki/Nexus_5">Nexus 5</a> and most recently the <a href="https://en.wikipedia.org/wiki/Pixel_3">Pixel 3</a>.  Before that, I had an <a href="https://en.wikipedia.org/wiki/IPhone_4">iPhone 4</a>.  But, I just switched back and now have a shiny new iPhone 13 mini.</p>
<div style="text-align: center;">
  <img src="/i-switched-to-an-iphone/iphone_13_mini.jpg" alt="iPhone 13 mini"/>
</div>

<p>So why did I switch?  It came down to 3 reasons:</p>
<ol>
<li>The size.  I like the smaller size and I can’t find a new, flagship Android phone in this size.</li>
<li>The app quality.  I like the Android operating system and think Google has really refined it.  But, the quality of the Google Play store apps are, in general, not as good as their iPhone counterparts.  It’s sad but I think it’s undeniably true.</li>
<li>I wanted to try something new.  <code>¯\_(ツ)_/¯</code></li>
</ol>
<h3 id="things-i-like">Things I Like</h3>
<ul>
<li>Using the Google ecosystem (apps, services) on the iPhone is very feasible.  One of my biggest hesitations in switching to iOS was the ease of continuing use of the Google ecosystem and I was not disappointed.  The only annoyance I’ve found in this area is that iOS defaults opening address and web links in Apple Maps and Safari.  But, it’s not as obtrusive as I expected.</li>
<li>The quality of the apps.  This was one of the reasons I switched and after using a few of my favorite apps I see a noticeable quality difference.  It’s obvious many companies prioritize the quality of their iOS apps.  Some examples: 1Password, Tablo TV, Zillow.  In particular, the experience using 1Password on iOS is significantly better (reliable and seamless autofill, Face ID speed) than Android and since I use it so often this is no small factor.</li>
<li>The Shortcuts app - I’m a sucker for automation and <a href="https://apps.apple.com/us/app/shortcuts/id915249334">Shortcuts</a> is a powerful, simple, and fun app.</li>
<li>Widgets - I always enjoyed Android home screen widgets but in recent years it appears they have fallen out of interest of app developers.  iOS has widgets and they are very polished and useful.  The Weather and Google Calendar widgets are two of my favorites (so far…).</li>
<li>The ability to enable “Do Not Disturb” mode “Until I Leave” a location.</li>
<li>macOS interoperability:<ul>
<li>Universal Clipboard - I love being able to copy text on my MacBook and paste it on my iPhone (and vise-versa)! This is very useful.</li>
<li>Calls and Messages - it is very convenient to be able to make / receive phone calls and send / receive messages on my Mac without having to pick up my phone, while sitting at my desk.  Granted, Google has “Messages for Web” that allows you to send / receive messages from a web app but this is only for messages and its <em>pairing</em> mechanism is finicky sometimes.</li>
</ul>
</li>
</ul>
<h3 id="things-i-dont-like">Things I Don’t Like</h3>
<ul>
<li>Voice Dictation - Voice Dictation on iOS is not up to par with Android.  Android’s voice dictation is much faster and more accurate.  I have come to rely upon it heavily, especially for texting.</li>
<li>Google Chrome interface - I don’t like the interface Chrome has on iOS.  Undoubtedly, Google is trying to make its interface more iOS-like, but I prefer the layout on Android with tabs and navigation buttons on the top.</li>
<li>Physical button location - Maybe it’s because I’m left handed but I very much prefer to have the power button on the left side of the phone and the volume buttons on the right side and on the iPhone 13, it is exactly the opposite of this.</li>
<li>Lightening adapter - Ugh, need I say more?  I had to buy a bunch of adapters which was not fun.</li>
<li>The inability to schedule text messages - I love this feature on Android and it simply doesn’t exist on iOS.</li>
</ul>
<p>With all things considered, I am very happy and glad I made the switch!</p>
]]>
      </description>
      <pubDate>Mon, 12 Dec 2022 00:00:00 +0000</pubDate>
      <link>https://www.geekytidbits.com/i-switched-to-an-iphone/</link>
      <guid isPermaLink="true">https://www.geekytidbits.com/i-switched-to-an-iphone/</guid>
    </item>
  </channel>
</rss>
