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