PostgreSQL: column "companyid" does not exist con Prisma

Abres psql (o cualquier cliente SQL) contra una base de datos creada por Prisma, escribes una consulta con los nombres que ves en schema.prisma y PostgreSQL responde con alguno de estos errores:

ERROR:  column "companyid" does not exist
LINE 1: SELECT companyId FROM "Pedido";
               ^
HINT:  Perhaps you meant to reference the column "Pedido.companyId".

ERROR:  column reference "status" is ambiguous

ERROR:  UNION could not convert type "DocumentStatus" to "OrderStatus"

Parecen tres problemas distintos, pero salen del mismo esquema: nombres en camelCase o PascalCase que PostgreSQL trata de forma diferente según lleven o no comillas dobles. Los mensajes de este artículo se reprodujeron en PostgreSQL 16.4 (con el SQL que genera Prisma 6.19.3) y en PostgreSQL 18.3.

Por qué pasa

1. Los nombres sin comillas se pasan a minúsculas. La documentación de PostgreSQL explica que los nombres sin comillas se convierten siempre a minúsculas, y que entrecomillar un identificador lo vuelve sensible a mayúsculas: "Foo" y foo son nombres distintos. Prisma, con su configuración por defecto (sin @map ni @@map), usa los nombres de modelos y campos tal cual. Este es el SQL que genera para un modelo Pedido:

CREATE TYPE "OrderStatus" AS ENUM ('PENDING', 'PAID', 'CANCELLED');

CREATE TABLE "Pedido" (
    "id" SERIAL NOT NULL,
    "companyId" INTEGER NOT NULL,
    "status" "OrderStatus" NOT NULL DEFAULT 'PENDING',
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT "Pedido_pkey" PRIMARY KEY ("id")
);

La tabla, las columnas y los tipos enumerados se crearon entre comillas con mayúsculas. Si escribes companyId sin comillas, PostgreSQL busca companyid. Lo mismo ocurre con la tabla: SELECT * FROM Pedido falla con relation "pedido" does not exist.

2. Dos tablas con una columna del mismo nombre. En un JOIN, status existe en ambas tablas y PostgreSQL no sabe cuál elegir. Los modelos de Prisma suelen compartir id, status, companyId o createdAt.

3. Dos enumeraciones distintas no se pueden unificar. Una enumeración como "OrderStatus" y otra como "DocumentStatus" son tipos diferentes y no existe conversión implícita entre ellos. La documentación de PostgreSQL sobre resolución de tipos en UNION indica que la consulta falla si no hay una conversión implícita hacia el tipo candidato.

Solución

  1. Entrecomilla con comillas dobles todo identificador que tenga mayúsculas, en cualquier parte de la consulta (SELECT, WHERE, GROUP BY, ORDER BY, ON):

    SELECT "companyId", COUNT(*) AS total
    FROM "Pedido"
    GROUP BY "companyId"
    ORDER BY "companyId";

    Las comillas dobles son para identificadores y las simples para valores. Las etiquetas de un enumerado distinguen mayúsculas: WHERE "status" = 'paid' falla con invalid input value for enum "OrderStatus": "paid"; el valor correcto es 'PAID'.

  2. Consulta los nombres reales en lugar de adivinarlos. En psql, el patrón de \d también se pasa a minúsculas salvo que lo entrecomilles, según su documentación:

    \d "Pedido"
    \dt

    Con cualquier cliente SQL sirve information_schema. Aquí 'Pedido' es un valor, así que conserva las mayúsculas:

    SELECT column_name, data_type
    FROM information_schema.columns
    WHERE table_schema = 'public' AND table_name = 'Pedido'
    ORDER BY ordinal_position;
  3. Califica las columnas con alias de tabla. El alias resuelve la ambigüedad y deja claro de qué tabla viene cada dato:

    SELECT p."companyId",
           p."status" AS estado_pedido,
           f."status" AS estado_factura
    FROM "Pedido" p
    JOIN "Factura" f ON f."pedidoId" = p."id";
  4. Convierte los enumerados a texto en cada rama del UNION:

    SELECT 'pedido' AS origen, "companyId", "status"::text AS estado FROM "Pedido"
    UNION ALL
    SELECT 'factura', "companyId", "status"::text FROM "Factura";

    Los nombres de las columnas del resultado salen de la primera consulta. El mismo problema aparece al comparar dos enumerados distintos con =: el error es operator does not exist: "OrderStatus" = "DocumentStatus", y se resuelve igual, con ::text en ambos lados.

  5. Opcional, para proyectos nuevos: mapea a snake_case desde el principio. La documentación de Prisma explica que @map y @@map desvinculan los nombres del modelo y de los campos de los nombres de tablas y columnas, y que el cliente sigue usando los nombres del esquema:

    model Pedido {
      id        Int         @id @default(autoincrement())
      companyId Int         @map("company_id")
      status    OrderStatus @default(PENDING)
      createdAt DateTime    @default(now()) @map("created_at")
    
      @@map("pedidos")
    }

    Con ese esquema, SELECT company_id, COUNT(*) FROM pedidos GROUP BY company_id funciona sin comillas. Los tipos enumerados siguen creándose como "OrderStatus" mientras no les añadas @@map. No lo apliques a ciegas a una base con datos: al añadir los mapeos a un esquema existente, prisma migrate diff generó DROP TABLE seguido de CREATE TABLE, lo que borra los datos. Revisa el SQL de la migración antes de ejecutarla y sustituye ese par por renombrados, por ejemplo ALTER TABLE "Pedido" RENAME TO pedidos; y ALTER TABLE pedidos RENAME COLUMN "companyId" TO company_id;. En la prueba, estos renombrados conservaron las filas.

Cómo verificar que quedó resuelto

Con tres pedidos de prueba (dos de la empresa 1 y uno de la empresa 2), la consulta del paso 1 debe devolver un recuento por empresa:

 companyId | total
-----------+-------
         1 |     2
         2 |     1
(2 rows)

Para comprobar qué nombre resuelve PostgreSQL, to_regclass distingue ambas formas:

SELECT to_regclass('"Pedido"') AS con_comillas, to_regclass('Pedido') AS sin_comillas;

La primera columna devuelve "Pedido" y la segunda NULL. Comprueba también la consulta con JOIN y la del UNION ALL: deben ejecutarse sin error y mostrar los estados como texto.

Variantes y errores relacionados

missing FROM-clause entry for table "pedido"

Aparece al calificar una columna con el nombre de la tabla sin comillas (Pedido.companyId) cuando la tabla se creó como "Pedido". Usa "Pedido"."companyId" o, mejor, un alias.

El HINT no siempre aparece

En la prueba, el error de columna incluyó la sugerencia con el nombre exacto ("Pedido.companyId"), pero el de tabla (relation "pedido" does not exist) no. Cuando falte, usa la consulta de information_schema del paso 2.

Los alias de columna también se pliegan a minúsculas

COUNT(*) AS totalPedidos devuelve una columna llamada totalpedidos, y AS "totalPedidos" conserva las mayúsculas. Si el código que lee el resultado espera totalPedidos, usa comillas en el alias o un nombre en minúsculas con guion bajo.

Comillas en SQL escrito dentro de código

Las comillas dobles de los identificadores deben llegar intactas a PostgreSQL. Si escribes el SQL en una cadena de JavaScript, delimítala con comillas simples o con backticks y deja las comillas dobles por dentro.

Resumen

  • PostgreSQL convierte a minúsculas los nombres sin comillas; Prisma crea tablas, columnas y enumerados con mayúsculas entre comillas, así que hay que entrecomillarlos: "companyId", "Pedido".
  • Revisa los nombres reales con \d "Tabla" o con information_schema.
  • Cuando dos tablas comparten una columna, califícala con un alias: p."status".
  • En un UNION con enumerados distintos, convierte cada rama con ::text.
  • Para proyectos nuevos, @map y @@map evitan las comillas; en una base con datos, revisa la migración porque puede borrar y recrear las tablas.

Referencias

Comentarios