En corto

MySQL, en el servidor origen:

1
2
3
4
mysqldump --databases app -h db.example -u dumper -p \
  --hex-blob --single-transaction --set-gtid-purged=OFF \
  --default-character-set=utf8mb4 --routines --triggers --events \
  | sed -E 's/DEFINER=`[^`]+`@`[^`]+`//g' | gzip > app.sql.gz

PostgreSQL, en el servidor origen:

1
2
pg_dump app -h db.example -U dumper --format=plain --no-owner --no-acl \
  | sed -E 's/(DROP|CREATE|COMMENT ON) EXTENSION/-- \1 EXTENSION/g' | gzip > app.sql.gz

Y en los dos casos:

1
2
3
4
5
6
7
SA=$(gcloud sql instances describe my-instance --format='value(serviceAccountEmailAddress)')
gcloud storage buckets add-iam-policy-binding gs://my-dumps \
  --member="serviceAccount:$SA" --role=roles/storage.objectAdmin
gcloud storage cp app.sql.gz gs://my-dumps/

gcloud sql import sql my-instance gs://my-dumps/app.sql.gz --database=app                 # MySQL
gcloud sql import sql my-instance gs://my-dumps/app.sql.gz --database=app --user=postgres  # PostgreSQL

gcloud sql import sql acepta SQL plano o .gz: el formato custom de pg_dump va por pg_restore (abajo).

Los errores y de dónde salen

En Cloud SQL nadie tiene SUPER (MySQL) ni es superusuario (PostgreSQL); el administrador es root@% o postgres, miembro de cloudsqlsuperuser. Todo lo que un dump normal da por hecho de un servidor propio se rompe justo ahí:

Error al importarMotorCausaArreglo
ERROR 1227 (42000) at line N: Access denied; you need (at least one of) the SUPER privilege(s) for this operation (línea de un CREATE … VIEW, TRIGGER, PROCEDURE, FUNCTION o EVENT)MySQLDEFINER=`user`@`host` de un usuario que no existe en la instancia, o de root@localhost, que en Cloud SQL nunca existe. El dump no trae usuarios (y no debe traerlos)Crear esos usuarios antes de importar, o quitar los DEFINER= con el sed
El mismo ERROR 1227 en las primeras líneas, en SET @@GLOBAL.GTID_PURGED=…MySQLEl origen tiene GTID y mysqldump escribe la posición, que solo SUPER puede fijar--set-gtid-purged=OFF
ERROR 1449 (HY000): The user specified as a definer ('app'@'localhost') does not exist al usar la vista, no al importarMySQLDejaste un DEFINER de un usuario que no existe en la instanciaQuitar el DEFINER o crear el usuario con ese host exacto
ERROR: must be owner of extension plpgsqlPostgreSQLCOMMENT ON EXTENSION plpgsql del dump: en Cloud SQL las extensiones pertenecen a cloudsqladminComentar las líneas EXTENSION con el sed
ERROR: role "app_owner" does not existPostgreSQLALTER … OWNER TO y GRANT … TO para roles del servidor viejo--no-owner --no-acl, o crear los roles antes
ERROR: permission denied for schema public o must be owner of … a mitad del dumpPostgreSQLImportaste con un --user que no es el dueño de lo que ya existe en la baseImporta como postgres en una base vacía; reasigna después

MySQL: qué hacer con los DEFINER

Primero mira cuántos y cuáles hay en el origen:

1
2
3
4
SELECT DISTINCT DEFINER FROM INFORMATION_SCHEMA.VIEWS;
SELECT DISTINCT DEFINER FROM INFORMATION_SCHEMA.TRIGGERS;
SELECT DISTINCT DEFINER FROM INFORMATION_SCHEMA.ROUTINES;
SELECT DISTINCT DEFINER FROM INFORMATION_SCHEMA.EVENTS;

Tres salidas, de más a menos fiel al original:

  1. Crear los usuarios en la instancia antes de importar, con el mismo user@host: gcloud sql users create app --host=% --instance=my-instance --password=…. Es lo que recomienda la documentación, y mantiene las vistas y procedimientos exactamente como estaban. root@localhost es la excepción: no se puede crear en Cloud SQL, así que un DEFINER con ese valor hay que cambiarlo sí o sí.
  2. Quitar el DEFINER con el sed de arriba: los objetos se crean con el usuario que importa como definer. Suficiente cuando los usuarios del origen no van a existir en Cloud SQL.
  3. Pasar a SQL SECURITY INVOKER (sed -E 's/SQL SECURITY DEFINER/SQL SECURITY INVOKER/g' además del anterior): la vista o el procedimiento corre con los privilegios de quien lo llama y el definer deja de importar. Cambia el modelo de permisos; revisa que los usuarios de la aplicación puedan leer las tablas de debajo.

El sed es deliberadamente amplio porque DEFINER= aparece también dentro de comentarios condicionales (/*!50013 DEFINER=… SQL SECURITY DEFINER */); borrar solo el DEFINER= deja el resto intacto y válido.

MySQL: los flags, uno por uno

FlagPor qué
--hex-blobColumnas binarias en hexadecimal; sin él, un BLOB con bytes raros se corrompe al pasar por el juego de caracteres
--single-transactionSnapshot consistente de InnoDB sin bloquear tablas; imprescindible en producción
--set-gtid-purged=OFFNo escribir SET @@GLOBAL.GTID_PURGED, que Cloud SQL no deja ejecutar
--default-character-set=utf8mb4Que el dump y el SET NAMES que trae coincidan con lo que hay en las tablas; evita el clásico é
--databases appObligatorio según la documentación, y nunca con la base mysql. Añade CREATE DATABASE IF NOT EXISTS y USE app, así --database en la importación es opcional
--routines --triggers --eventsmysqldump solo incluye triggers por defecto; procedimientos, funciones y eventos hay que pedirlos. Todos llevan DEFINER

Si el usuario con el que haces el dump no tiene PROCESS, mysqldump 8.0.21+ falla al inicio con “Access denied; you need (at least one of) the PROCESS privilege(s)” al consultar tablespaces: añade --no-tablespaces.

PostgreSQL: plano o custom

--no-owner --no-acl hacen que todo lo importado pertenezca al --user y no haya GRANT a roles fantasma. Si la aplicación se conecta con otro rol, crea el rol antes y usa --user=app, o reasigna después:

1
REASSIGN OWNED BY postgres TO app;   -- conectado como postgres, en la base importada

Las extensiones que necesite la base (pg_trgm, postgis, uuid-ossp…) créalas antes de importar, como postgres, en la base destino: CREATE EXTENSION IF NOT EXISTS pg_trgm;. Solo funcionan las de la lista de extensiones soportadas de Cloud SQL; plpgsql ya está. Con eso, comentar los CREATE EXTENSION del dump no quita nada.

Si prefieres el formato custom (-Fc), el filtro de extensiones va en la lista de contenidos de pg_restore, tal como lo documenta Google, y restauras a través del Cloud SQL Auth Proxy:

1
2
3
pg_dump app -h db.example -U dumper --format=custom --no-owner --no-acl > app.dmp
pg_restore --list app.dmp | sed -E 's/(.*EXTENSION )/; \1/g' > app.toc
pg_restore --use-list=app.toc --no-owner --no-acl -j 4 -h 127.0.0.1 -U postgres -d app app.dmp

-j 4 restaura en paralelo, cosa que la importación desde el bucket no hace.

Trampas

  • El rol del bucket va a la service account de la instancia, no a la tuya: la documentación pide storage.objectAdmin (para importar bastan storage.objects.get y list, es decir, objectViewer). Sin él, “The service account does not have the required permissions for the bucket” aunque tú puedas leer el objeto.
  • Una operación a la vez por instancia: si hay un backup o export en marcha, la importación falla con “operation already in progress”. gcloud sql operations list --instance=my-instance lo muestra.
  • Dumps grandes: la importación es en serie y tarda; lanza con --async y vigila con gcloud sql operations wait OPERATION_ID --timeout=unlimited, o el gcloud se te muere por el camino.
  • Versión de pg_dump: usa la misma versión mayor que el destino o inferior. pg_dump 17 escribe SET transaction_timeout = 0;, que un PostgreSQL 16 rechaza con “unrecognized configuration parameter” y aborta la importación.
  • utf8mb4 en la instancia: --default-character-set=utf8mb4 en el dump no arregla una instancia creada con latin1; comprueba character_set_server en los flags de la instancia antes de importar.
  • El sed de DEFINER también toca datos: si alguna fila contiene literalmente DEFINER=`x`@`y` (raro, pero un CMS que guarda SQL puede), sáltala con un patrón anclado a CREATE, o revisa el diff del dump.

Ver también