---
name: pg_list_tables
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def pg_list_tables(dsn: str, schema: str = 'public') -> dict"
description: "Introspeccion read-only de un schema PostgreSQL: lista las tablas base con sus columnas leyendo information_schema.tables e information_schema.columns. Devuelve {status:'ok', schema, tables:[{name, columns:[{name, type, nullable}]}]} en exito y {status:'error', error} en fallo (sin lanzar). Excluye vistas (table_type='BASE TABLE'). Tablas ordenadas por nombre, columnas por posicion ordinal. Marca la transaccion read-only y nunca hace commit. El schema va por placeholder %s (no se interpola). Cierra la conexion siempre en try/finally. Depende de psycopg2 (2.9.x en python/.venv)."
tags: [postgres, postgresql, sql, introspection, schema, readonly, infra]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_py_core"
imports: [psycopg2]
params:
  - name: dsn
    desc: "Cadena de conexion PostgreSQL en formato postgresql://user:pass@host:port/dbname."
  - name: schema
    desc: "Nombre del schema a introspeccionar (default 'public'). Va por placeholder; un schema inexistente devuelve {status:'ok', tables:[]} (lista vacia, no error)."
output: "dict. En exito: {status:'ok', schema:str, tables:[{name:str, columns:[{name:str, type:str, nullable:bool}, ...]}, ...]}; tables ordenadas por nombre, columns por posicion ordinal. En error (sin lanzar): {status:'error', error:str}."
tested: true
tests: ["test_skip_sin_pg_test_dsn", "test_lista_tabla_creada_con_sus_columnas", "test_reporta_nullable_correctamente", "test_schema_inexistente_devuelve_lista_vacia"]
test_file_path: "python/functions/infra/pg_list_tables_test.py"
file_path: "python/functions/infra/pg_list_tables.py"
---

## Ejemplo

```python
import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from infra.pg_list_tables import pg_list_tables

dsn = "postgresql://user:pass@localhost:5433/trends"

res = pg_list_tables(dsn, schema="public")
print(res["status"])    # ok
print(res["schema"])    # public
for t in res["tables"]:
    print(t["name"], "->", [c["name"] for c in t["columns"]])
# leads -> ['email', 'name', 'score', 'active', 'joined']
# prices -> ['id', 'product', 'price', 'source', 'snapshot_date']
print(res["tables"][0]["columns"][0])
# {'name': 'email', 'type': 'text', 'nullable': False}
```

## Cuando usarla

Usala cuando necesitas saber que tablas y columnas existen en un Postgres antes de
escribir o consultar: validar que `pg_create_table_from_rows` dejo el schema
esperado, descubrir el shape de una base ajena, alimentar un selector de tablas en
una UI/agente, o comprobar `nullable`/`type` de una columna antes de un upsert. Es la
contraparte de introspeccion del grupo postgres. Para leer datos usa `pg_query`.

## Gotchas

- Lectura real contra el servidor (impura), pero solo del catalogo del sistema
  (`information_schema`). La transaccion se marca read-only y se hace rollback al
  final: no escribe nada.
- **Solo tablas base**: filtra `table_type = 'BASE TABLE'`, asi que NO lista vistas,
  vistas materializadas ni tablas foreign. Si necesitas vistas, amplia la consulta.
- El campo `type` es el `data_type` de `information_schema.columns`: nombres
  "lógicos" del estandar (`integer`, `text`, `timestamp without time zone`,
  `numeric`), no el tipo interno de `pg_catalog` (`int4`, `int8`). No incluye
  longitud/escala (`varchar(50)` aparece como `character varying`). Para detalle fino
  consulta `pg_catalog` directamente con `pg_query`.
- **Permisos**: `information_schema` solo muestra objetos sobre los que el rol de
  conexion tiene algun privilegio. Con un usuario de permisos limitados puede faltar
  alguna tabla aunque exista — no es un error, es visibilidad del catalogo.
- Un schema inexistente NO es error: devuelve `{status:'ok', schema, tables:[]}`. Un
  DSN invalido o servidor caido si vuelve como `{status:'error', ...}`.
- El `schema` va por placeholder `%s`, no se interpola: la consulta solo lee catalogo
  (no hay DDL/DML que inyectar), pero el placeholder evita ademas romper el SQL con un
  nombre raro.
- Nunca lanza: DSN invalido, servidor caido o falta de psycopg2 vuelven como
  `{status:'error', error:str}`.
