Schemint

paste PostgreSQL or JSON, get your schema reviewed like a senior would

TEXT vs VARCHAR(n) in PostgreSQL: the length limit myth

In PostgreSQL, TEXT and VARCHAR(n) are the same type with different clothes. Same storage (varlena, TOAST when large), same performance, same indexes. The (n) is not an optimization hint and does not reserve space; it's a length check the server runs on write, nothing more. If you came from Oracle or MySQL expecting VARCHAR(255) to be leaner than TEXT, Postgres has no such distinction.

So the limit is useless?

No, it's a constraint, and constraints are about intent, not speed. The question for each column is: does a maximum length mean something in the domain?

What a wrong guess costs

The failure arrives as ERROR: value too long for type character varying(255) in production, on data a user legitimately typed. The fix is cheap in modern Postgres, but not free:

ALTER TABLE articles ALTER COLUMN summary TYPE VARCHAR(2000);
-- or drop the bound entirely
ALTER TABLE articles ALTER COLUMN summary TYPE TEXT;

Widening a varchar doesn't rewrite the table, but it does take an ACCESS EXCLUSIVE lock for its (brief) duration, needs a migration, a deploy and usually a matching change in the application layer (@Column(length), DTO validation, form field). Multiply by every column where 255 was a reflex.

A sane default policy

CREATE TABLE articles (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    slug VARCHAR(80) UNIQUE NOT NULL,   -- bounded: it's a URL segment
    title VARCHAR(200) NOT NULL,        -- bounded: UI and SEO contract
    summary TEXT,                       -- unbounded prose
    body TEXT NOT NULL                  -- unbounded prose
);

Bound what has a real limit, use TEXT for prose, and avoid the third option people reach for: VARCHAR with no length. It's legal and it behaves exactly like TEXT, but it reads like someone forgot the number, and every ORM and codegen tool has to pick a default for it (Schemint flags it and defaults the Entity to 255, which is exactly the kind of silent guess you don't want).

If you want a bound on a TEXT-typed column without the type-change ceremony later, a CHECK gives you the same protection and drops without rewriting anything:

ALTER TABLE articles
  ADD CONSTRAINT summary_len CHECK (length(summary) <= 2000);

How it maps to code

Either way you get String in Java and string in TypeScript; the length never changes the type. What it changes is metadata: Schemint carries VARCHAR(n) into @Column(length = n) on the JPA entity and into z.string().max(n) in the Zod schema, so the same bound is enforced at the API edge, in the model and in the database. A TEXT column produces an unbounded String, which is also correct: it tells the reader "prose lives here".

One MySQL habit worth dropping at the border: TINYTEXT, MEDIUMTEXT and LONGTEXT don't exist in Postgres, and CHAR(n) pads with spaces to exactly n, which then leaks into comparisons and trailing-space bugs; unless a value is truly fixed-width, prefer VARCHAR(n).

Paste your DDL and Schemint will point at the lengthless VARCHARs and CHAR(n) columns before they become an Entity with a guessed 255.

Related