feat(infra): conexion y consulta directa a SQL Server (Navision) via pymssql
Grupo de capacidad nuevo 'sql-connect' (3 funciones) para conectar a un
Microsoft SQL Server (donde corre Navision) y consultar directamente, en
lugar del ida y vuelta manual de pegar CSVs.
- mssql_connect_py_infra: abre conexion pymssql (login_timeout acotado,
credenciales por argumento, RuntimeError claro si falla).
- mssql_query_py_infra: SELECT parametrizada con binding seguro (sin
inyeccion) sobre conexion abierta; devuelve {columns, rows, row_count};
0 filas -> lista vacia; max_rows con fetchmany; read-only.
- run_mssql_query_py_pipelines: one-shot que compone connect+query y cierra
siempre; CLI imprime JSON o CSV; contrasena desde env var (pass).
Pagina madre docs/capabilities/sql-connect.md + fila en INDEX.md.
Dependencia pymssql>=2.3.13 anadida a python/pyproject.toml + uv.lock.
Tests mock-based (11) verdes; error path verificado end-to-end contra el
driver real (host inalcanzable -> RuntimeError, acotado por login_timeout).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: mssql_query
|
||||
kind: function
|
||||
lang: py
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def mssql_query(conn, sql: str, params=None, max_rows: int | None = None) -> dict"
|
||||
description: "Ejecuta una SELECT parametrizada (binding seguro de pymssql, sin inyeccion) sobre una conexion SQL Server/Navision ya abierta y devuelve {columns, rows como lista de dicts, row_count}. Opcion max_rows para limitar las filas."
|
||||
tags: [mssql, sqlserver, navision, sql-connect, infra]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: true
|
||||
tests: ["test_golden_maps_rows_to_dicts", "test_binding_passes_params_to_driver", "test_zero_rows_no_error", "test_max_rows_uses_fetchmany", "test_description_none_empty_columns", "test_execution_error_raises_runtimeerror"]
|
||||
test_file_path: "python/functions/infra/mssql_query_test.py"
|
||||
params:
|
||||
- name: conn
|
||||
desc: "Conexion abierta (la que devuelve mssql_connect). No se abre ni cierra aqui; se reutiliza por duck typing via conn.cursor()."
|
||||
- name: sql
|
||||
desc: "Sentencia SELECT con placeholders pymssql %s (posicional) o %(nombre)s (nombrado) para los valores a vincular."
|
||||
- name: params
|
||||
desc: "Tuple/list para placeholders posicionales, dict para nombrados, o None. Se pasa a cursor.execute(sql, params) para binding seguro del driver (nunca interpolacion)."
|
||||
- name: max_rows
|
||||
desc: "Si es int>0, limita a las primeras max_rows filas (fetchmany). Si None, devuelve todas (fetchall)."
|
||||
output: "Dict con tres claves: 'columns' (lista de nombres de columna en orden, vacia si no hubo result set), 'rows' (lista de dicts columna->valor, una por fila), 'row_count' (int len(rows))."
|
||||
file_path: "python/functions/infra/mssql_query.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join("python", "functions"))
|
||||
from infra.mssql_connect import mssql_connect
|
||||
from infra.mssql_query import mssql_query
|
||||
|
||||
conn = mssql_connect(
|
||||
host="10.0.0.5", database="navdb", user="readonly", password="<desde pass>"
|
||||
)
|
||||
try:
|
||||
res = mssql_query(
|
||||
conn,
|
||||
"SELECT TOP 10 No_, Amount FROM [dbo].[Cartera] WHERE [Customer No_] = %s",
|
||||
("CLI-0001",),
|
||||
)
|
||||
print(res["columns"]) # ['No_', 'Amount']
|
||||
print(res["row_count"]) # numero de filas devueltas
|
||||
for fila in res["rows"]:
|
||||
print(fila["No_"], fila["Amount"])
|
||||
finally:
|
||||
conn.close()
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando ya tienes una conexion abierta con `mssql_connect` y quieres iterar
|
||||
consultas SELECT sobre Navision / SQL Server sin reabrir la conexion en cada
|
||||
una. Pasa los valores variables como `params` para que el driver los vincule de
|
||||
forma segura (sin inyeccion) en lugar de construir el SQL con f-strings.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Los placeholders de pymssql son `%s` (posicional) y `%(nombre)s` (nombrado),
|
||||
NO el `?` de pyodbc. Si usas el placeholder equivocado, el binding falla.
|
||||
- Pasa los valores SIEMPRE por el argumento `params`, jamas con f-string o `%`
|
||||
dentro del SQL: interpolar abre la puerta a inyeccion SQL.
|
||||
- No hace commit: es read-only, pensada para SELECT.
|
||||
- No cierra la conexion — la gestiona el caller (abrir una vez, consultar
|
||||
muchas, cerrar al final).
|
||||
- `max_rows` usa `cursor.fetchmany(max_rows)`; con None usa `fetchall()`.
|
||||
- Si la sentencia no produce result set (`cursor.description is None`),
|
||||
`columns` y `rows` vuelven como listas vacias en lugar de fallar.
|
||||
- El mensaje de error es generico a proposito: no incluye el SQL ni los params
|
||||
para no filtrar datos sensibles.
|
||||
Reference in New Issue
Block a user