Librería Elixir que implementa un adapter de Ecto para Google BigQuery, enfocado exclusivamente en migraciones DDL y seeding de datos. No es un adapter de queries genérico: soporta mix ecto.create/drop/migrate/rollback y seeds, pero NO Repo.all, Repo.insert, etc. para tablas de aplicación. Decisión de diseño intencional — documentada en el README.
- Elixir 1.19
ecto+ecto_sql~> 3.12google_api_big_query~> 0.79 — cliente HTTP generado para la API de BigQuerygoth~> 1.4 — autenticación con Google Cloudmox~> 1.2 — mocking en tests
EctoBigquery.Adapter — punto de entrada, implementa los 5 behaviours Ecto.Adapter.*
EctoBigquery.Migration — DDL: execute_ddl/3, retorna {:ok, []}
EctoBigquery.DDL — genera SQL BigQuery desde comandos Ecto migration
EctoBigquery.Connection — ejecuta SQL via BigQuery Jobs API (QueryRequest)
EctoBigquery.ConnectionBehaviour — behaviour para mockear Connection en tests
EctoBigquery.Storage — dataset create/drop/status via Datasets API
EctoBigquery.SchemaMigrations — tabla schema_migrations: ensure, all_versions, up, down
EctoBigquery.Query — ejecuta SELECT y parsea filas a lista de mapas
EctoBigquery.Seeds — INSERT, DELETE y transacciones multi-tabla via DML SQL
EctoBigquery.Repo — macro que pre-configura migration_primary_key: false
Mix.Tasks.EctoBigquery.Gen.Migration — generador de migraciones con detección de patrones
Migration.execute_ddl/3 debe retornar {:ok, []}, no :ok. ecto_sql hace pattern match {:ok, logs} = ... internamente. Retornar :ok causa MatchError en Runner.log_and_execute_ddl/3.
EctoBigquery.Repo inyecta migration_primary_key: false en init/2. BigQuery no tiene BIGSERIAL ni auto-increment. Sin esto, Ecto agrega id BIGSERIAL a todas las tablas lo que falla con "Type not found: BIGSERIAL".
ecto_sql usa insert/6 (no insert_all/8) para registrar cada versión migrada. Está implementado en Adapter.insert/6 y llama a SchemaMigrations.up/4.
Ecto envía {:drop_if_exists, table, :restrict} cuando se usa drop_if_exists/1 en una migración. DDL.to_sql/1 tiene cláusulas para la 2-tupla y la 3-tupla, ignorando el tercer argumento (modo cascade/restrict no aplica en BigQuery).
Ecto valida los tipos en add/3 antes de llamar al DDL y solo acepta :atom, {:array, t}, {:map, t}. El tipo {:struct, fields} es rechazado. Para columnas STRUCT se debe usar execute/1 con SQL BigQuery crudo. DDL.to_sql/1 tiene una cláusula when is_binary(sql) que pasa el string directo.
Todo DDL y DML se ejecuta enviando SQL al endpoint /projects/{id}/queries con defaultDataset en el QueryRequest. Esto permite ALTER TABLE que la REST Tables API no soporta nativamente.
supports_ddl_transaction? retorna false. Si un alter con múltiples cambios falla a mitad, los cambios previos quedan aplicados. BigQuery no tiene transacciones DDL.
mix ecto.create/migrate no inician el árbol de supervisión de la app. Storage.build_conn/1, Adapter.get_token/1 y Seeds.build_conn/1 llaman Application.ensure_all_started(:goth) + Goth.start_link condicional antes de Goth.fetch/1.
En tests: Application.put_env(:ecto_bigquery, :connection_module, EctoBigquery.ConnectionMock). El mock se define en test_helper.exs con Mox. Los *_with_conn en Storage y Seeds reciben la conexión directamente para tests sin Goth.
Implementados SOLO para schema_migrations. Cualquier otra tabla lanza error explícito. Permite que mix ecto.migrate/rollback funcione sin query translator completo.
Tipos Ecto → BigQuery en Connection.map_type/1:
| Ecto | BigQuery |
|---|---|
:string |
STRING |
:integer |
INTEGER |
:float |
FLOAT |
:boolean |
BOOLEAN |
:date |
DATE |
:time |
TIME |
:naive_datetime |
DATETIME |
:utc_datetime |
TIMESTAMP |
:binary |
BYTES |
:decimal |
NUMERIC |
:map |
JSON |
:geography |
GEOGRAPHY |
:bignumeric |
BIGNUMERIC |
{:array, t} |
ARRAY |
{:struct, kw} |
STRUCT<..> (solo via execute/1 en migraciones) |
En add/3 via opción mode::
add :email, :string, mode: :required # NOT NULL
add :tags, :string, mode: :repeated # ARRAY<STRING>
add :nickname, :string, mode: :nullable # sin constraint
add :score, :integer, null: false # NOT NULL (forma estándar Ecto)Se pasan en options: de create table:
create table(:events, options: [
partition_by: {:time, :created_at, :day},
clustering: [:user_id, :event_type]
])Tipos de partición: :ingestion_time, {:time, col}, {:time, col, granularity}, {:range, col, start, finish, interval}.
# Via Repo
Seeds.insert(MyApp.Repo, "users", [%{id: "1", name: "Alice"}])
Seeds.delete(MyApp.Repo, "users", "active = false")
Seeds.truncate(MyApp.Repo, "users")
Seeds.insert_all(MyApp.Repo, [{"users", [...]}, {"products", [...]}]) # transacción
# Via conexión directa (útil en scripts/tests)
Seeds.insert_with_conn(conn, project_id, dataset_id, "users", rows)
Seeds.delete_with_conn(conn, project_id, dataset_id, "users", "true")
Seeds.truncate_with_conn(conn, project_id, dataset_id, "users")
Seeds.insert_all_with_conn(conn, project_id, dataset_id, table_rows)insert_all_with_conn envía un BEGIN TRANSACTION; ...; COMMIT TRANSACTION; como job único.
mix ecto_bigquery.gen.migration create_users name:string email:string:required
mix ecto_bigquery.gen.migration add_phone_to_users phone:string
mix ecto_bigquery.gen.migration remove_legacy_from_users legacy_col
mix ecto_bigquery.gen.migration drop_old_tableDetecta el patrón del nombre (create_*, add_*_to_*, remove_*_from_*, drop_*) y genera el cuerpo de la migración. Soporta required, repeated, nullable como modificadores de columna, y tipos compuestos como array[string].
Goth debe estar antes del Repo en el árbol de supervisión:
children = [
{Goth, name: MyApp.Goth, source: {:default, []}},
MyApp.Repo
]Para desarrollo local: gcloud auth application-default login
gcloud instalado en /opt/homebrew/bin/gcloud.
Configura las siguientes variables de entorno antes de correr los tests de integración o el proyecto example:
export BIGQUERY_PROJECT_ID=your-gcp-project-id
export BIGQUERY_DATASET_ID=your_datasetmix test # 100 tests unitarios (excluye :integration)
mix test --include integration # 121 tests incluyendo integración contra BigQuery realLos tests de integración crean y eliminan ecto_bigquery_test en cada run. Requieren credenciales válidas.
En example/ hay un proyecto Elixir standalone que demuestra el uso completo de la librería con 19 migraciones. Requiere las variables de entorno:
export BIGQUERY_PROJECT_ID=your-gcp-project-id
export BIGQUERY_DATASET_ID=example_dataset
cd example && mix ecto.create && mix ecto.migrateEn lib/ecto_bigquery/connection.ex, agregar una cláusula a map_type/1:
def map_type(:my_type), do: "BIGQUERY_TYPE"- Agregar la generación SQL en
lib/ecto_bigquery/ddl.ex— funciónto_sql/1 - Tests DDL en
test/ecto_bigquery/ddl_test.exs - Tests de integración en
test/integration/migration_test.exs