australorp.dev

Query this Blog with DuckDB

Written

I wanted to have some fun with DuckDB, and for some reason, I thought it would be fun to query my blog using it. So, I added two endpoints for querying the current posts and resource pages as CSV files. This is already enough for DuckDB, because it has CSV parsing and an HTTP client built-in:

Note: I used .mode html in the DuckDB CLI to give me a headstart on formatting the query results.

Query (DuckDB CLI)

SELECT title, date, link
FROM 'https://australorp.dev/blog.csv'
ORDER BY date DESC

Results

title date link
Query this Blog with DuckDB 2026-06-25 https://australorp.dev/blog/query-this-blog
Lists or Tables? 2026-06-24 https://australorp.dev/blog/lists-vs-tables
Write Game UIs like a Caveman 2026-06-23 https://australorp.dev/blog/write-game-ui-like-a-caveman
Being a Professional Programmer 2026-05-04 https://australorp.dev/blog/professional-programmer-talk
Godot Virtual Joystick 2026-04-22 https://australorp.dev/blog/godot-virtual-joystick
Making a Lichess TV Viewer with Hyperscript 2026-04-16 https://australorp.dev/blog/lichess-tv-hyperscript

I love how easy DuckDB makes it to query a CSV. Let’s do one more for fun. Let’s find the largest pages by the length of their source Markdown files.

Query (DuckDB CLI)

WITH pages AS (
  SELECT title, markdown
  FROM 'https://australorp.dev/resources.csv'
  UNION
  SELECT title, markdown
  FROM 'https://australorp.dev/blog.csv'
)
SELECT title, length(markdown) as length
FROM pages
ORDER BY length(markdown) DESC

Results

title length
Write Game UIs like a Caveman 9525
Lists or Tables? 8170
Being a Professional Programmer 5817
Programming 3719
Query this Blog with DuckDB 3009
Web Design 1938
Romans 1045
Making a Lichess TV Viewer with Hyperscript 880
Godot Virtual Joystick 603

Nice. The length of the Markdown source is not a great way to estimate the length or reading time of the page, but it’s not the worst way either.

Serving the pages as CSV files almost feels like a poor man’s RSS. Since I am using Astro, it was very easy to add. All I had to do was make a new static file endpoint that queries my blog collection. I wrote the CSV escaping and formatting myself because I hate adding new dependencies if I don’t have to.

import { getCollection } from 'astro:content'

export async function GET(_) {
  const posts = await getCollection('blog')
  const data = csv(posts.map(x => [
    x.id,
    `https://australorp.dev/blog/${x.id}`,
    x.data.title,
    x.data.date,
    x.data.tags.join(','),
    x.body,
  ]))
  return new Response(data)
}

function csv(rows) {
  const data = rows
    .map(row => 
      row.map(field => formatField(field)).join(',')
    )
  data.unshift(['id','link','title','date','tags','markdown'])
  return data.join('\n')
}

function formatField(x) {
  return `"${escape(x)}"`
}

function escape(x) {
  return x.replaceAll('"', '""')
}

That’s all I have for now. Go download DuckDB and write some queries against my blog.