NEWS


lupa 0.1.0

Ronda 33: la primera ronda con el porton por severidad

Desde esta ronda la etapa de refutacion cierra cuando una ronda no encuentra nada alto ni medio; lo bajo se documenta. Tres refutaciones sobre la ronda 32 y sus cuatro decisiones: la de la proteccion encontro uno alto -por la via por bloques el piso por valor no corria sobre las columnas no personales: las cedulas y los nombres que repetia otra columna se publicaban- y dos medios en las colecciones; la de las decisiones, cuatro medios -la NoNulo de una columna opcional, divergencias falsas con columnas_codigo, la evaluacion de analizar() sin su configuracion, el centinela con coma decimal-; la de las piezas compartidas, uno medio -con LC_NUMERIC en coma se perdia la fraccion de segundo-. Al arreglarlos aparecieron dos mas: la media de una columna entera salia recortada en SQL Server, MySQL y MariaDB -y la corroboracion publicaba una divergencia falsa en cada una-, y bajo el locale C una columna con tilde en SQL Server hacia caer a R.

La proteccion:

Las decisiones de la ronda 32:

Una sesion que no puede escribir un nombre (alta):

La sesion:

Limitacion conocida, declarada:

La via DBI:

La bandera y los argumentos que viajan por ...:

Ronda 32: las entradas, la sesion otra vez, la proteccion y cuatro decisiones

Tres refutaciones y una vuelta de decisiones. La de las entradas encontro once hallazgos -dos funciones que apagaban la proteccion con una bandera mal escrita, una fecha de texto leida en el huso de la sesion, un nombre de columna largo que abortaba, unos treinta argumentos sin validar-; la de la sesion, nueve -digits.secs decidia si una conversion era reversible, y la configuracion que se compara entre corridas cambiaba entre un Rscript de cron y RStudio- y uno mas al arreglarlos: con LC_NUMERIC en coma los controladores de PostgreSQL y SQL Server leian 2.5 como 2; la de la proteccion, seis -un numero partido en lineas en el mensaje del motor- y uno mas, alto, al arreglarlos: la lectura por bloques publicaba los valores de las columnas protegidas. Y cuatro decisiones: las columnas de codigo se declaran, el id de una sugerencia del modelo se arma con lo que mide, la NoNulo con faltantes disfrazados se mide, y el paquete ya no tapa stop() ni warning() de base. Despues, sin proteccion, el mensaje del motor se publica entero.

La proteccion:

La fecha y la entrada:

La misma respuesta en cualquier sesion:

Cuatro decisiones de diseno:

El mensaje del motor sin proteccion (decision D8, antes de la ronda 33):

Rendimiento:

Decision documentada:

La documentacion:

Ronda 31: la misma respuesta en cualquier sesion, la proteccion otra vez y una regla por hecho

Tres refutaciones. La de la sesion encontro ocho defectos -bajo un locale turco ABRIL 2024 no era una fecha ni point(1 2) WKT; con scipen o OutDec el nombre de una regla cambiaba y la deriva perdia el par, la proteccion tapaba los estadisticos de una columna no protegida y perfilar_dbi() publicaba divergencias entre dos cifras iguales; en Adak dos instantes salian con la misma etiqueta-; la de la proteccion, seis -la coleccion descartaba valores que el mensaje del motor lleva, y el celular escrito con separador de miles salia en claro-; la de dos funciones que contestan el mismo hecho, diez -un factor con nivel NA era clave para detectar_claves() y clave con ausentes para perfilar(), y aplicar() no eliminaba las columnas que el plan llamaba duplicadas-, y la auditoria uno mas: una conversion regional que no se ejecuta salia lista por un camino que la ronda 30 no cerro.

La proteccion:

Dos funciones, dos respuestas:

La misma respuesta en cualquier sesion:

Ronda 30: la proteccion otra vez, el paquete bajo el locale C y las clases de columna

Tres refutaciones. La de la proteccion encontro cinco defectos -el mensaje del motor publicaba un numero con separadores que la lista de lo permitido tomaba por signos, y la lectura de los decimales en cero de la ronda 29 tapaba uno de cada cinco importes de siete cifras con centavos en cero-; la del paquete entero bajo el locale C, ocho -tolower() de base no bajaba las mayusculas con tilde, una columna si/no dejaba de ser logica, y el orden de la intercalacion cambiaba los identificadores de las sugerencias-; la de las clases de columna por todo el catalogo, nueve -una columna vctrs_rcrd hacia abortar 22 de 29 llamadas-.

La proteccion:

El locale C:

Las clases de columna:

Ronda 29: la proteccion otra vez, el texto y la codificacion, y la misma tabla por todos los caminos

Tres refutaciones. La de la proteccion encontro ocho defectos -el mapa de la imputacion de una columna protegida salia en claro, y el mensaje del motor citaba el valor sin comillas, en otros idiomas o con comillas que la limpieza no conocia: la limpieza paso de tapar formas a publicar solo lo permitido-; la del texto y la codificacion, ocho -la reparacion de codificacion dependia del locale y la tabla de clases combinantes tenia 25 de 51 valores mal-; la de la misma tabla por todos los caminos, seis -con bloque_filas, una sola columna integer64 truncaba a entero las medias de toda la tabla-, y el costo de instrumentar, que llevaba 23 filas a un minuto.

La proteccion:

El texto y la codificacion:

Los caminos de perfilar_dbi() y perfilar_por():

Ronda 28: las geometrias, la comparacion de perfiles, los diagnosticos numericos y la proteccion otra vez

Cuatro refutaciones. La de las geometrias encontro doce defectos -con un universo declarado, el perfil contaba como evaluadas geometrias que nunca evaluo y publicaba 0 invalidas donde habia 50-; la de la comparacion de perfiles, ocho -una columna protegida cuyo rango se corria setenta veces su ancho no producia ninguna fila-; la de los diagnosticos numericos, diez -una edad de 18 a 90 salia como identificador y Benford se callaba con muchas filas-; y la de la proteccion, nueve -el mensaje de error del motor citaba la celda que lo provocaba-.

La proteccion:

Las geometrias:

La comparacion de perfiles:

Benford:

Centinelas declarados:

Relaciones aritmeticas:

Ausencia estructural:

clasificar_variables():

La numeracion, en todo el perfil:

Ronda 27: la proteccion otra vez, los validadores y las fechas, los duplicados y la distribucion

Tres refutaciones en paralelo. La de la proteccion encontro siete fugas, tres familias de tapado de mas, un aviso que con warn = 2 abortaba perfilar() y un aborto con un nombre de columna marcado bytes -la marca de una columna protegida volvia a llegar tapada a los datos del usuario por otra puerta-; la de los validadores y las fechas, doce defectos -el perfil declaraba la coma decimal y resumia 1.500 como 1,5-; la de los duplicados, la distribucion, las asociaciones y las estimaciones, trece.

La proteccion:

Los validadores, los tipos y las fechas:

Las senales redundantes:

Duplicados aproximados:

Distribucion y asociaciones:

Estimaciones externas:

Ronda 26: la proteccion, el informe HTML y el plan de limpieza

Tres refutaciones en paralelo. La de la proteccion encontro nueve fugas y cuatro familias de tapado de mas -una de ellas renombraba la columna de los datos del usuario con el texto tapado-; la del informe HTML, diez defectos; la del plan de limpieza, catorce.

La proteccion:

Lo que se tapaba de mas:

El informe HTML:

El plan de limpieza:

Ronda 25: la proteccion otra vez, el historico entre sesiones y las claves

Tres refutaciones en paralelo. La de la proteccion encontro un aborto -la proteccion por omision hacia abortar perfilar() sobre una tabla con una columna personal y un nombre de columna con tilde-, cinco fugas y seis familias de tapado de mas; la del historico y la deriva entre sesiones, ocho defectos; la de las claves, las relaciones y el referencial, diez.

La proteccion:

El historico y la deriva:

Claves, relaciones y el referencial:

Ronda 24: los niveles institucionales, las cifras del perfil y la proteccion otra vez

Tres refutaciones en paralelo: la de los niveles institucionales encontro once defectos; la del perfil contra conteos a mano, otros once -uno lo defendia una entrada anterior de este archivo, que se corrige-; y la de la proteccion, nueve fugas y siete familias de tapado de mas.

Los niveles institucionales (agregar() de la coleccion para arriba):

Lo que afirma el perfil:

La proteccion:

Ronda 23: el tablero, el indice y la proteccion otra vez

Una refutacion de la agregacion, el tablero y el indice encontro doce defectos, y la suite uno mas que venia de antes; una de la proteccion, nueve fugas y tres familias de tapado de mas en lo que la ronda 22 habia aflojado.

El tablero y el indice:

La proteccion:

Ronda 22: el modelo de calidad

Una refutacion de medir(), evaluar(), el historico y la deriva encontro doce defectos.

Y una refutacion de la proteccion de datos personales encontro ocho fugas y una familia de tapado de mas en la regla de digitos de la ronda 21:

La cuarta evaluacion real

Ronda 21: el informe HTML

Una refutacion del informe de reportar() encontro siete defectos.

Y una refutacion de la proteccion de datos personales encontro catorce, cinco de ellos abiertos por lo que la ronda 20 aflojo.

Ronda 20: el plan de limpieza, y la proteccion otra vez

Dos refutaciones en paralelo. La de la capa de limpieza encontro veintidos defectos -no se atacaba desde la ronda 9-; la de privacidad, nueve, tres de ellos abiertos por la ronda 19.

El plan de limpieza:

La proteccion de datos personales:

Ronda 19: la proteccion de datos personales, por las dos caras

Diez hallazgos de una refutacion con dos encargos: que un valor protegido se publique, y que la proteccion tape lo que no es un valor de nadie.

Un metodo propio que mide fuera de su universo

Ronda 18-B: la proteccion de datos personales, refutada otra vez

La proteccion de datos personales, mas rapida

Ronda 18: la proteccion de datos personales, refutada

Ronda 17, normalizacion de texto

Ronda 17: los diagnosticos numericos

Tercera evaluacion sobre bases reales

Ronda 15: claves, relaciones y el referencial

Ronda 16: el historico entre sesiones y las geometrias

Ronda 15, segunda parte: estimaciones, senales, patrones y correos

Ronda 14: fechas, validadores y perfil por grupos

Ronda 13: reparacion de texto, validadores y deriva entre perfiles

Ronda 12: lo que afirma el perfil, y el informe que lo comparte

Ronda 11: el motor contra la memoria, y los agregados encadenados

Ronda 10: lo que publica un metodo propio

Ronda 9: el contrato de medir() y lo que aplica el plan de limpieza

analizar() sobre millones de filas: el detalle de la medicion ya no se arma entero

La proteccion de datos personales tapaba numeros que son estructura

Ronda 8: el modelo de calidad y el informe dicen lo que sostienen

Un metodo que falla ya no se lleva la medicion de las demas metricas

La proteccion de datos personales enmascaraba nombres de columna

La cifra que se rompio tres veces por ser una resta, y una identidad que se verifica

Lo que abrio publicar la unidad: el NaN con dos signos, una cifra restada dos veces y una duracion muda

Cuatro decisiones tomadas: difftime publica, las hermanas se igualan y el estado se parte en dos

La unidad viaja al lado de la cifra, y la comparacion la ve

La coleccion declara con que alcance calculo cada numero

La celda del tablero dice de quien es, y el indice deja de afirmar lo que desmiente

El agregado no puede tirar lo que la medicion declaro, y el promedio mira el tipo

La magnitud que perdio exactitud no se publica, y una columna compuesta cuenta filas

El universo aplicable manda en el conteo Y en la traza

Conteos que describen lo que miraron, y una deriva que dice cuando no pudo comparar

La cadena que se compara es la que el objeto declara

El libro de cobertura por grupos cierra, y la etiqueta de un grupo vuelve a su valor

El informe no publica el valor que viajaba unido, y la remediacion declara lo que cambia

Modelo y deriva: que cuenta como un cambio de modelo

Los estadisticos declaran su universo, y una columna exotica no rompe el analisis

La ventana que no puede gobernar se declara

Lo que no se declaro no es parte del modelo

La suite dice en que archivo va mientras trabaja

El enmascarado de la salida agrupa las hojas

La cola del valor protegido mas largo ya no se publica

Un contrato incompleto se abstiene, no se lleva la corrida

La metrica dice cuantas midio de cuantas

La frontera y sus pesos hablan el mismo idioma, y el informe publica el hueco

perfilar() no aborta sobre una columna con marca en el minimo declarado

La regla declara mas de lo que el veredicto cubre, y el historico lo respeta

La medicion de la perdida no puede abortar en el minimo declarado

Lo que el perfil concluye, y quien puede declarar un dato personal

La perdida se mide en la puerta comun

El plan declara los tres huecos, no dos

El marco que se publica contiene lo que se publica

Un recorte dice cuantos son, y cada numero dice en que unidad cuenta

El minimo declarado se comporta como el declarado

La capa de modelado declara sus limites

Lo que se publica y lo que se declara proteger

La perdida se mide en el resultado

Colecciones: alcance y trazabilidad de las lecturas

Columnas con clase propia y silencios del plan

Correcciones de marco y deriva

Correcciones de cobertura y publicación

Texto declarado independiente del locale

Identificadores declarados independientes del locale

Escritura independiente del locale

Lo que se mide contra lo que se declara

Una declaración vale en todas las salidas, no sólo en la primera

La deriva de calidad distingue un cambio en los datos de un cambio en la vara

El modelo de calidad ya no publica el padrón ni un 1 que no midió

El plan dice en qué unidad cuenta, y aplicar() no miente sobre lo que hizo

perfilar_dbi() compara sus dos bloques y declara la divergencia

Tres puertas por las que se escapaba un valor protegido

Una columna clasificada como dato personal se protege: la moda sale como [valor protegido], el mínimo y el máximo quedan en NA. Tres salidas publicaban igual un valor real, incluso con proteger_datos_personales = TRUE pedido explícitamente. En las tres se conserva la señal: lo que no puede salir es el valor, no el aviso.

Lo sostiene un barrido que recorre los tipos de hallazgo y las salidas —consola incluida, capturando también lo que emite cli— y que comprueba primero que cada uno se haya emitido: un caso donde la regla no dispara da el mismo resultado que uno donde funciona bien.

Los dos conteos de un resumen parcial cuentan cosas distintas

Una política declarada no se disfraza de cambio en los datos

perfilar_dbi() dice qué políticas no llegan al motor

El período AAAA-MM es una fecha de granularidad mes

Un resumen parcial lo declara siempre, no sólo al muestrear

Un centinela que el usuario declara vale lo mismo que un NA

Un resumen calculado sobre parte de la columna lo dice

Las figuras del banco de rendimiento salen de un CSV

La tasa de doscientos caracteres, medida otra vez

Fechas personales de fallecimiento

Ronda de arreglos DBI contra bases reales

El resultado deja de depender de la configuración regional

iconv("ASCII//TRANSLIT") era una de las dependencias de plataforma, no la única. Estas son de la misma familia y todas se reprodujeron antes de tocarlas.

Las pruebas nuevas fijan el locale dentro de la prueba y lo restauran; si la plataforma no acepta el locale pedido, se saltean diciendo por qué.

Documentación que se puede desmentir

Geometrías con Z/M o con SRID mixto: se acabaron los falsos positivos

Medido contra un PostGIS 3.4.3 real, sembrado a propósito con lo que la base real de la ronda anterior no tenía: GEOMETRYCOLLECTION anidada y vacía, POINT Z, POINT M, POINT ZM, vacías legítimas mezcladas con no vacías, y SRID 31981 junto a 4326.

Las guardas geométricas no le entregan datos corruptos a GDAL

Ronda de arreglos del núcleo

Ronda de arreglos DBI

Integración DBI — spool y contratos de muestra_motor (Etapa I2)

Integración DBI — fuente por bloques (Etapa I1)

Estimacion de derrame de moda y mediana en DBI

Mediana y conteos bajo muestreo DBI

Memoria de trabajo en la auditoría DBI

Comparación de perfiles y estado del tipo inferido

Ejecutor interno por bloques — Etapa 4: LSH externo

Ejecutor interno por bloques — Etapa 3: índice

Ejecutor interno por bloques — Etapa 2: valor

Ejecutor interno por bloques — Etapa 1

perfilar_dbi() separa universo, métricas y estrategias

La sonda de mediana consolidada conserva el error del motor

La cobertura DBI espeja el tope de muestra

El plan DBI aprovecha reltuples como estimación

Señal de concentración modal

Los bytes WKB basura no llegan a GDAL

Avisos DBI para moda y mediana

Una muestra vacía no publica métricas no medidas

La política de costo separa moda y mediana

Topes de la muestra en memoria y DBI

estrategia_distintos = "catalogo" en PostgreSQL

Validación de claves

El plan no repite sus supuestos

La clave que se publica es la de la tabla que se midió

La mediana consolidada se activa donde el motor la acepta

README, API y recorrido guiado

La clave declarada separa ausencias y repeticiones

La trazabilidad de filas duplicadas conserva todos los participantes

El minimo de R conserva la conducta en BLOB y duplicados matriciales

El plan DBI declara el muestreo que no puede construir

El plan declara la memoria del procesamiento como no estimada

La regla de vocabulario también mide el largo que compara

La clave primaria contempla los dos esquemas que resuelve SQL Server

Los avisos DBI de costo tienen controles independientes

La clave primaria se lee de la tabla que se midió, y de ninguna otra

Una garantía desconocida dice por qué no se pudo saber

El tope de largo mide la cadena que se compara

Los valores largos se cuentan en valores, y las filas en filas

El derrame de COUNT(DISTINCT) se avisa antes de pagarlo

Topes declarados para valores largos y tablas anchas

El parseo de fechas también trabaja sobre el vocabulario

Después de la detección de formatos, el parseo era lo que quedaba recorriendo todos los valores. Ahora deduplica, parsea las formas distintas y mapea de vuelta, que es el mismo camino.

Sobre una columna de fechas realista —1.096 fechas distintas en tres años, 120.000 filas—, perfilar() baja de 4,23 s a 3,24 s: un 23 % menos. El costo del parseo queda prácticamente plano respecto de la cantidad de filas.

El vector devuelto es idéntico: 257 comparaciones aleatorias contra la implementación anterior, con comparación exacta y no de un resumen, incluyendo meses en texto, zonas horarias, granularidad de mes, formatos mezclados, valores no parseables, columnas ya Date y ya POSIXct.

La clave primaria queda publicada en el perfil DBI

perfilar_dbi() consulta siempre el catalogo de la clave primaria y conserva la respuesta en resumen_tabla$meta$clave, junto a sus columnas, fuente, motivo, garantia y estado. Una tabla sin clave declarada queda con garantia = "no_declarada"; una consulta fallida o una visibilidad que no se pudo establecer queda con garantia = "desconocida" y su motivo. Se conservan sin cambios los estados ya definidos para garantia, visibilidad, restricciones diferibles, descendientes e indice de respaldo.

La lectura agrega una sola consulta de catalogo y ninguna consulta de datos. El metodo de impresion muestra la diferencia entre una clave garantizada, una tabla sin clave declarada y un catalogo que no se pudo consultar. El plan sigue sin recorrer datos: la consulta adicional es de metadatos.

La moda conserva su guardian de la misma sentencia

El detector de formatos de fecha trabaja sobre el vocabulario

Una columna de fechas suele tener pocas formas distintas repetidas muchas veces, y el detector las evaluaba todas. Ahora clasifica las formas del vocabulario y pondera por su frecuencia, que es el mismo camino que ya usaba la clasificación de tipos.

Los conteos no cambian: el detector devuelve los mismos formatos, las mismas frecuencias, las mismas ambigüedades y los mismos atributos. Trabajar por vocabulario y ponderar no altera un solo número, y hay pruebas de equivalencia campo por campo y atributo por atributo que lo fijan.

Medido sobre 120.000 filas y cinco columnas, el detector pasa de 1,16 s a 0,050 s, y perfilar() completo baja de 5,30 s a 3,93 s: un 26 % menos. La ganancia crece con las filas, porque el trabajo pasa a ser proporcional a los valores distintos y no a la cantidad de filas.

La mediana conserva una sola instantanea

La proyección temporal de distintos usa su propia unidad

resumen_tabla$sql declara su unidad sumable

La política de costo no ciega las fuentes estructurales

Cada agregado queda ligado a la foto que lo produjo

Cada lote de agregados planos que calcula n_validos trae también COUNT(*) AS n_total_consulta en la misma sentencia. La completitud usa ese denominador local, incluso cuando la bisección separa un lote; no combina el total de una consulta con válidos de otra. El total del universo se conserva aparte sólo cuando el perfil sobre una muestra lo necesita o cuando no hay un agregado que pueda llevarlo.

La consulta exacta de distintos trae COUNT(columna) AS n_validos_guard junto a COUNT(DISTINCT columna). consulta_id, ya publicado en resumen_tabla$sql, identifica la sentencia y el grupo de consistencia: las cotas duras sólo se aplican dentro de ese grupo. Cuando una capacidad aproximada no puede traer su guardián, la comprobación queda declarada como no disponible y el motivo no culpa al motor.

Se agregaron regresiones con INSERT y DELETE entre consultas, además de rastreo de SQL para comprobar que el denominador viaja en la consulta que ya se emitía y no agrega una ida y vuelta.

La garantía de una clave mira el índice que la impone

pg_constraint dice que hay una restricción; el que impone la unicidad es el índice que la respalda. La lectura del catálogo trae ahora indisunique de ese índice, con un LEFT JOIN en la consulta que ya se emitía, y si no es único la garantía baja aunque la restricción figure validada.

No es un estado alcanzable por DDL normal —al adjuntar una partición el motor crea solo el índice único y válido—, así que es defensa ante un catálogo alterado. Entra igual porque cuesta una columna y porque cambia la garantía de «el catálogo declara una restricción» a «el índice que la impone es único».

Ronda 154: el muestreo publica lo que se obtuvo y distingue su incertidumbre

resumen_tabla$meta$muestreo ahora publica juntas las filas solicitadas y las filas obtenidas por la lectura de perfil_muestra. tamano_muestra se conserva por compatibilidad como el tamaño efectivo solicitado a la consulta; filas_solicitadas identifica el pedido original y filas_obtenidas el hecho observado. Si la lectura no se hizo o falló antes de devolver filas, el último campo queda en NA.

En las métricas SQL muestreadas, error_esperado deja de ser uniforme: no_estimado indica que el error podría calcularse bajo un plan probabilístico pero no se calculó; no_estimable cubre la moda, la mediana y la cardinalidad observada, sin una cota simple o un estimador declarado; y no_aplica indica que no hubo muestreo efectivo. El motivo se conserva en cada registro junto con el método, el tamaño y la fracción. No se agregan cotas numéricas inventadas.

Cambios en desarrollo

Una clave heredada ya no se declara garantizada sobre otro universo

En PostgreSQL, una consulta sin ONLY incluye a las tablas que heredan, y la clave primaria del padre no gobierna las filas de los hijos. El catálogo sigue informando la restricción como válida, así que perfilar_dbi() publicaba garantia = "garantizada" sobre un universo donde la unicidad puede no cumplirse: medido, una tabla con un hijo que repite un valor da 6001 valores y 6000 distintos en la consulta que ejecuta el paquete.

Ahora la misma consulta de catálogo trae si la tabla tiene descendientes —sin agregar una ida y vuelta— y en ese caso la garantía baja a declarada_no_garantizada, con el hecho anotado en estado$universo_incluye_descendientes. La restricción existe y es válida; lo que no vale es sobre las filas que se van a perfilar.

El particionado declarativo no pierde la garantía. pg_inherits registra tanto la herencia tradicional como las particiones, y sólo la primera deja filas fuera del alcance de la clave: el motor exige que la clave de una tabla particionada incluya sus columnas de partición, justamente para poder garantizarla sobre el árbol. Medido: una tabla regular con un hijo que repite un valor da 6001 válidos y 6000 distintos; una particionada con dos particiones da 19999 y 19999. Se distinguen por relkind, en la misma consulta. En PostgreSQL anterior a la versión 10 no existe el particionado declarativo, y ahí toda descendencia es herencia.

La misma consulta trae también si la restricción es DEFERRABLE. Una clave diferible puede estar violada mientras una transacción sigue abierta, y el catálogo la informa validada igual: medido, dentro de una transacción que inserta un duplicado la tabla tiene 3 valores válidos y 2 distintos. Desde el paquete no se puede saber si hay una transacción con violaciones pendientes, así que la garantía no se afirma y queda estado$restriccion_diferible.

El estado publicado queda atado a la consulta ejecutada

Los conteos aproximados sólo se consolidan cuando el adaptador entrega una expresión que se puede incrustar en el SELECT. Una capacidad que sólo construye consultas completas conserva los conteos de válidos y distintos por separado, sin presentar COUNT(DISTINCT ...) como una aproximación. Un resultado no emitido no se publica como estimado, y el registro fuerza no_disponible cuando ok es falso.

La cota n_distintos <= n_validos se valida antes del único registro. Los valores imposibles no se recortan ni se publican; el valor bruto queda en el motivo de auditoría.

Catalogos de claves sin confundir ausencia con falta de visibilidad

La lectura DBI ya no llama no_declarada a cualquier consulta de catalogo que vuelve vacia. Una consulta fallida queda con garantia = "desconocida", visible = NA y su motivo; una vista que devolvio cero filas pero puede ocultar metadatos queda con garantia = "desconocida", visible = NA y la ambiguedad escrita en motivo. Un error de permisos identificable queda con visible = FALSE; solo las vias cuyo catalogo es visible para una tabla accesible conservan no_declarada.

Medido contra MariaDB 11 y MySQL 8 reales, con una cuenta propietaria y otra con solo SELECT, SHOW INDEX devolvio las filas PRIMARY en las dos cuentas para las claves simples y compuestas, y cero filas en las tablas sin clave. MariaDB usa esa via en una sola consulta y conserva el orden de las columnas con Seq_in_index; su garantia sigue desconocida porque esa salida no publica un estado comparable de aplicacion y validacion.

PostgreSQL deja de partir de information_schema.table_constraints, que oculta restricciones a un rol que solo tiene SELECT, y consulta directamente pg_constraint, pg_class, pg_namespace y pg_attribute, conservando el orden de una clave compuesta y el estado convalidated. La ruta contra un servidor PostgreSQL con una credencial que solo tiene SELECT queda pendiente de verificacion en esta entrega.

SQLite pide tambien notnull. Su estado separa unicidad = "garantizada" entre los valores no nulos de la PRIMARY KEY de ausencia_de_nulos: es "garantizada" cuando todas sus columnas devuelven notnull = 1 y "no_verificada" en los demas casos. La garantia conjunta queda desconocida cuando la no-nulidad no se puede sostener. Esto subclasifica honestamente los casos especiales que pueden garantizar no-nulidad sin que este camino distinga su declaracion, como INTEGER PRIMARY KEY; no se lanza un recorrido de los datos. Se verifico con dos NULL reales en una PRIMARY KEY de texto sin NOT NULL, y con el rechazo de un NULL en otra con NOT NULL.

La selección de columnas pasa por una primitiva con semántica declarada

El paquete deja de escribir datos[, columnas, drop = FALSE] en cada sitio que recibe una tabla y lo hace a través de una función interna cuya semántica de referencia está declarada, de modo que el significado no dependa de la clase de la tabla ni del estado del espacio de nombres. Esto corrige además selecciones de una dimensión que quedaban en agregacion, claves-relaciones, duplicados-aproximados, referencial, remediacion y tablero-calidad: tabla["columna"] selecciona una columna en un data.frame y en un tibble, pero en un data.table intenta un cruce y aborta. No agrega conversiones: sigue habiendo una sola por llamada.

SQL Server lee su catálogo por la vista estándar

La clasificación de visibilidad de information_schema.table_constraints deja de tratar a SQL Server como ambiguo. Medido con un rol de sólo SELECT sobre tablas con clave simple, compuesta y sin clave, la vista devuelve las restricciones: la vía es exhaustiva para esa credencial, así que un resultado vacío significa que la clave no está declarada. Lo sostienen dos mediciones independientes sobre dos versiones distintas del motor.

La unicidad de una clave se evalúa entre las filas con la clave completa

perfilar(clave = ...) evaluaba la unicidad sobre todas las filas con la semántica de R, donde dos ausentes de la misma posición colisionan. Eso hacía que una clave cuyas únicas repeticiones venían de filas incompletas se informara como «no es única», cuando en SQL dos NULL no son iguales y no violarían nada.

Ahora la unicidad se evalúa sólo entre las filas con la clave completa, y la colisión entre ausentes se informa donde corresponde: en trazabilidad, porque lo que deja ambiguo es el localizador. unicidad$semantica pasa de "R" a "claves_completas", y se agrega unicidad$filas_totales junto a filas_evaluadas, que ahora cuenta las filas evaluables.

Cuando ninguna fila tiene la clave completa, el estado es "sin_casos_evaluables". Decir "verificada" sería cierto sobre un conjunto vacío y engañoso a la vez, y "no_verificada" sería falso si se recorrió la tabla entera y se comprobó que no había casos.

El conteo de duplicados no depende de ajustes globales de la sesión

data.table::setNumericRounding() es un ajuste global que cambia cuántos bits se comparan de un doble al ordenar. Como la vía rápida ordena, con los valores 1 y 2 agrupaba valores que duplicated() distingue: medido sobre dobles separados por un eps, sobre POSIXct con diferencias de microsegundos y sobre magnitudes grandes, los tres divergían. La vía rápida ahora sólo corre con precisión completa, y sólo consulta el ajuste si hay columnas dobles. lupa no lo modifica: cambiarlo alteraría el comportamiento de código ajeno.

Se comprobó además que el número de hilos, la configuración regional, la zona horaria, la codificación y las opciones de data.table no cambian el resultado.

Un duplicated() que ignora fromLast ya no cambia el conteo

bit64 devuelve para una columna integer64 el mismo vector con fromLast = TRUE que sin él. La cuenta de filas en grupos repetidos heredaba ese defecto: una tabla de 30 filas con 3 valores distintos daba 27 en vez de 30, y la respuesta cambiaba según el umbral de filas, porque la vía rápida sí daba 30. El conteo hacia atrás se calcula ahora dando vuelta las filas, que no depende de que el método respete el argumento.

La clase de la tabla ya no cambia el resultado

perfilar(), analizar() y el resto de la API que recibe tablas dan el mismo resultado con un data.frame, un data.table o un tibble. Las entradas se normalizan en la frontera —una sola vez por llamada— y las tablas internas del modelo de calidad también, de modo que la sintaxis de selección de columnas significa lo mismo en todos los caminos.

Esto corrige además cinco selecciones de una dimensión que no eran portables: tabla["columna"] selecciona una columna en un data.frame y en un tibble, pero en un data.table intenta un cruce y aborta. Ahora son selecciones explícitas de dos dimensiones.

Conteo exacto de filas duplicadas sin mutar la entrada

El contador de filas duplicadas de perfilar() obtiene los grupos de filas con data.table::frank() en vez de armar la estructura intermedia que combina todas las columnas, que es lo que hace duplicated() sobre un data.frame y lo que hace crecer su costo con el ancho de la tabla. La entrada no se convierte ni se copia.

data.table se llama con :: y no se importa al espacio de nombres, a propósito: data.table decide la semántica de [ según si el paquete que llama lo tiene entre sus imports, de modo que importarlo cambiaría el significado de tabla[, columnas, drop = FALSE] en toda función que recibe una tabla de quien usa el paquete. frank() no depende de esa condición; duplicated() sobre un data.table sí, y por eso no se usa.

Las columnas de lista o matriz se detectan antes y usan la semántica de base, y también las tablas con NaN: frank() ordena y no distingue NaN de NA, mientras que duplicated() sobre un data.frame sí los distingue. La dependencia pasa de Suggests a Imports, y el resultado queda cubierto por pruebas de equivalencia contra base sobre data.frame, data.table y tibble.

Claves declaradas: unicidad, ausencias y trazabilidad separadas

perfilar(clave = ...) informa por separado si la combinación es única bajo la semántica de R y si sus componentes están completos. Cada eje queda como verificada, refutada o no_verificada; una ausencia no se presenta como un duplicado SQL. Si la semántica de R agrupa dos ausentes para localizar filas, esa colisión y la falta de valores se informan juntas y quedan en meta$clave, junto a la trazabilidad. Una clave única y completa conserva el objeto histórico sin metadatos adicionales.

Garantía de claves primarias según el estado del catálogo

La lectura DBI de claves primarias separa la fuente (fuente) de la garantía (garantia) y conserva el estado consultado. Oracle lee STATUS y VALIDATED: una restricción deshabilitada o no validada queda declarada en el catálogo, pero no garantizada; si esos estados no se pueden consultar, quedan desconocidos. PostgreSQL consulta enforced y su estado de validación, y MySQL enforced; MariaDB, SQL Server, SQLite y DuckDB no ofrecen en esta vía un estado comparable, por lo que el resultado no inventa una garantía. Los casos Oracle se verifican con respuestas DBI simuladas; no se afirma una prueba contra un servidor Oracle real.

CPU del cliente y política explícita para métricas caras

La instrumentación de DBI publica ahora cpu_ms junto a duracion_ms en resumen_tabla$sql y en resumen_tabla$tiempos. Se calcula con proc.time() como user.self + sys.self: cerca de cero distingue espera del trabajo del proceso cliente, y cerca de uno al dividir CPU por tiempo transcurrido indica que el cliente está trabajando. Cero es una medición válida; NA queda reservado para lo que no se pudo medir, y instrumentar = FALSE deja el campo en NA como los demás. En un microbenchmark de un millón de consultas simuladas, dos lecturas de proc.time() costaron 1,122 microsegundos por consulta; dos de Sys.time() costaron 1,778 microsegundos. En la tabla ancha de 158 columnas, el agregado no duplicó el costo de instrumentar: cinco corridas con reloj solamente tuvieron mediana de 1,093 s y cinco con reloj más CPU de 0,962 s, una diferencia dominada por la variación de SQLite. El número reproducible que se publica es el costo directo de 1,122 microsegundos por consulta.

Moda y mediana se pueden controlar con politica_costo. El valor por omisión es "todas", que conserva el perfil anterior; "por_cardinalidad" hace primero los conteos baratos de valores válidos y distintos y decide luego por columna. Si n_distintos / n_validos alcanza umbral_cardinalidad, omite la moda de esa columna. La omisión no desaparece ni se vuelve NA silencioso: queda omitido_por_costo con el motivo, la proporción observada y la forma de pedirla igual (politica_costo = "todas") o mover el umbral.

Cuando se escribió esta entrada el umbral valía 0.95 y omitía moda y mediana. Las dos cosas cambiaron al medirlas —el umbral es 0.5 y la mediana ya no se omite por proporción, ver la entrada de arriba—, y el texto se corrigió acá para que no queden dos descripciones del mismo argumento diciendo cosas distintas en el mismo archivo.

La política hace explícito el plan en dos etapas. En una tabla reproducible de 158 columnas, 80 numéricas, 200 filas y 60 columnas con cardinalidad al menos 0,95, politica_costo = "todas" emitió 260 consultas y la política selectiva 140: se ahorraron 120 consultas, no tiempo. Se omitieron 60 modas y 60 medianas. El valor por omisión no cambia: esos 60 casos muestran el ahorro posible, pero no autorizan al paquete a elegir qué métrica sacrificar.

Cuando la sonda reconoce un motor con PERCENTILE_CONT(...) WITHIN GROUP, las medianas numéricas se consolidan en un SELECT por lote; el camino actual, una mediana por columna, queda como respaldo si la capacidad no está disponible o falla la consulta consolidada.

El recorrido que se pagaba sólo por contar, y qué filas vio cada métrica

El COUNT(*) exacto ya viaja en la primera consulta de agregados. Si el lote completo es rechazado, lupa emite un COUNT(*) solo como repliegue y continúa con la bisección: la completitud sigue derivando n_faltantes y prop_faltantes de un denominador medido, nunca estimado. El plan sigue pagando su propio conteo porque necesita conocer el total antes de estimar el trabajo.

En una tabla en memoria de 12 filas y tres columnas, con tamano_lote = 4 y bloque_muestra = "solo_agregados", la traza SQL dio estos conteos. La columna de recorridos cuenta las apariciones de la fuente en el SQL; no se usó tiempo.

| modo | consultas antes | consultas ahora | recorridos antes | recorridos ahora | ahorro de recorridos | | --- | ---: | ---: | ---: | ---: | ---: | | exacto | 14 | 13 | 14 | 13 | 1 | | seguro | 8 | 7 | 8 | 7 | 1 | | conteos | 6 | 5 | 6 | 5 | 1 | | muestreado | 23 | 22 | 23 | 23 | 0 | | aproximado | 23 | 22 | 23 | 22 | 1 |

En resumen_tabla$sql, id_consulta identifica la consulta de datos: el mismo identificador garantiza exactamente las mismas filas. Moda, frecuencia de la moda y mediana son métricas por columna y quedan con NA; también queda NA cualquier camino que no pueda sostener esa garantía. Así la comparabilidad se comprueba por comparación directa, sin cruzar lote ni columnas_compartidas.

Aislar la columna culpable en pocas consultas, y no recorrer tres veces lo que cabe en una

Los lotes de agregados que el motor rechaza ya no se reintentan columna por columna sin información. La vía de agregados reutiliza la bisección de la lectura de muestras: sondea mitades, conserva los grupos aceptados como datos medidos y reintenta por métrica sólo las columnas culpables. El tope sigue siendo de hasta 2n - 1 sondas para un lote de n columnas y, si el presupuesto se agota, las hojas pendientes quedan no_disponible sin ser supuestas. El tamaño mayor de lote aceptado se guarda sólo en el presupuesto de la corrida y se publica como meta$tamano_lote_funciono; no queda estado global asociado a la conexión.

Los agregados planos sobre la misma tabla y filtro —COUNT(col), mínimos, máximos, medias, ceros, negativos y desvío— comparten ahora una consulta por lote. COUNT(DISTINCT ...) conserva su clase separada. La fusión no cambia las métricas ni su disponibilidad: una falla de un agregado en una columna todavía se prueba por separado antes de declararla no disponible.

La medición se hizo sobre una tabla en memoria de 20 columnas y 100 filas, recreando la expresión en cada vuelta, con muestra = 12 y bloque_muestra = "solo_agregados". Las consultas emitidas antes y después fueron, en orden exacto, seguro, conteos, muestreado, aproximado:

| modo | antes | después | ahorro | | --- | ---: | ---: | ---: | | exacto | 50 | 49 | 1 | | seguro | 10 | 8 | 2 | | conteos | 6 | 6 | 0 | | muestreado | 59 | 58 | 1 | | aproximado | 59 | 58 | 1 |

El ahorro es deliberadamente pequeño en el modo por omisión porque COUNT(DISTINCT ...) y la moda siguen fuera de la consulta plana; la mediana también, salvo en los motores cuya sonda acepta la consolidación con PERCENTILE_CONT. La fusión paga sobre todo en seguro, donde las tres pasadas planas pasan a una. plan_perfilado_dbi() refleja esas clases y su rango de sondas por bisección.

El plan ya no cobra trabajo de R que no va a ocurrir

Con bloque_muestra = "solo_agregados" no se trae ninguna fila a R, así que el detector de vocabulario no corre. El plan lo reflejaba bien en cuatro modos y mal en "muestreado": seguía anunciando los pares de formas a comparar en R, y el texto impreso se contradecía a dos líneas de distancia —«el plan incluye sólo agregados SQL» y después «más 4.000.000 pares de formas a comparar en R»—.

El conteo colgaba de un conjunto de alcances que mete en la misma bolsa dos cosas distintas: el muestreo del motor, que en ese modo ocurre igual, y el bloque del cliente, que es lo único que trae filas. Ahora cuelga sólo del segundo.

Instrumentación de consultas y etapas R

perfilar_dbi() agrega a resumen_tabla$sql duracion_ms, cpu_ms, n_filas_resultado, bytes_resultado_r, consulta_id y etapa. También publica resumen_tabla$tiempos, con las duraciones en milisegundos de la lectura y el perfilado de la muestra, el perfilado por columna y los análisis opcionales. Las ramas sin consulta dejan NA; no se publican ceros por falta de resolución del reloj. instrumentar = FALSE apaga la medición sin cambiar el plan ni el orden o la cantidad de consultas.

Un microbenchmark de 158 columnas y 262 consultas pasó de 0,2230 s sin instrumentar a 0,2368 s con reloj, filas y bytes (+0,0138 s; 6,188 %); dos lecturas de Sys.time() solas costaron aproximadamente 10 µs por consulta. En el flujo real de perfilar_dbi() (158 columnas, 10.000 filas y 347 consultas), cinco pares alternados dieron medianas de 2,398 s y 2,419 s (+0,021 s; 0,876 %). Por eso la medición queda activa por omisión y conserva un interruptor explícito para corridas donde ese costo relativo importe.

La poda y el informe preguntan ahora por la misma función

detectar_dependencias() decide dos veces lo mismo: si un par puede alcanzar el umbral —para descartarlo sin calcularlo— y si lo alcanzó —para informarlo—. Las dos decisiones estaban escritas como desigualdades separadas, y dos expresiones algebraicamente equivalentes no son iguales en punto flotante: cada forma pierde el borde con su propio conjunto de umbrales, y ahí la poda descarta un par que el informe habría publicado.

Ahora las dos preguntan por .alcanza_umbral_dependencia(). Mientras fueran expresiones distintas, saber que coinciden exigía probar todos los umbrales; compartiendo función, no hay nada que coincidir.

No lleva tolerancia a propósito: una tolerancia cambiaría lo que el umbral significa, y eso sería parte del contrato público y no un detalle interno.

Dos podas descartaban lo que igualaba el umbral

detectar_dependencias() y detectar_relaciones() descartan pares sin calcularlos cuando el umbral es inalcanzable. Las dos comparaciones se escribían multiplicando, y la multiplicación redondea: 25 * 0.56 da 14.000000000000002, así que un par cuyo cumplimiento máximo vale exactamente 0,56 quedaba por debajo y se descartaba. Como el informe descarta sólo lo que está por debajo del umbral, ese par debía informarse.

Lo mismo en la poda por cardinalidades de detectar_relaciones(): con siete valores distintos contra veinticinco y umbral 0,28, el producto daba 7.0000000000000009 y declaraba cardinalidades_imposibles una cobertura que sí era alcanzable.

Las dos comparan ahora dividiendo. Los umbrales por omisión del paquete no disparaban el defecto —por eso no se había visto—, pero umbral y umbral_cobertura admiten cualquier proporción, y un barrido exhaustivo con la forma anterior encontró setecientas dos podas de más.

Pedir sólo agregados sin leer la muestra

perfilar_dbi() y perfilar_coleccion() aceptan bloque_muestra = "solo_agregados" para calcular los agregados SQL sin traer filas a R ni ejecutar el perfil de la muestra. El valor por omisión "con_muestra" conserva el comportamiento anterior. plan_perfilado_dbi() omite la fila y el costo de la muestra cuando corresponde, y la cobertura usa no_solicitado para distinguirlo de una muestra que se intentó leer y no estuvo disponible.

La inferencia de tipo clasifica el vocabulario repetido una sola vez

.inferir_tipo() usa el vocabulario textual cuando su cardinalidad hace conveniente el recorrido, y pondera cada forma por su frecuencia en las filas. El camino que no activa el atajo conserva las mismas pasadas sobre cada valor. La detección de fechas queda sobre las filas muestreadas: sus atributos compatibles, total, analizados, muestreado y el caché interno de meses forman parte del contrato del perfilado y no se reemplazan por conteos de formas.

Dos números que las optimizaciones habían movido

Los cambios que quitaron recorridos internos prometían no mover nada de lo informado. Movían dos cosas, las dos en el borde.

La mediana no es el cuantil 0,5 hasta el último bit. median() promedia los dos valores centrales con (a + b) / 2 y quantile(type = 7) interpola con a + 0,5 * (b - a); cuando los centrales son de magnitudes muy dispares redondean distinto. Sobre c(-1000, 0.000111, 0.25, 1000) la mediana informada pasaba de 0,12505549999999999 a 0,12505550000000001. Vuelve a salir de median(); los cuartiles siguen compartiendo una sola llamada, así que el recorrido que se ahorra sigue ahorrado.

Y la poda de dependencias callaba el par que iguala el umbral. La cota se comparaba como k_y - k_x > n * (1 - umbral), y 1 - 0.8 vale 0,19999999999999996: con cinco filas, la resta hacía que 1 > 0,99999999999999978 y el par se descartaba. Su cumplimiento era exactamente 0,8, y el filtro de informe descarta sólo lo que está por debajo del umbral, así que ese par debía informarse. Escrita como el máximo alcanzable contra lo que el umbral exige, el borde deja de perderse: medido sobre 200.000 combinaciones, la forma anterior podaba de más 88 veces y la nueva ninguna.

La deteccion de dependencias conserva el resultado y reduce el costo

La particion de parejas (determinante, dependiente) usa una clave entera cuando el producto de sus cardinalidades no supera 2^53; en el borde o fuera de el conserva el camino con interaction(). La clave solo renumera parejas, por lo que no cambia sus conteos, los grupos en conflicto ni ninguna dependencia informada.

La deteccion agrega una cota de poda basada en la cantidad P de parejas distintas del subconjunto valido: P - k_x. La particion y sus conteos se reutilizan si el par debe evaluarse, y la bateria de equivalencia compara el objeto completo con ambos atajos y con sus caminos de referencia.

Tres recorridos internos se eliminan sin cambiar lo informado

El resumen cuantitativo comparte una sola llamada a quantile() para obtener los cuartiles y la mediana, deriva el IQR de esos valores y pasa q1 y q3 al diagnóstico de sentinelas. La guarda que evita ese diagnóstico con menos de 20 valores sigue decidiendo antes de sus cuantiles. Sobre un millón de valores, la expresión equivalente pasó de una mediana de 0,048 s a 0,027 s; el resultado fue identical().

El conteo general de duplicados calcula una vez cada dirección de duplicated() y deriva los dos conteos. Los tryCatch siguen aislando un fallo inicial (NA, NA) de un fallo sólo en fromLast (valor, NA). Sobre 300.000 × 5, la mediana pasó de 0,928 s a 0,707 s; los dos conteos fueron idénticos.

La detección de dependencias poda un par sólo cuando ninguna de sus columnas tiene ausentes y la cota k_y - k_x > n * (1 - umbral) hace inalcanzable el umbral. Las cardinalidades usadas son las de estadisticas, que en ese caso son exactamente las del subconjunto válido; con ausentes el par se evalúa completo. Sobre 20.000 × 12 se podaron 36 de 132 pares y se hicieron 96 llamadas al resumen: la mediana pasó de 0,575 s a 0,444 s. El objeto completo fue identical() con la ejecución sin poda, y el caso que cumple 0,996 se conserva.

El plan declaraba cero trabajo para la tabla entera

plan_perfilado_dbi() subdeclaraba el costo justo en el caso por omisión. Con muestra = Inf —que trae la tabla entera— el bloque de muestra se contaba como cero filas leídas y cero pares de formas a comparar, de modo que pedir todas las filas declaraba menos trabajo que pedir mil. Sobre una tabla de 200.000 × 4, muestra = Inf anunciaba 400.000 lecturas y muestra = 200000 —que pide exactamente las mismas filas— anunciaba 600.000.

Como la magnitud del trabajo se decide sobre esos números, el caso por omisión caía en «baja» y el plan no imprimía las palancas para bajar el costo.

Las dos formas de pedir la tabla entera declaran ahora lo mismo. Los valores que sí son inválidos siguen tratándose como antes.

Y el hueco simétrico: una muestra finita mayor que las filas de la tabla tampoco quedaba acotada. Pedir un millón de filas de una tabla de cien no trae más de cien —la lectura real es min(n_total, muestra)—, pero el plan imputaba un millón de lecturas. La inconsistencia era interna: el trabajo del cliente sí se acotaba, así que las dos mitades de la misma cuenta usaban tamaños de muestra distintos. Ahora el tamaño efectivo se calcula una sola vez y las dos mitades lo comparten.

Las geometrías que vienen de una base ya no pierden su trazabilidad

Un hallazgo sobre una columna de geometría —una coordenada fuera del dominio, una geometría inválida, una vacía— devolvía las filas que lo respaldan sólo si la columna era un objeto sfc. Por DBI las geometrías llegan como texto WKT o como blob WKB, así que la traza salía no_disponible y el paquete avisaba de una incoherencia que no existía: los índices ya estaban calculados, y se descartaban por la clase de la columna.

Contra una tabla PostGIS real eso producía coordenada_fuera_dominio en geom: traza no disponible.

La condición pasa a ser que el análisis haya dejado sus índices, no de qué clase es la columna: son posiciones de fila y valen igual esté la geometría como sfc, como texto o como binario. Una columna común sin análisis de geometría sigue sin traza, que es lo correcto.

Faltar tablas se rechaza antes, y se nombran todas

medir() acepta los datos como un data frame o como una lista con nombre. La primera forma exigía que el modelo tuviera una sola entidad; la segunda comprobaba la forma de la lista pero no que estuvieran las tablas de todas las entidades que el modelo necesita.

El faltante aparecía después, al ligar cada métrica, con un mensaje que nombraba una sola entidad. Con tres ausentes eso son tres vueltas para el mismo problema: agregar la primera, volver a correr, chocar con la segunda.

Ahora se rechaza en la frontera, nombrando todas las que faltan y diciendo cuáles se recibieron —que es la mitad que deja ver un error de tipeo—.

No es igualdad exacta, a propósito. Una tabla que el modelo no pide se sigue ignorando: exigir que sobren cero habría apagado un uso legítimo.

El muestreo en el motor declina una muestra infinita, en un solo lugar

Desde que muestra = Inf —la tabla entera— es el valor por omisión, el muestreo en el motor no tiene sentido: quedarse con «todas» las filas no es muestrear. Eso estaba guardado rama por rama, y de tres ramas se guardaron dos. La tercera —la de TABLESAMPLE RESERVOIR (n ROWS)— pasaba Inf directo al constructor y contra un motor real producía RESERVOIR (Inf ROWS) y un error de sintaxis.

El paquete lo declaraba honestamente —«El resumen SQL se calculó y se devuelve, pero la muestra no»— pero el usuario se quedaba sin perfil de muestra sin haber pedido nada raro.

La guarda pasa a estar en la entrada de la función y no dentro de cada rama. Antes de moverla se comprobó que valiera para las tres: con Inf la fracción se satura en 1, o sea TABLESAMPLE (100 PERCENT), que es la tabla entera. Si no se hubiera saturado, la rama de porcentaje habría sido un caso legítimo y la guarda única, un error.

Guardar caso por caso es cómo se olvidó una de tres, y además deja sin proteger a la rama que se agregue después.

El aviso de trazabilidad dice de quién es el problema

Cuando un hallazgo quedaba con su trazabilidad inconsistente, el aviso decía «La trazabilidad contiene incoherencias» seguido del detalle interno. Una corrida contra bases reales lo describió como honesto pero críptico, y al mirarlo resultó peor que críptico: estaba dirigido a la persona equivocada.

Las seis condiciones que lo disparan comparan dos salidas del propio lupa —lo que el hallazgo afirma contra las filas que se le adjuntaron— y ninguna mira los datos del usuario. Es una guarda de invariante contra regresiones: la prueba que la cubre la dispara mutando n_afectados a mano después de perfilar, y comprueba que un perfil intacto no la emite.

Ahora el aviso dice, en ese orden: que es un problema de lupa y no de sus datos, que no hay nada que corregir en la tabla, qué significa para el hallazgo que está leyendo —las filas que lo respaldan pueden no corresponderse con su conteo—, que se conserva para no ocultar la evidencia, y el detalle interno con la etiqueta del hallazgo para poder reportarlo.

El alcance LSH declara que sus candidatos dependen del orden

Con MinHash/LSH el conjunto de candidatos depende del orden de las filas: el vocabulario de q-gramas se numera por orden de primera aparición y esa numeración alimenta las firmas. No es un defecto —ocurre dentro de la garantía declarada, y está medido: de los pares que cambian al barajar, ninguno supera un Jaccard de q-gramas de 0,8, donde el recall declarado es 0,9998— pero era una propiedad del resultado que vivía sólo en la documentación.

alcance gana lsh_candidatos_dependen_orden_filas y lsh_orden_vocabulario. El segundo no es decorativo: nombra el mecanismo, así que si alguna vez la numeración se canoniza, ese valor cambia y la dependencia deja de declararse sola. Ninguno de los dos aparece en el camino exhaustivo, que no tiene esa dependencia.

Una barra de progreso para las corridas largas

Ahora que la tabla entera es el valor por omisión, una corrida sobre millones de filas puede tardar minutos. Y una corrida callada no se distingue de una colgada.

En una sesión interactiva perfilar_dbi() muestra una barra que avanza contra las consultas que el plan dice que va a emitir: un total conocido, no una estimación. Y perfilar_coleccion() avanza por tabla, diciendo cuál está perfilando —que es lo que permite ver si una se trabó— y sin aparecer cuando hay dos tablas o menos. No aparece cuando la corrida es de menos de una docena de consultas —termina antes de que sirva— ni fuera de una sesión interactiva, para que la salida de un guion no traiga ruido. options(lupa.progreso = ) manda sobre eso en los dos sentidos.

No cambia ningún valor de lo que se mide, y hay una prueba que compara el perfil con la barra y sin ella para que siga siendo cierto.

La tabla entera pasa a ser el valor por omisión

perfilar_dbi(), plan_perfilado_dbi() y perfilar_coleccion() traían 1.000 filas por omisión para el perfil de muestra. Ahora traen Inf: la tabla entera.

Antes ni siquiera se podía pedir. muestra exigía una cantidad finita, así que perfilar la tabla completa obligaba a averiguar cuántas filas tenía y pasarlas a mano: se podía hacerlo, no se podía decirlo. Ahora Inf significa «toda, sea del tamaño que sea», la consulta sale sin LIMIT y tabla_completa queda en TRUE.

El cambio de omisión sigue el criterio del paquete: un análisis de calidad no se corre todos los días, y mirar menos filas para terminar antes es la concesión equivocada. Quien necesite acotar lo pide con muestra = n, y el plan lo avisa antes de empezar: «El perfil de muestra trae la tabla entera —N filas—: es el valor por omisión y sobre una tabla grande puede demorar».

Importa más de lo que parece. El resumen de tabla no se muestrea —con modo = "exacto" se agrega en el motor sobre todas las filas—, pero los diagnósticos que necesitan los valores en R —patrones, formatos, casi-duplicados— salen de esa muestra, y sin orden_muestra son las primeras filas que devuelva el motor, no una muestra aleatoria. Un defecto que viva al final de la tabla no se ve.

Medido sobre 200.000 filas con un defecto plantado al final: con el valor por omisión aparecen tres hallazgos y con Inf aparecen cinco, a cambio de 10 segundos en vez de 2. Un análisis de calidad no se corre todos los días; si el tiempo no es la restricción, Inf es la opción honesta.

El plan nombra las palancas también cuando el trabajo es medio

plan_perfilado_dbi() avisa antes de pagar, y cuando el trabajo estimado era alto listaba las opciones concretas para acotarlo. Cuando era medio, se limitaba a avisar.

Eso dejaba la decisión a medias justo donde importa. Corriendo contra motores reales, una tabla de 4,5 millones de filas en PostgreSQL tardó 6,2 minutos con las opciones por omisión, y su plan la clasificaba media: el aviso avisaba, pero quien no conociera modo = "muestreado" —que baja esa misma tabla a 39 segundos, 9,5 veces menos— no tenía cómo enterarse.

Ahora las nombra en los dos casos: modo, metricas, muestra, max_consultas, y max_trabajo_vocabulario cuando lo que pesa es la comparación de formas en R. En una tabla chica no las lista: ahí el usuario ya tiene su respuesta y cuatro viñetas serían ruido en cada corrida.

El recorte de duplicados ya no depende del orden de las filas

detectar_duplicados_aproximados() recorta con max_resultados conservando los pares más cercanos. Entre pares empatados en distancia desempataba por posición de fila, así que cuáles sobrevivían dependía del orden en que llegaron las filas y no de los datos: la misma tabla exportada con otro ORDER BY podía devolver otro subconjunto.

Medido sobre 60 grupos cuyos pares internos comparten exactamente la misma distancia, con el corte en 30: cinco órdenes distintos —natural, inverso y tres barajados— devolvían 30 grupos cada uno y no compartían ninguno. Ahora los cinco devuelven exactamente los mismos.

El desempate usa el rango canónico del valor, que es la misma decisión que ya gobernaba el recorte del vocabulario —ahí tomar las formas en orden de llegada daba 26 grupos y en orden alfabético 148—. La clave es simétrica, min y max del rango, para que tampoco dependa de cuál fila quedó primera dentro del par, y el rango se calcula una vez sobre el universo completo de la corrida: una numeración por lote sería local y haría que la comparación entre lotes dependiera del reparto.

Lo que ordenar no arregla —que un corte dentro de un empate deje afuera pares igual de cercanos— se declara. alcance gana distancia_corte, n_en_distancia_corte y corte_en_empate, y ese último no es truncado con otro nombre: da FALSE cuando el corte cae en una distancia única.

corte_en_empate se mide contra lo que el recorte descartó, no contra lo que quedó. Contar cuántos de los conservados comparten la distancia del borde no alcanza: si en el borde sobrevive uno solo, el conteo da 1 y la señal caería en FALSE aunque se hubieran tirado pares a esa misma distancia. Pasaba con cuatro filas y max_resultados = 1.

Y queda un límite que ningún orden saca: si varias filas comparten el valor comparado, el conjunto de pares de valores es idéntico en cualquier orden, pero cuáles instancias de fila los representan cambia. Es irreducible —esas filas son indistinguibles en esa columna— y está declarado en la documentación.

El modo aproximado dice qué métrica aproximó, y con qué función

perfilar_dbi(modo = "aproximado") sondea las funciones aproximadas nativas del motor y cae a la medida exacta cuando el motor las rechaza. Esa contabilidad ya estaba completa por métrica en resumen_tabla$sql —estado, metodo y error_esperado—, pero al imprimir el perfil se leía Métricas: calculado 24, estimado 6 sin saber cuáles eran las seis ni con qué función se calcularon.

Ahora el print lo dice: Aproximaciones aplicadas: distintos por APPROX_COUNT_DISTINCT y mediana por approx_quantile, o bien Sin aproximación para distintos y mediana: el motor no la aceptó y las métricas se midieron exactas. No cambia ningún número: publica donde se lee lo que ya estaba medido.

error_esperado sigue respondiendo desconocido cuando el motor no documenta una cota. Medir un error sobre una tabla no autoriza a declararlo como garantía sobre todas.

Los motores reales se verifican en cada push

La fila «probado contra el motor real» de la tabla de motores dependia de que alguien se acordara de correrlo a mano: se media una vez, quedaba en notas, y la tabla envejecia sin avisar.

benchmark/verificar_motor.R comprueba contra un motor real que las columnas se perfilan, que el dialecto se resuelve por sonda, que la media del motor coincide con la de R sobre la misma columna -que corra no alcanza-, que la clave primaria se lee del catalogo, y que la cobertura existe. Se configura por variables de entorno, asi que el mismo guion sirve en una maquina y en integracion continua, y sale con codigo distinto de cero si algo falla.

El flujo motores.yaml lo corre contra PostgreSQL 16 y MariaDB 11 levantados como servicios. Los que exigen un cliente propietario se siguen verificando a mano, y su fila de la tabla dice con que fecha.

El catalogo entra en la identidad de una tabla

catalogo.esquema.tabla es el nombre de tres partes que usan SQL Server y otros motores, y lupa perdia el del medio: se quedaba con la primera y la ultima. Como las dos que quedaban eran sintacticamente validas, el SQL salia bien formado contra una tabla inexistente y el error volvia al usuario como un problema suyo de permisos —lo mandaba a pedirle al DBA acceso a una tabla que ya podia leer—.

Ahora el catalogo es una columna estructurada, siempre presente y NA cuando no existe. El identificador une las partes que hay y sigue siendo inyectivo: t, esq.t, cat.esq.t y cat.t son cuatro identidades distintas.

Y la unicidad pasa a mirar la identidad completa. Antes, dos tablas con el mismo nombre y esquema en catalogos distintos colapsaban y la coleccion las rechazaba como repetidas: se rechazaba una frontera valida.

Lo que no cambia: sin catalogo el identificador es identico al de antes, asi que ninguna coleccion guardada deja de cruzar con su frontera; y un identificador mal formado -cuatro partes, comillas sin cerrar, punto inicial o final- se sigue rechazando nombrando la causa.

Una clave declarada excluye Benford, y queda declarado

Sobre una clave primaria se emitia una desviacion de Benford: se le afirmaba un problema de calidad a una columna que no tiene distribucion que analizar.

Se intento adivinar cual columna era clave por la forma de sus valores y ese camino se retiro, porque el criterio terminaba dependiendo de cuantas filas se habian cargado y callaba magnitudes reales. Declarada no hay nada que adivinar: si se pasa clave a perfilar(), o se lee del catalogo de la base, Benford no corre sobre esas columnas y la cobertura lo declara. Sin declararla el comportamiento no cambia.

Y el motivo dice cual de las dos cosas paso. «Parece un identificador» es una inferencia del paquete; «la clave fue declarada» es un hecho que trajo el usuario, y publicar la primera cuando corresponde la segunda le atribuye al paquete una deduccion que no hizo.

Menos codigo repetido, y el que quedaba ya habia divergido

Seis reglas estaban escritas mas de una vez. Dos de ellas tenian que coincidir o el paquete mentia, y una ya habia divergido en tres cosas a la vez.

Los ejemplos no nombran organismos

El ejemplo de organizacion() y las pruebas de esa granularidad usaban el nombre de organismos reales. Un ejemplo de roxygen viaja al .Rd y al sitio publicado, y las pruebas viajan en el tarball. Ahora usan nombres genericos -"Organismo A", "Organismo B"-, que es lo que corresponde: la frontera de una organizacion la declara quien la conoce, y el paquete no tiene por que nombrar a ninguno.

Lo que no se evaluo llega a las cuatro puertas

Cuando lupa decide no correr un diagnostico lo anota con su motivo, pero esa tabla solo llegaba a quien mirara el perfil. Ahora llega a las cuatro puertas desde las que se trabaja:

Una gramatica de entidades HTML, no dos

La misma expresion estaba escrita dos veces: una para detectar entidades y otra para repararlas. Si divergian se detectaba lo que no se reparaba, o al reves, y ninguna de las dos avisaba. Es el mismo acoplamiento que tenian las tres copias de la generalizacion de patrones.

En Oracle la cadena vacia es el nulo, y eso se declara

Las mismas tres filas -"", NA, "x"- dan n_faltantes = 2 por Oracle y 1 por un motor que las distingue. La misma columna tiene una completitud distinta segun el motor, y no porque el dato cambie.

Es la semantica documentada de Oracle, y lupa la declara. Lo que no hay es una corrida contra un servidor Oracle real que la respalde: el catalogo de motores da las dos variantes de Oracle como esperado, y esta nota decia antes "medido contra Oracle Free 23 real" sin un log que lo sostuviera.

No es un defecto que se pueda arreglar -es la semantica del motor- pero callarlo si lo seria: quien compare completitud entre entregas de motores distintos leeria una diferencia que no esta en los datos. Queda en la cobertura del resumen, y solo en los motores donde corresponde.

La clave primaria se lee del catalogo cuando esta declarada

Sobre un data.frame no hay a quien preguntarle cual es la clave, y por eso sugerir_clave() ordena candidatas. En una base relacional esa pregunta ya tiene respuesta escrita: la clave primaria esta en el catalogo del motor. Ahi no hay nada que sugerir, hay que leerla.

Se resuelve en una sola consulta, elegida por el controlador y no probando una tras otra: probar hasta acertar gastaria un numero de consultas que depende del motor, y plan_perfilado_dbi() promete exactamente cuantas emite. Es el mismo criterio por el que la sonda del desvio gasta siempre dos aunque acierte en la primera.

Y se distinguen dos respuestas que se confundian. SQLite devuelve cero filas sin error para una tabla que no existe, asi que filtrando en el SQL "no declara clave" y "no se pudo preguntar" llegaban iguales. Se piden todas las columnas y se filtra despues: cero filas significa que la tabla no esta, y filas sin ninguna marcada significa que no tiene clave.

Lo que no se evaluo se dice donde se decide, y la clave se pregunta

Dos arreglos medidos y retirados, y los limites que quedan declarados

Se probaron dos reglas para cerrar dos falsos hallazgos conocidos. Las dos funcionaban sobre el caso que las motivo y las dos callaban algo real, asi que ninguna entra: la regla del paquete es que una guarda solo se acepta si no calla nada verdadero. Lo que se midio queda escrito en el codigo, y los limites quedan fijados en test-ronda124.R para que esten declarados y no se redescubran.

Once senales falsas sobre una base real, y ninguna se apaga en silencio

Una corrida contra tres tablas administrativas reales dio 24 senales, de las cuales once eran falsas. El calculo estaba bien en las once; lo que fallaba era el juicio de si la prueba corresponde. Un identificador no es una magnitud, y hay pruebas que solo describen magnitudes.

Ninguno de los seis se apaga en silencio. Bajar el ruido callando seria mejorar el numero sin mejorar el paquete, asi que cada prueba que no se corre deja su fila en cobertura_diagnosticos, con el motivo medido -que porcentaje de los enteros cubre la columna, cuantos valores se habrian senalado, cuantas filas de cuantas trae la muestra- y que hacer si el usuario no esta de acuerdo. Los pares de orden descartados quedan nombrados en meta$orden_columnas$pares_identificador_descartados.

La ganancia por hilos no era una propiedad del paquete

La vinieta de escala publicaba una tabla -133,28 s con dos hilos, 70,31 con dieciseis- medida sobre un padron que no se distribuye, asi que nadie podia rehacerla. Ahora benchmark/medir_escala_hilos.R genera un padron sintetico y mide la misma curva, y no da lo mismo: sobre 100.000 filas con 23.800 nombres distintos, de dos a dieciseis hilos se gana un 12 %, no la mitad del reloj.

La explicacion es que los hilos los usa stringdist al comparar, y generar los candidatos y armar los grupos no los usa. Segun cuanto pese cada parte en un conjunto concreto, la ganancia va del 90 % al 12 %. La vinieta publica las dos tablas y dice que quien vaya a subir el valor conviene que mida su caso.

Lo que si vale en las dos: pasados dieciseis hilos no hay ganancia medible, y el resultado no cambia. Y esto ultimo recien ahora esta comprobado de verdad. El banco comparaba cuantos pares devolvia cada configuracion, y como el tope de resultados se alcanza en todas, el numero era siempre el mismo: la comprobacion se cumplia sola. Ahora compara los 50.000 pares uno por uno.

Otros dos defectos del mismo banco, encontrados antes de que publicara ningun numero: el generador daba 80 nombres distintos para 100.000 filas -el detector trabaja sobre formas distintas, no sobre filas, asi que no ejercitaba nada- y nrow() sobre el resultado devolvia NULL, porque es una lista con $pares y no un data frame.

Un diagnostico nuevo: el centinela que ninguna lista puede declarar

Un 9999 es una edad imposible y un codigo postal perfectamente valido. Por eso la lista de sentinelas_numericos no lo trae por omision, y hace bien: marcarlo siempre romperia cualquier columna donde ese numero es un dato.

Lo que si lo distingue es cumplir las tres cosas a la vez: quedar fuera de los limites de la columna, repetirse cinco veces o mas, y tener forma de digito repetido. posible_centinela_numerico lo informa sin contarlo como ausencia -esa decision es de quien conoce la columna, agregandolo a la lista- y nombra las filas donde esta.

Medido sobre doce columnas con la respuesta conocida, acierta las doce. Los tres controles que podrian haberlo roto: un codigo postal 9999 repetido treinta veces no es extremo dentro de su columna, un monto real de 9999 no se repite, y un ano 1999 no tiene esa forma.

Es el unico diagnostico del paquete que ninguna senal sola podia dar: la forma sin los limites marca codigos validos, los limites sin la forma marcan cualquier extremo, y la repeticion sin las otras dos marca cualquier valor frecuente.

Un valor centinela se escapaba de la proteccion de datos personales

Es lo mas serio de la tanda y lo introdujo la tanda misma. El perfil pasa a medir cual es el valor que una columna usa para decir "sin dato" -un 9999 entre edades- y ese campo publicaba un valor de celda sin pasar por el enmascarado. Sobre una columna de documentos protegida se veia asi:

moda                    [valor protegido]
minimo                  NA
centinela_valor         9999

Y la descripcion del hallazgo lo nombraba en texto, con lo cual llegaba hasta el informe HTML. La evidencia si salia protegida: la fuga entraba por la puerta de al lado.

Ahora centinela_valor se enmascara igual que el minimo, la moda y la media, y la descripcion se limpia en la misma capa que ya limpiaba la evidencia. Las repeticiones se siguen informando, porque son un conteo y no un valor.

Que el valor sea casi seguro un centinela y no un documento no cambia la regla: la proteccion no adivina cuales valores son inocentes.

Dos diagnosticos que se contradecian sobre la misma columna

999 esta en la lista de centinelas declarados, asi que faltantes_disfrazados lo cuenta como ausencia con severidad error. El diagnostico nuevo decia, sobre esa misma columna, que "no se cuenta como ausencia porque no esta declarado". Los valores que la lista ya cubre dejan de producirlo: el diagnostico existe para los que no estan declarados, como un 8888.

perfilar() dentro de do.call() rompia el informe

deparse() de una expresion larga devuelve varias lineas, y con do.call(perfilar, list(tabla, ...)) la expresion es la tabla entera: el nombre del conjunto salia con ocho elementos y reportar() fallaba con "values must be length 1". Pasar los argumentos en una lista es lo natural cuando se perfila en un bucle. Si la expresion no cabe en una linea, no la escribio nadie y se usa una etiqueta generica.

Lo que el idioma no deja resolver

La comparacion que separa una errata de la misma palabra escrita de otra manera ignora acentos, y en español el acento distingue palabras. El mismo mecanismo que junta Jose con José -que si es la misma- junta papa con papá y ano con año, que no lo son. Separarlos pide un diccionario del idioma.

Asi que se corrigio la afirmacion, no la medida: el texto decia "son la forma dominante escrita distinto" y ahora dice en que difieren, que es verificable, y avisa de que en español el acento puede cambiar la palabra.

La misma numeracion, descrita de dos maneras

Aparecio al intentar romper el arreglo de arriba. Un codigo 1..284 con 179 valores fuera de los limites de Tukey:

La guarda dependia de como estaba almacenado el numero, no de que es. Y eso pesa mas de lo que parece: por la puerta DBI casi todo llega como doble -SQLite entrega asi hasta las fechas- y varios lectores de CSV tambien, asi que el arreglo no cubria el caso que lo motivo cuando el identificador venia de una base.

La causa estaba en la raiz. .resumen_secuencia_entera exigia tipo entero y dos lineas mas abajo comprobaba que todos los valores fueran enteros. La segunda condicion es la real; la primera solo dejaba columnas afuera, y con ellas su densidad_secuencia_entera, que quedaba en NA.

Lo que dijeron las cinco auditorias externas

Cinco revisiones independientes -coherencia entre las dos puertas, prueba de las afirmaciones publicadas, barrido de codigo, bancos y redaccion- dejaron sus informes. Lo que sigue es lo que quedaba abierto de ellas.

Una prueba que pasaba sin probar nada

test-formatos-adicionales.R verifica que los meses escritos se detecten igual en cualquier LC_TIME. Recorria tres locales con try(Sys.setlocale(...)) y comparaba contra la base medida en C. En una maquina donde esos locales no estan generados -una imagen minima de contenedor no los trae- la llamada falla, el try se traga el fallo, el locale nunca cambia y el test compara el resultado contra si mismo.

Pasaba. Y la unica senal de que no habia medido nada era un aviso suelto en el resumen de la suite: "OS reports request to set locale cannot be honored".

Ahora comprueba que el locale quedo puesto, y si ninguno se puede poner se saltea declarando el motivo en vez de contar un exito vacio.

El plan de consultas dice rango en todas partes

attr(plan, "supuesto") ya declaraba un rango, pero el metodo de impresion, el @return de la ayuda, los dos README y la vinieta seguian diciendo "techo". Un techo que el propio objeto desmiente dos lineas mas abajo es peor que no decir nada.

Una tabla con acentos rompia el perfil entero

Es el defecto mas serio de la tanda, y lo introdujo el arreglo del orden del vocabulario de ayer. Sobre cualquier tabla leida con read.csv() que tuviera acentos:

Error: Character encoding must be UTF-8, Latin-1 or bytes

El orden por bytes de R rechaza una cadena marcada unknown que contenga bytes no ASCII, aunque sean UTF-8 perfectamente validos, y asi llega cualquier CSV en espanol por el camino mas comun que hay: "Combustibles liquidos", "Energia Electrica". El mismo error estaba en el desempate de la moda.

La tabla de evidencia dice ahora con que se reproduce cada fila

Llego a publicar tres numeros que nadie podia comprobar desde el repositorio. El problema de fondo no eran los tres numeros sino que la tabla no obligaba a que cada afirmacion tuviera un reproductor. Ahora tiene una columna que lo dice.

Un token que es marca de formato ya no genera un falso duplicado

Mirando que reportaba el detector de vocabulario sobre una tabla real de vuelos aparecieron dos familias mezcladas:

[1:48 p.m. (27)  / 1:48 p.m.            Delayed (1)]   <- el estado del vuelo pegado
[12:00 a.m. (5)  / 12:00 p.m. (42)]                    <- doce horas de diferencia

La primera es un hallazgo real. La segunda es un falso positivo: 12:00 a.m. y 12:00 p.m. son dos valores legitimos distintos, y no hay forma de saber mirando la columna cual fue tipeado mal. Marcarlos a todos no es detectar, es sospechar en bloque de todos los valores de una forma y acertar por casualidad los que estaban mal: la precision de ese diagnostico era 0,259, tres de cada cuatro marcados eran valores correctos.

Una columna en Latin-1 perdia sus acentos en silencio

Es el defecto mas grave que encontro esta tanda, y no es un caso de borde: es un CSV viejo en espanol, que es la mayoria de lo que hay en datos publicos de la region. Sobre una columna con cinco valores distintos, el perfil informaba:

n_distintos: 2        <- son 5
n_faltantes: 0        <- dice que no falta nada
cobertura:   0 filas  <- no declara nada

validUTF8() mira los bytes, y los de un texto marcado latin1 no son UTF-8 validos, asi que CAFE, ANO y NUMERO con tilde se volvian NA antes de llegar a cualquier diagnostico. El invariante del paquete roto en su forma mas directa: informar como medido lo que se descarto.

La proteccion de datos personales dependia de por que puerta entraras

La media de una columna de cedulas salia expuesta por perfilar() y tapada por perfilar_dbi():

perfilar()      media = 5108024      detalle: [estadisticos de orden protegidos]
perfilar_dbi()  media = NA           detalle: [estadisticos de orden y momentos protegidos]

El argumento estaba escrito del lado DBI desde antes —"la media de las cedulas de una tabla chica reconstruye demasiado"— y el camino principal no lo aplicaba. Ahora las dos tapan la media, y el texto distingue si se taparon estadisticos de orden, momentos o los dos, para no declarar una proteccion que no se aplico.

El total del plan no era un techo, y ahora se publica como rango

attr(plan, "supuesto") decia que el total era un techo y declaraba solo la direccion "menos": una columna sin valores validos no emite sus metricas. Nunca declaraba la direccion "mas". Medido contra un motor que rechaza lotes —el caso exacto que motivo la consolidacion—:

plan (techo) = 22 consultas        real emitidas = 30

Si un lote falla, se emite la consulta del lote y ademas una por columna. Quien decide la viabilidad de una corrida con ese numero se quedaba corto justo en el escenario de degradacion. Ahora el plan publica total —lo que cuesta si ningun lote se rechaza— y total_lotes_rechazados —si se rechazaran todos—, y el costo real cae entre los dos.

Dos numeros publicados que no resistieron que otro los midiera

Los dos eran nuestros y de esta semana, y los dos son la misma forma de error: medir una cosa y publicarla como otra.

La tabla de evidencia del README, medida de nuevo

Publicaba 43 tablas de control y 25 senales. El generador esta en el repositorio, se redujo a 31 tablas, y el ruido bajo a 8 —el paquete mejoro— pero el README siguio publicando los numeros viejos porque ninguna prueba los ataba. Ahora dice 31 tablas, 0 errores y 8 senales, y los tres estan fijados en test-ronda107.R: si cambian, la suite falla y hay que actualizarlos a proposito.

Se saco la fila de los nueve defectos plantados. El numero es real —se midio en tres rondas— pero el fixture no esta en el repositorio, y reconstruirlo de memoria daria nueve defectos parecidos y no los mismos. En la tabla que sostiene el argumento del paquete, una fila menos es mejor que una fila que no se puede comprobar. Vuelve cuando exista su test.

Restos de correcciones anteriores

El veredicto ya no depende de como venga ordenado el archivo

Cuando el vocabulario de una columna de texto excede el presupuesto, hay que elegir que formas comparar. Se elegian las primeras en aparecer, y eso hacia que el resultado dependiera del orden de las filas.

Medido sobre la columna nombre de Ejes de vias de circulacion de Montevideo —45.400 filas, 8.318 formas distintas, del catalogo nacional de datos abiertos—, las mismas filas daban:

| orden de las filas | grupos de casi-duplicados | | --- | --- | | tal como viene el archivo | 26 | | desordenado (semillas 11, 202, 7777) | 71, 85, 70 | | alfabetico | 148 |

De 26 a 148 segun como estuviera ordenado el archivo. Un perfilador que hace eso mide la forma fisica de la tabla, no los datos, que es exactamente lo que el paquete promete no hacer.

Lo destapo la tercera vuelta contra bases reales, que dejo esta afirmacion sin verificar por no encontrar una columna que la ejercitara. La columna existia.

Un conteo del perfil ya no cambia de clase segun que tenga instalado el usuario

perfilar_dbi() devolvia n_validos, n_faltantes, n_distintos, frecuencia_moda, meta$filas y filas_totales_fuente como integer64 siempre que bit64 estuviera instalado, incluido un conteo de 20.

El R minimo declarado ahora es un R medido

DESCRIPTION declaraba R (>= 3.6.0), y era una promesa que el paquete no cumple. Pasa a R (>= 4.1.0).

Tres pruebas que exigian justo lo que decian no tener

Las tres tenian la misma forma, y solo se ven en un entorno donde el paquete opcional de verdad no esta.

Nota sobre el instrumento: _R_CHECK_DEPENDS_ONLY_=true ocultaba bit64 para skip_if_not_installed() —saltaba ocho pruebas por eso— y aun asi las dos de integer64 pasaban ahi, mientras fallaban en el contenedor. El check con dependencias recortadas no es equivalente a un entorno que no tiene el paquete.

El reintento que contra el driver real no se disparaba nunca

La version anterior agrego un reintento: si la lectura de la muestra falla y hay columnas de tipo largo declaradas, se reintenta sin ellas y se declara que quedaron afuera. Contra una base real no se disparo ni una vez. En la tabla que motivo el arreglo —158 columnas, 90 de ellas varchar(max)— el patron de tipos reconocio 0 de 90, la muestra se perdio entera y no aparecio el aviso de alcance: exactamente lo que el arreglo prometia evitar.

Un plan que contaba la mitad del reloj

plan_perfilado_dbi() le ponia magnitud "baja" a una tabla de 3.912 filas que tardaba 35 segundos. Cada numero que informaba era cierto —64.592 lecturas de fila, cero ordenaciones— y el juicio era falso: el trabajo no estaba en el motor sino en R, comparando formas de una columna de geometria en texto. Medir una mitad y llamarla el total es informar como completo algo parcial.

Presupuestos que miden trabajo, no que cuentan unidades

Una tabla del catalogo de PostGIS —spatial_ref_sys, 3.912 filas y 5 columnas— tardaba 243 segundos. No era la geometria: eran cadenas largas, WKT de proyecciones, y el detector de vocabulario se llevaba el 99,6 % del costo. Tenia dos topes, max_valores = 5000 y max_pares = 2000000, y ninguno de los dos miraba cuanto costaba cada unidad: 800 valores son 319.600 pares, muy por debajo del tope, pero cada comparacion era una Jaro-Winkler sobre 900 caracteres.

Un plan que dice cuanto cuesta, no solo cuantas consultas son

plan_perfilado_dbi() contaba consultas, y contar consultas no responde la pregunta que trae quien lo mira: si la corrida tarda segundos, minutos u horas. Catorce consultas sobre dos millones de filas son mucho mas trabajo que doscientas sobre mil.

Una columna que el controlador no sabe traer ya no se lleva la muestra entera

Hay columnas que muchos controladores no pueden devolver en una lectura corriente: TEXT y NTEXT en SQL Server, CLOB y BLOB en Oracle, bytea en PostgreSQL. Pedirlas junto con el resto hace fallar la consulta completa, y con ella se perdia toda la muestra. En una de las tablas de la corrida real, 90 de 158 columnas eran de esos tipos.

Un mensaje que se leia mal

El aviso de acciones destructivas de plan_limpieza mostraba p\u00e9rdida en pantalla: la cadena tenia la barra invertida duplicada, asi que el escape nunca se resolvia. Es un error que no ve nadie —el paquete instala, la suite pasa y R CMD check no protesta, porque la cadena es ASCII perfectamente valida— y solo se nota leyendo el mensaje. Hay ahora un barrido que recorre los literales de cadena del espacio de nombres y falla si alguno lleva un escape sin resolver. Era el unico caso en el paquete.

La primera version del barrido miraba solo el cuerpo de cada funcion, y la refutacion mostro que eso dejaba fuera dos sitios donde de verdad viven mensajes: los valores por omision de los argumentos y los atributos. Ahora los recorre. Lo que sigue sin ver es una constante capturada por closure desde un ambito local, y eso queda dicho en el propio test en vez de suponerse cubierto: en lupa no es un agujero, porque las constantes del paquete son enlaces del espacio de nombres y el barrido las recorre una por una.

El costo a escala: de una consulta por columna a una por lote

La segunda corrida contra bases reales dejo una sola reserva seria, y era esta: una tabla de decenas de millones de filas no terminaba de perfilarse ni en modo = "muestreado". La causa no era el muestreo sino la cantidad de escaneos: el paquete emitia una consulta por columna para cada bloque de metricas.

Lo que rompio la refutacion sobre estos mismos cambios

Cuatro correcciones de honestidad

Las cuatro salieron de mirar los datos crudos de la corrida real, y tres de ellas de reproducir lo que el informe atribuia a otra causa.

Leer un perfil sin conocer su forma, y saber que falta para cada motor

El camino de Oracle, contemplado y sin corrida que lo respalde

Lo que encontro una refutacion adversarial

Esta tanda se reviso al reves: buscando romper cada afirmacion en vez de confirmarla. Encontro ocho defectos, y los tres peores tenian la misma forma: la tabla con la que se verificaban los motores era comoda. Tenia 5.000 filas de tipos faciles, sin fecha nativa, sin enteros sin signo y nunca mas chica que la muestra pedida. Es el mismo error que el paquete ya persigue en los demas —el fixture que comparte la propiedad cuya ausencia es el fallo— aplicado al propio verificador.

benchmark/verificar_motor.R incorpora ahora los tipos y tamanos que escondian esos defectos, para que la proxima tabla comoda no certifique un motor que no lo esta.

MariaDB, y las diez granularidades del marco

DuckDB, y una sonda que mentia

La senal que faltaba: nadie declara lo que no sabe que existe

La via DBI sobre tablas que no entran en memoria

Bases enteras, y el costo de compararlas

Lo que el detector de vocabulario no puede ver, dicho

Costos declarados donde antes solo se tardaba

Infraestructura

Cuatro motores reales

Numeros que no pueden ser

La via DBI deja de asumir un dialecto y de tirar lo que ya midio

El nivel coleccion deja de informar cero donde no midio

El perfilado espacial deja de ser inviable por tiempo

El vacio por diseno se declara y deja de contarse como defecto

Privacidad: ante la duda se protege

Conteos que no se pueden contar

Recortes declarados donde se los busca

Patrones raros: ventana de operacion visible

Patrones raros y trazas accionables

La traza de vocabulario y la guarda de coherencia cierran el circuito

Conteos y trazabilidad dejan de mezclar unidades

La trazabilidad deja de recalcular lo que el detector ya decidió

Un hallazgo dice cuántas unidades afecta y, cuando puede, cuáles. Ese «cuáles» lo resolvía una rama de índices aparte que en varios casos aplicaba un criterio distinto del detector que había producido el hallazgo. Los dos no coincidían, y el desacuerdo no se veía porque nada comparaba la evidencia contra los índices.

El principio que unifica los cuatro: la trazabilidad no recalcula lo que el detector ya resolvió. Cada vez que lo recalculaba, los dos criterios se separaban en silencio.

Y la prueba que faltaba. La suite verificaba conteos, no identidades: una prueba que comprueba n_afectados == 10 pasa igual si el paquete nombra diez filas equivocadas, ninguna, o seiscientas. Por eso ninguno de estos desajustes se veía con toda la suite en verde. Ahora hay fixtures que construyen tablas con índices corrompidos conocidos de antemano y verifican aciertos, falsos positivos y pérdidas.

El piso de asimetría del vocabulario declara lo que deja afuera

Colecciones: el séptimo nivel de granularidad deja de estar sólo declarado

Evaluar estimaciones que calculó otra herramienta

Señales redundantes: la contradicción que ninguna columna muestra sola

Los umbrales de una regla salen del closure y se pueden consultar

Trazabilidad por clave declarada: del hallazgo que se lee al que se verifica

La cobertura del vocabulario deja de contradecir al hallazgo

Dos cosas más que el objeto ahora declara

Spearman para relaciones monótonas que no son lineales

Tres afirmaciones que el paquete hacía sin fundamento suficiente

El vínculo entre una acción del plan y su hallazgo ya no depende de la prosa

El perfilado no toca los datos, y ahora está probado

Duplicados: el hallazgo no afirma una igualdad que produjo la normalización

Casi-claves y precedencia de ausencias

Tablero, indice declarado y medicion agregada

Secuencias enteras densas y vocabularios breves

Orientacion explicita de las metricas

Marco CEA/CEPAL de aseguramiento de la calidad

Severidad del vocabulario y escala de las relaciones

Perfil de una muestra DBI con universo explícito

Desenlaces declarados por reglas

Ley de Benford con aplicabilidad explícita

URLs, unidades y celdas multivaluadas

Relaciones aritméticas entre columnas

Capa de marcos

Perfilado de geometrías

Fechas con meses escritos

Variantes del vocabulario

Referenciales

Perfil de normalización para comparar

Diagnósticos de texto invisible

Reparación de texto y licencia

Recursos de comparación

Marcos declarables y alcance internacional

Examinar datos

Medir y evaluar calidad

Mejorar y monitorear

Informar