Skip to content

Latest commit

 

History

History
190 lines (140 loc) · 8.19 KB

AGENT.md

File metadata and controls

190 lines (140 loc) · 8.19 KB

AGENT.md — Contexto del proyecto ecto_bigquery

Qué es este proyecto

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.

Stack

  • Elixir 1.19
  • ecto + ecto_sql ~> 3.12
  • google_api_big_query ~> 0.79 — cliente HTTP generado para la API de BigQuery
  • goth ~> 1.4 — autenticación con Google Cloud
  • mox ~> 1.2 — mocking en tests

Arquitectura

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

Decisiones de diseño importantes

execute_ddl retorna {:ok, []}

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.

migration_primary_key: false por defecto

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".

insert/6 implementado para schema_migrations

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.

drop/drop_if_exists en forma de 3-tupla

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).

execute/1 para tipos STRUCT

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.

SQL via Jobs API, no REST Tables API

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.

Sin transacciones DDL

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.

Goth en mix tasks

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.

ConnectionBehaviour para testabilidad

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.

Queryable/Schema mínimo

Implementados SOLO para schema_migrations. Cualquier otra tabla lanza error explícito. Permite que mix ecto.migrate/rollback funcione sin query translator completo.

Tipos BigQuery soportados

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)

Modos de columna

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)

Partitioning y clustering

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}.

Seeds API

# 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.

Generador de migraciones

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_table

Detecta 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].

Flujo de autenticación

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.

Proyecto BigQuery de desarrollo

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_dataset

Tests

mix test                      # 100 tests unitarios (excluye :integration)
mix test --include integration # 121 tests incluyendo integración contra BigQuery real

Los tests de integración crean y eliminan ecto_bigquery_test en cada run. Requieren credenciales válidas.

Proyecto example

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.migrate

Cómo agregar soporte para un nuevo tipo BigQuery

En lib/ecto_bigquery/connection.ex, agregar una cláusula a map_type/1:

def map_type(:my_type), do: "BIGQUERY_TYPE"

Cómo agregar una nueva operación DDL

  1. Agregar la generación SQL en lib/ecto_bigquery/ddl.ex — función to_sql/1
  2. Tests DDL en test/ecto_bigquery/ddl_test.exs
  3. Tests de integración en test/integration/migration_test.exs