Migrating to PlanetScale
First off, huge amount of credit to Mati for migrating our database to PlanetScale. I highly recommend reading the blog post. He does a good job talking about the boring stuff, which is to say, talking about the interesting stuff when reflecting on how this project went.
Three other things come to mind, from my side of the fence:
- This is the largest infrastructural project that Buttondown has done which I have not been a part of — a fact that is both surreal and, frankly, very cool.
- Mati hinted at this in the blog post, but outside of the obvious quantitative improvements brought by PlanetScale, the insights dashboard is worth its weight in gold. Being able to very easily see problematic queries and fix them has drastically changed our database posture, perhaps even more than the change in hardware itself. I find myself staring at the insights dashboard and thinking about what other parts of the infrastructure — CI tests, outbound SMTP, etc. — we need this for as well.
- PlanetScale folks recommended we start using sqlcommenter to annotate our queries with the API routes that generated them. This was a good suggestion, but that package has the same malnourishment problem that so many Google-born OSS projects do: you end up pulling in a lot of dependencies, including some very old ones, to execute what is essentially 50 lines of code. Rather than do that, I asked Mr. Claude to vend the relevant snippets into a middleware that we can plop into our codebase. It is below:
import urllib.parse
import django
from django.db import connection
from django.db.backends.utils import CursorDebugWrapper
DJANGO_VERSION = django.get_version()
def url_quote(s: str) -> str:
"""URL-encode a string, escaping percent signs for SQL compatibility."""
quoted = urllib.parse.quote(s)
# Since SQL uses '%' as a keyword, '%' is a by-product of url quoting
# e.g. foo,bar --> foo%2Cbar
# thus in our quoting, we need to escape it too to finally give
# foo,bar --> foo%%2Cbar
return quoted.replace("%", "%%")
def generate_sql_comment(
controller: str | None, route: str | None, framework: str
) -> str:
"""Return a SQL comment with key=value pairs for the given metadata."""
parts = [f"framework={url_quote(framework)!r}"]
if controller:
parts.append(f"controller={url_quote(controller)!r}")
if route:
parts.append(f"route={url_quote(route)!r}")
return " /*" + ",".join(sorted(parts)) + "*/"
class Middleware:
"""
Middleware to append a comment to each database query with details about
the framework and the execution context.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
with connection.execute_wrapper(QueryWrapper(request)):
return self.get_response(request)
class QueryWrapper:
def __init__(self, request):
self.request = request
def __call__(self, execute, sql, params, many, context):
resolver_match = self.request.resolver_match
sql_comment = generate_sql_comment(
controller=resolver_match.view_name if resolver_match else None,
route=resolver_match.route if resolver_match else None,
framework=f"django:{DJANGO_VERSION}",
)
sql += sql_comment
if context["cursor"].__class__ is CursorDebugWrapper:
context["connection"].queries_log.append(sql)
return execute(sql, params, many, context)