今日已更新 240 条资讯 | 累计 23823 条内容
关于我们

标签:#Postgres

找到 76 篇相关文章

AI 资讯

Why I Chose Neon (dev.to Database Partner) for My AI Routing Platform

When Neon became the official database partner of DEV Community, I was already a user. But the partnership made me look closer at why I chose Neon — and whether those reasons apply to other AI developers. They do. Here's why Neon is the ideal database for AI applications in 2026. The Problem: AI Apps Have Unique Database Needs AI applications have database requirements that traditional web apps don't: High write volume — every AI request generates logs, metrics, and cost data Variable load — traffic spikes when a model goes viral, then drops to zero Schema evolution — you're constantly adding models, routing rules, and analytics tables Dev/prod parity — you need to test routing changes against real production data Edge compatibility — AI APIs need sub-100ms response times globally Traditional PostgreSQL (RDS, Aurora) struggles with all five. Neon was built for them. Feature 1: Database Branching (The Game-Changer) This is Neon's killer feature. It works like git branch but for your entire database: # Create a branch from production neon branches create --parent main --name test-deepseek-v31 # Get a connection string for the branch neon connection-string test-deepseek-v31 # → postgresql://...@ep-test-deepseek...neon.tech/neondb # Run migrations on the branch npx prisma db push --url $BRANCH_URL # Test your new routing algorithm against REAL data # (the branch is a copy-on-write clone of production) # When tests pass, merge neon branches merge test-deepseek-v31 Why This Matters for AI Apps When I added DeepSeek V3.1 to my model pool, I needed to test: Would the new model break existing routing rules? Would the cost calculations be correct? Would the latency meet my SLA? With traditional PostgreSQL, testing against real data meant either: Copying production to a staging DB (hours, $$) Testing with synthetic data (unreliable) With Neon branching, I branched, tested in 30 seconds, and merged. Zero downtime, zero risk. Feature 2: Scale-to-Zero (Cost Optimization) Neon's c

2026-07-10 原文 →
AI 资讯

“PostgreSQL resolves uniqueness through heap tuple visibility”

I recently commented on Jonathan Lewis’s blog, Savepoint Funny , where I compared how PostgreSQL handles uniqueness differently: “PostgreSQL resolves uniqueness through heap tuple visibility". This deserves a more detailed explanation. In Oracle, unique indexes store unique entries because the B-tree key is the index key, preventing duplicates. Non-unique indexes add the ROWID to ensure that all entries are physically unique, even when indexed column values are duplicated. In PostgreSQL, all indexes, even unique ones, created explicitly by CREATE UNIQUE INDEX or implicitly to enforce a unique constraint, behave like non-unique indexes by appending the TID (tuple ID, similar to Oracle's ROWID) to the index key. This indicates that the index itself doesn't guarantee physical uniqueness, allowing multiple entries to have identical logical keys but point to different heap tuples. The actual uniqueness verification occurs at the heap level, not within the index entries. Initially, this might seem unusual—a unique index that permits duplicates. However, PostgreSQL requires this because of its MVCC system. MVCC allows duplicate entries to coexist in an index, since they can represent different versions of the same logical row. Still, PostgreSQL must guarantee that no MVCC snapshot views two rows with the same index key. Oracle doesn't face this issue because its MVCC implementation also versions index blocks, allowing a single index version to maintain unique keys. Let’s show that. Page inspect In PostgreSQL, the heap contains the table data, and index entries point to heap tuples. Visibility depends on the heap header, especially the transaction information. Index scans often visit the heap pages to check visibility, except for index-only scans, which use the heap's visibility maps as an optimization. B-tree indexes can store entries for multiple versions of the same logical row, including versions that are no longer visible to current snapshots. To ensure uniqueness, the

2026-07-09 原文 →
AI 资讯

RLS recursion infinite loop: why I gave up policies and bet everything on a JWT custom claims hook

Episode 1/4 — 3 incidents, one root: default GRANTs open more than you think — [CANONICAL URL EPISODE 1: fill in after push] Episode 2/4 — await mutation() lies when nobody opens the { error } envelope — [CANONICAL URL EPISODE 2: fill in after push] The morning Françoise sees zero rows, again It's a Tuesday in April 2026. I've just added the agent_readonly role to the authenticated membership — a one-liner, meant to share a GRANT for a reporting job. First SELECT on cours , Sentry receives infinite recursion detected in policy for relation "user_roles" , code 42P17 . From the office next door, Françoise is already on the phone with the Maisons-Laffitte branch: "So they can't see anything over there — is that normal?" Foreman tone, not really a question. I read the error on my screen. The difference from episode 1: this time Postgres is talking. What came out of Sentry was no longer a silent empty set — it was an explicit error. That difference saved me two days. When Postgres shouts, you listen. The trap is that what it says isn't where you're looking. I won't pretend this is obscure. A policy on user_roles that queries user_roles to decide who can read user_roles is a loop. You avoid it, you work around it with SECURITY DEFINER , you move on. The problem: my user_roles policy didn't reference user_roles . I had already cleaned it up three weeks earlier. The recursion was coming from somewhere else. The diagnostic that targets the wrong object First reflex: re-read the user_roles policy. It's clean, reads auth.email() , never calls itself. Second reflex: disable policies one by one to find the culprit. Wrong angle. -- supabase/migrations/20260420_admin_write_cours_v1.sql -- "Admin write cours" policy — original version that loops CREATE POLICY "Admin write cours" ON public . cours FOR ALL TO authenticated USING ( EXISTS ( SELECT 1 FROM public . user_roles WHERE email = auth . email () AND role IN ( 'admin' , 'super_admin' ) ) ); The recursion doesn't come from a fau

2026-07-09 原文 →
AI 资讯

The PostgREST query that silently ORDER BY ctid: a Supabase week, distilled

The fourth call of the week Catherine calls from the Maisons-Laffitte site on a Tuesday afternoon in early May. "It's broken, but it's a quick fix." That's her line — I know it, and she's usually right. She describes it in three sentences: the newsletter export for the enrolled-students segment comes back with ninety-two names, the planning view shows ninety-two active courses, but the counter page shows eighty-nine. Three enrolled students missing. She'd checked the database directly — they're all there. "Why three steps for that?" She's not asking for my benefit. She's asking for herself. Except this time, hanging up, I realize it's the fourth time this week I've hung up thinking the same thing. Four Supabase incidents, four fixes, four closed tickets. And not a single exception raised by the database. I reopen the three previous ones and lay all four side by side on screen. This isn't four bugs. It's one failure mode, declinated four times. The first three Episode 1 was about the default GRANT s Supabase places on functions and policies. A SQL function created without an explicit REVOKE inherits anon access that nobody wrote in the migration, and that nobody caught in review because the diff doesn't show it. The function works. It's just callable from outside. [CANONICAL URL EPISODE 1: to fill in after publication of #48 — "3 Supabase security incidents, one shared root cause: SECURITY DEFINER inherits EXECUTE TO PUBLIC"] Episode 2, an ON DELETE SET NULL cascade coupled with a CHECK NOT NULL on the target column. The parent DELETE attempts the SET NULL , the CHECK rejects it, and the transaction surfaces an error we read as a deletion failure — while it actually masks a consistency assumption we'd held for three months. The query fails loudly, which is more charitable than the other three cases, but the diagnosis heads in the wrong direction because nobody had declared that the two constraints lived in tension. [CANONICAL URL EPISODE 2: to fill in after publicati

2026-07-09 原文 →
AI 资讯

Multi-tenant SaaS architecture patterns

Multi-tenancy is the decision that quietly shapes your entire SaaS backend. Get it right and you scale smoothly to thousands of accounts. Get it wrong and you're rewriting your data layer under load, mid-growth, with customers watching. The good news: for most products the right answer is simpler than the internet suggests. The three models There are three canonical ways to isolate tenants, and they trade isolation against operational cost: Row-level (shared schema). Every table has a tenant_id\ column, and every query filters on it. One database, one schema, all tenants together. Schema-per-tenant. Each tenant gets its own PostgreSQL schema inside a shared database. Stronger isolation, more objects to manage. Database-per-tenant. Each tenant gets a dedicated database or instance. Maximum isolation, maximum operational weight. Why row-level wins for most SaaS For the overwhelming majority of B2B SaaS products, row-level multi-tenancy is the right default. It's the cheapest to operate, the easiest to run migrations against, and it scales further than founders expect. The objection is always "but isolation" — and Postgres has a strong answer. Row-Level Security (RLS) lets the database itself enforce that a query can only see its own tenant's rows. With Supabase , RLS is the native model: you set a policy once, and even a buggy query can't leak across tenants. Combined with a tenant_id\ on every table and an index that leads with it, this pattern comfortably serves large customer bases. One caution from hard experience: write RLS policies so helper functions run once per query, not once per row . A policy that re-evaluates a lookup for every row will quietly turn fast endpoints slow as tables grow. Wrap the check so the planner runs it as an init-plan. When to reach for stronger isolation Escalate deliberately, not reflexively: Regulatory or contractual isolation — a customer requires their data in a physically separate database. Noisy-neighbor risk — one whale tenant'

2026-07-09 原文 →
AI 资讯

Como servir os 68 milhões de CNPJs da Receita com ~10ms de latência em Go

Todo dev brasileiro que já precisou consultar CNPJ conhece o dilema: ou você usa uma API que faz proxy da Receita (3 a 10 segundos por consulta, quando não cai), ou baixa o dump de dados abertos e monta a própria base — e descobre que "baixar um CSV" era a parte fácil. Eu montei a própria base. Este post é o diário honesto do que funcionou, do que quebrou e dos números reais — 217 milhões de linhas servidas em ~10ms de p50 dentro do datacenter, num Postgres de 1 vCPU. A arquitetura em uma frase Não consulte a Receita em tempo real. Ingira o dump mensal e sirva da sua infra. O resto é decorrência. Receita (dump mensal, ~6GB zip) ──▶ ingestão Go (COPY) ──▶ Postgres ──▶ API (chi) CGU (CEIS/CNEP, zip diário) ──▶ job diário ──┘ O dump da Receita: as pegadinhas que ninguém documenta O layout oficial existe, mas o que quebra parser de verdade é o que está fora dele: Encoding latin1 (ISO-8859-1) — acento vira lixo se você ler como UTF-8. Em Go: charmap.ISO8859_1.NewDecoder() num transform.Reader streaming. Decimal com vírgula ( "1000000,00" ) e datas YYYYMMDD onde 0 e 00000000 significam nulo. CNPJ quebrado em 3 colunas (básico 8 + ordem 4 + DV 2). A chave de junção entre empresas, estabelecimentos e sócios é o básico — errar isso custa um dia. As partições 0–9 não se alinham entre arquivos. O estabelecimento da partição 3 pode ser de uma empresa da partição 7. Foreign key rígida entre as tabelas = COPY quebrando no meio da carga. A solução: sem FK; a integridade vem da fonte. Bytes NUL ( 0x00 ) no meio dos dados. O Postgres rejeita NUL em text . Um strings.ReplaceAll(s, "\x00", "") no parser economizou três recargas. Desde jan/2026 o repositório é um Nextcloud do SERPRO+ com WebDAV público — dá pra listar meses com PROPFIND e baixar com o token do share como usuário. Adeus, scraping. COPY ou morte A diferença entre INSERT em lote e o protocolo COPY não é incremental — é outra categoria. Com pgx.CopyFrom e lotes de 50k: 28,1 milhões de empresas em 1m28s (~320k linhas/s) num

2026-07-06 原文 →
AI 资讯

PostgreSQL query planner parameters and prepared statements

PostgreSQL provides several planner configuration parameters, such as enable_seqscan and enable_indexscan , that influence how execution plans are generated. These settings affect planning, not the execution of an already-generated plan. With prepared statements, this raises an interesting question. Should planner settings be applied before PREPARE, before EXECUTE, or both? Let's look at a simple example: a "tasks" table with a due date and a "done" status: \ c drop table if exists tasks ; -- a table of tasks with status (done or not) and due date create table tasks ( id bigint generated always as identity primary key , due timestamptz , done boolean ); -- insert 500 tasks, with 1% not done insert into tasks ( due , done ) select now () + interval '1 day' * n , 42 != n % 100 from generate_series ( 1 , 500 ) n ; -- index the todo (partial index) create index on tasks ( due , id ) where done = false ; vacuum analyze tasks ; With a partial index, I indexed only the tasks that are not yet done ( done = false ) because that's my most frequent query pattern: postgres =# explain select id , due , done from tasks where done = false and id > 0 order by due limit 1 ; QUERY PLAN --------------------------------------------------------------------------------------- Limit ( cost = 0 . 13 .. 3 . 60 rows = 1 width = 17 ) -> Index Scan using tasks_due_id_idx1 on tasks ( cost = 0 . 13 .. 17 . 47 rows = 5 width = 17 ) Index Cond : ( id > 0 ) ( 3 rows ) With partial indexes, the condition covered by the index is not even visible in the execution plan because the index itself enforces the condition. Prepared statement I decided to use a prepared statement with all values as parameters. It is probably not a good idea in this case. When a parameter can have only a few different values and you expect different cardinalities for each, you should probably define one query per value, using literals. I'm doing this to illustrate what can happen, with a simple, extreme example: postgres =# pr

2026-07-06 原文 →
AI 资讯

3 Supabase security incidents, one shared root cause: SECURITY DEFINER inherits EXECUTE TO PUBLIC

Episode 1/4 of the mini-series The week Supabase lied to me 4 times . The three following episodes cover a mutation silently swallowed by the SDK [CANONICAL URL EPISODE 2: to fill in after push], an RLS recursion resolved by a JWT hook [CANONICAL URL EPISODE 3: to fill in after push], and a query that stops at exactly 1000 rows without saying so [CANONICAL URL EPISODE 4: to fill in after push]. The Tuesday the security probe spoke It's 9:12am on a Tuesday in May. The daily drift probe has been running automatically for three weeks — an aclexplode query across all public objects, filtered on anon . I don't open it every morning. That morning, it's waiting for me with a row that has no business being there. Niran sets a coffee on the corner of my desk without a word. He reads the output over my shoulder. A PII backup table — personal data in plaintext, created two days earlier for a bulk reclassification — shows up in the list with SELECT , INSERT , UPDATE , DELETE granted to anon . Accessible to any unauthenticated curl request. He lets three seconds pass and says: "It's not RLS." Then he goes back to his hoodie. He's right. It's not an RLS bug. The table itself is open, at the GRANT layer, before RLS even applies. Three objects, three doors, one mechanism That week, I realize I'm not dealing with an isolated incident. Three distinct objects, in three different migrations, each open a door nobody thought they'd opened. The backup table first. Then a policy set TO public because the public landing page needs it, which lets a POST {} from anon through with an HTTP 400 NOT NULL response instead of 401 Unauthorized . And finally four SECURITY DEFINER functions written to execute transactional operations with their owner's privileges, all invocable by anon because EXECUTE defaults to TO PUBLIC at CREATE time. Three objects, three superficially distinct mechanisms, yet one shared root. At every CREATE , Postgres completes the migration with an implicit GRANT the author nev

2026-07-05 原文 →
AI 资讯

From My Machine to the Cloud: Connecting Power BI to SQL Databases; PostgreSQL (Local vs Aiven)

Introduction I used to think "connecting to a database" was one skill. Turns out it's two: connecting to a database chilling quietly on your own laptop, and connecting to one living in the cloud, behind a login, in this case, an SSL certificate that will not let you in until you treat it with respect. This week I did both. Same tool (Power BI), same dataset, two very different vibes. Grab a coffee, here's the full walkthrough local PostgreSQL first, then Aiven's cloud version, side by side, screenshots and all. Part 1: Local PostgreSQL → Power BI Step 1 : Create a schema Nothing fancy, just giving my table a home: CREATE SCHEMA powerbi ; Step 2 : Import the dataset Right-click the new schema → Import Data in DBeaver, point it at your CSV, and let the wizard do its thing. Step 3 : Check the table landed properly A quick peek at the columns to make sure nothing got mangled on the way in. Step 4 : Connect Power BI In Power BI Desktop: Get Data → Database → PostgreSQL database. In the Server field, type localhost (or 127.0.0.1 ) and your database name. localhost Choose Import , hit OK, and log in with your local username and password. Click Load . That's it. That's the whole local experience. Part 2: Aiven PostgreSQL (Cloud) → Power BI Now for the part that actually taught me something. Step 1 : Grab your connection details Everything you need lives on Aiven's Overview page: Host, Port, Database name, User, SSL mode. Your service URI will look something like this (don't worry, this isn't a real password, Aiven masks it in the console): postgres : // avnadmin : •••••••• @ pg - xxxxxxxx - yourproject . c . aivencloud . com : 22016 / defaultdb ? sslmode = require Step 2 : Import the dataset into Aiven Same DBeaver wizard as before, just pointed at the Aiven connection instead of local. CREATE SCHEMA powerbi ; Step 3 : Aiven's certificate. Download the CA cert from the Overview page: Now here's the part that actually tripped me up: Power BI's PostgreSQL connector doesn't ha

2026-07-05 原文 →
AI 资讯

I just published Postgres MCP Server in Go!

I open sourced a project I have been building on the side: a Go MCP server that connects Claude Code (or Cursor) directly to a live PostgreSQL database. Repo: github.com/gupta-akshay/postgres-mcp The problem it solves Most "AI plus database" workflows still look like this: copy SQL out of a chat window, paste it into a DB client, run it, copy the output back. It breaks flow, and the assistant never sees your actual schema, so it guesses. MCP fixes the connection problem. This server is what sits on the other end for Postgres. What it does The server exposes nine tools over MCP: Schema introspection - real tables, columns, indexes, constraints execute_sql - run queries directly (read only in restricted mode) explain_query - EXPLAIN ANALYZE, including against a hypothetical index get_top_queries - pull slow queries from pg_stat_statements Index advisors - recommend indexes using a greedy Database Tuning Advisor built on hypopg analyze_db_health - vacuum, XID wraparound, replication lag, invalid indexes, and more, checked in parallel That means you can ask "why is this query slow" and the assistant actually runs the EXPLAIN, checks the stats, and can simulate an index before anyone touches the schema. Why Go The project is inspired by the Python crystaldba/postgres-mcp . I rebuilt it from scratch in Go so it ships as a single ~15 MB static binary. No Python runtime, no dependency chasing. docker build , point Claude Code at it, done. Restricted mode wraps every call in a read only transaction, so write protection comes from Postgres itself, not string matching on the query text. Where to look The repo has the full setup instructions, the Docker config, and the test suite (unit, integration, and end to end against a real Postgres container with pg_stat_statements and hypopg ). CI fails under 95% coverage. If you spend real time in Claude Code or Cursor and also spend real time worrying about Postgres performance, take a look: github.com/gupta-akshay/postgres-mcp I wrote

2026-07-04 原文 →
AI 资讯

I Moved My Next.js Dashboard Logic Into Postgres. My Frontend Got Boring (And That's the Point).

My dashboard had a useMemo doing arithmetic it had no business doing. It was a Pokémon TCG Pocket collection tracker, but that part doesn't matter. What matters is that the home page needed to show three things: overall completion, completion per set, and which set you were closest to finishing. The way I'd built it, the browser was fetching every card and every owned record, then grinding through the math on each render to figure all of that out. It worked. It also got slower and harder to read every time the data grew or I added a metric. So I moved the aggregation out of React and into Postgres, and the surprising result was that my frontend got boring . Fewer hooks, less state, almost nothing left to break. That's the whole argument of this post: aggregation belongs in the database, and when you put it there, the React code that's left over is the kind of boring you actually want in the layer your users touch. What "fetching everything into React" actually looks like Here's the shape of the original dashboard. Load all the cards, load the user's owned rows, then derive everything on the client. const [ cards , setCards ] = useState < Card [] > ([]); const [ owned , setOwned ] = useState < OwnedCard [] > ([]); useEffect (() => { ( async () => { const { data : allCards } = await supabase . from ( " cards " ). select ( " * " ); const { data : ownedRows } = await supabase . from ( " user_cards " ) . select ( " card_id " ); setCards ( allCards ?? []); setOwned ( ownedRows ?? []); })(); }, []); const ownedIds = useMemo (() => new Set ( owned . map (( o ) => o . card_id )), [ owned ]); const perSet = useMemo (() => { const groups : Record < string , { total : number ; have : number } > = {}; for ( const card of cards ) { const g = ( groups [ card . set_id ] ??= { total : 0 , have : 0 }); g . total += 1 ; if ( ownedIds . has ( card . id )) g . have += 1 ; } return groups ; }, [ cards , ownedIds ]); const overall = useMemo (() => { const total = cards . length ; const ha

2026-06-30 原文 →
AI 资讯

Shifting Left: How TDD Became the Foundation of SokoFlow's Core Engine

SokoFlow Build Log — Month 1 of 4 Last semester I set out on a new strategic plan to level up my software development skills through deliberate, project-based learning. That work produced one of the most ambitious things I've built so far: Sim-Pesa , a local-first transactional appliance that lets developers working in the M-Pesa ecosystem test and simulate STK Push workflows entirely on their own machines, without depending on the Daraja sandbox. I documented that build in 16 weekly posts, which you can find here . This semester, the focus shifts — from fintech foundations to cloud-native integration and real-world systems. The flagship project is SokoFlow , a conversational ERP for small Kenyan shopkeepers to track inventory and record sales entirely through WhatsApp chat. No app to download, no training session required — just natural language. Where Sim-Pesa lived in a controlled, predictable transactional world, SokoFlow steps into the mess of cloud-native reality: third-party API failures, webhook signature verification, the statelessness of HTTP, and container orchestration. The target audience shifts too — Kenyan SMEs operating on infrastructure that is often unreliable by design, not by exception. It's an ambitious project, but the goal was always to learn as much as possible from it. With the plan in place, I got to work. 1. The Vision of a Headless ERP The first real question I had to answer before writing a line of code: what does "headless" actually mean? Headless architecture decouples the frontend — the "head," or user interface — from the backend, the "body" that holds the data and business logic. A conventional ERP bundles both: backend plus a dashboard or UI on top. A headless ERP, by contrast, is just the engine. The brain. There's no built-in screen. So how do users interact with a system that has no interface of its own? SokoFlow doesn't actually care. It could be: WhatsApp SMS A web app A mobile app A voice assistant In this case, the "frontend

2026-06-30 原文 →
AI 资讯

My "serverless" database was billing me like it never slept.

My "serverless" database was billing me like it never slept. Neon has this great feature called scale-to-zero. Your Postgres compute suspends when it's idle, so you only pay for the queries you actually run. For a pre-revenue product, that should cost a few cents a month. Mine ran 24/7. The compute never once scaled to zero. The culprit wasn't my app logic. It was my database driver. I was using postgres-js, which holds a persistent connection open to the database. From Neon's side, an open connection looks like activity, so it never suspended. It just stayed awake and kept quietly billing me. The fix was basically one conceptual change: switch to @neondatabase/serverless, the HTTP driver. Instead of a long-lived connection, every query becomes a stateless one-shot HTTP request. The query fires, the connection closes, and the compute is free to suspend. Scale-to-zero finally worked. The lesson I keep relearning as a solo founder: "serverless" is a property of your whole stack, not a checkbox on one service. One persistent connection upstream and the whole cost model breaks.

2026-06-30 原文 →
AI 资讯

🗄️ The JPA Enum Default Quietly Corrupts Your Data

You add an enum to an entity, slap @Enumerated on it, and move on. Five seconds. It is the kind of decision nobody writes a design doc for. Then six months later a row comes back as SHIPPED when it was PAID , no exception was thrown, no query failed, and you spend an afternoon learning that the default you never thought about has been silently rewriting history. Here is the order lifecycle we will use the whole way through: public enum OrderStatus { PENDING , PAID , SHIPPED , DELIVERED } Five ways to store it. They are not equivalent, and the gap between them only shows up under change. @Enumerated(ORDINAL): store the position This is the default. Leave the annotation bare and JPA stores the enum's ordinal, its index in the declaration order. @Enumerated ( EnumType . ORDINAL ) private OrderStatus status ; PENDING is 0, PAID is 1, SHIPPED is 2, DELIVERED is 3. The column is a tidy little smallint . Everything works. Until someone needs a new status and adds it where it reads well: public enum OrderStatus { PENDING , PAID , CANCELLED , // inserted here SHIPPED , DELIVERED } CANCELLED is now 2. SHIPPED is 3. DELIVERED is 4. Every row written before this change still holds the old integer, so every order that was SHIPPED (2) now reads back as CANCELLED . The database is correct. Your data is wrong. And nothing told you. If you are stuck with ORDINAL on a legacy schema, pin it with a test that fails the build the moment someone reorders: @Test void ordinalsAreFrozen () { assertEquals ( 0 , OrderStatus . PENDING . ordinal ()); assertEquals ( 1 , OrderStatus . PAID . ordinal ()); assertEquals ( 2 , OrderStatus . SHIPPED . ordinal ()); assertEquals ( 3 , OrderStatus . DELIVERED . ordinal ()); } New constants may only be appended. The test turns an invisible runtime corruption into a loud compile-time-ish failure. It is a guardrail, not a fix. @Enumerated(STRING): store the name Store the constant name instead of its position. @Enumerated ( EnumType . STRING ) private OrderS

2026-06-30 原文 →
AI 资讯

Redis Isn't PostgreSQL: Building a Hybrid Change Data Capture Runtime in Ruby

I Built Commercial Redis CDC Source Drivers for Ruby — Here's What I Learned For the past couple of years I've been building a Change Data Capture (CDC) ecosystem for Ruby. Like many CDC projects, it started with PostgreSQL. PostgreSQL's Write-Ahead Log (WAL) is an excellent source of truth: durable, ordered, replayable, and well understood. It provides exactly the properties you want when you're building reliable event pipelines. But the deeper I went into distributed systems, the more I realized something important. Many systems don't observe change from PostgreSQL first. They observe it from Redis. Redis often sits at the front of modern architectures: Redis Streams carry application events. Pub/Sub distributes transient state changes. Keyspace notifications react to cache invalidation and key expiry. Redis Cluster routes events across multiple primaries. In many systems, Redis sees a change before PostgreSQL ever commits it. That raised an interesting question: Can Redis become a first-class Change Data Capture source? The obvious answer is "yes." The interesting answer is "yes—but not in the same way PostgreSQL does." That distinction eventually became cdc-redis-pro , a commercial Redis source driver for the Ruby CDC ecosystem. This article isn't a product announcement. It's an engineering write-up about the architectural decisions behind the project, the tradeoffs Redis forces you to make, and the execution model that ultimately emerged. Redis Doesn't Have One CDC Interface One misconception I frequently encounter is the assumption that Redis has an equivalent of PostgreSQL's WAL. It doesn't. Instead, Redis exposes several completely different mechanisms for observing change. Source Delivery Replay Streams At-least-once Yes Pub/Sub At-most-once No Sharded Pub/Sub At-most-once No Keyspace Notifications At-most-once No At first glance they all look like "events." Operationally they're completely different systems. Streams are durable. Pub/Sub isn't. Keyspace not

2026-06-27 原文 →
AI 资讯

How I built multi-tenant Row Level Security with Aurora PostgreSQL for a B2B SaaS — H0 Hackathon

I'll be honest: I almost did multi-tenancy the wrong way. When I started building InspectIQ "a SaaS platform for Florida home inspectors" my first instinct was to add a tenant_id column to every table and filter it in the application layer. Every query would have a WHERE tenant_id = :current_tenant clause. Simple, familiar, done. Then I thought about what happens when you forget one. One missing WHERE clause. One endpoint that skips the filter. One inspector sees another inspector's client data. In a home inspection business, that's not just a bug — it's a HIPAA-adjacent nightmare and a trust-destroying moment with your first customer. So I did it properly from day one: Row Level Security at the database layer. What is Row Level Security? RLS is a PostgreSQL feature that lets you define policies directly on tables. When a user queries a table, the policy runs automatically, before your application code even sees the results. You can't forget to apply it. You can't bypass it with a careless JOIN. It's enforced at the lowest possible layer. For a multi-tenant SaaS, this is exactly what you want. How I implemented it Every table in InspectIQ has this pattern: ALTER TABLE inspections ENABLE ROW LEVEL SECURITY ; ALTER TABLE inspections FORCE ROW LEVEL SECURITY ; CREATE POLICY tenant_isolation ON inspections USING ( tenant_id = NULLIF ( current_setting ( 'app.current_tenant_id' , true ), '' ):: uuid ); The FORCE is important — it applies the policy even to the table owner. No superuser backdoor. The tenant context comes from the JWT. When an inspector logs in, their tenant_id is embedded as a custom Cognito claim. The FastAPI middleware extracts it and sets it at the start of every request: await session . execute ( text ( f " SET LOCAL app.current_tenant_id = ' { tenant_id } '" ) ) SET LOCAL scopes the setting to the current transaction. When the transaction ends, it's gone. No leakage between requests. Aurora PostgreSQL Serverless v2 I'm running this on Aurora PostgreSQ

2026-06-26 原文 →
AI 资讯

Deploying a Multi-Module Spring Boot App to Render with PostgreSQL, Redis, Docker, and Flyway

Deploying a Spring Boot backend should be simple in theory. Build the JAR, set the environment variables, connect the database, and ship it. In practice, my deployment exposed several assumptions that worked locally but failed immediately in the cloud. I recently deployed a modular Spring Boot application to Render using Docker, Render Blueprint, PostgreSQL, Redis, Flyway migrations, Spring profiles, Hibernate/JPA, and environment variables. The application worked locally with MySQL and Redis, but deployment exposed several production-specific issues that were easy to miss in local development. This article documents the problems, why they happened, and how I fixed them properly. Who This Article Is For This article is useful if you are deploying a Spring Boot application to Render and your local setup uses MySQL, Redis, Flyway, Docker, or a multi-module Maven structure. It is especially relevant if you are moving from a local MySQL setup to PostgreSQL in the cloud. The Stack The backend was a Java 17 Spring Boot application with multiple Maven modules: alagbafo/ ├── api-contracts ├── core ├── users ├── orders ├── payments ├── wallet ├── notifications ├── admin ├── subscriptions └── app The app module was the actual Spring Boot entry point. Locally, the project used MySQL and Redis: spring.datasource.url = jdbc:mysql://localhost:3306/alagbafo spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver spring.data.redis.host = localhost spring.data.redis.port = 6379 For Render, the target setup was: Spring Boot app PostgreSQL database Redis-compatible Key Value store Docker deployment Flyway migrations Render Blueprint was the best fit because it allowed the infrastructure to be described in a render.yaml file. Step 1: Dockerfile for a Multi-Module Spring Boot App Because the project was a multi-module Maven application, the Dockerfile had to copy all module pom.xml files before copying the source code. This improves Docker layer caching because dependencies can b

2026-06-23 原文 →
AI 资讯

Closing Chapter 1: From Query to Data

We opened Chapter 1 with a single line, SELECT * FROM users WHERE id = 1 . For that line to leave the client and come back as a result row, the PostgreSQL backend went through five stages. First it decided which processing path the message should take; then the parser and analyzer turned the text into a tree and gave it meaning from the catalog. The rewriter expanded views and injected policies to transform the tree, the planner weighed the possible execution paths by cost and picked the cheapest one, and the executor followed that plan, pulling up one tuple at a time and sending them back to the client. Chapter 1 was a story about how a query is processed . What tree a given SQL becomes, what plan it turns into, in what order it runs. From start to finish, a chain of logical transformations. But what every one of those stages ultimately deals with is data. The executor pulls up tuples, yet where on disk those tuples lie and in what shape, how they come up into memory, Chapter 1 never asked. When the planner judged an index scan cheaper than a sequential scan, it never opened up what that index physically is. Chapter 1 followed only the logical journey of a query, leaving untouched the substance of the data that journey stands on. Chapter 2, Storage & Access Methods, opens up that substance. In what unit data sits on disk (page), where disk and memory meet (buffer manager), where and how a row survives (heap), and how that row is found quickly (B-tree and the specialized indexes). The very tuple the planner weighed by cost and the executor pulled up in Chapter 1, where it actually came from and how it came to be there, is what Chapter 2 reveals. If Chapter 1 was the logical life of a query, Chapter 2 is the physical dwelling of data. We now look at how the data a query reaches for actually lives on disk.

2026-06-21 原文 →