<?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: Mukhtar</title>
    <description>The latest articles on DEV Community by Mukhtar (@mukhtar_onif).</description>
    <link>https://dev.to/mukhtar_onif</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%2F595040%2F8151d7ee-efb8-44be-adbf-e2bb1bc1d460.JPG</url>
      <title>DEV Community: Mukhtar</title>
      <link>https://dev.to/mukhtar_onif</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mukhtar_onif"/>
    <language>en</language>
    <item>
      <title>Turn Any API Into a SQL Database</title>
      <dc:creator>Mukhtar</dc:creator>
      <pubDate>Wed, 20 May 2026 20:29:36 +0000</pubDate>
      <link>https://dev.to/mukhtar_onif/turn-any-api-into-a-sql-database-5dg0</link>
      <guid>https://dev.to/mukhtar_onif/turn-any-api-into-a-sql-database-5dg0</guid>
      <description>&lt;p&gt;&lt;em&gt;GitHub, Jira, Salesforce, Stripe—600+ sources → SQLite&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The 2-Minute Version:&lt;/strong&gt;&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;# Install surveilr&lt;/span&gt;
brew tap surveilr/tap &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; brew &lt;span class="nb"&gt;install &lt;/span&gt;surveilr

&lt;span class="c"&gt;# Create a Singer tap script (see below for example)&lt;/span&gt;
&lt;span class="c"&gt;# then ingest it&lt;/span&gt;
surveilr admin init &lt;span class="nt"&gt;-d&lt;/span&gt; project.db
surveilr ingest files &lt;span class="nt"&gt;-r&lt;/span&gt; ./github.surveilr[singer].py &lt;span class="nt"&gt;-d&lt;/span&gt; project.db
surveilr orchestrate adapt-singer &lt;span class="nt"&gt;-d&lt;/span&gt; project.db &lt;span class="nt"&gt;--stream-prefix&lt;/span&gt; github_

&lt;span class="c"&gt;# Query it&lt;/span&gt;
surveilr shell &lt;span class="nt"&gt;-d&lt;/span&gt; project.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Query GitHub commits from the last 7 days&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;github_commits&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'now'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'-7 days'&lt;/span&gt;&lt;span class="p"&gt;)&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;timestamp&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your API data is now SQL. Query it like any other database.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem with API Data
&lt;/h2&gt;

&lt;p&gt;You've got project data scattered across platforms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt; — Issues, commits, pull requests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jira&lt;/strong&gt; — Tickets, sprints, workflows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitLab&lt;/strong&gt; — Merge requests, pipelines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Salesforce&lt;/strong&gt; — CRM data, opportunities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stripe&lt;/strong&gt; — Payments, subscriptions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each has its own API. Each has rate limits. Each has different authentication.&lt;/p&gt;

&lt;p&gt;And you need to answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Show me all commits related to high-priority Jira tickets"&lt;/li&gt;
&lt;li&gt;"Which GitHub issues have no linked pull requests?"&lt;/li&gt;
&lt;li&gt;"Track deployment frequency over time across all repos"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The usual approach:&lt;/strong&gt;&lt;br&gt;
Write custom scripts for each API, export to JSON/CSV, wrangle in pandas, hope nothing breaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The surveilr approach:&lt;/strong&gt;&lt;br&gt;
Turn everything into &lt;strong&gt;standard SQLite tables&lt;/strong&gt; you can query with SQL—forever.&lt;/p&gt;


&lt;h2&gt;
  
  
  What surveilr Does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://surveilr.com" rel="noopener noreferrer"&gt;surveilr&lt;/a&gt; ingests data from APIs using &lt;a href="https://hub.meltano.com/" rel="noopener noreferrer"&gt;Singer taps&lt;/a&gt;—Python scripts that follow the Singer protocol.&lt;/p&gt;

&lt;p&gt;There are &lt;strong&gt;600+ pre-built Singer taps&lt;/strong&gt; for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub, GitLab, Bitbucket&lt;/li&gt;
&lt;li&gt;Jira, Linear, Asana&lt;/li&gt;
&lt;li&gt;Salesforce, HubSpot, Zendesk&lt;/li&gt;
&lt;li&gt;Stripe, Shopify, QuickBooks&lt;/li&gt;
&lt;li&gt;Postgres, MySQL, MongoDB&lt;/li&gt;
&lt;li&gt;Google Analytics, Mixpanel&lt;/li&gt;
&lt;li&gt;...and hundreds more&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;surveilr executes these taps and transforms their output into &lt;strong&gt;queryable SQL tables&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  How It Works: Singer Taps
&lt;/h2&gt;

&lt;p&gt;Singer taps are Python scripts that output JSONL (JSON Lines):&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="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;"SCHEMA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"stream"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"commits"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"schema"&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="err"&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;"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;"RECORD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"stream"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"commits"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"record"&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;"sha"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&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;"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;"RECORD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"stream"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"commits"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"record"&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;"sha"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"def456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&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;"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;"STATE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&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;"bookmark"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-05-12"&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;p&gt;surveilr reads this output and creates SQL tables automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You don't write SQL schemas.&lt;/strong&gt; surveilr infers them from the Singer output.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Find or Create a Singer Tap
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option 1: Use an Existing Tap
&lt;/h3&gt;

&lt;p&gt;Browse &lt;a href="https://hub.meltano.com/" rel="noopener noreferrer"&gt;Singer Hub&lt;/a&gt; for 600+ taps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tap-github&lt;/code&gt; — GitHub repos, issues, commits, PRs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tap-gitlab&lt;/code&gt; — GitLab projects, merge requests, pipelines&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tap-jira&lt;/code&gt; — Jira issues, sprints, users&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tap-postgres&lt;/code&gt; — Extract from Postgres databases&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tap-stripe&lt;/code&gt; — Payments, customers, subscriptions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Install with pip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;tap-github
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option 2: Write Your Own
&lt;/h3&gt;

&lt;p&gt;Singer taps are just Python scripts. Here's a minimal example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="c1"&gt;# Print schema
&lt;/span&gt;&lt;span class="nf"&gt;print&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="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SCHEMA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;users&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;integer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}}}&lt;/span&gt;
&lt;span class="p"&gt;}))&lt;/span&gt;

&lt;span class="c1"&gt;# Print records
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;fetch_users_from_api&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;print&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="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RECORD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;users&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;record&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&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;That's it. surveilr handles the rest.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Create a Capturable Executable
&lt;/h2&gt;

&lt;p&gt;surveilr runs Singer taps as &lt;strong&gt;capturable executables&lt;/strong&gt;—scripts with a special naming pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;name&amp;gt;.surveilr[singer].&amp;lt;extension&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;github.surveilr[singer].py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jira.surveilr[singer].sh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stripe.surveilr[singer].js&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;[singer]&lt;/code&gt; marker tells surveilr: "This script outputs Singer format."&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: GitHub Tap Script
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="c1"&gt;# Configuration
&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GITHUB_ACCESS_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repository&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GITHUB_REPOSITORY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# e.g., "owner/repo"
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GITHUB_START_DATE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2024-01-01T00:00:00Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Run the Singer tap
&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tap-github&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--config&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save as &lt;code&gt;github.surveilr[singer].py&lt;/code&gt; and make it executable:&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="nb"&gt;chmod&lt;/span&gt; +x github.surveilr[singer].py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3: Set Environment Variables
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GITHUB_ACCESS_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ghp_yourtoken"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GITHUB_REPOSITORY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"microsoft/vscode"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GITHUB_START_DATE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"2024-01-01T00:00:00Z"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 4: Initialize Database
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr admin init &lt;span class="nt"&gt;-d&lt;/span&gt; project.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Standard SQLite database.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Ingest the Singer Tap
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr ingest files &lt;span class="nt"&gt;-r&lt;/span&gt; ./github.surveilr[singer].py &lt;span class="nt"&gt;-d&lt;/span&gt; project.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;surveilr:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Executes the script&lt;/li&gt;
&lt;li&gt;Reads the Singer JSONL output&lt;/li&gt;
&lt;li&gt;Stores raw data in &lt;code&gt;uniform_resource&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Step 6: Transform to SQL Views
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr orchestrate adapt-singer &lt;span class="nt"&gt;-d&lt;/span&gt; project.db &lt;span class="nt"&gt;--stream-prefix&lt;/span&gt; github_
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates SQL views like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;github_commits&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;github_issues&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;github_pull_requests&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;github_users&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now you can query them with standard SQL.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7: Query Your Data
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr shell &lt;span class="nt"&gt;-d&lt;/span&gt; project.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Find all commits from the last 7 days
&lt;/h3&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;commit_sha&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;timestamp&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;github_commits&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'now'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'-7 days'&lt;/span&gt;&lt;span class="p"&gt;)&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;timestamp&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Track commit activity by author
&lt;/h3&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;author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&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;commits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&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;first_commit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&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;last_commit&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;github_commits&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;commits&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Find open issues without pull requests
&lt;/h3&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;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&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;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;github_issues&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;github_pull_requests&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;issue_number&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'open'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cross-platform query: Jira issues + GitHub commits
&lt;/h3&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;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;jira_ticket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commit_sha&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;jira_issues&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;github_commits&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'%'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Done'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is impossible with platform APIs alone.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Not Just Use API Clients?
&lt;/h2&gt;

&lt;p&gt;You could write Python scripts with &lt;code&gt;requests&lt;/code&gt; or use platform SDKs.&lt;/p&gt;

&lt;p&gt;But:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API clients don't give you SQL.&lt;/strong&gt;&lt;br&gt;
You're stuck with JSON responses you have to parse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;surveilr outputs queryable tables.&lt;/strong&gt;&lt;br&gt;
Standard SQL from day one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;APIs have rate limits.&lt;/strong&gt;&lt;br&gt;
Hitting them repeatedly during analysis is painful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;surveilr stores data locally.&lt;/strong&gt;&lt;br&gt;
Query as much as you want, no API calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom scripts don't compose.&lt;/strong&gt;&lt;br&gt;
How do you join GitHub data with Jira data in your script?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;surveilr stores everything in one database.&lt;/strong&gt;&lt;br&gt;
Cross-platform joins just work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scripts don't track history.&lt;/strong&gt;&lt;br&gt;
You get a snapshot, then it's gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;surveilr supports incremental updates.&lt;/strong&gt;&lt;br&gt;
Singer taps use STATE messages to bookmark progress.&lt;/p&gt;


&lt;h2&gt;
  
  
  Forensic Project Analysis
&lt;/h2&gt;

&lt;p&gt;Once your project data is queryable, you can investigate patterns:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Find commits made during incident windows&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;commit_sha&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;github_commits&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="s1"&gt;'2024-05-10 14:00'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="s1"&gt;'2024-05-10 16:00'&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;timestamp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Track issue resolution time&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;SELECT&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;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;julianday&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolved_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;julianday&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&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;days_to_resolve&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;jira_issues&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Done'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;days_to_resolve&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Find high-priority issues that took too long&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;SELECT&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;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;resolved_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;jira_issues&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'High'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Critical'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;julianday&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolved_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;julianday&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These insights are buried in platform UIs. SQL makes them visible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-World Uses
&lt;/h2&gt;

&lt;h3&gt;
  
  
  For Developers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Track your own commit patterns&lt;/li&gt;
&lt;li&gt;Find issues assigned to you across platforms&lt;/li&gt;
&lt;li&gt;Identify repos with low test coverage&lt;/li&gt;
&lt;li&gt;Build custom dashboards with Datasette&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For Engineering Managers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Calculate team velocity over time&lt;/li&gt;
&lt;li&gt;Track deployment frequency&lt;/li&gt;
&lt;li&gt;Analyze code review turnaround&lt;/li&gt;
&lt;li&gt;Identify process bottlenecks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For Product Teams
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Link customer requests to shipped features&lt;/li&gt;
&lt;li&gt;Track feature adoption via API usage&lt;/li&gt;
&lt;li&gt;Measure time from idea to deployment&lt;/li&gt;
&lt;li&gt;Build product analytics pipelines&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For Compliance (Oh, By the Way)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SOC 2:&lt;/strong&gt; Show complete change management history&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change Control:&lt;/strong&gt; Link every production change to an approved ticket&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit Trails:&lt;/strong&gt; Provide queryable evidence of who changed what when&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separation of Duties:&lt;/strong&gt; Prove different people committed vs. approved&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the real value is &lt;strong&gt;permanent, queryable operational data&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Combine Data Sources
&lt;/h2&gt;

&lt;p&gt;The power comes from &lt;strong&gt;joining data across platforms&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub + Jira: Find unlinked work
&lt;/h3&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="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commit_sha&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;github_commits&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;jira_issues&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;key&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="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Stripe + Salesforce: Match payments to deals
&lt;/h3&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opportunity_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;payment_date&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;salesforce_opportunities&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;stripe_payments&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stripe_customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'succeeded'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is why SQLite matters—everything is in one database.&lt;/p&gt;




&lt;h2&gt;
  
  
  Automate Daily Syncs
&lt;/h2&gt;

&lt;p&gt;Create a script to keep your database up to date:&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;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# daily-project-sync.sh&lt;/span&gt;

&lt;span class="nv"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/secure/project-tracking.db"&lt;/span&gt;

&lt;span class="c"&gt;# Ingest GitHub&lt;/span&gt;
surveilr ingest files &lt;span class="nt"&gt;-r&lt;/span&gt; ./github.surveilr[singer].py &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DB&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Ingest Jira&lt;/span&gt;
surveilr ingest files &lt;span class="nt"&gt;-r&lt;/span&gt; ./jira.surveilr[singer].py &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DB&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Transform to views&lt;/span&gt;
surveilr orchestrate adapt-singer &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DB&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--stream-prefix&lt;/span&gt; github_
surveilr orchestrate adapt-singer &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DB&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--stream-prefix&lt;/span&gt; jira_

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✓ Project data sync completed: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it daily, and your database accumulates history automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Open the Database in Other Tools
&lt;/h2&gt;

&lt;p&gt;Because it's SQLite, you can use &lt;strong&gt;any SQLite tool&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Datasette&lt;/strong&gt; — Instant web UI for your project data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metabase&lt;/strong&gt; — Build team dashboards&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DuckDB&lt;/strong&gt; — Fast analytics on SQLite files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python pandas&lt;/strong&gt; — &lt;code&gt;pd.read_sql("SELECT * FROM github_commits", conn)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grafana&lt;/strong&gt; — Visualize metrics over time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observable&lt;/strong&gt; — Create interactive notebooks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You own the database. Analyze it however you want.&lt;/p&gt;




&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tap script not executing?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make sure it's executable: &lt;code&gt;chmod +x script.surveilr[singer].py&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Check that it outputs valid JSONL to stdout&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;No tables created after adapt-singer?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verify the stream prefix matches: &lt;code&gt;--stream-prefix github_&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Check that the Singer tap output SCHEMA messages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;"Module not found" errors?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install the Singer tap: &lt;code&gt;pip install tap-github&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Make sure it's in your PATH&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;You just turned GitHub, Jira, or other APIs into queryable SQL tables.&lt;/p&gt;

&lt;p&gt;Now combine with other data sources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/mukhtar_onif/turn-your-file-system-into-a-sql-database-57jb"&gt;Query Your File System&lt;/a&gt;&lt;/strong&gt; — Join commits with file changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/mukhtar_onif/turn-your-email-into-a-sql-database-1noa"&gt;Turn Email Into SQL&lt;/a&gt;&lt;/strong&gt; — Correlate issues with email threads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Or explore more:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://surveilr.com/" rel="noopener noreferrer"&gt;surveilr.com&lt;/a&gt;&lt;/strong&gt; — Full documentation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://hub.meltano.com/" rel="noopener noreferrer"&gt;Singer Hub&lt;/a&gt;&lt;/strong&gt; — 600+ pre-built taps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://hub.meltano.com/singer/spec/" rel="noopener noreferrer"&gt;Singer Spec&lt;/a&gt;&lt;/strong&gt; — Build custom taps&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;API data shouldn't be trapped in JSON responses.&lt;/p&gt;

&lt;p&gt;Platforms give you REST APIs and pagination. That's it.&lt;/p&gt;

&lt;p&gt;surveilr gives you &lt;strong&gt;SQL&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Want to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What changed in your codebase last quarter?&lt;/li&gt;
&lt;li&gt;Which issues took longest to resolve?&lt;/li&gt;
&lt;li&gt;How deployment frequency correlates with bug reports?&lt;/li&gt;
&lt;li&gt;Which team members are most active?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just write a query.&lt;/p&gt;

&lt;p&gt;Your API data is now a &lt;strong&gt;SQLite database you own forever&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;No API rate limits. No vendor lock-in. Just standard SQL.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Ready to query your project data? Install surveilr and ingest your first API.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://surveilr.com/" rel="noopener noreferrer"&gt;Get Started →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>database</category>
      <category>sql</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Turn Your Email Into a SQL Database</title>
      <dc:creator>Mukhtar</dc:creator>
      <pubDate>Tue, 19 May 2026 16:56:44 +0000</pubDate>
      <link>https://dev.to/mukhtar_onif/turn-your-email-into-a-sql-database-1noa</link>
      <guid>https://dev.to/mukhtar_onif/turn-your-email-into-a-sql-database-1noa</guid>
      <description>&lt;p&gt;&lt;em&gt;Your inbox is data. Query it like data.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The 2-Minute Version:&lt;/strong&gt;&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;# Install and ingest your Gmail&lt;/span&gt;
brew tap surveilr/tap &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; brew &lt;span class="nb"&gt;install &lt;/span&gt;surveilr
surveilr admin init &lt;span class="nt"&gt;-d&lt;/span&gt; email.db
surveilr ingest imap &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; you@gmail.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"your-app-password"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-a&lt;/span&gt; imap.gmail.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; email.db

&lt;span class="c"&gt;# Query it&lt;/span&gt;
surveilr shell &lt;span class="nt"&gt;-d&lt;/span&gt; email.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Find every email from a specific sender&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;subject&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="nv"&gt;"from"&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;emails&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nv"&gt;"from"&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%@vendor.com%'&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="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your inbox is now a SQL database. Query it like any other data.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem with Email Search
&lt;/h2&gt;

&lt;p&gt;Gmail and Outlook have search boxes. They work for simple queries.&lt;/p&gt;

&lt;p&gt;But try this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Show me all emails from Q1 2024 where attachments were over 10MB"&lt;/li&gt;
&lt;li&gt;"Find email threads that mention both 'invoice' and 'overdue'"&lt;/li&gt;
&lt;li&gt;"List all external recipients I've emailed in the last 6 months"&lt;/li&gt;
&lt;li&gt;"Which emails have attachments but no reply?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most email clients can't answer these questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here's why:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Email search is designed for &lt;strong&gt;finding messages&lt;/strong&gt;, not &lt;strong&gt;analyzing communication patterns&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;surveilr turns your inbox into a &lt;strong&gt;SQLite database&lt;/strong&gt; where these queries become trivial.&lt;/p&gt;




&lt;h2&gt;
  
  
  What surveilr Does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://surveilr.com" rel="noopener noreferrer"&gt;surveilr&lt;/a&gt; connects to any IMAP server and extracts everything into SQL tables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;From, To, CC, BCC&lt;/li&gt;
&lt;li&gt;Subject lines and dates&lt;/li&gt;
&lt;li&gt;Message content and headers&lt;/li&gt;
&lt;li&gt;Attachments (with optional extraction)&lt;/li&gt;
&lt;li&gt;Thread relationships&lt;/li&gt;
&lt;li&gt;Folder structure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The output is &lt;strong&gt;standard SQLite&lt;/strong&gt;. Use any SQL tool you want.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Get an App Password
&lt;/h2&gt;

&lt;h3&gt;
  
  
  For Gmail:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://myaccount.google.com/apppasswords" rel="noopener noreferrer"&gt;Google Account → App Passwords&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Generate an app password&lt;/li&gt;
&lt;li&gt;Copy it (you'll use this instead of your regular password)&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Step 2: Create a Database
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr admin init &lt;span class="nt"&gt;-d&lt;/span&gt; email.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Standard SQLite file. Nothing fancy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Ingest Your Email
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr ingest imap &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; email.db &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; your.email@gmail.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"your-app-password"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-a&lt;/span&gt; imap.gmail.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"INBOX"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;surveilr connects via IMAP and inserts messages into &lt;code&gt;ur_ingest_session_imap_acct_folder_message&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Useful flags:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Filter by status:&lt;/strong&gt;&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="nt"&gt;--status&lt;/span&gt; unread    &lt;span class="c"&gt;# Only unread messages&lt;/span&gt;
&lt;span class="nt"&gt;--status&lt;/span&gt; starred   &lt;span class="c"&gt;# Starred/flagged&lt;/span&gt;
&lt;span class="nt"&gt;--status&lt;/span&gt; all       &lt;span class="c"&gt;# Everything (default)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Extract attachments:&lt;/strong&gt;&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="nt"&gt;--extract-attachments&lt;/span&gt; uniform-resource  &lt;span class="c"&gt;# Store in database&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Show progress:&lt;/strong&gt;&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="nt"&gt;--progress&lt;/span&gt;  &lt;span class="c"&gt;# Display download progress&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Limit messages:&lt;/strong&gt;&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="nt"&gt;-b&lt;/span&gt; 1000  &lt;span class="c"&gt;# Process up to 1000 messages&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 4: Query Your Inbox
&lt;/h2&gt;

&lt;p&gt;Open the SQL shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr shell &lt;span class="nt"&gt;-d&lt;/span&gt; email.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can ask questions that Gmail search can't answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Find all emails from a specific domain
&lt;/h3&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="nv"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;date&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ur_ingest_session_imap_acct_folder_message&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nv"&gt;"from"&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%@vendor.com%'&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="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: &lt;code&gt;from&lt;/code&gt; is quoted because it's a SQL keyword.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Find emails with large attachments
&lt;/h3&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;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;m&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;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;attachment_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size_bytes&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;size_mb&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ur_ingest_session_imap_acct_folder_message&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;ur_ingest_session_attachment&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size_bytes&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10485760&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size_bytes&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Track email volume over time
&lt;/h3&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="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&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;emails&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ur_ingest_session_imap_acct_folder_message&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'now'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'-30 days'&lt;/span&gt;&lt;span class="p"&gt;)&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;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Find emails you sent but never got a reply to
&lt;/h3&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;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;date&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ur_ingest_session_imap_acct_folder_message&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nv"&gt;"from"&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%you@company.com%'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;message_id&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&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;in_reply_to&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ur_ingest_session_imap_acct_folder_message&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;in_reply_to&lt;/span&gt; &lt;span class="k"&gt;IS&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="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="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These queries are impossible in Gmail.&lt;/p&gt;




&lt;h2&gt;
  
  
  List Available Folders
&lt;/h2&gt;

&lt;p&gt;Not sure what folders you have?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr ingest imap &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; your.email@gmail.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"your-app-password"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-a&lt;/span&gt; imap.gmail.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--list-folders&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ingest specific folders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr ingest imap &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; your.email@gmail.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"your-app-password"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-a&lt;/span&gt; imap.gmail.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"[Gmail]/Sent Mail"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; email.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Export Results
&lt;/h2&gt;

&lt;p&gt;Need to share findings? Export as JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr shell &lt;span class="nt"&gt;-d&lt;/span&gt; email.db &lt;span class="nt"&gt;--cmd&lt;/span&gt; &lt;span class="s2"&gt;"
SELECT subject, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;from&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, date
FROM ur_ingest_session_imap_acct_folder_message
WHERE subject LIKE '%invoice%'
"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; invoices.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or CSV for spreadsheet analysis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr shell &lt;span class="nt"&gt;-d&lt;/span&gt; email.db &lt;span class="nt"&gt;--cmd&lt;/span&gt; &lt;span class="s2"&gt;"
SELECT * FROM ur_ingest_session_imap_acct_folder_message
"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; csv &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; all-emails.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why Not Just Export to CSV?
&lt;/h2&gt;

&lt;p&gt;You could export your inbox to CSV or use a backup tool.&lt;/p&gt;

&lt;p&gt;But:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exports are point-in-time snapshots.&lt;/strong&gt;&lt;br&gt;
You can't incrementally update them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;surveilr supports continuous ingestion.&lt;/strong&gt;&lt;br&gt;
Run it weekly and accumulate history over time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CSVs don't preserve relationships.&lt;/strong&gt;&lt;br&gt;
How do you link attachments to messages?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;surveilr uses proper foreign keys.&lt;/strong&gt;&lt;br&gt;
Attachments reference their parent messages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CSVs are hard to query.&lt;/strong&gt;&lt;br&gt;
You're back to scripting with pandas or awk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;surveilr gives you SQL.&lt;/strong&gt;&lt;br&gt;
Join, filter, aggregate—all standard SQL.&lt;/p&gt;


&lt;h2&gt;
  
  
  Forensic Email Analysis
&lt;/h2&gt;

&lt;p&gt;Once your inbox is queryable, you can investigate patterns:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Find emails that mention specific keywords across years&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;emails&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%confidential%'&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%confidential%'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2020-01-01'&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="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Track who you email most frequently&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;SELECT&lt;/span&gt;
    &lt;span class="nv"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&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;email_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;emails&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nv"&gt;"from"&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%you@company.com%'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="nv"&gt;"to"&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;email_count&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Find emails sent on weekends or late at night&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;emails&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%w'&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="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'6'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;-- Sunday or Saturday&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%H'&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="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%H'&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="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;6&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="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These insights are buried in your inbox. SQL makes them visible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-World Uses
&lt;/h2&gt;

&lt;h3&gt;
  
  
  For Developers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Track bug reports sent via email&lt;/li&gt;
&lt;li&gt;Find API key or credential leaks in sent mail&lt;/li&gt;
&lt;li&gt;Analyze support ticket volume&lt;/li&gt;
&lt;li&gt;Search across years of technical discussions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For Operations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Audit external communications&lt;/li&gt;
&lt;li&gt;Track alert emails and response times&lt;/li&gt;
&lt;li&gt;Find recurring issues mentioned in email&lt;/li&gt;
&lt;li&gt;Monitor vendor correspondence&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For Legal/E-Discovery
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Respond to discovery requests with SQL queries&lt;/li&gt;
&lt;li&gt;Prove communication timelines&lt;/li&gt;
&lt;li&gt;Find all emails related to a specific matter&lt;/li&gt;
&lt;li&gt;Extract conversations by date range&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For Compliance (Oh, By the Way)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HIPAA:&lt;/strong&gt; Track communications containing PHI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SOX:&lt;/strong&gt; Maintain 7-year email records with queryable evidence&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GDPR:&lt;/strong&gt; Respond to "right to access" requests with SQL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;E-Discovery:&lt;/strong&gt; Legal hold and litigation support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the real value is &lt;strong&gt;permanent, queryable communication history&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Open the Database in Other Tools
&lt;/h2&gt;

&lt;p&gt;Because it's SQLite, you can use &lt;strong&gt;any SQLite tool&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DB Browser for SQLite&lt;/strong&gt; — Visual inspection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Datasette&lt;/strong&gt; — Instant web UI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python pandas&lt;/strong&gt; — &lt;code&gt;pd.read_sql("SELECT * FROM emails", conn)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DuckDB&lt;/strong&gt; — Fast analytical queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metabase&lt;/strong&gt; — Build dashboards&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;R / tidyverse&lt;/strong&gt; — Data analysis workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You own the database. Query it however you want.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security Best Practices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Use App Passwords, Not Your Main Password
&lt;/h3&gt;

&lt;p&gt;Never put your main email password in scripts. Use app-specific passwords that you can revoke.&lt;/p&gt;

&lt;h3&gt;
  
  
  Store Passwords Securely
&lt;/h3&gt;

&lt;p&gt;Use environment variables:&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="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;IMAP_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-app-password"&lt;/span&gt;
surveilr ingest imap &lt;span class="nt"&gt;-u&lt;/span&gt; you@gmail.com &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMAP_PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; imap.gmail.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use a password manager's CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr ingest imap &lt;span class="nt"&gt;-u&lt;/span&gt; you@gmail.com &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;pass show gmail/app-password&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; imap.gmail.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Encrypt the Database
&lt;/h3&gt;

&lt;p&gt;SQLite databases can be encrypted. Use tools like SQLCipher if you need encryption at rest.&lt;/p&gt;




&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Authentication failed"?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make sure you're using an app password, not your main password&lt;/li&gt;
&lt;li&gt;Verify IMAP is enabled in your email settings&lt;/li&gt;
&lt;li&gt;Check that 2FA isn't blocking IMAP access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;"Connection timed out"?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verify the IMAP server address&lt;/li&gt;
&lt;li&gt;Check if your firewall blocks port 993&lt;/li&gt;
&lt;li&gt;Some providers require enabling "less secure app access"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;"No messages found"?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;--list-folders&lt;/code&gt; to see available folders&lt;/li&gt;
&lt;li&gt;Folder names are case-sensitive&lt;/li&gt;
&lt;li&gt;Gmail uses &lt;code&gt;[Gmail]/All Mail&lt;/code&gt; not &lt;code&gt;All Mail&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;You just turned your inbox into a queryable database.&lt;/p&gt;

&lt;p&gt;Now combine it with other data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/mukhtar_onif/turn-your-file-system-into-a-sql-database-57jb"&gt;Query Your File System&lt;/a&gt;&lt;/strong&gt; — Join emails with file metadata&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/mukhtar_onif/turn-any-api-into-a-sql-database-5dg0"&gt;Extract API Data&lt;/a&gt;&lt;/strong&gt; — Add GitHub, Jira, or other sources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Or explore more:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://surveilr.com/" rel="noopener noreferrer"&gt;surveilr.com&lt;/a&gt;&lt;/strong&gt; — Full documentation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/surveilr/packages#installation" rel="noopener noreferrer"&gt;Installation Guide&lt;/a&gt;&lt;/strong&gt; — Other platforms&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Your inbox shouldn't be a black box.&lt;/p&gt;

&lt;p&gt;Email clients give you search bars and folders. That's it.&lt;/p&gt;

&lt;p&gt;surveilr gives you &lt;strong&gt;SQL&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Want to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What you emailed last quarter?&lt;/li&gt;
&lt;li&gt;Who sends you the most messages?&lt;/li&gt;
&lt;li&gt;Which threads have attachments but no replies?&lt;/li&gt;
&lt;li&gt;How your communication patterns changed over time?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just write a query.&lt;/p&gt;

&lt;p&gt;Your email is now a &lt;strong&gt;SQLite database you own forever&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;No export limits. No API restrictions. Just standard SQL.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Ready to query your inbox? Install surveilr and ingest your first folder.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://surveilr.com/" rel="noopener noreferrer"&gt;Get Started →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>discuss</category>
      <category>buildinpublic</category>
      <category>data</category>
    </item>
    <item>
      <title>Turn Your File System Into a SQL Database</title>
      <dc:creator>Mukhtar</dc:creator>
      <pubDate>Sun, 17 May 2026 17:05:29 +0000</pubDate>
      <link>https://dev.to/mukhtar_onif/turn-your-file-system-into-a-sql-database-57jb</link>
      <guid>https://dev.to/mukhtar_onif/turn-your-file-system-into-a-sql-database-57jb</guid>
      <description>&lt;p&gt;&lt;em&gt;Stop grepping. Start querying.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The 2-Minute Version:&lt;/strong&gt;&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;# Install and scan your Documents folder&lt;/span&gt;
brew tap surveilr/tap &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; brew &lt;span class="nb"&gt;install &lt;/span&gt;surveilr
surveilr admin init &lt;span class="nt"&gt;-d&lt;/span&gt; fs.db
surveilr ingest files &lt;span class="nt"&gt;-r&lt;/span&gt; ~/Documents &lt;span class="nt"&gt;-d&lt;/span&gt; fs.db

&lt;span class="c"&gt;# Query it&lt;/span&gt;
surveilr shell &lt;span class="nt"&gt;-d&lt;/span&gt; fs.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Find every PDF modified this month&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_modified&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;extension&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pdf'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;last_modified&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'now'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'start of month'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Your filesystem is now a SQLite database you can query forever.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem with grep and find
&lt;/h2&gt;

&lt;p&gt;You've been there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find ~/Documents &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.pdf"&lt;/span&gt; &lt;span class="nt"&gt;-mtime&lt;/span&gt; &lt;span class="nt"&gt;-30&lt;/span&gt; | xargs &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-lh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works. Until you need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Track changes over time&lt;/li&gt;
&lt;li&gt;Join file metadata with other data&lt;/li&gt;
&lt;li&gt;Build complex filters&lt;/li&gt;
&lt;li&gt;Share results in a structured format&lt;/li&gt;
&lt;li&gt;Query the same data six months later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then your one-liner becomes a 200-line Bash script nobody can maintain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here's a different approach:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Turn your filesystem into a &lt;strong&gt;permanent SQLite database&lt;/strong&gt; you can keep querying with standard SQL.&lt;/p&gt;

&lt;p&gt;No scripts. No parsing. Just tables.&lt;/p&gt;




&lt;h2&gt;
  
  
  What surveilr Does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://surveilr.com" rel="noopener noreferrer"&gt;surveilr&lt;/a&gt; scans directories and captures everything into SQLite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;File paths and names&lt;/li&gt;
&lt;li&gt;Sizes and timestamps&lt;/li&gt;
&lt;li&gt;Content hashes (SHA-256)&lt;/li&gt;
&lt;li&gt;Extensions and MIME types&lt;/li&gt;
&lt;li&gt;Permissions and ownership&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The output is a &lt;strong&gt;standard &lt;code&gt;.db&lt;/code&gt; file&lt;/strong&gt; you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Query with any SQLite tool&lt;/li&gt;
&lt;li&gt;Open in DB Browser, DBeaver, DataGrip&lt;/li&gt;
&lt;li&gt;Read with Python pandas or R&lt;/li&gt;
&lt;li&gt;Keep forever (SQLite files last decades)&lt;/li&gt;
&lt;li&gt;Share with teammates (just copy the file)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1: Create a Database
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr admin init &lt;span class="nt"&gt;-d&lt;/span&gt; my-files.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates an empty SQLite database. That's it. No configuration, no setup, no servers.&lt;/p&gt;

&lt;p&gt;You can open this file in any SQLite client right now and see the schema.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Scan a Directory
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr ingest files &lt;span class="nt"&gt;-r&lt;/span&gt; ~/Documents &lt;span class="nt"&gt;-d&lt;/span&gt; my-files.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;surveilr walks the directory tree and inserts metadata into two tables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;uniform_resource&lt;/code&gt; — Core metadata (hash, size, timestamps)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ur_ingest_session_fs_path_entry&lt;/code&gt; — File-specific data (paths, extensions)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; Run multiple times to track changes over time. Each scan creates a new session.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Query Like You Mean It
&lt;/h2&gt;

&lt;p&gt;Open the SQL shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr shell &lt;span class="nt"&gt;-d&lt;/span&gt; my-files.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can run queries that would be painful in Bash.&lt;/p&gt;

&lt;h3&gt;
  
  
  Find all PDFs larger than 5MB
&lt;/h3&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file_path_abs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file_basename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size_bytes&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;size_mb&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;uniform_resource&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;ur_ingest_session_fs_path_entry&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uniform_resource_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uniform_resource_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file_extn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pdf'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size_bytes&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5242880&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size_bytes&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Track file modifications over time
&lt;/h3&gt;

&lt;p&gt;Run multiple scans, then query across sessions:&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file_basename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content_digest&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;versions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_modified_at&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;first_seen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_modified_at&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;last_modified&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;uniform_resource&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;ur_ingest_session_fs_path_entry&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uniform_resource_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uniform_resource_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file_basename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'report.docx'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file_basename&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Find duplicate files by content hash
&lt;/h3&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;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content_digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&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;copies&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;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_mb_wasted&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;uniform_resource&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;ur_ingest_session_fs_path_entry&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uniform_resource_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uniform_resource_id&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content_digest&lt;/span&gt;
&lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="n"&gt;copies&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_mb_wasted&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Find files that changed in the last 7 days
&lt;/h3&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file_path_abs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_modified_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size_bytes&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;uniform_resource&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;ur_ingest_session_fs_path_entry&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uniform_resource_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uniform_resource_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_modified_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'now'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'-7 days'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_modified_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These queries work on any SQLite database—forever.&lt;/p&gt;




&lt;h2&gt;
  
  
  Transform Structured Files Into Queryable Tables
&lt;/h2&gt;

&lt;p&gt;surveilr doesn't just capture file metadata—it can &lt;strong&gt;extract and transform content&lt;/strong&gt; from structured files.&lt;/p&gt;

&lt;p&gt;This is where it gets interesting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transform CSV Files Into SQL Tables
&lt;/h3&gt;

&lt;p&gt;Got CSV files? Turn them into queryable tables automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr orchestrate transform-csv &lt;span class="nt"&gt;-d&lt;/span&gt; my-files.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every CSV file you ingested becomes a SQL table. If you scanned a directory with &lt;code&gt;sales-2024.csv&lt;/code&gt;, you can now query it:&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales_2024&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No manual schema definition. No import scripts. Just automatic table creation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extract Data from HTML Files with CSS Selectors
&lt;/h3&gt;

&lt;p&gt;Ingested HTML files? Extract specific elements with CSS selectors:&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;# Extract all paragraphs inside divs&lt;/span&gt;
surveilr orchestrate transform-html &lt;span class="nt"&gt;-d&lt;/span&gt; my-files.db &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--css-select&lt;/span&gt; &lt;span class="s2"&gt;"paragraphs:div &amp;gt; p"&lt;/span&gt;

&lt;span class="c"&gt;# Extract all links&lt;/span&gt;
surveilr orchestrate transform-html &lt;span class="nt"&gt;-d&lt;/span&gt; my-files.db &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--css-select&lt;/span&gt; &lt;span class="s2"&gt;"links:a[href]"&lt;/span&gt;

&lt;span class="c"&gt;# Extract headers&lt;/span&gt;
surveilr orchestrate transform-html &lt;span class="nt"&gt;-d&lt;/span&gt; my-files.db &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--css-select&lt;/span&gt; &lt;span class="s2"&gt;"headers:h1, h2, h3"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extracted data goes into JSON format you can query:&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;file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;json_extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transformed_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.paragraphs'&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;paragraphs&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;uniform_resource&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;file_extn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'html'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Real-world use case:&lt;/strong&gt; Scrape product data from saved HTML pages, extract structured content from documentation, parse scraped web pages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transform Markdown Files
&lt;/h3&gt;

&lt;p&gt;Extract specific sections from Markdown files:&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;# Extract all headers&lt;/span&gt;
surveilr orchestrate transform-markdown &lt;span class="nt"&gt;-d&lt;/span&gt; my-files.db &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--md-select&lt;/span&gt; &lt;span class="s2"&gt;"headers:# *"&lt;/span&gt;

&lt;span class="c"&gt;# Extract code blocks&lt;/span&gt;
surveilr orchestrate transform-markdown &lt;span class="nt"&gt;-d&lt;/span&gt; my-files.db &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--md-select&lt;/span&gt; &lt;span class="s2"&gt;"code:&lt;/span&gt;&lt;span class="sb"&gt;```&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;% endraw %&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;% raw %&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="sb"&gt;```&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Real-world use case:&lt;/strong&gt; Index documentation, extract code examples from README files, parse technical notes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transform XML Files
&lt;/h3&gt;

&lt;p&gt;XML files get automatically parsed into queryable JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr orchestrate transform-xml &lt;span class="nt"&gt;-d&lt;/span&gt; my-files.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then query XML content with SQLite's JSON functions:&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;file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;json_extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transformed_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.root.item[0].name'&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;item_name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;uniform_resource&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;file_extn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'xml'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Real-world use case:&lt;/strong&gt; Parse configuration files, process exported data from tools, analyze XML logs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Matters
&lt;/h3&gt;

&lt;p&gt;Most file indexing tools just capture metadata.&lt;/p&gt;

&lt;p&gt;surveilr &lt;strong&gt;extracts and transforms content&lt;/strong&gt; so you can query &lt;em&gt;inside&lt;/em&gt; your files.&lt;/p&gt;

&lt;p&gt;This means you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Query values &lt;em&gt;within&lt;/em&gt; CSV files, not just the file existence&lt;/li&gt;
&lt;li&gt;Search for specific HTML elements across hundreds of pages&lt;/li&gt;
&lt;li&gt;Extract code snippets from all Markdown docs&lt;/li&gt;
&lt;li&gt;Parse XML configs and query their structure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example workflow:&lt;/strong&gt;&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;# 1. Ingest a directory of mixed files&lt;/span&gt;
surveilr ingest files &lt;span class="nt"&gt;-r&lt;/span&gt; ~/project-docs &lt;span class="nt"&gt;-d&lt;/span&gt; docs.db

&lt;span class="c"&gt;# 2. Transform CSVs into tables&lt;/span&gt;
surveilr orchestrate transform-csv &lt;span class="nt"&gt;-d&lt;/span&gt; docs.db

&lt;span class="c"&gt;# 3. Extract headers from Markdown&lt;/span&gt;
surveilr orchestrate transform-markdown &lt;span class="nt"&gt;-d&lt;/span&gt; docs.db &lt;span class="nt"&gt;--md-select&lt;/span&gt; &lt;span class="s2"&gt;"headers:# *"&lt;/span&gt;

&lt;span class="c"&gt;# 4. Query across file types&lt;/span&gt;
surveilr shell &lt;span class="nt"&gt;-d&lt;/span&gt; docs.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Find all CSV files with revenue data&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales_data&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;50000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Find all Markdown headers mentioning "API"&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json_extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transformed_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.headers'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;uniform_resource&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;file_extn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'md'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;json_extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transformed_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.headers'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%API%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You just turned a directory of mixed files into a &lt;strong&gt;queryable knowledge base&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Watch Mode: Continuous Indexing
&lt;/h2&gt;

&lt;p&gt;Want surveilr to automatically re-index when files change?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr ingest files &lt;span class="nt"&gt;-r&lt;/span&gt; ~/Documents &lt;span class="nt"&gt;-d&lt;/span&gt; my-files.db &lt;span class="nt"&gt;--watch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;surveilr monitors the directory and updates the database in real-time.&lt;/p&gt;

&lt;p&gt;Press &lt;code&gt;Ctrl+C&lt;/code&gt; to stop.&lt;/p&gt;




&lt;h2&gt;
  
  
  Export Results
&lt;/h2&gt;

&lt;p&gt;Need to share findings? Export as JSON, CSV, or Markdown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr shell &lt;span class="nt"&gt;-d&lt;/span&gt; my-files.db &lt;span class="nt"&gt;--cmd&lt;/span&gt; &lt;span class="s2"&gt;"
SELECT file_path, size_bytes, last_modified
FROM files
WHERE extension = 'pdf'
"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; pdfs.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or save queries as reusable SQL files:&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="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; find-large-files.sql &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
SELECT file_path, size_bytes / 1024 / 1024 AS size_mb
FROM files
WHERE size_bytes &amp;gt; 10485760
ORDER BY size_bytes DESC
LIMIT 20;
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;surveilr shell &lt;span class="nt"&gt;-d&lt;/span&gt; my-files.db find-large-files.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why Not Just Use Scripts?
&lt;/h2&gt;

&lt;p&gt;You could write a Python script to walk directories and output JSON.&lt;/p&gt;

&lt;p&gt;But:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scripts have no schema.&lt;/strong&gt;&lt;br&gt;
Next month, you won't remember what fields you captured or what format you used.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;surveilr creates stable SQL tables.&lt;/strong&gt;&lt;br&gt;
The schema is self-documenting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scripts don't track history.&lt;/strong&gt;&lt;br&gt;
You get a point-in-time snapshot, then it's gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;surveilr accumulates sessions over time.&lt;/strong&gt;&lt;br&gt;
Run it weekly and query across months of history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scripts don't compose.&lt;/strong&gt;&lt;br&gt;
How do you join your file scan with your email data?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;surveilr stores everything in one database.&lt;/strong&gt;&lt;br&gt;
Cross-domain joins just work.&lt;/p&gt;


&lt;h2&gt;
  
  
  Forensic Investigation Workflows
&lt;/h2&gt;

&lt;p&gt;Once your filesystem is queryable, you can ask surprising questions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Find files that disappeared&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="c1"&gt;-- Files that existed in session 1 but not session 2&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;file_path&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'session_1'&lt;/span&gt;
&lt;span class="k"&gt;EXCEPT&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;file_path&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'session_2'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Track when specific files were modified&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;file_basename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_modified_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size_bytes&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;file_basename&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%contract%'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;last_modified_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Find files created during a specific time window&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="c1"&gt;-- Files created during a deployment window&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_modified&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;last_modified&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="s1"&gt;'2024-05-10 14:00'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="s1"&gt;'2024-05-10 15:00'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These queries are impossible with &lt;code&gt;find&lt;/code&gt; and &lt;code&gt;grep&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-World Uses
&lt;/h2&gt;

&lt;h3&gt;
  
  
  For Developers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Track which files changed before/after a bug appeared&lt;/li&gt;
&lt;li&gt;Find orphaned build artifacts&lt;/li&gt;
&lt;li&gt;Identify large files bloating repos&lt;/li&gt;
&lt;li&gt;Build custom file search tools&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For Operations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Audit log file growth over time&lt;/li&gt;
&lt;li&gt;Track config file changes&lt;/li&gt;
&lt;li&gt;Find stale backups&lt;/li&gt;
&lt;li&gt;Monitor disk usage trends&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For Security Teams
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Establish filesystem baselines&lt;/li&gt;
&lt;li&gt;Detect unauthorized file modifications&lt;/li&gt;
&lt;li&gt;Track file access patterns&lt;/li&gt;
&lt;li&gt;Identify anomalous file creation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For Compliance (Oh, By the Way)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Document where regulated data lives (HIPAA PHI, GDPR PII)&lt;/li&gt;
&lt;li&gt;Prove file retention policies&lt;/li&gt;
&lt;li&gt;Provide audit evidence&lt;/li&gt;
&lt;li&gt;Respond to data access requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the real power is &lt;strong&gt;permanent queryability&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Open the Database in Other Tools
&lt;/h2&gt;

&lt;p&gt;Because surveilr outputs standard SQLite, you can use &lt;strong&gt;any SQLite tool&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DB Browser for SQLite&lt;/strong&gt; — Visual database inspector&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DBeaver&lt;/strong&gt; — Universal database client&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Datasette&lt;/strong&gt; — Instant web UI for your database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python pandas&lt;/strong&gt; — &lt;code&gt;pd.read_sql_query(sql, 'sqlite:///my-files.db')&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DuckDB&lt;/strong&gt; — Fast analytical queries on SQLite files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VS Code SQLite extensions&lt;/strong&gt; — Query in your editor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You own the &lt;code&gt;.db&lt;/code&gt; file. Use whatever tools you want.&lt;/p&gt;




&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Permission denied" errors?&lt;/strong&gt;&lt;br&gt;
Run with appropriate permissions or scan a different directory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database getting too large?&lt;/strong&gt;&lt;br&gt;
Use &lt;code&gt;--dry-run&lt;/code&gt; to preview:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr ingest files &lt;span class="nt"&gt;-r&lt;/span&gt; ~/Documents &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Want to see what happened?&lt;/strong&gt;&lt;br&gt;
Add &lt;code&gt;--stats&lt;/code&gt; to show ingestion summary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr ingest files &lt;span class="nt"&gt;-r&lt;/span&gt; ~/Documents &lt;span class="nt"&gt;-d&lt;/span&gt; my-files.db &lt;span class="nt"&gt;--stats&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;You just turned your filesystem into a queryable database.&lt;/p&gt;

&lt;p&gt;Now combine it with other data sources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/mukhtar_onif/turn-your-email-into-a-sql-database-1noa"&gt;Turn Email Into SQL&lt;/a&gt;&lt;/strong&gt; — Query Gmail/Outlook alongside files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/mukhtar_onif/turn-any-api-into-a-sql-database-5dg0"&gt;Extract API Data&lt;/a&gt;&lt;/strong&gt; — Add GitHub, Jira, or 600+ other sources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Or explore more:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://surveilr.com/" rel="noopener noreferrer"&gt;surveilr.com&lt;/a&gt;&lt;/strong&gt; — Full documentation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/surveilr/packages#installation" rel="noopener noreferrer"&gt;Installation Guide&lt;/a&gt;&lt;/strong&gt; — Other install methods&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Your filesystem should be &lt;strong&gt;queryable&lt;/strong&gt;, not just searchable.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;find&lt;/code&gt; and &lt;code&gt;grep&lt;/code&gt; are great for one-off questions.&lt;/p&gt;

&lt;p&gt;But when you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Historical tracking&lt;/li&gt;
&lt;li&gt;Complex queries&lt;/li&gt;
&lt;li&gt;Structured output&lt;/li&gt;
&lt;li&gt;Composability&lt;/li&gt;
&lt;li&gt;Permanence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...you want SQL.&lt;/p&gt;

&lt;p&gt;surveilr turns your filesystem into a &lt;strong&gt;SQLite database you own forever&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;No scripts. No parsing. Just queries.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Ready to query your filesystem? Install surveilr and run your first scan.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://surveilr.com/" rel="noopener noreferrer"&gt;Get Started →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>software</category>
      <category>discuss</category>
      <category>rust</category>
    </item>
    <item>
      <title>Turn Anything Into a Queryable SQLite Database</title>
      <dc:creator>Mukhtar</dc:creator>
      <pubDate>Sun, 17 May 2026 17:02:38 +0000</pubDate>
      <link>https://dev.to/mukhtar_onif/turn-anything-into-a-queryable-sqlite-database-3991</link>
      <guid>https://dev.to/mukhtar_onif/turn-anything-into-a-queryable-sqlite-database-3991</guid>
      <description>&lt;p&gt;&lt;em&gt;grep, jq, ETL, and forensic indexing collapsed into one local SQL primitive&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Your audit trail should be a &lt;strong&gt;database you own&lt;/strong&gt;, not a SaaS UI you rent.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://surveilr.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;surveilr&lt;/strong&gt;&lt;/a&gt; turns your files, emails, and APIs into standard SQLite databases you can query forever—with any tool, offline, on your machine.&lt;/p&gt;

&lt;p&gt;No cloud. No dashboards. Just SQL.&lt;/p&gt;




&lt;h2&gt;
  
  
  See It Work in 2 Minutes
&lt;/h2&gt;

&lt;p&gt;Install surveilr and immediately query your filesystem:&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;# Install (macOS/Linux)&lt;/span&gt;
brew tap surveilr/tap &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; brew &lt;span class="nb"&gt;install &lt;/span&gt;surveilr

&lt;span class="c"&gt;# Create a database and scan your Documents folder&lt;/span&gt;
surveilr admin init &lt;span class="nt"&gt;-d&lt;/span&gt; my-data.db
surveilr ingest files &lt;span class="nt"&gt;-r&lt;/span&gt; ~/Documents &lt;span class="nt"&gt;-d&lt;/span&gt; my-data.db

&lt;span class="c"&gt;# Open SQL shell and query&lt;/span&gt;
surveilr shell &lt;span class="nt"&gt;-d&lt;/span&gt; my-data.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now run surprisingly powerful queries:&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="c1"&gt;-- Find every PDF modified after a specific date&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_modified&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size_bytes&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;extension&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pdf'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;last_modified&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2024-05-01'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Track file changes over time&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;file_basename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&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;versions&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;file_basename&lt;/span&gt;
&lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="n"&gt;versions&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Find orphaned large files&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size_bytes&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;size_mb&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;size_bytes&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10485760&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;size_bytes&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. You just turned your filesystem into a queryable database you can keep investigating.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is surveilr?
&lt;/h2&gt;

&lt;p&gt;surveilr is a &lt;strong&gt;local-first universal SQL layer&lt;/strong&gt; that ingests operational data and outputs standard SQLite databases.&lt;/p&gt;

&lt;p&gt;It's not a platform. It's not a dashboard. It's an &lt;strong&gt;ingestion layer&lt;/strong&gt; that speaks SQL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Capabilities
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;📂 &lt;strong&gt;File system indexing&lt;/strong&gt; — Turn any directory into queryable metadata&lt;/li&gt;
&lt;li&gt;🔄 &lt;strong&gt;Content transformation&lt;/strong&gt; — CSV→SQL tables, HTML→JSON (CSS selectors), Markdown/XML→queryable data&lt;/li&gt;
&lt;li&gt;📧 &lt;strong&gt;Email ingestion&lt;/strong&gt; — IMAP to SQLite (Gmail, Outlook, any server)&lt;/li&gt;
&lt;li&gt;🔌 &lt;strong&gt;API extraction&lt;/strong&gt; — 600+ Singer taps (GitHub, Jira, Salesforce, databases)&lt;/li&gt;
&lt;li&gt;🔍 &lt;strong&gt;Standard SQL&lt;/strong&gt; — No custom DSL, just SQLite&lt;/li&gt;
&lt;li&gt;🏠 &lt;strong&gt;Local-first&lt;/strong&gt; — Everything runs on your machine, offline-capable&lt;/li&gt;
&lt;li&gt;🔒 &lt;strong&gt;You own the data&lt;/strong&gt; — Portable &lt;code&gt;.db&lt;/code&gt; files you control forever&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why This Matters
&lt;/h3&gt;

&lt;p&gt;Most operational data ends up in one of three dead ends:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;SaaS dashboards&lt;/strong&gt; you can't query your way&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-off scripts&lt;/strong&gt; that become unmaintainable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSV exports&lt;/strong&gt; that lose context over time&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;surveilr gives you a different option: &lt;strong&gt;permanent, queryable SQLite databases&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Years from now, you'll still be able to open that &lt;code&gt;.db&lt;/code&gt; file with any SQLite client and run new queries you haven't thought of yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  The SQLite Advantage
&lt;/h2&gt;

&lt;p&gt;surveilr doesn't invent a new database format. It uses &lt;strong&gt;SQLite&lt;/strong&gt;—the world's most deployed database.&lt;/p&gt;

&lt;p&gt;You already trust SQLite. It's in your phone, your browser, your laptop's OS.&lt;/p&gt;

&lt;p&gt;What surveilr adds is &lt;strong&gt;disciplined ingestion patterns&lt;/strong&gt; that turn messy operational data into clean, queryable tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inspectable&lt;/strong&gt;: Open any &lt;code&gt;.db&lt;/code&gt; file with DB Browser, VS Code, or &lt;code&gt;sqlite3&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Durable&lt;/strong&gt;: SQLite files last decades&lt;br&gt;
&lt;strong&gt;Interoper able&lt;/strong&gt;: Works with Datasette, DuckDB, pandas, Observable, Grafana&lt;br&gt;
&lt;strong&gt;Portable&lt;/strong&gt;: One file, zero dependencies&lt;br&gt;
&lt;strong&gt;Permanent&lt;/strong&gt;: Your data doesn't disappear when a vendor shuts down&lt;/p&gt;

&lt;p&gt;This is the opposite of SaaS data lock-in.&lt;/p&gt;


&lt;h2&gt;
  
  
  Queries That Make You Go "Wait, I Can Do THAT?"
&lt;/h2&gt;

&lt;p&gt;The power comes from &lt;strong&gt;cross-domain queries&lt;/strong&gt; you can't run anywhere else.&lt;/p&gt;
&lt;h3&gt;
  
  
  Find documents mentioned in emails after they were modified
&lt;/h3&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;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&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;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_modified&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;emails&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file_basename&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'%'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_modified&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Track all GitHub commits made within 24 hours of a production incident
&lt;/h3&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="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commit_sha&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;github_commits&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&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;incident_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'24 hours'&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;incidents&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'INC-123'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;AND&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;incident_time&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;incidents&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'INC-123'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Find invoices discussed in email without matching purchase orders in Jira
&lt;/h3&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&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="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;emails&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%invoice%'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;jira_issues&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;SUBSTR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;INSTR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'INV-'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;10&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;These aren't theoretical examples. These are real forensic workflows you can build.&lt;/p&gt;


&lt;h2&gt;
  
  
  Three Practical Guides
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Guide 1: Query Your File System
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/mukhtar_onif/turn-your-file-system-into-a-sql-database-57jb"&gt;Read the full guide →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Scan directories and query file metadata with SQL. &lt;strong&gt;Plus:&lt;/strong&gt; Transform CSVs into SQL tables, extract data from HTML with CSS selectors, parse Markdown and XML into queryable JSON.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2-minute win&lt;/strong&gt;: Find all PDFs modified in the last 30 days&lt;br&gt;
&lt;strong&gt;Bonus&lt;/strong&gt;: Turn CSV files into queryable SQL tables automatically&lt;/p&gt;


&lt;h3&gt;
  
  
  Guide 2: Turn Email Into SQL
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/mukhtar_onif/turn-your-email-into-a-sql-database-1noa"&gt;Read the full guide →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ingest Gmail/Outlook via IMAP. Query conversations, track threads, extract attachments, search across years of email history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2-minute win&lt;/strong&gt;: Find all emails from a specific sender mentioning "invoice"&lt;/p&gt;


&lt;h3&gt;
  
  
  Guide 3: Extract API Data
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/mukhtar_onif/turn-any-api-into-a-sql-database-5dg0"&gt;Read the full guide →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use Singer taps to pull data from GitHub, Jira, GitLab, Salesforce, or 600+ other sources. Join across platforms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2-minute win&lt;/strong&gt;: Query all GitHub commits from the last 7 days&lt;/p&gt;


&lt;h2&gt;
  
  
  Why Not Just Write a Script?
&lt;/h2&gt;

&lt;p&gt;You could. But:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scripts become dead ends.&lt;/strong&gt;&lt;br&gt;
Six months later, you can't remember what format you used or where you saved the output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;surveilr outputs permanent, standard SQLite databases.&lt;/strong&gt;&lt;br&gt;
Open them years later with any SQL tool and keep querying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scripts don't compose.&lt;/strong&gt;&lt;br&gt;
You can't easily join your email script's output with your file scan script's output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;surveilr stores everything in one queryable database.&lt;/strong&gt;&lt;br&gt;
Cross-domain joins just work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scripts have no schema.&lt;/strong&gt;&lt;br&gt;
You're parsing JSON with jq and hoping the structure doesn't change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;surveilr normalizes data into stable SQL tables.&lt;/strong&gt;&lt;br&gt;
Query with confidence.&lt;/p&gt;


&lt;h2&gt;
  
  
  Ecosystem Integration
&lt;/h2&gt;

&lt;p&gt;surveilr isn't the destination. It's the &lt;strong&gt;ingestion layer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Once your data is in SQLite, you can use any tool in the SQLite ecosystem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://datasette.io/" rel="noopener noreferrer"&gt;Datasette&lt;/a&gt;&lt;/strong&gt; — Instant web UI for your databases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://duckdb.org/" rel="noopener noreferrer"&gt;DuckDB&lt;/a&gt;&lt;/strong&gt; — Fast analytical queries on SQLite files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://sqlitebrowser.org/" rel="noopener noreferrer"&gt;DB Browser for SQLite&lt;/a&gt;&lt;/strong&gt; — Visual database inspector&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=alexcvzz.vscode-sqlite" rel="noopener noreferrer"&gt;VS Code SQLite extensions&lt;/a&gt;&lt;/strong&gt; — Query in your editor&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://pandas.pydata.org/" rel="noopener noreferrer"&gt;Python pandas&lt;/a&gt;&lt;/strong&gt; — Read SQLite directly into dataframes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://grafana.com/" rel="noopener noreferrer"&gt;Grafana&lt;/a&gt;&lt;/strong&gt; — Build dashboards from SQLite&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://observablehq.com/" rel="noopener noreferrer"&gt;Observable&lt;/a&gt;&lt;/strong&gt; — Interactive notebooks with SQL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You own the database. Use whatever tools you want.&lt;/p&gt;


&lt;h2&gt;
  
  
  Local-First. No Cloud. You Own It.
&lt;/h2&gt;

&lt;p&gt;This is a big deal in 2026.&lt;/p&gt;

&lt;p&gt;Most "compliance platforms" hide your own data behind proprietary dashboards.&lt;/p&gt;

&lt;p&gt;surveilr gives you &lt;strong&gt;raw SQL access to everything&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;All data stays on your machine&lt;/strong&gt; — No upload, no sync, no cloud dependency&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Works completely offline&lt;/strong&gt; — Internet not required&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Inspectable&lt;/strong&gt; — Open the &lt;code&gt;.db&lt;/code&gt; file with any SQLite client&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Portable&lt;/strong&gt; — Copy the file, query it anywhere&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;No vendor lock-in&lt;/strong&gt; — Standard SQLite format, not proprietary&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Privacy-first&lt;/strong&gt; — Your data never leaves your control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're tired of SaaS platforms that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Charge per seat&lt;/li&gt;
&lt;li&gt;Lock your data behind APIs&lt;/li&gt;
&lt;li&gt;Disappear when the startup shuts down&lt;/li&gt;
&lt;li&gt;Require constant internet connectivity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...then surveilr is for you.&lt;/p&gt;


&lt;h2&gt;
  
  
  Oh, By the Way: Compliance Teams Love This Too
&lt;/h2&gt;

&lt;p&gt;If you work in healthcare, finance, or regulated industries, surveilr happens to be perfect for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HIPAA audits&lt;/strong&gt; — Track where PHI files are stored&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SOX compliance&lt;/strong&gt; — Maintain 7-year email records with queryable evidence&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GDPR requests&lt;/strong&gt; — Respond to "right to access" with SQL queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SOC 2 audits&lt;/strong&gt; — Show complete change management history&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But that's a side effect of the real value: &lt;strong&gt;permanent, queryable operational data you control&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Forensic Curiosity
&lt;/h2&gt;

&lt;p&gt;Once you start using surveilr, you'll find yourself asking new questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What files were deleted but are still referenced in emails?&lt;/li&gt;
&lt;li&gt;Which documents were modified right before a deployment?&lt;/li&gt;
&lt;li&gt;What code changes correlate with customer support tickets?&lt;/li&gt;
&lt;li&gt;Which team members touched files in a specific directory over the last year?&lt;/li&gt;
&lt;li&gt;What attachments were sent externally that match internal file hashes?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These questions are impossible to answer with dashboards.&lt;/p&gt;

&lt;p&gt;But with SQL, they're just queries.&lt;/p&gt;


&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;
&lt;h3&gt;
  
  
  macOS / Linux
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew tap surveilr/tap &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; brew &lt;span class="nb"&gt;install &lt;/span&gt;surveilr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Verify
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr &lt;span class="nt"&gt;--version&lt;/span&gt;
surveilr doctor  &lt;span class="c"&gt;# Check environment&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For other platforms, see the &lt;a href="https://github.com/surveilr/packages#installation" rel="noopener noreferrer"&gt;installation guide&lt;/a&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Run This Now
&lt;/h2&gt;

&lt;p&gt;Pick your 2-minute win:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1: Query your filesystem&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr admin init &lt;span class="nt"&gt;-d&lt;/span&gt; fs.db
surveilr ingest files &lt;span class="nt"&gt;-r&lt;/span&gt; ~/Documents &lt;span class="nt"&gt;-d&lt;/span&gt; fs.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option 2: Query your email&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr admin init &lt;span class="nt"&gt;-d&lt;/span&gt; email.db
surveilr ingest imap &lt;span class="nt"&gt;-u&lt;/span&gt; you@gmail.com &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"app-password"&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; imap.gmail.com &lt;span class="nt"&gt;-d&lt;/span&gt; email.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option 3: Query GitHub&lt;/strong&gt; (Ingest Singer taps)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;surveilr admin init &lt;span class="nt"&gt;-d&lt;/span&gt; github.db
surveilr ingest files &lt;span class="nt"&gt;-r&lt;/span&gt; ./github-tap-script.py &lt;span class="nt"&gt;-d&lt;/span&gt; github.db
surveilr orchestrate adapt-singer &lt;span class="o"&gt;(&lt;/span&gt;convert data to views&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open the &lt;code&gt;.db&lt;/code&gt; file in any SQLite tool and start exploring.&lt;/p&gt;




&lt;h2&gt;
  
  
  Learn More
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://surveilr.com/" rel="noopener noreferrer"&gt;surveilr.com&lt;/a&gt;&lt;/strong&gt; — Official website and documentation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/surveilr/packages#installation" rel="noopener noreferrer"&gt;Installation Guide&lt;/a&gt;&lt;/strong&gt; — Download and install&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/surveilr/packages/issues" rel="noopener noreferrer"&gt;Report Issues&lt;/a&gt;&lt;/strong&gt; — Bug reports and feature requests&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Your operational data—files, emails, API responses—should be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Queryable&lt;/strong&gt; (SQL, not grep)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permanent&lt;/strong&gt; (SQLite, not CSVs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Yours&lt;/strong&gt; (local, not SaaS)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composable&lt;/strong&gt; (join across domains)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspectable&lt;/strong&gt; (open in any tool)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's what surveilr gives you.&lt;/p&gt;

&lt;p&gt;It's not compliance software. It's not enterprise governance.&lt;/p&gt;

&lt;p&gt;It's &lt;strong&gt;grep, jq, ETL, and forensic indexing collapsed into one local SQL primitive&lt;/strong&gt; you can build on forever.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Ready to own your data? Install surveilr and query something surprising.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://surveilr.com/" rel="noopener noreferrer"&gt;Get Started →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>sql</category>
      <category>discuss</category>
      <category>buildinpublic</category>
      <category>data</category>
    </item>
    <item>
      <title>Implementing OpenDAL with Filesystem (FS) In Rust</title>
      <dc:creator>Mukhtar</dc:creator>
      <pubDate>Sat, 19 Jul 2025 18:55:47 +0000</pubDate>
      <link>https://dev.to/mukhtar_onif/implementing-opendal-with-filesystem-fs-in-rust-2ha8</link>
      <guid>https://dev.to/mukhtar_onif/implementing-opendal-with-filesystem-fs-in-rust-2ha8</guid>
      <description>&lt;h2&gt;
  
  
  Introduction to OpenDAL with SQLite Virtual Tables
&lt;/h2&gt;

&lt;p&gt;OpenDAL is a powerful and unified data access layer that provides an abstraction for different storage backends such as local filesystems, cloud storage, and object stores. It simplifies file and metadata operations by offering a unified API, allowing seamless interaction with different storage solutions.&lt;/p&gt;

&lt;p&gt;This guide explains the &lt;strong&gt;concepts&lt;/strong&gt; behind integrating OpenDAL with SQLite virtual tables, allowing you to query filesystem metadata using SQL. The code examples demonstrate key concepts rather than complete implementations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Concepts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. OpenDAL Operator
&lt;/h3&gt;

&lt;p&gt;The foundation of any OpenDAL integration is the &lt;strong&gt;Operator&lt;/strong&gt; - your interface to the storage backend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concept&lt;/strong&gt;: Create a configured operator for your storage type&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Conceptual example - actual implementation needs error handling&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;fs_builder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Fs&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.root&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;operator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Operator&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fs_builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="nf"&gt;.finish&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Ideas&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The operator abstracts away storage-specific details&lt;/li&gt;
&lt;li&gt;Different services (Fs, S3, Azure, etc.) use the same Operator interface&lt;/li&gt;
&lt;li&gt;Configuration happens through service-specific builders&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. SQLite Virtual Table Architecture
&lt;/h3&gt;

&lt;p&gt;Virtual tables in SQLite allow you to present non-SQL data as queryable tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concept&lt;/strong&gt;: Bridge between OpenDAL and SQLite&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Conceptual structure - real implementation much more complex&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;FileSystemTable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sqlite3_vtab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// Required SQLite base&lt;/span&gt;
    &lt;span class="n"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Rc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Operator&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// Your OpenDAL operator&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Ideas&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Virtual tables implement a specific SQLite interface&lt;/li&gt;
&lt;li&gt;They translate SQL queries into storage operations&lt;/li&gt;
&lt;li&gt;The table schema defines what data is queryable&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Schema Design
&lt;/h3&gt;

&lt;p&gt;Design your virtual table schema to expose useful file metadata.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concept&lt;/strong&gt;: Map file attributes to SQL columns&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;filesystem_view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                    &lt;span class="c1"&gt;-- File name&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                    &lt;span class="c1"&gt;-- Full path&lt;/span&gt;
    &lt;span class="n"&gt;last_modified&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;-- When file was changed&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="nb"&gt;BLOB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                 &lt;span class="c1"&gt;-- File contents (lazy-loaded)&lt;/span&gt;
    &lt;span class="k"&gt;size&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                 &lt;span class="c1"&gt;-- File size in bytes&lt;/span&gt;
    &lt;span class="n"&gt;content_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;-- MIME type&lt;/span&gt;
    &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                  &lt;span class="c1"&gt;-- File hash/checksum&lt;/span&gt;
    &lt;span class="n"&gt;arg_path&lt;/span&gt; &lt;span class="n"&gt;HIDDEN&lt;/span&gt;              &lt;span class="c1"&gt;-- Query parameter (hidden column)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Ideas&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hidden columns can accept query parameters&lt;/li&gt;
&lt;li&gt;BLOB columns can hold binary data&lt;/li&gt;
&lt;li&gt;Schema should balance utility with performance&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Query Translation Flow
&lt;/h3&gt;

&lt;p&gt;The magic happens in translating SQL queries into OpenDAL operations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concept&lt;/strong&gt;: SQL WHERE clause becomes OpenDAL list operations&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Conceptual flow - real implementation involves cursor management&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;handle_query_with_path_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FileMetadata&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Use OpenDAL to list files in the specified path&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;lister&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;operator&lt;/span&gt;&lt;span class="nf"&gt;.lister_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.metakey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Metakey&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ContentLength&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;Metakey&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;LastModified&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.call&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Convert OpenDAL entries to your table format&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lister&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="nf"&gt;.metadata&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.mode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nn"&gt;EntryMode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;FILE&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;convert_to_table_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;results&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Ideas&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQL filters become OpenDAL query parameters&lt;/li&gt;
&lt;li&gt;Metadata is fetched on-demand to avoid performance issues&lt;/li&gt;
&lt;li&gt;Results are converted to SQLite-compatible format&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Lazy Loading Strategy
&lt;/h3&gt;

&lt;p&gt;For performance, expensive operations (like reading file contents) should be lazy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concept&lt;/strong&gt;: Only load data when specifically requested&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Conceptual approach to lazy loading&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get_column_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SqliteValue&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;SqliteValue&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.file_name&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;SqliteValue&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.file_path&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;SqliteValue&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.file_size&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Expensive operation - only do when column is requested&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.operator&lt;/span&gt;&lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.file_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nn"&gt;SqliteValue&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;SqliteValue&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Null&lt;/span&gt;&lt;span class="p"&gt;,&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;&lt;strong&gt;Key Ideas&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Separate metadata operations from content operations&lt;/li&gt;
&lt;li&gt;Use OpenDAL's &lt;code&gt;stat()&lt;/code&gt; for metadata, &lt;code&gt;read()&lt;/code&gt; for content&lt;/li&gt;
&lt;li&gt;Only perform expensive operations when columns are actually selected&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. Error Handling Patterns
&lt;/h3&gt;

&lt;p&gt;Bridge OpenDAL errors to SQLite errors appropriately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concept&lt;/strong&gt;: Convert storage errors to SQL errors&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Conceptual error mapping&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;map_opendal_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OpenDalError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SqliteError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="nf"&gt;.kind&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;ErrorKind&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NotFound&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;SqliteError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nn"&gt;ErrorKind&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;PermissionDenied&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;SqliteError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nn"&gt;ErrorKind&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Unexpected&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;SqliteError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Internal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;SqliteError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Internal&lt;/span&gt;&lt;span class="p"&gt;,&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;&lt;strong&gt;Key Ideas&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenDAL errors need translation to SQLite error codes&lt;/li&gt;
&lt;li&gt;Some errors should be handled gracefully (missing files)&lt;/li&gt;
&lt;li&gt;Others should propagate up to the SQL layer&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Usage Patterns
&lt;/h2&gt;

&lt;p&gt;Once implemented, your virtual table enables powerful queries:&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="c1"&gt;-- List all files in a directory&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;size&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;filesystem_view&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;arg_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/documents'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Find large files&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;size&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;filesystem_view&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;arg_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/media'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Search by file type (if you implement content_type detection)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;filesystem_view&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;arg_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/projects'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;content_type&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'text/%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Benefits of This Approach
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why combine OpenDAL with SQLite virtual tables?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unified Query Interface&lt;/strong&gt;: Use SQL to query any storage backend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexibility&lt;/strong&gt;: Same queries work across local files, S3, Azure, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration&lt;/strong&gt;: Easy to embed in existing SQLite-based applications&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: SQLite's query optimization benefits your file operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ecosystem&lt;/strong&gt;: Leverage SQLite's rich ecosystem of tools and extensions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;To implement this approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Study SQLite's virtual table interface documentation&lt;/li&gt;
&lt;li&gt;Explore OpenDAL's service implementations for your storage needs&lt;/li&gt;
&lt;li&gt;Design your schema based on your specific use cases&lt;/li&gt;
&lt;li&gt;Implement the virtual table interface with proper error handling&lt;/li&gt;
&lt;li&gt;Add performance optimizations like caching and pagination&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This pattern opens up powerful possibilities for unified data access across different storage systems while maintaining the familiar SQL interface.&lt;/p&gt;

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

&lt;p&gt;This guide demonstrated how to use OpenDAL for filesystem operations in Rust and integrate it with SQLite virtual tables. By leveraging OpenDAL, you can seamlessly interact with local storage while maintaining a structured query interface via SQLite.&lt;/p&gt;

&lt;p&gt;For more details, check out &lt;a href="https://github.com/apache/opendal" rel="noopener noreferrer"&gt;OpenDAL's documentation&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>rust</category>
      <category>opendal</category>
      <category>sqlite</category>
    </item>
    <item>
      <title>Getting Started with Rust in 2025: Why Now Is a Great Time to Learn Rust 🦀</title>
      <dc:creator>Mukhtar</dc:creator>
      <pubDate>Sun, 13 Jul 2025 20:48:12 +0000</pubDate>
      <link>https://dev.to/mukhtar_onif/getting-started-with-rust-in-2025-why-now-is-a-great-time-to-learn-rust-1d89</link>
      <guid>https://dev.to/mukhtar_onif/getting-started-with-rust-in-2025-why-now-is-a-great-time-to-learn-rust-1d89</guid>
      <description>&lt;h2&gt;
  
  
  Getting Started with Rust in 2025 🦀
&lt;/h2&gt;

&lt;p&gt;Rust has been growing steadily over the years, and in 2025, it's more relevant than ever. If you've been on the fence about learning it, now's the perfect time to jump in.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 Why Learn Rust?
&lt;/h2&gt;

&lt;p&gt;Rust is a modern systems programming language focused on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt; (like C/C++)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safety&lt;/strong&gt; (no nulls, no data races)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modern developer experience&lt;/strong&gt; (great tooling, compiler messages)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Companies like Microsoft, Amazon, Cloudflare, and Dropbox use Rust in production. It's built to help you write &lt;strong&gt;fast, reliable, and maintainable&lt;/strong&gt; software.&lt;/p&gt;




&lt;h2&gt;
  
  
  🌟 What Makes Rust Special?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Memory safety without garbage collection&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fearless concurrency&lt;/strong&gt; – build multi-threaded apps without race conditions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Helpful compiler&lt;/strong&gt; – the compiler actually teaches you!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Powerful tooling&lt;/strong&gt; – &lt;code&gt;cargo&lt;/code&gt;, &lt;code&gt;clippy&lt;/code&gt;, &lt;code&gt;rustfmt&lt;/code&gt;, and more&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-cost abstractions&lt;/strong&gt; – safety and performance, no tradeoffs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've dealt with C/C++ bugs or hit performance walls in Python or JavaScript, Rust is the best of both worlds.&lt;/p&gt;




&lt;h2&gt;
  
  
  🆚 Rust vs Other Languages
&lt;/h2&gt;

&lt;p&gt;Rust sits in a unique position compared to other programming languages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt; – As fast as C/C++ but much safer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory Management&lt;/strong&gt; – Manual control without the crashes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type Safety&lt;/strong&gt; – Catches bugs at compile time, not runtime&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency&lt;/strong&gt; – Built-in support for safe multi-threading&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learning Curve&lt;/strong&gt; – Steeper initially, but pays off long-term&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unlike garbage-collected languages, Rust gives you control. Unlike manual memory management languages, Rust prevents common bugs.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Code Examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Hello World
&lt;/h3&gt;

&lt;p&gt;Every Rust program starts with a main() function. This is your entry point!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This is the main function - where your program starts&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// println! is a macro that prints to the console&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hello, world!"&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;h3&gt;
  
  
  Variables and Types
&lt;/h3&gt;

&lt;p&gt;Rust is statically typed but can often infer types. Variables are immutable by default - you need mut to change them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Variables are immutable by default&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// string slice (&amp;amp;str)&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// unsigned 32-bit integer&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;// 'mut' makes it mutable (changeable)&lt;/span&gt;

    &lt;span class="c1"&gt;// We can change mutable variables&lt;/span&gt;
    &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// {} are placeholders for variables in println!&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{} is {} years old with a score of {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&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;h3&gt;
  
  
  Functions
&lt;/h3&gt;

&lt;p&gt;Functions in Rust are defined with fn. The last expression (without semicolon) is the return value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Function that takes two i32 parameters and returns an i32&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;i32&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;  &lt;span class="c1"&gt;// no semicolon = this is the return value&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Call the function and store the result&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&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="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"5 + 3 = {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&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;h3&gt;
  
  
  Ownership (Rust's Superpower)
&lt;/h3&gt;

&lt;p&gt;Ownership is Rust's way of managing memory safely. Each value has one owner, and when the owner goes out of scope, the value is dropped.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Create a String on the heap&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;s1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;String&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hello"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// This MOVES s1 to s2. s1 is no longer valid!&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;s2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// This would cause a compile error because s1 was moved&lt;/span&gt;
    &lt;span class="c1"&gt;// println!("{}", s1); &lt;/span&gt;

    &lt;span class="c1"&gt;// Only s2 is valid now&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s2&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;h3&gt;
  
  
  Borrowing
&lt;/h3&gt;

&lt;p&gt;Borrowing lets you use a value without taking ownership. Think of it like borrowing a book - you can read it, but you have to give it back!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This function borrows a String reference (&amp;amp;String)&lt;/span&gt;
&lt;span class="c1"&gt;// It doesn't take ownership, just looks at the data&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;print_length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Length: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;my_string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;String&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hello"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Pass a reference (&amp;amp;) to the function&lt;/span&gt;
    &lt;span class="nf"&gt;print_length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;my_string&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// We can still use my_string because we only borrowed it&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;my_string&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;h3&gt;
  
  
  Structs &amp;amp; Methods
&lt;/h3&gt;

&lt;p&gt;Structs are like classes in other languages - they group related data together. You can add methods to them using impl blocks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Define a struct (like a class in other languages)&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Person&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Implementation block - where we define methods&lt;/span&gt;
&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;Person&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Associated function (like a constructor)&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Person&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Person&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;// shorthand for name: name, age: age&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Method that takes &amp;amp;self (borrows the instance)&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hi, I'm {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Create a new Person instance&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;person&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Person&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Bob"&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Call the method&lt;/span&gt;
    &lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="nf"&gt;.greet&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;h3&gt;
  
  
  Error Handling
&lt;/h3&gt;

&lt;p&gt;Rust doesn't have exceptions. Instead, it uses Result to handle errors explicitly. This forces you to deal with potential failures.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Function returns Result&amp;lt;T, E&amp;gt; - either Ok(value) or Err(error)&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;divide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;String&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="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Return an error&lt;/span&gt;
        &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Cannot divide by zero"&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Return the successful result&lt;/span&gt;
        &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Use match to handle both success and error cases&lt;/span&gt;
    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="nf"&gt;divide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Result: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Error: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;),&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;
  
  
  🛠️ Getting Started
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install Rust&lt;/strong&gt;: &lt;code&gt;curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create new project&lt;/strong&gt;: &lt;code&gt;cargo new my_project&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build and run&lt;/strong&gt;: &lt;code&gt;cargo run&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read "The Rust Book"&lt;/strong&gt; online for free&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🔥 What You Can Build
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Web servers&lt;/strong&gt; (Actix, Rocket frameworks)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Command-line tools&lt;/strong&gt; (ripgrep, bat)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Game engines&lt;/strong&gt; (Bevy)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blockchain projects&lt;/strong&gt; (Solana, Polkadot)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WebAssembly applications&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operating systems&lt;/strong&gt; (Redox OS)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  💡 Why 2025 is Perfect
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mature ecosystem&lt;/strong&gt; – tons of quality crates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Industry adoption&lt;/strong&gt; – major companies using it in production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Great learning resources&lt;/strong&gt; – excellent documentation and community&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Job market&lt;/strong&gt; – high demand, good salaries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Future-proof&lt;/strong&gt; – language designed for next-generation computing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rust's combination of performance, safety, and growing ecosystem makes it an excellent choice for modern development in 2025!&lt;/p&gt;

</description>
      <category>rust</category>
      <category>programming</category>
      <category>c</category>
      <category>community</category>
    </item>
  </channel>
</rss>
