iatoolkit 0.3.7__py3-none-any.whl → 0.3.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of iatoolkit might be problematic. Click here for more details.

@@ -0,0 +1,1337 @@
1
+ ## 🧠 Contexto general
2
+
3
+ - La base de datos es **PostgreSQL**.
4
+ - Muchas columnas contienen información en formato **jsonb**.
5
+ - Todas las consultas deben ser **sintácticamente correctas** y compatibles con PostgreSQL.
6
+
7
+ ## 🔍 Uso correcto de campos JSONB
8
+
9
+ ### Regla central: ¡NO ASUMAS QUE LAS CLAVES JSONB SON COLUMNAS!
10
+
11
+ - **Error frecuente:** El error `column "X" does not exist` ocurre cuando se intenta
12
+ usar una clave de un JSONB como si fuera una columna directa.
13
+ **¡Esto es incorrecto y la causa principal de errores!**
14
+ - **Solución obligatoria:** Usa SIEMPRE el nombre del campo JSONB contenedor seguido
15
+ del operador `->>` para acceder al valor.
16
+
17
+ #### Ejemplo incorrecto:
18
+ ```sql
19
+ -- INCORRECTO: "line_type" no es una columna de bcu_customer.
20
+ SELECT line_type FROM bcu_customer GROUP BY line_type;
21
+ ```
22
+
23
+ #### Ejemplo correcto:
24
+ ```sql
25
+ -- CORRECTO: Se accede a "line_type" desde jsonb_credit.
26
+ SELECT jsonb_credit->>'line_type' AS line_type
27
+ FROM bcu_customer
28
+ GROUP BY jsonb_credit->>'line_type';
29
+ ```
30
+ Nunca asumas que un campo como 'product_type', 'created', 'current_debt', 'amount', etc.
31
+ existe directamente como columna en la tabla base.
32
+ Si el campo está dentro de un objeto JSONB, debes acceder a él explícitamente usando
33
+ la sintaxis: <jsonb_field>->>'<subfield>' o <jsonb_field>->'<subfield>' según el
34
+ tipo requerido (texto o json).
35
+
36
+ No hagas SELECT de columnas inexistentes en tablas físicas.
37
+ Antes de usar columnas como `amount`, `certificate_ids`, `adjudication_date`, etc.,
38
+ verifica si están en la tabla física (como `bcu_tender`) o si provienen
39
+ de un campo JSONB (como `jsonb_collection` en `bcu_customer`).
40
+ Si están en un JSONB, accede a ellas mediante el operador `->>` sobre el JSON,
41
+ no como columna del alias SQL.
42
+
43
+
44
+ ### Aplicación general:
45
+ Esta regla aplica a todas las cláusulas SQL: `SELECT`, `WHERE`, `GROUP BY`, `ORDER BY`.
46
+
47
+ - si un campo JSONB puede venir como null, primero debe verificarse antes de castear.
48
+
49
+ ## 🔢 Casting y tipos de datos
50
+
51
+ ### Regla general de casting
52
+
53
+ - 🎯 El tipo de casting debe coincidir con el tipo declarado en el schema:
54
+ - `integer` → usa `::INTEGER`
55
+ - `number` o `numeric` → usa `::NUMERIC`
56
+
57
+
58
+ 🎯 Regla para castear montos numéricos desde JSONB
59
+ • 🔒 No asumas que los montos en JSONB son siempre enteros.
60
+ Algunos campos pueden venir como strings con decimales implícitos,
61
+ por ejemplo: "3503722.0".
62
+ • Si el valor puede incluir punto decimal, incluso .0, nunca lo cases
63
+ directamente como ::INTEGER, ya que eso genera error de casting.
64
+ • Nunca intentes castear un campo jsonb a jsonb[].
65
+ • En estos casos, usa ::NUMERIC para asegurar compatibilidad y precisión:
66
+ ´´´sql
67
+ (jsonb_field->>'field_name')::NUMERIC
68
+ ´´´
69
+
70
+ • Si necesitas un entero redondeado, usa ROUND(...) sobre el valor casteado:
71
+ ´´´sql
72
+ ROUND((jsonb_field->>'field_name')::NUMERIC) AS field_name
73
+ ´´´
74
+ - Siempre usa NULLIF(..., '')::NUMERIC y COALESCE(..., 0) al castear valores numéricos
75
+ provenientes de JSONB para evitar errores de conversión si el campo está vacío.
76
+ ejemplo en ´´´sql
77
+ COALESCE(
78
+ (NULLIF(jsonb_field->>'public_line_available', ''))::NUMERIC, 0
79
+ ) AS public_line_available
80
+
81
+ - Siempre que un campo numérico provenga de una clave de tipo JSONB que podría contener string vacío, utiliza la estructura:
82
+ COALESCE(NULLIF(jsonb_objeto->>'campo', '')::NUMERIC, 0)
83
+ para evitar errores de conversión cuando el campo esté vacío.
84
+
85
+ - Esta regla aplica para conversiones a NUMERIC, INTEGER, FLOAT, DOUBLE PRECISION, etc. y debe aplicarse de forma consistente en todos los campos relevantes.
86
+ - Ejemplo correcto:
87
+ COALESCE(NULLIF(jsonb_column->>'field_name', '')::NUMERIC, 0) AS field_name
88
+ - Nunca uses NULLIF(..., '') para valores numéricos ya calculados.
89
+ Aplica NULLIF(..., '') sólo cuando trabajes directamente con strings extraídos
90
+ del JSONB. Para resultados de SUM/ROUND usa solo COALESCE(..., 0).
91
+
92
+ - Nunca castees directamente a NUMERIC si el valor puede ser vacío, ya que provocará un error de sintaxis en Postgres.
93
+ • Como principio general:
94
+ • Usa ::NUMERIC si hay posibilidad de decimales.
95
+ • Usa ::INTEGER solo si estás seguro de que los valores siempre serán enteros sin .0.
96
+
97
+ - Cuando utilices funciones agregadas como SUM, AVG, MIN, MAX
98
+ sobre valores extraídos de un JSONB (usando ->>),
99
+ asegúrate de convertir cada elemento al tipo numérico dentro de la función agregada.
100
+
101
+ Ejemplo correcto: SUM((value->>'monto')::NUMERIC)
102
+ Ejemplo incorrecto: SUM(value->>'monto')::NUMERIC
103
+
104
+ 🎯 Regla para castear fechas desde JSONB
105
+
106
+ - Si el campo de fecha proviene de un JSONB y está en formato `DD-MM-YYYY`,
107
+ **no puedes castear directamente a DATE**.
108
+ - Usa `TO_DATE(...)` para convertir el string a fecha:
109
+ ```sql
110
+ TO_DATE(jsonb_credit->>'created', 'DD-MM-YYYY')
111
+ ´´´
112
+
113
+ - antes de comparar con `CURRENT_DATE` un campo de tipo jsonb,
114
+ **siempre** castea explícitamente
115
+
116
+ - No utilices TO_DATE si la columna ya es de tipo DATE o TIMESTAMP.
117
+ Úsala solo si el campo es tipo TEXT o VARCHAR, y asegúrate de castearlo explícitamente si es necesario.
118
+ TO_DATE espera un string, no una fecha ya en formato DATE, y causará un error de tipo de datos.
119
+
120
+ - Si comparas fechas, asegúrate que ambos lados sean del mismo tipo.
121
+ ejemplo:
122
+ ```sql
123
+ -- Si 'init_date' es DATE:
124
+ TO_CHAR(init_date, 'DD-MM-YYYY') = ...
125
+ -- Si 'created' es texto en formato 'DD-MM-YYYY':
126
+ TO_DATE(jsonb_guarantee->>'created', 'DD-MM-YYYY')
127
+ - Evitar comparar campos de tipo DATE o TIMESTAMP con strings generados por TO_CHAR.
128
+ Usar funciones como date_trunc() o INTERVAL directamente.
129
+
130
+ - Cuando necesites construir fechas a partir de valores numéricos
131
+ (por ejemplo, año y mes), utiliza TO_DATE() con un string en formato 'YYYY-MM-DD',
132
+ no uses concatenación sobre literales de tipo DATE.
133
+
134
+ - Si el campo de fecha puede venir vacío (''), usa:
135
+ TO_DATE(NULLIF(campo, ''), 'DD-MM-YYYY')
136
+ Esto evita errores de conversión cuando la fecha está ausente o mal registrada.
137
+
138
+ ### operaciones sobre campos de tipo JSONB
139
+ - Cuando uses SUM o alguna otra función agregada sobre un valor extraído
140
+ de un JSONB con ->> (texto), aplica el cast a NUMERIC dentro del SUM y no después.
141
+ - ✅ Siempre castea **antes** de aplicar `SUM`:
142
+ ```sql
143
+ SUM((json_field->>'monto')::NUMERIC)
144
+ ```
145
+
146
+ - ❌ Nunca castees después del `SUM`:
147
+ ```sql
148
+ -- INCORRECTO
149
+ SUM(json_field->>'monto')::NUMERIC
150
+ ```
151
+
152
+ ### Ejemplo correcto:
153
+ ```sql
154
+ SELECT SUM((gv->>'amount')::NUMERIC) AS total
155
+ FROM ...
156
+ ```
157
+
158
+ Cuando uses SUM(...) con FILTER, aplica COALESCE sobre los valores internos dentro
159
+ del SUM, no sobre el resultado del SUM completo.
160
+
161
+ Siempre que se conviertan campos numéricos extraídos de objetos JSONB
162
+ usando `->>` (por ejemplo, `collection_amount`, `fogape_amount`),
163
+ debes envolver la conversión con `NULLIF(..., '')::NUMERIC` y `COALESCE(..., 0)`
164
+ para evitar errores cuando el valor sea una cadena vacía o `null`.
165
+
166
+ Cuando debas agregar valores numéricos extraídos de campos JSONB que pueden contener texto,
167
+ valores vacíos o null, siempre utiliza la siguiente estructura dentro de la
168
+ función SUM, AVG, COUNT u otra función agregada:
169
+ SUM(COALESCE(NULLIF(<campo_jsonb>, '')::NUMERIC, 0))
170
+
171
+ - Cuando utilices subconsultas agregadas (con SUM, COUNT, etc.) en SQL,
172
+ asegúrate de que todos los campos que aparecen en el SELECT del subquery
173
+ estén también en el GROUP BY de ese subquery, o que se usen únicamente
174
+ dentro de funciones de agregación como SUM(), COUNT(), MAX(), MIN(), etc.
175
+
176
+ - Cuando debas comparar un valor de un campo jsonb contra un string,
177
+ asegúrate de convertir el valor jsonb a texto con ::text antes de la comparación.
178
+ Por ejemplo, para saber si un objeto jsonb está vacío,
179
+ usa (jsonb_field)::text != '{}' en vez de jsonb_field != '{}'.
180
+
181
+ - Siempre que debas acceder a elementos contenidos en un campo JSONB definido como array,
182
+ como `judicial_collection`, utiliza estrictamente la función `jsonb_array_elements(campo_jsonb->'array')`.
183
+ Nunca uses `jsonb_each`, ya que esta función solo aplica a objetos JSONB, no arrays.
184
+
185
+ - Para recorrer arrays dentro de columnas JSONB,
186
+ utiliza la función jsonb_array_elements en la cláusula FROM o en LEFT JOIN LATERAL.
187
+ Ejemplo:
188
+ ```sql
189
+ LEFT JOIN LATERAL jsonb_array_elements(tabla.columna_jsonb->'nombre_array') AS alias(elemento) ON TRUE
190
+
191
+ Si necesitas recorrer un array dentro de cada elemento del array anterior,
192
+ anida los jsonb_array_elements usando otro LEFT JOIN LATERAL sobre el alias anterior.
193
+
194
+ - Cuando uses `jsonb_array_elements(...) AS alias(columna)` en una
195
+ cláusula `FROM` o `JOIN`, recuerda que el alias (`alias`) no representa un objeto
196
+ con múltiples columnas, sino una tupla con una sola columna llamada como
197
+ se indica en `AS alias(columna)`. Por lo tanto:
198
+ - Para acceder a los valores internos del JSON debes usar `alias.columna->>'campo'`.
199
+ - Nunca uses `alias.campo` directamente, ya que causará un error de columna no definida.
200
+ - Ejemplo correcto: `alias.value->>'offer_status'`.
201
+ - Ejemplo incorrecto: `alias.offer_status`.
202
+
203
+ - No uses funciones que devuelven múltiples filas (set-returning functions) como
204
+ jsonb_array_elements(), unnest(), generate_series(), etc.
205
+ directamente dentro de funciones de agregación como SUM(), COUNT(), AVG(), etc.
206
+ En su lugar, mueve la función a una subconsulta o usa un LATERAL JOIN para expandir
207
+ primero los elementos y luego aplicar la agregación.
208
+ Ejemplo correcto:
209
+ SELECT SUM((elem->>'amount')::NUMERIC)
210
+ FROM tabla
211
+ LEFT JOIN LATERAL jsonb_array_elements(tabla.json_col) AS elem ON TRUE;
212
+ Ejemplo incorrecto:
213
+ SELECT SUM((jsonb_array_elements(tabla.json_col)->>'amount')::NUMERIC)
214
+ FROM tabla;
215
+
216
+ ### Evaluación de booleanos desde JSONB
217
+
218
+ - Las claves booleanas dentro de JSONB devuelven texto.
219
+ - Para evaluarlas correctamente, usa cast explícito:
220
+ ```sql
221
+ (jsonb_field->>'pre_judicial')::BOOLEAN IS TRUE
222
+
223
+ - aplica esta regla a todos los campos que definan true/false
224
+
225
+ 📌 Evita casteos múltiples de un mismo campo
226
+ • Si vas a usar (jsonb->>'campo')::TIPO varias veces en la query,
227
+ guarda el resultado con WITH o como subquery para evitar repetir el casteo.
228
+ ejemplo:
229
+ ´´´sql
230
+ WITH datos AS (
231
+ SELECT
232
+ rut,
233
+ (jsonb_column->>'field_name')::NUMERIC AS field_name
234
+ FROM bcu_customer
235
+ )
236
+
237
+ 📌 No uses jsonb -> 'obj'::text != '{}'
238
+ • Esa comparación es lenta y poco confiable. prefiere:
239
+ ´´´sql
240
+ jsonb_typeof(jsonb->'obj') = 'object'
241
+ AND jsonb_object_keys(jsonb->'obj') IS NOT NULL
242
+
243
+ 🟢 Regla para uso de operadores JSONB sobre arrays y objetos
244
+
245
+ - Cuando uses la función jsonb_each sobre un objeto jsonb,
246
+ **usa el alias AS e(key, value)** solo dentro de FROM o LATERAL,
247
+ nunca en un subquery escalar sin alias de tabla. Ejemplo correcto:
248
+ FROM jsonb_each(jsonb_object) AS e(key, value)
249
+ - Si necesitas solo value, puedes usar: FROM jsonb_each(jsonb_object) AS e
250
+ - Cuando hagas subconsultas escalares que usan jsonb_each, **recuerda que el alias debe ser simple (AS e), y accede a los valores usando e.value->>'campo'**.
251
+ Ejemplo correcto para sumar montos dentro de un objeto:
252
+ (SELECT SUM((e.value->>'amount')::NUMERIC) FROM jsonb_each(objeto_jsonb) AS e WHERE e.value->>'amount' IS NOT NULL)
253
+
254
+ - Antes de utilizar operadores JSONB como jsonb_each o jsonb_array_elements en un campo,
255
+ asegúrate de que el tipo de dato corresponde:
256
+ • Utiliza jsonb_each solo sobre campos JSONB de tipo objeto (dict/JSON {}).
257
+ . Nunca uses jsonb_each sobre arrays, nulls o strings.
258
+ • Utiliza jsonb_array_elements solo sobre campos JSONB de tipo array ([]).
259
+
260
+ - Nunca apliques jsonb_each sobre un campo que pueda contener un array o valor nulo.
261
+ - Nunca utilices jsonb_each sobre un campo JSONB si puede ser un array.
262
+ Siempre verifica el tipo con jsonb_typeof.
263
+ Si puede ser un array, utiliza jsonb_array_elements y filtra por tipo si es necesario.
264
+
265
+ - Cuando uses funciones como jsonb_each(jsonb_column) asegúrate de que el
266
+ campo sea un objeto JSONB. usa la condición:
267
+ ```sql
268
+ LEFT JOIN LATERAL jsonb_each(c.jsonb_object->'operation') AS e ON
269
+ c.jsonb_object->'operation' IS NOT NULL
270
+ AND jsonb_typeof(c.jsonb_object->'operation') = 'object'
271
+
272
+ - Cuando debas recorrer elementos dentro de un campo JSONB:
273
+ • Usa jsonb_each si el campo es un objeto (dict/{}).
274
+ • Usa jsonb_array_elements si el campo es un array ([]).
275
+
276
+ Si necesitas recorrer un campo JSONB que es un array,
277
+ utiliza siempre jsonb_array_elements.
278
+ Usa jsonb_each solo si el campo es un objeto (tipo diccionario).
279
+ Ejemplo para arrays:
280
+ ```sql
281
+ SELECT ... FROM jsonb_array_elements(campo_jsonb->'mi_array')
282
+
283
+ Ejemplo para objetos:
284
+ ```sql
285
+ SELECT ... FROM jsonb_each(campo_jsonb->'mi_objeto')
286
+
287
+ ## 📐 Cálculos y expresiones
288
+
289
+ ### Porcentajes y divisiones
290
+
291
+ - Usa `* 100.0` para forzar el uso de decimales en porcentajes.
292
+ - Usa `NULLIF` para proteger divisiones por cero.
293
+
294
+ ### Coherencia en expresiones
295
+
296
+ - Asegura que todos los componentes de una expresión numérica compartan el mismo tipo.
297
+ - Protege `COALESCE` y literales con el mismo tipo que el campo:
298
+ ```sql
299
+ COALESCE(SUM(...), 0::INTEGER)
300
+ COALESCE(SUM(...), 0::NUMERIC)
301
+ ```
302
+
303
+ ### ➕ SUM sobre valores booleanos
304
+ - `SUM()` y otras funciones de agregación no aceptan directamente expresiones booleanas.
305
+ - Para contar condiciones booleanas, transforma el resultado a número:
306
+
307
+ ```sql
308
+ SUM(CASE WHEN condición_booleana THEN 1 ELSE 0 END)
309
+
310
+ Ejemplo correcto:
311
+ ```sql
312
+ SUM(CASE WHEN (jsonb_field->'field_name')::jsonb @> '[{"status": "vigente"}]'::jsonb THEN 1 ELSE 0 END)
313
+
314
+ ❌ Incorrecto:
315
+ ´´´sql
316
+ SUM((jsonb_guarantee->'field_name')::jsonb @> '[{"status": "vigente"}]'::jsonb)
317
+
318
+ ### Evita mezcla implícita de tipos
319
+
320
+ - 🚫 No mezcles `INTEGER` y `NUMERIC` sin casting explícito.
321
+ - Castea siempre al tipo de mayor precisión si los mezclas.
322
+
323
+ ### ⚠️ Validación segura al usar jsonb_array_length()
324
+
325
+ Siempre que uses jsonb_array_length(jsonb_column->'key'),
326
+ asegúrate de anteponer una condición en WHERE como:
327
+ jsonb_typeof(jsonb_column->'key') = 'array'
328
+
329
+ Añade explícitamente la verificación jsonb_typeof(jsonb_column->'key') = 'array'
330
+ **antes** de llamar a jsonb_array_length.
331
+ Por ejemplo:
332
+ ```
333
+ WHERE jsonb_column IS NOT NULL
334
+ AND jsonb_typeof(jsonb_column->'key') = 'array'
335
+ AND jsonb_array_length(jsonb_column->'key') > 0
336
+ ```
337
+ Esto evita que jsonb_array_length falle si el campo no es un array.
338
+
339
+ Esta regla se aplica especialmente a cualquier campo que pueda
340
+ contener una lista variable
341
+
342
+
343
+ ### ⚠️ Uso de CTEs
344
+
345
+ - PostgreSQL **no permite usar alias definidos en el SELECT en la cláusula ORDER BY dentro del mismo nivel** si se hace `GROUP BY`.
346
+ - Solución: Repite la expresión original en `ORDER BY`.
347
+
348
+ ❌ Esto puede fallar:
349
+ ```sql
350
+ SELECT to_char(init_date, 'MM-YYYY') AS mes
351
+ GROUP BY mes
352
+ ORDER BY to_date('01-' || mes, 'DD-MM-YYYY')
353
+
354
+ Haz esto en su lugar:
355
+ ´´´sql
356
+ ORDER BY to_date('01-' || to_char(init_date, 'MM-YYYY'), 'DD-MM-YYYY')
357
+ ´´
358
+
359
+ Esta regla aplica en CTEs y subqueries donde defines alias y haces GROUP BY y ORDER BY juntos.
360
+ - Si una consulta usa GROUP BY, todas las columnas del SELECT que no estén dentro
361
+ de funciones agregadas deben estar explícitamente en la cláusula GROUP BY.
362
+ - Además, si en el ORDER BY se hace referencia a una columna (ej: c.public_line),
363
+ esa columna también debe agregarse al SELECT y al GROUP BY,
364
+ a menos que esté en una función de agregación.
365
+ - Esto incluye columnas provenientes de CTEs (WITH) como clientes_top u otras subconsultas.
366
+ - esto mismo aplica si vas a referenciar campos de una tabla en subconsultas,
367
+ LEFT JOIN LATERAL o expresiones posteriores,
368
+ asegúrate de seleccionar esos campos explícitamente en el CTE o subconsulta de origen.
369
+
370
+ -- Incorrecto:
371
+ -- GROUP BY c.rut, c.client_name
372
+ -- ORDER BY c.public_line ← Error porque no está ni en SELECT ni en GROUP BY
373
+
374
+ -- Correcto:
375
+ -- SELECT c.rut, c.client_name, c.public_line
376
+ -- GROUP BY c.rut, c.client_name, c.public_line
377
+ -- ORDER BY c.public_line
378
+
379
+ - detecta automáticamente si hay columnas en el SELECT u ORDER BY que no
380
+ están en GROUP BY y agregarlas.
381
+ - las columnas de CTEs también deben respetar esta regla, ya que PostgreSQL aplica
382
+ las restricciones de agrupamiento a nivel global.
383
+
384
+ Siempre que haya columnas con el mismo nombre en más de una CTE,
385
+ usa el nombre de la tabla o CTE como prefijo en el SELECT final para
386
+ evitar ambigüedad. Ejemplo: cartera.garantias_vigentes o cobranza_judicial.garantias_vigentes.
387
+
388
+ - Cuando definas múltiples CTEs con WITH, cada CTE debe estar separado por una coma (,)
389
+ Nunca olvides la coma entre cada definición de CTE, excepto después del último CTE antes del SELECT final.
390
+ Ejemplo:
391
+ ```sql
392
+ WITH a AS (SELECT 1),
393
+ b AS (SELECT 2)
394
+ SELECT * FROM a, b;
395
+
396
+ Incorrecto (genera error):
397
+ ```sql
398
+ WITH a AS (SELECT 1)
399
+ b AS (SELECT 2) -- ❌ falta la coma
400
+ SELECT * FROM a, b;
401
+
402
+ - Nunca uses directamente los alias definidos dentro de un CTE en el SELECT principal.
403
+ Si necesitas usar una columna de un CTE, accede a ella mediante una subconsulta
404
+ completa que incluya el CTE o usa una tabla derivada con alias.
405
+
406
+ Reutiliza exactamente los nombres de columnas definidos en las CTE previas o SELECT
407
+ anteriores. Evita variaciones similares como 'executive' vs 'executable'."
408
+ Si necesitas retornar una fila de una CTE o subquery con varias columnas como
409
+ un solo campo, usa row_to_json(...).
410
+ Si solo necesitas columnas específicas, trae cada columna por separado en el SELECT.
411
+ ejemplo:
412
+ ´´´sql
413
+ SELECT
414
+ (SELECT row_to_json(prejudicial) FROM prejudicial) AS prejudicial
415
+
416
+ o bien:
417
+ ´´´sql
418
+ SELECT
419
+ (SELECT clientes_prejudicial FROM prejudicial) AS clientes_prejudicial,
420
+ (SELECT monto_prejudicial FROM prejudicial) AS monto_prejudicial
421
+
422
+ Si usas el mismo nombre de columna proveniente de varias tablas en un JOIN
423
+ o en una CTE, asegúrate de que en el SELECT final no quede ambigüedad,
424
+ usando siempre el prefijo de la tabla o seleccionando explícitamente solo
425
+ una de las columnas.
426
+
427
+ Cuando uses un alias de columna en el SELECT de una consulta, recuerda que en PostgreSQL no puedes referenciar ese alias directamente en el ORDER BY a menos que la consulta esté envuelta en una subquery.
428
+ Si necesitas ordenar por ese alias, repite la expresión o usa una subconsulta.
429
+
430
+ - Cuando uses JOIN entre tablas que tienen columnas con el mismo nombre (por ejemplo 'rut'),
431
+ nunca hagas SELECT t1.columna, t2.*.
432
+ En su lugar, selecciona los campos de forma explícita o usa alias para desambiguar.
433
+ ejemplo:
434
+ ```sql
435
+ SELECT cert.rut AS rut_cert, cert.status, cert.guarantee_amount_pesos FROM clientes_kaponte c JOIN bcu_certificate cert ON cert.rut = c.rut
436
+
437
+ - Siempre que escribas una consulta SQL con más de una tabla o subconsulta,
438
+ debes anteponer el alias de la tabla o subconsulta a cada columna que aparece en
439
+ la cláusula SELECT, JOIN, ON, WHERE, GROUP BY, HAVING u ORDER BY.
440
+ Ejemplo correcto:
441
+ ```sql
442
+ SELECT b.rut, c.client_name FROM base b JOIN clientes c ON c.rut = b.rut
443
+
444
+ - Nunca uses nombres de columnas sin el alias de su tabla correspondiente cuando
445
+ puedan existir en varias tablas o subconsultas (por ejemplo: rut, folio, status, etc.).
446
+
447
+ - Si usas LATERAL o subconsultas, también antepone el alias de tabla/subconsulta.
448
+ Ejemplo:
449
+ ```sql
450
+ LEFT JOIN LATERAL (SELECT ... FROM jsonb_array_elements(b.jsonb_collection->'judicial_collection')) ...
451
+
452
+ Nunca uses alias de tabla (c.) fuera del alcance donde fueron definidos.
453
+ Sólo usa alias cuando la tabla está explícitamente definida con alias en
454
+ el FROM de esa subconsulta o CTE.
455
+
456
+
457
+ - Cuando crees un CTE (subconsulta con WITH), los campos disponibles en los
458
+ siguientes CTE o en el SELECT principal son solo los que seleccionaste en
459
+ el SELECT del CTE anterior.
460
+ - No intentes acceder a columnas originales de la tabla base que no están
461
+ proyectadas explícitamente en el CTE.
462
+
463
+ - Si necesitas un campo más adelante, asegúrate de incluirlo en el SELECT del CTE.
464
+ - Por ejemplo, si extraes jsonb_column->>'guarantee_executive' AS ejecutiva,
465
+ luego usa ejecutiva, no intentes usar de nuevo jsonb_column->>'guarantee_executive' en ese CTE.
466
+
467
+ - Nunca pongas un WHERE entre la definición de un alias lateral y el GROUP BY.
468
+ Si necesitas filtrar resultados de un LATERAL, pon el filtro dentro de la subconsulta LATERAL.
469
+
470
+ - Si tu campo es un array, usa siempre jsonb_array_elements (no jsonb_each).
471
+ - Si necesitas filtrar resultados de una función LATERAL (como jsonb_each o jsonb_array_elements),
472
+ pon el filtro en una subconsulta lateral y nunca en el ON.
473
+ - No repitas nombres de columnas o alias en diferentes scopes;
474
+ usa nombres claros, especialmente en LATERAL.
475
+
476
+
477
+ ## Evitar referencias a alias fuera de su contexto
478
+
479
+ Cuando generes subconsultas, CTEs (WITH) o expresiones LATERAL que utilicen una
480
+ tabla virtual creada con VALUES, asegúrate de **no referenciar alias que no
481
+ estén definidos en ese scope**.
482
+
483
+ Por ejemplo, en una subconsulta tipo:
484
+ SELECT ... FROM (VALUES ...) AS m(col1, col2)
485
+ Solo puedes referenciar m.col1, m.col2, etc. en ese contexto.
486
+ Si la subconsulta recibe un alias (por ejemplo, j), solo puedes referenciar j.columna en el SELECT externo. **Nunca uses alias como g.columna si g no está definido en ese nivel.**
487
+
488
+ Ejemplo incorrecto:
489
+ SELECT g.col1 FROM (SELECT m.col1 FROM (VALUES ...) AS m) j
490
+
491
+ Ejemplo correcto:
492
+ SELECT j.col1 FROM (SELECT m.col1 FROM (VALUES ...) AS m) j
493
+
494
+ **Siempre asegúrate de que los campos referenciados en el SELECT provengan de los alias definidos en ese mismo FROM/JOIN.**
495
+
496
+ 📘 Reglas para Optimización de Consultas jsonb Numéricas
497
+
498
+ 🔁 1. Evitar casteos repetidos
499
+ • Descripción: No repitas el casteo de un mismo campo jsonb->>'campo' a NUMERIC, INTEGER, etc.
500
+ • Instrucción:
501
+ Usa un alias intermedio o un CTE para calcular el valor una sola vez.
502
+ Ejemplo (malo):
503
+ ´´´sql
504
+ ORDER BY (jsonb_column->>'public_line')::NUMERIC DESC
505
+ LIMIT 50
506
+
507
+ Ejemplo (bueno):
508
+ ´´´sql
509
+ WITH base AS (
510
+ SELECT
511
+ rut,
512
+ client_name,
513
+ (jsonb_column->>'public_line')::NUMERIC AS public_line
514
+ FROM bcu_customer
515
+ )
516
+ SELECT * FROM base ORDER BY public_line DESC LIMIT 50
517
+
518
+ 📦 2. Usar CTEs para mejorar legibilidad
519
+ • Descripción: Si estás usando varios campos jsonb->>'...'::NUMERIC, sepáralos con un WITH al inicio.
520
+ • Instrucción:
521
+ Calcula todos los campos requeridos en un CTE y luego trabaja solo con sus alias.
522
+
523
+ 🏗️ USO DE CTE Y REFERENCIAS DE COLUMNAS
524
+
525
+ - Cuando uses un CTE, debes referenciar únicamente los nombres de columnas que defines en el SELECT del CTE.
526
+ - Ejemplo: Si el CTE `base` define `rut`, usa `base.rut` (no `base.client_rut`).
527
+ - Si necesitas un alias distinto para la columna, utiliza `AS` en el SELECT del CTE:
528
+ ```sql
529
+ b.rut AS client_rut
530
+
531
+ 📊 4. Ordenar por campos casteados
532
+ • Descripción: Si necesitas ORDER BY sobre un campo jsonb casteado:
533
+ transpórtalo primero.
534
+ • Instrucción:
535
+ Usa un alias para ordenar. Evita reescribir el cast dentro del ORDER BY.
536
+
537
+ 📌 Cuidado con el ORDER BY de expresiones sin índice
538
+ • Si ordenas por un campo jsonb, considera:
539
+ • Promoverlo a una columna normal (materializada).
540
+ • Crear un índice funcional (si es factible).
541
+
542
+ Cuando crees un alias con una función (por ejemplo, TO_CHAR(init_date, 'MM-YYYY') AS mes),
543
+ usa siempre la expresión completa en el GROUP BY y en el ORDER BY,
544
+ a menos que estés en una subconsulta o CTE donde el alias ya está definido.
545
+ No uses el alias directamente en el mismo nivel del SELECT.
546
+ ejemplo : ´´´sql
547
+ SELECT TO_CHAR(init_date, 'MM-YYYY') AS mes, ...
548
+ GROUP BY TO_CHAR(init_date, 'MM-YYYY')
549
+ ORDER BY TO_DATE('01-' || TO_CHAR(init_date, 'MM-YYYY'), 'DD-MM-YYYY') DESC
550
+
551
+ ### 🧠 No reutilices campos JSONB que ya descompusiste
552
+
553
+ - Si en un CTE extraes subcampos desde un JSONB, por ejemplo:
554
+ ```sql
555
+ jsonb_column->>'guarantee_executive' AS ejecutiva,
556
+ (jsonb_column->>'active')::INTEGER AS garantias_vigentes,
557
+ no podrás referenciar jsonb_column directamente después en el mismo CTE.
558
+
559
+ ❌ Incorrecto:
560
+ ´´´sql
561
+ SELECT ... FROM cartera WHERE TO_DATE(jsonb_column->>'created', ...) >= ...
562
+ ´´
563
+
564
+ ✅ Correcto:
565
+ • Si necesitas seguir accediendo al objeto completo (jsonb_column),
566
+ inclúyelo también explícitamente en el SELECT del CTE.
567
+ • O extrae directamente el campo requerido:
568
+ ´´´sql TO_DATE(jsonb_column->>'created','DD-MM-YYYY') AS fecha_creacion ´´sql
569
+ y usa ese alias (fecha_creacion) más adelante.
570
+
571
+ ### ✅ Evita `jsonb_object_keys` en `WHERE`
572
+
573
+ - **No uses `jsonb_object_keys(...)` dentro de `WHERE` o funciones escalares como `jsonb_array_length(...)`**.
574
+ PostgreSQL no permite aplicar funciones escalares sobre funciones set-returning dentro de filtros.
575
+
576
+ - Si quieres validar si un objeto JSONB tiene claves (no está vacío), haz:
577
+ ```sql
578
+ jsonb_column IS NOT NULL
579
+ AND jsonb_typeof(jsonb_column) = 'object'
580
+ AND jsonb_column::text != '{}'
581
+
582
+ Correcto:
583
+ ´´´sql
584
+ WHERE jsonb_column IS NOT NULL
585
+ AND jsonb_typeof(jsonb_column) = 'object'
586
+ AND jsonb_column::text != '{}'
587
+
588
+ ❌ Incorrecto:
589
+ ´´´sql
590
+ WHERE jsonb_array_length(jsonb_object_keys(jsonb_column)) > 0
591
+
592
+ ## validación de array de jsonb no vacio
593
+ Para verificar si un array JSONB no está vacío, usa siempre:
594
+ ```sql
595
+ jsonb_typeof(jsonb_column) = 'array'
596
+ AND jsonb_array_length(jsonb_column) > 0
597
+
598
+ ### ✅ Agregaciones condicionales con JSONB
599
+
600
+ - Nunca uses `SUM(<expresión booleana>)` directamente. PostgreSQL no admite sumar valores booleanos sin convertirlos de forma explícita.
601
+ - En lugar de eso, usa `CASE WHEN ... THEN 1 ELSE 0 END` para transformar condiciones booleanas en enteros antes de agregarlas.
602
+
603
+ ✅ Correcto:
604
+
605
+ ```sql
606
+ SUM(CASE WHEN jsonb_column->'subkey'::text != '{}' THEN 1 ELSE 0 END)
607
+
608
+ ❌ Incorrecto:
609
+ ´´´sql
610
+ SUM((jsonb_column->'subkey')::text != '{}'::text)
611
+
612
+ ### ✅ consultas anidadas
613
+ Cuando utilices funciones anidadas como `SUM`, `COALESCE`, `ROUND` y `NULLIF`,
614
+ asegúrate de que los paréntesis estén correctamente balanceados.
615
+ La estructura recomendada para calcular porcentajes sobre agregados es:
616
+
617
+ COALESCE(
618
+ ROUND(
619
+ 100.0 * SUM(...) / NULLIF(SUM(...), 0),
620
+ 0
621
+ ),
622
+ 0
623
+ )
624
+
625
+ Cuando utilices COALESCE en una expresión compleja,
626
+ no anides múltiples COALESCE en la misma expresión.
627
+ Solo usa un COALESCE externo para manejar valores nulos.
628
+ Por ejemplo: COALESCE(ROUND(...), 0) es correcto.
629
+ Evita COALESCE(..., 0), 0 o anidaciones similares.
630
+
631
+ - Evita envolver `ROUND` dentro de otro `COALESCE` si ya estás manejando los nulos con `NULLIF`.
632
+ - No uses `SUM((...)::BOOLEAN)` porque `SUM` espera un valor numérico. Para contar condiciones,
633
+ usa `SUM(CASE WHEN ... THEN 1 ELSE 0 END)`.
634
+ - Si estás usando subconsultas `SELECT ... FROM jsonb_array_elements(...)`,
635
+ asegúrate de que estén dentro de la agregación correctamente y no mezcles `SUM(SELECT SUM(...))` — eso debe evitarse.
636
+ Usa `SUM(...)` directamente sobre los elementos o con una subconsulta que retorne un valor escalar.
637
+
638
+ - Cuando uses jsonb_array_elements sobre un campo JSONB (por ejemplo, jsonb_column->'contract'),
639
+ el resultado es una sola columna llamada value (o el alias que definas, por ejemplo, contract).
640
+ Para acceder a subcampos dentro del JSON, debes usar la notación alias->>'campo' y no alias.campo.
641
+ Nunca intentes seleccionar directamente alias.campo si no existe como columna nativa.
642
+ Ejemplo correcto:
643
+ ```sql
644
+ LEFT JOIN LATERAL jsonb_array_elements(c.jsonb_column->'contract') AS o(contract) ON TRUE
645
+ SELECT o.contract->>'contract_date' AS contract_date
646
+
647
+ Ejemplo incorrecto (genera error):
648
+ ```sql
649
+ SELECT o.contract_date -- Esto fallará porque 'contract_date' no es una columna, sino un subcampo del JSON.
650
+
651
+
652
+ ## funciones de agregación en subconsultas
653
+ - Nunca incluyas funciones de agregación como COUNT(*), SUM(...), etc.,
654
+ directamente dentro de subconsultas del tipo LEFT JOIN LATERAL (...) ...
655
+ si el agregado depende del contexto de la fila padre.
656
+
657
+ - Si necesitas sumar o contar datos relacionados, primero usa el LATERAL para expandir los elementos,
658
+ y solo después, en la consulta exterior, agrupa y aplica la agregación.
659
+
660
+ Ejemplo correcto:
661
+ ```sql
662
+ LEFT JOIN LATERAL (
663
+ SELECT
664
+ (contr.value->>'contract_amount')::NUMERIC AS monto_contrato
665
+ FROM jsonb_array_elements(c.jsonb_column->'contract') AS contr(value)
666
+ WHERE contr.value->>'status' = 'vigente'
667
+ ) v ON TRUE
668
+
669
+ Luego, en el SELECT principal puedes usar SUM(v.monto) o COUNT(*).
670
+
671
+ - No uses CASE WHEN ... THEN ... ELSE ... END como
672
+ subconsulta dentro de un LEFT JOIN LATERAL (...).
673
+ - Las subconsultas de JOIN LATERAL deben ser expresiones que empiecen por SELECT ...,
674
+ o una función set-returning válida como jsonb_array_elements(...).
675
+ - Si necesitas filtrar o condicionar un lateral,
676
+ usa WHERE dentro de la subconsulta o pon la condición en el ON.
677
+ - Para expandir arrays JSONB, usa directamente:
678
+ ```sql
679
+ LEFT JOIN LATERAL jsonb_array_elements(b.jsonb_field) AS alias ON ...
680
+
681
+ - Si quieres filtrar que solo se ejecute cuando sea un array y no esté vacío:
682
+ ```sql
683
+ LEFT JOIN LATERAL jsonb_array_elements(b.jsonb_field) AS alias
684
+ ON b.jsonb_field IS NOT NULL AND jsonb_typeof(b.jsonb_field) = 'array' AND jsonb_array_length(b.jsonb_field) > 0
685
+
686
+ - Cuando necesites devolver un array de objetos JSON
687
+ (por ejemplo, al mostrar un perfil, una lista de clientes o resultados agrupados)
688
+ utiliza únicamente json_agg() sobre una subconsulta, SIN envolverlo en row_to_json().
689
+ Ejemplo correcto:
690
+ (SELECT json_agg(t) FROM (SELECT * FROM cartera_grupo ORDER BY ejecutiva) t) AS perfil_cartera
691
+ Ejemplo incorrecto (NO USAR):
692
+ row_to_json((SELECT json_agg(t) FROM (SELECT * FROM cartera_grupo ORDER BY ejecutiva) t)) AS perfil_cartera
693
+
694
+ - Si la consulta debe retornar un arreglo JSON, **usa solamente `json_agg`** en la subconsulta.
695
+ - **No utilices `row_to_json` sobre un resultado que ya viene en formato JSON**.
696
+ - Si quieres un objeto JSON de UNA SOLA fila, usa `row_to_json`. Si es un ARREGLO de filas, usa solo `json_agg`.
697
+
698
+ - No incluyas ORDER BY directo dentro de json_agg a menos que lo hagas en un subselect,
699
+ porque causará errores de grouping.
700
+ Ejemplo correcto:
701
+ SELECT json_agg(row_to_json(x)) FROM (SELECT * FROM tabla ORDER BY campo) x;
702
+
703
+ ## ✅ Buenas prácticas generales
704
+
705
+ - Usa nombres estándar de campos según el schema.
706
+ - Respeta los alias comunes:
707
+ - `folio` ≡ `certificate_id`
708
+ - `client_rut` en certificados corresponde a `rut` en clientes
709
+
710
+ - Usa `snake_case` y aliases consistentes en las consultas.
711
+ - cuando filtres por ejecutiva o grupos de garantias o credito,
712
+ siempre usa su ´**username**´ nunca su nombre completo.
713
+ - siempre que compares strings utiliza ilike para que sea case insensitive
714
+
715
+ - - Cuando combines condiciones con AND y OR, **usa paréntesis** para agrupar correctamente y evitar resultados inesperados.
716
+ - Ejemplo correcto:
717
+ ```sql
718
+ WHERE ...
719
+ AND (
720
+ status ILIKE 'vigente'
721
+ OR status ILIKE 'terminada'
722
+ )
723
+ ```
724
+
725
+ - 📝 Regla para búsqueda de RUT en SQL (formato limpio, sin puntos)
726
+ - siempre elimina los puntos del valor buscado
727
+ - Haz la comparación directamente con el campo, ya que ya está en formato limpio.
728
+ - El campo rut en la base está guardado con guion y sin puntos.
729
+ ejemplo:
730
+ ´´´sql
731
+ -- Incorrecto:
732
+ WHERE rut = '76.768.219-0'
733
+
734
+ -- Correcto:
735
+ WHERE rut = REPLACE('76.768.219-0', '.', '')
736
+
737
+ • No elimines el guion al comparar, ya que en la base el
738
+ RUT debe tener formato: 76768219-0.
739
+
740
+ - Nunca utilices columnas o campos JSONB de una tabla si esa columna no existe en la tabla.
741
+ - Nunca hagas dos SELECTs seguidos usando los mismos CTEs.
742
+ Si necesitas múltiples datasets, combínalos en un solo SELECT
743
+ (usando subqueries, agregados o json_agg), o ejecuta varias queries separadas.
744
+ - Si una subconsulta puede retornar varias filas,
745
+ encapsúlala con json_agg(row_to_json(...)) en vez de solo row_to_json(...).
746
+ - Nunca uses CASE unnest(array[...]) directamente, ni dentro de la función.
747
+ Siempre primero aplica unnest(...) en el FROM y luego el CASE sobre el campo resultante.
748
+
749
+ - cuando el modelo necesite realizar una función agregada condicional
750
+ (por ejemplo, contar o sumar solo bajo ciertas condiciones),
751
+ debe usar la sintaxis con CASE WHEN ... THEN ... ELSE ... END dentro de la
752
+ función agregada.
753
+
754
+ - No debe utilizar la sintaxis AGGREGATE(...) FILTER (WHERE ...).
755
+ ejemplo correcto:
756
+ ```sql
757
+ SUM(CASE WHEN <condición> THEN <valor> ELSE 0 END)
758
+ COUNT(CASE WHEN <condición> THEN 1 ELSE NULL END)
759
+
760
+ Ejemplo incorrecto:
761
+ ```sql
762
+ SUM(<valor>) FILTER (WHERE <condición>)
763
+ COUNT(*) FILTER (WHERE <condición>)
764
+
765
+ - Evita utilizar '.value' para acceder a columnas cuando hagas JOIN lateral a tablas reales (por ejemplo, bcu_certificate).
766
+ Usa '.value' únicamente cuando la referencia provenga directamente de funciones JSON como jsonb_each() o jsonb_array_elements().
767
+ Si el JOIN lateral apunta directamente a tablas SQL estándar, accede a las columnas directamente por su nombre original sin '.value'.
768
+
769
+ - Cuando hagas JOIN o subconsultas a la misma tabla original (`bcu_customer`),
770
+ **evita usar los mismos nombres de columnas sin alias**,
771
+ y califica siempre con el alias correspondiente (`c.`, `cc.`, etc.)."
772
+
773
+ - Al combinar resultados con UNION, INTERSECT o EXCEPT,
774
+ nunca uses funciones o expresiones en la cláusula ORDER BY como TO_DATE(...) o concatenaciones.
775
+ Solo puedes ordenar por columnas explícitamente seleccionadas en el SELECT externo.
776
+
777
+ - Si deseas agregar un campo calculado al resultado de una subconsulta o CTE,
778
+ no utilices `SELECT *` y luego calcules sobre los campos resultantes.
779
+ En su lugar, expón todos los campos explícitamente y realiza el cálculo directamente
780
+ dentro del `SELECT`, evitando aplicar funciones como `ROUND()` sobre registros
781
+ completos.
782
+
783
+ - Cuando expandas un array JSONB usando `jsonb_array_elements(...)`,
784
+ todos los campos internos (como `tender_id`) deben extraerse desde `value->>'campo'`,
785
+ no como columnas normales.
786
+ Ejemplo:
787
+ ```sql
788
+ SELECT value->>'tender_id' AS tender_id FROM jsonb_array_elements(...) AS value
789
+
790
+ - Al crear un alias para una columna o expresión en SQL,
791
+ utiliza siempre la sintaxis `AS alias` y nunca `alias = expresión`.
792
+ Esto aplica especialmente a expresiones `CASE`, agregaciones y conversiones de tipo.
793
+ Ejemplo:
794
+ ✅ CASE WHEN condicion THEN valor ELSE otro_valor END AS nombre_columna
795
+ ❌ nombre_columna = CASE WHEN condicion THEN valor ELSE otro_valor END
796
+
797
+ - No utilices comentarios con // en SQL, ya que no son válidos en PostgreSQL.
798
+
799
+ — Reglas para expandir JSONB sin errores en agregaciones
800
+
801
+ # SRF-LATERAL-001 (PROHIBIDO):
802
+ # No llames funciones que retornan sets (SRF) como jsonb_array_elements/jsonb_each/jsonb_each_text
803
+ # dentro de agregados (json_agg/jsonb_agg/array_agg). Causa: "aggregate function calls cannot contain set-returning function calls".
804
+ # ❌ Malo:
805
+ # json_agg(jsonb_array_elements(t.obj->'items'))
806
+
807
+ # SRF-LATERAL-002 (OBLIGATORIO):
808
+ # Para expandir arrays/objetos JSONB usa siempre un JOIN LATERAL y luego agrega.
809
+ # ✅ Bueno (patrón base):
810
+ # SELECT
811
+ # k,
812
+ # COALESCE(jsonb_agg(e.elem), '[]'::jsonb) AS items
813
+ # FROM base b
814
+ # LEFT JOIN LATERAL jsonb_array_elements(b.obj->'items') AS e(elem)
815
+ # ON b.obj IS NOT NULL
816
+ # AND b.obj ? 'items'
817
+ # AND jsonb_typeof(b.obj->'items') = 'array'
818
+ # GROUP BY k;
819
+
820
+ # SRF-LATERAL-003 (GUARDAS DE TIPO):
821
+ # Antes de expandir, valida existencia y tipo para evitar errores en tiempo de ejecución.
822
+ # b.obj IS NOT NULL
823
+ # AND b.obj ? 'ruta'
824
+ # AND jsonb_typeof(b.obj->'ruta') = 'array' -- o 'object' según corresponda
825
+
826
+ # SRF-LATERAL-004 (AGREGADO RECOMENDADO):
827
+ # Prefiere jsonb_agg para producir JSONB y normaliza a '[]' cuando no hay elementos.
828
+ # COALESCE(jsonb_agg(e.elem), '[]'::jsonb)
829
+
830
+ # SRF-LATERAL-005 (ORDEN DENTRO DEL AGREGADO):
831
+ # Si necesitas determinismo, ordena dentro del agregado.
832
+ # jsonb_agg(e.elem ORDER BY e.elem->>'campo')
833
+
834
+ # SRF-LATERAL-006 (TEMPLATE REUTILIZABLE):
835
+ # Usa este molde cuando necesites coleccionar elementos desde un JSONB.
836
+ # -- INPUT: tabla base b(pk, obj jsonb)
837
+ # SELECT
838
+ # b.pk,
839
+ # COALESCE(jsonb_agg(x.elem), '[]'::jsonb) AS elementos
840
+ # FROM base b
841
+ # LEFT JOIN LATERAL jsonb_array_elements(b.obj->'ruta') AS x(elem)
842
+ # ON b.obj IS NOT NULL
843
+ # AND b.obj ? 'ruta'
844
+ # AND jsonb_typeof(b.obj->'ruta') = 'array'
845
+ # GROUP BY b.pk;
846
+
847
+ # SRF-LATERAL-007 (CASO CONCRETO: CONTRATOS):
848
+ # Cuando necesites agrupar contratos contenidos en jsonb_operation->'contract':
849
+ # WITH datos_base AS (
850
+ # SELECT ... , c.jsonb_operation
851
+ # FROM bcu_customer c
852
+ # WHERE c.rut = :rut
853
+ # ),
854
+ # contratos AS (
855
+ # SELECT
856
+ # b.rut,
857
+ # COALESCE(jsonb_agg(o.value), '[]'::jsonb) AS contratos
858
+ # FROM datos_base b
859
+ # LEFT JOIN LATERAL jsonb_array_elements(b.jsonb_operation->'contract') AS o(value)
860
+ # ON b.jsonb_operation IS NOT NULL
861
+ # AND b.jsonb_operation ? 'contract'
862
+ # AND jsonb_typeof(b.jsonb_operation->'contract') = 'array'
863
+ # GROUP BY b.rut
864
+ # )
865
+ # SELECT ...
866
+ # FROM datos_base b
867
+ # LEFT JOIN contratos c ON c.rut = b.rut;
868
+
869
+ # SRF-LATERAL-008 (TEXTOS DERIVADOS):
870
+ # Si necesitas concatenar campos (p. ej., lista de emails en texto):
871
+ # SELECT
872
+ # b.rut,
873
+ # array_to_string(ARRAY_AGG(o.value->>'email' ORDER BY o.value->>'email'), ', ') AS emails
874
+ # FROM datos_base b
875
+ # LEFT JOIN LATERAL jsonb_array_elements(b.jsonb_operation->'contract') AS o(value)
876
+ # ON b.jsonb_operation IS NOT NULL
877
+ # AND b.jsonb_operation ? 'contract'
878
+ # AND jsonb_typeof(b.jsonb_operation->'contract') = 'array'
879
+ # GROUP BY b.rut;
880
+
881
+ # SRF-LATERAL-009 (EVITA WHERE QUE EXCLUYA TODO ANTES DEL LATERAL):
882
+ # Coloca las guardas de tipo en la condición del JOIN LATERAL (ON), no en WHERE, para no eliminar filas base.
883
+
884
+ # SRF-LATERAL-010 (NOMBRADO CONSISTENTE):
885
+ # Nombra la columna expandida como (alias).value o (alias).elem para claridad y reutilización en agregados.
886
+
887
+ ### SRF-ALIAS & CLAVES — Reglas para no inventar columnas al expandir JSONB
888
+
889
+ # SRF-ALIAS-001 (COLUMNAS DISPONIBLES POR SRF):
890
+ # - jsonb_array_elements(x) → alias(elem|value) # 1 sola columna
891
+ # - jsonb_each(x) / jsonb_each_text(x) → alias(key, value) # 2 columnas
892
+ # - jsonb_object_keys(x) → alias(key) # 1 columna
893
+ # Al usar estas SRF, el alias NO trae columnas de la tabla base (ej.: ct.rut NO existe).
894
+ # Las claves/PK (ej.: rut) deben seleccionarse desde la tabla padre (ej.: b.rut).
895
+
896
+ # SRF-ALIAS-002 (CLAVE PADRE SIEMPRE DESDE LA TABLA BASE):
897
+ # En joins LATERAL encadenados, proyecta siempre la clave de la fila base:
898
+ # SELECT b.rut, ct.value->>'email' ...
899
+ # FROM base b
900
+ # LEFT JOIN LATERAL jsonb_array_elements(b.obj->'arr') AS c(value) ON ...
901
+ # -- NUNCA: SELECT c.rut ...
902
+
903
+ # SRF-ALIAS-003 (GUARDAS DE TIPO EN EL ON DEL LATERAL):
904
+ # Coloca las validaciones de existencia/tipo en el ON del JOIN LATERAL:
905
+ # LEFT JOIN LATERAL jsonb_array_elements(b.obj->'arr') AS c(value)
906
+ # ON b.obj ? 'arr' AND jsonb_typeof(b.obj->'arr') = 'array'
907
+
908
+ # SRF-ALIAS-004 (DEDUPLICACIÓN):
909
+ # Si solo necesitas eliminar duplicados sin agregar, usa DISTINCT en vez de GROUP BY sin agregados.
910
+
911
+ # SRF-ALIAS-005 (PLANTILLA SEGURA):
912
+ # SELECT DISTINCT
913
+ # b.rut,
914
+ # ct.value->>'name' AS name,
915
+ # ct.value->>'email' AS email
916
+ # FROM base b
917
+ # LEFT JOIN LATERAL jsonb_array_elements(b.jsonb_operation->'contract') AS contrato(value)
918
+ # ON b.jsonb_operation ? 'contract'
919
+ # AND jsonb_typeof(b.jsonb_operation->'contract') = 'array'
920
+ # LEFT JOIN LATERAL jsonb_array_elements(contrato.value->'signatories') AS ct(value)
921
+ # ON contrato.value ? 'signatories'
922
+ # AND jsonb_typeof(contrato.value->'signatories') = 'array'
923
+ # WHERE ct.value->>'email' IS NOT NULL;
924
+
925
+ # SRF-ALIAS-006 (AGREGAR COMO JSON):
926
+ # Para devolver arreglos JSON por rut:
927
+ # SELECT
928
+ # b.rut,
929
+ # COALESCE(jsonb_agg(ct.value), '[]'::jsonb) AS signatories
930
+ # FROM base b
931
+ # LEFT JOIN LATERAL jsonb_array_elements(b.jsonb_operation->'contract') AS contrato(value)
932
+ # ON b.jsonb_operation ? 'contract'
933
+ # AND jsonb_typeof(b.jsonb_operation->'contract') = 'array'
934
+ # LEFT JOIN LATERAL jsonb_array_elements(contrato.value->'signatories') AS ct(value)
935
+ # ON contrato.value ? 'signatories'
936
+ # AND jsonb_typeof(contrato.value->'signatories') = 'array'
937
+ # GROUP BY b.rut;
938
+
939
+ ### JSONB-AGG vs row_to_json — Reglas para agregar JSON/JSONB
940
+
941
+ # JSONB-001 (NO usar row_to_json sobre json/jsonb):
942
+ # Si el dato ya viene de jsonb_array_elements/jsonb_each como JSONB, NO lo envuelvas en row_to_json().
943
+ # Usa jsonb_agg(...) para JSONB o json_agg((...)::json) si necesitas JSON.
944
+ # ❌ Malo:
945
+ # json_agg(row_to_json(con.value)) -- con.value es JSONB
946
+ # ✅ Bueno:
947
+ # jsonb_agg(con.value) -- mantiene JSONB
948
+ # json_agg((con.value)::json) -- si necesitas JSON (no JSONB)
949
+
950
+ # JSONB-002 (COALESCE tipado):
951
+ # Asegura que COALESCE tenga el mismo tipo:
952
+ # COALESCE(jsonb_agg(...), '[]'::jsonb)
953
+ # COALESCE(json_agg(...), '[]'::json)
954
+
955
+ # JSONB-003 (DISTINCT en agregados JSONB):
956
+ # Puedes usar DISTINCT dentro de jsonb_agg/json_agg:
957
+ # jsonb_agg(DISTINCT ct.value)
958
+
959
+ # JSONB-004 (Guardas de tipo antes de expandir):
960
+ # Antes de expandir, valida existencia y tipo en el ON del JOIN LATERAL:
961
+ # b.obj ? 'ruta' AND jsonb_typeof(b.obj->'ruta') = 'array'
962
+ # Evita WHERE que elimine la fila base accidentalmente.
963
+
964
+ # JSONB-005 (Convertir al final si hace falta):
965
+ # Si la API de salida exige JSON (no JSONB), castea sólo al final:
966
+ # (jsonb_agg(...))::json
967
+
968
+ # Al convertir arrays JSONB a texto separado por comas, NO uses aliases no definidos.
969
+ Usa `array_to_string(ARRAY(SELECT jsonb_array_elements_text(col_jsonb)), ', ')`
970
+ o bien alias explícito: `FROM jsonb_array_elements_text(col_jsonb) AS elem` y `SELECT elem`.
971
+ examples:
972
+ - bad: "array_to_string(ARRAY(SELECT c FROM jsonb_array_elements_text(x)), ', ')"
973
+ - good: "array_to_string(ARRAY(SELECT jsonb_array_elements_text(x)), ', ')"
974
+ - good: "array_to_string(ARRAY(SELECT elem FROM jsonb_array_elements_text(x) AS elem), ', ')"
975
+ also:
976
+ - "Si el array puede ser nulo o vacío, usa COALESCE(..., '') para retornar cadena vacía."
977
+
978
+ # En una subconsulta ESCALAR (usada como expresión en el SELECT),
979
+ NO usar `SELECT *` si la fuente tiene múltiples columnas.
980
+ Debe devolver **exactamente una columna**. Para filas compuestas,
981
+ envolver en `row_to_json(alias)` o seleccionar columnas individuales.
982
+ patterns:
983
+ - bad: "(SELECT * FROM some_cte WHERE ... ) AS col"
984
+ - good: "(SELECT row_to_json(s) FROM some_cte s WHERE ... ) AS col"
985
+ - good: "(SELECT s.una_col FROM some_cte s WHERE ... ) AS col"
986
+
987
+
988
+ # Nunca agrupes por columnas tipo JSON. Si necesitas agregar o devolver arrays/objetos,
989
+ usa tipos JSONB y funciones jsonb_*.
990
+ rules:
991
+ - "Usar jsonb_agg/to_jsonb/jsonb_build_object en lugar de json_agg/to_json."
992
+ - "Defaults con '[]'::jsonb o '{}'::jsonb, NO ::json."
993
+ - "Si una columna será parte del SELECT con GROUP BY, asegúrate de que sea jsonb (o cámbiala en el CTE)."
994
+ - "Para filas compuestas en una escalar, usar row_to_json(...) o to_jsonb(...)."
995
+ examples:
996
+ - bad: "COALESCE(json_agg(x), '[]'::json) AS datos; ... GROUP BY datos"
997
+ - good: "COALESCE(jsonb_agg(x), '[]'::jsonb) AS datos; ... GROUP BY datos"
998
+ - good: "(SELECT row_to_json(r) FROM resumen r WHERE r.id = t.id) AS resumen"
999
+
1000
+ # Al seleccionar desde una CTE que ya AGRUPÓ datos, no mezclar funciones
1001
+ agregadas nuevas (COUNT/MIN/...) con columnas no agregadas de esa misma CTE.
1002
+ Usa directamente las columnas preagregadas o agrega TODO con GROUP BY.
1003
+ bad_example: |
1004
+ SELECT COUNT(*), min(col), otros_campos FROM cte_agrupada;
1005
+ good_example: |
1006
+ SELECT otros_campos FROM cte_agrupada;
1007
+ -- o, si necesitas re-agrupar:
1008
+ SELECT MAX(otros_campos) FROM cte_agrupada GROUP BY clave;
1009
+
1010
+ # Toda columna referenciada en SELECT/WHERE debe estar presente en el scope
1011
+ (CTE o tabla) con el alias usado. Si usas alias `b`, asegúrate que la CTE
1012
+ `base` seleccione esos campos.
1013
+ patterns:
1014
+ - bad: "WITH base AS (SELECT rut FROM t) ... SELECT b.rut, b.flag FROM base b"
1015
+ - good: "WITH base AS (SELECT rut, flag FROM t) ... SELECT b.rut, b.flag FROM base b"
1016
+ also:
1017
+ - "Si un flag no existe, derivarlo desde JSONB (no asumir columnas)."
1018
+
1019
+ En PostgreSQL no usar comentarios con '#'. Usa '--' para línea
1020
+ o '/* ... */' para bloque.
1021
+ 2) Al usar jsonb_array_elements sobre posibles NULL, siempre proteger
1022
+ con LEFT JOIN LATERAL + guardas en ON.
1023
+ examples:
1024
+ - bad: "# comentario"
1025
+ - good: "-- comentario"
1026
+ - bad: "LEFT JOIN LATERAL jsonb_array_elements(c.x->'arr') AS e(value) ON TRUE"
1027
+ - good: "LEFT JOIN LATERAL jsonb_array_elements(c.x->'arr') AS e(value)
1028
+ ON c.x IS NOT NULL AND c.x ? 'arr' AND jsonb_typeof(c.x->'arr')='array'"
1029
+
1030
+ - id: scalar_subquery_json_row
1031
+ description: >
1032
+ Las subconsultas escalares deben devolver UNA sola columna. Si necesitas
1033
+ varias columnas, envuelve la fila en JSON con row_to_json(alias).
1034
+ patterns:
1035
+ - bad: "(SELECT * FROM cte) AS x"
1036
+ - good: "(SELECT row_to_json(c) FROM cte c) AS x"
1037
+ - good: "(SELECT c.una_col FROM cte c) AS x"
1038
+ also:
1039
+ - "Al usar jsonb_array_elements sobre posibles NULL, envuelve con COALESCE(...,'[]'::jsonb)."
1040
+
1041
+ - id: no_truncated_sql
1042
+ description: "Nunca dejes '...' u otros placeholders en SQL: el parser fallará."
1043
+ examples:
1044
+ - bad: "ROUND(100.0*total_con_maxxa/lici..."
1045
+ - good: "ROUND(100.0*total_con_maxxa/NULLIF(licitaciones_ganadas,0),1)"
1046
+
1047
+ - id: avoid_srf_in_select_list
1048
+ description: >
1049
+ No uses funciones que devuelven sets (json[b]_array_elements*, unnest, etc.)
1050
+ en la lista SELECT para contarlas. Usa jsonb_array_length(col) o un
1051
+ LEFT JOIN LATERAL + COUNT(*).
1052
+ examples:
1053
+ - bad: "COUNT(jsonb_array_elements_text(ct.certificate_ids))"
1054
+ - good: "SUM(COALESCE(jsonb_array_length(ct.certificate_ids),0))"
1055
+ - good: |
1056
+ LEFT JOIN LATERAL jsonb_array_elements(ct.certificate_ids) AS e(x) ON TRUE
1057
+ COUNT(e.x)
1058
+
1059
+ - id: percent_with_nullif
1060
+ description: "Al calcular porcentajes, usa NULLIF(den,0) para evitar división por cero."
1061
+ examples:
1062
+ - good: "ROUND(100.0 * ganadas / NULLIF(total,0), 1)"
1063
+
1064
+ - id: date_casts_vs_to_date
1065
+ description: >
1066
+ No uses TO_DATE sobre columnas DATE/TIMESTAMP. TO_DATE solo acepta texto.
1067
+ Para DATE usa MIN(col); para TIMESTAMP usa ::date o date_trunc.
1068
+ Si la columna es texto, entonces sí usar TO_DATE(text, 'YYYY-MM-DD').
1069
+ examples:
1070
+ - bad: "TO_DATE(ts_col, 'YYYY-MM-DD')" # ts_col es TIMESTAMP
1071
+ - good: "ts_col::date"
1072
+ - bad: "TO_DATE(date_col, 'YYYY-MM-DD')" # date_col es DATE
1073
+ - good: "date_col"
1074
+ - good: "TO_DATE(text_col, 'YYYY-MM-DD')" # text_col es TEXT
1075
+
1076
+ - id: aggregate_from_correct_table
1077
+ description: >
1078
+ No agregues columnas (status, montos) desde una CTE/tabla que no las tiene.
1079
+ Si los datos están en otra tabla (p.ej. bcu_certificate), usa subselect
1080
+ correlacionado o un JOIN/CTE agregado por rut.
1081
+ bad: "FROM bcu_customer ... SUM(CASE WHEN status ...)"
1082
+ good: "FROM bcu_customer c ... (SELECT SUM(...) FROM bcu_certificate bc WHERE bc.rut=c.rut ...)"
1083
+
1084
+ - id: cte_scope_single_statement
1085
+ description: >
1086
+ Las CTE (WITH ...) solo viven durante UNA sentencia SQL. Si haces
1087
+ múltiples SELECTs separados por ';', debes repetir el WITH o consolidar
1088
+ todo en una sola sentencia (p.ej. usando subselects o agregando a JSON).
1089
+ bad: |
1090
+ WITH a AS (...) SELECT ...;
1091
+ SELECT * FROM a; # falla: 'a' ya no existe
1092
+ good: |
1093
+ WITH a AS (...), b AS (...) SELECT ... FROM a ...;
1094
+ -- o repetir el WITH antes del segundo SELECT
1095
+
1096
+ - Si una columna (jsonb_*) se usará después, debe proyectarse en el CTE inicial con el mismo alias.
1097
+ No referenciar columnas que no estén proyectadas.
1098
+ Si necesitas el objeto completo y un campo derivado, incluye ambos.
1099
+
1100
+ - Usa EXACTAMENTE el mismo nombre para cada CTE/tabla en su definición y en los JOIN/SELECT posteriores (respeta mayúsculas/minúsculas y ortografía).
1101
+ Antes del SELECT final, verifica que todos los identificadores referenciados (p. ej., guarantee_summary) existen y coinciden con los CTE definidos (p. ej., guarante_summary).
1102
+ Prohíbe crear o unir CTE con nombres “parecidos”; si cambias un nombre, actualízalo en todas las referencias.
1103
+
1104
+ En el SELECT final, cualquier subconsulta debe ser escalar.
1105
+ • Para objetos: SELECT row_to_json(sub) FROM (SELECT col1, col2, ...) sub.
1106
+ • Para listas: SELECT json_agg(row_to_json(sub)) FROM (SELECT ...) sub.
1107
+ • Nunca SELECT * dentro del SELECT final.
1108
+
1109
+ - No usar row_to_json() sobre valores json/jsonb.
1110
+ Si el LLM necesita serializar un json/jsonb, usar to_json()/to_jsonb() o devolver el campo tal cual.
1111
+ Reservar row_to_json() solo para records (filas) provenientes de subconsultas/CTEs o SELECT ... con columnas.
1112
+
1113
+ ### RULE-XXX — No usar `ctid` fuera de su tabla de origen
1114
+ ⚠️ **Prohibido referenciar la columna `ctid` fuera de la consulta directa a su tabla de origen.**
1115
+
1116
+ - `ctid` es una columna interna de PostgreSQL que solo puede ser usada directamente en la tabla donde existe.
1117
+ - No puede referenciarse desde subconsultas o alias si ya no está presente en el conjunto de columnas seleccionadas.
1118
+ - Si necesitas contar o verificar filas, usa una columna real (por ejemplo, `tender_id`) o `COUNT(*)` sin `ctid`.
1119
+
1120
+ #### Ejemplo incorrecto:
1121
+ ```sql
1122
+ COUNT(*) FILTER (WHERE ctid IS NOT NULL) AS garantias_maxxa
1123
+
1124
+ ### RULE-AGG-001 — No mezclar agregados con columnas sin agrupar
1125
+ Cuando uses agregaciones (`COUNT`, `SUM`, `AVG`, `STRING_AGG`, etc.) **sin `GROUP BY`**, todo lo que aparezca en el `SELECT` debe estar dentro de una función agregada.
1126
+
1127
+ ✅ Correcto:
1128
+ ```sql
1129
+ SELECT STRING_AGG(
1130
+ ej.ejecutiva_garantia || ' (GC: ' || ej.grupo_garantia || ')'
1131
+ || ' | '
1132
+ || ej.ejecutiva_credito || ' (CC: ' || ej.grupo_credito || ')',
1133
+ ', '
1134
+ ) AS ejecutivos
1135
+ FROM ejecutivos ej;
1136
+
1137
+ RULE-AGG-003:
1138
+ description: No usar MAX(boolean); convertir booleanos a enteros con CASE WHEN ... THEN 1 ELSE 0 END antes de usar funciones agregadas como MAX o SUM.
1139
+ match: MAX\(CASE\s+WHEN\s+.*\s+THEN\s+(TRUE|FALSE)\s+ELSE\s+(TRUE|FALSE)\s+END\)
1140
+ fix: Reemplazar el CASE para devolver 1/0 en lugar de TRUE/FALSE. Ejemplo: CASE WHEN condición THEN 1 ELSE 0 END
1141
+
1142
+ RULE-GROUPBY-001:
1143
+ description: Toda columna que aparece en SELECT dentro de una subconsulta con GROUP BY debe también estar en el GROUP BY o estar dentro de una función agregada.
1144
+ match: SELECT\s+(.*?)\s+FROM\s+(.*?)\s+GROUP\s+BY\s+(?!.*\1)
1145
+ fix: Asegúrate de incluir en el GROUP BY todas las columnas del SELECT que no estén envueltas en funciones agregadas.
1146
+
1147
+
1148
+ RULE-JSON-005:
1149
+ description: Al concatenar valores extraídos de JSON (`->>`), asegúrate de envolver cada fragmento con `COALESCE(..., '')` y tratarlo explícitamente como texto (`::text`) para evitar errores de representación inválida.
1150
+ match: \|\|\s*contract\.value->>'\w+'
1151
+ fix: Usar `COALESCE(contract.value->>'campo', '')` y aplicar cast `::text` al resultado de la concatenación si es necesario.
1152
+
1153
+ RULE-CTE-001:
1154
+ description: Si usas una tabla o CTE como `base` dentro de un subselect, asegúrate de incluir `FROM base` dentro de ese subselect.
1155
+ match: SELECT\s+.*base\.
1156
+ fix: Agrega `FROM base` dentro del subselect que usa columnas prefijadas con `base.`.
1157
+
1158
+
1159
+ RULE-SUBSELECT-MULTICOLUMN-ERROR-001:
1160
+ description:
1161
+ No puedes usar `SELECT *` desde una subconsulta (CTE o tabla derivada) dentro del SELECT final
1162
+ si esa subconsulta devuelve más de una columna. PostgreSQL lanza error: "subquery must return only one column".
1163
+
1164
+ context_where_this_happens:
1165
+ Cuando escribes en el SELECT final algo como `(SELECT * FROM algo)` y `algo` tiene más de una columna.
1166
+
1167
+ why_this_fails:
1168
+ PostgreSQL espera que la subconsulta devuelva una sola columna. Pero `*` devuelve todas, causando el error.
1169
+
1170
+ how_to_fix_it:
1171
+ - Si quieres agrupar varias columnas en una sola columna tipo objeto, usa:
1172
+ `(SELECT row_to_json(alias) FROM algo alias)`
1173
+ - Si solo necesitas una columna, escríbela explícitamente:
1174
+ `(SELECT columna FROM algo)`
1175
+
1176
+ bad_example: |
1177
+ SELECT
1178
+ nombre,
1179
+ (SELECT * FROM resumen_licitaciones) AS licitaciones
1180
+ FROM base
1181
+
1182
+ good_example_option_1: |
1183
+ SELECT
1184
+ nombre,
1185
+ (SELECT row_to_json(r) FROM resumen_licitaciones r) AS licitaciones
1186
+ FROM base
1187
+
1188
+ good_example_option_2: |
1189
+ SELECT
1190
+ nombre,
1191
+ (SELECT total_participa FROM resumen_licitaciones) AS licitaciones
1192
+ FROM base
1193
+
1194
+ RULE-JSONB-OPERATOR-001:
1195
+ description: >
1196
+ El operador `->>` solo puede usarse sobre columnas de tipo `jsonb`, no sobre strings literales ni columnas de tipo `text`.
1197
+ bad_examples:
1198
+ - "'guarantee_executive'->>'group'"
1199
+ - "'algo'->>'otra_cosa'"
1200
+ - "'text_literal'->>'key'"
1201
+ good_examples:
1202
+ - "c.jsonb_guarantee->>'guarantee_group'"
1203
+ - "cb.jsonb_credit->>'credit_executive'"
1204
+
1205
+ RULE-CONCATENATION-001:
1206
+ description: >
1207
+ No uses `%%` en concatenaciones de texto. PostgreSQL no lo interpreta como un símbolo de `%`, y lanzará un error de sintaxis.
1208
+ Usa solo `%` si estás concatenando strings.
1209
+ bad_examples:
1210
+ - "'('||ROUND(ganadas*100.0/NULLIF(total,0),1)||'%%') de ellas.'"
1211
+ - "CONCAT('Avance: ', ROUND(x*100,2), '%% completado')"
1212
+ good_examples:
1213
+ - "'('||ROUND(ganadas*100.0/NULLIF(total,0),1)||'%'||') de ellas.'"
1214
+ - "CONCAT('Avance: ', ROUND(x*100,2), '% completado')"
1215
+
1216
+ RULE-JSONB-ACCESS-VALUE-001:
1217
+ description: >
1218
+ No se puede acceder a un campo de un objeto JSONB con notación de punto (por ejemplo: `cc.comment`) cuando se itera con `jsonb_array_elements`.
1219
+ En su lugar, debes usar `cc.value->>'campo'` o `cc.value->'campo'`.
1220
+
1221
+ when_to_apply: >
1222
+ Si estás usando `jsonb_array_elements(...) AS alias` en una subconsulta, debes acceder a los campos con `alias.value->>'campo'`.
1223
+
1224
+ how_to_fix:
1225
+ - Reemplaza `cc.campo` por `cc.value->>'campo'` si necesitas el valor como texto.
1226
+ - Usa `cc.value->'campo'` si necesitas el valor como JSONB.
1227
+
1228
+ bad_example: |
1229
+ SELECT cc.comment FROM jsonb_array_elements(jsonb_col) cc
1230
+
1231
+ good_example: |
1232
+ SELECT cc.value->>'comment' FROM jsonb_array_elements(jsonb_col) cc
1233
+
1234
+ RULE-SUBQUERY-SINGLE-ROW-001:
1235
+ description: >
1236
+ Si usas una subconsulta escalar en el `SELECT`, como `(SELECT row_to_json(...) FROM ...)`, debes garantizar que la subconsulta devuelve **solo una fila**.
1237
+ Si no hay `WHERE`, podrías obtener múltiples filas, lo que causará un error en tiempo de ejecución.
1238
+
1239
+ when_to_apply: >
1240
+ Siempre que estés usando una subconsulta en el `SELECT` que espera una sola fila (por ejemplo: `(SELECT row_to_json(...) FROM nombre_cte)`), debes filtrar por una clave única como `rut`.
1241
+
1242
+ how_to_fix:
1243
+ - Agrega un `WHERE` en la subconsulta que filtre por una columna que asegure una sola fila (como `rut = b.rut`).
1244
+ - Alternativamente, limita a una fila con `LIMIT 1`, pero no es recomendable si puede haber inconsistencias.
1245
+
1246
+ bad_example: |
1247
+ (SELECT row_to_json(s) FROM resumen_garantias s)
1248
+
1249
+ good_example: |
1250
+ (SELECT row_to_json(s) FROM resumen_garantias s WHERE s.rut = b.rut)
1251
+
1252
+ RULE-SCALAR-SUBQUERY-LIMIT-001:
1253
+ description: >
1254
+ Cuando uses una subconsulta escalar dentro de un `SELECT`, debes asegurarte de que la subconsulta devuelve solo una fila.
1255
+ Si existe la posibilidad de múltiples filas, se producirá un error.
1256
+
1257
+ when_to_apply: >
1258
+ Si estás usando `(SELECT ... FROM ...)` como un campo en el SELECT principal.
1259
+
1260
+ how_to_fix:
1261
+ - Asegúrate de que la subconsulta solo puede devolver una fila.
1262
+ - Usa `LIMIT 1` si estás seguro de que cualquier fila es válida, o filtra con `WHERE` para obtener una única.
1263
+
1264
+ bad_example: |
1265
+ (SELECT texto FROM tabla)
1266
+
1267
+ good_example: |
1268
+ (SELECT texto FROM tabla LIMIT 1)
1269
+ -- o
1270
+ (SELECT texto FROM tabla WHERE id = b.id)
1271
+
1272
+ RULE-JSONB-FUNCTION-NULL-PROTECTION-001:
1273
+ description: >
1274
+ Antes de aplicar funciones como `jsonb_each`, `jsonb_array_elements`, `jsonb_object_keys`, asegúrate que el campo JSONB no sea NULL.
1275
+ Estas funciones no aceptan NULL como entrada, y generarán errores incluso si luego filtras con condiciones como `WHERE 1=0`.
1276
+
1277
+ when_to_apply: >
1278
+ En subconsultas, joins laterales o expresiones donde accedes a funciones jsonb_*.
1279
+
1280
+ how_to_fix:
1281
+ - Agrega `WHERE campo IS NOT NULL` o `ON campo IS NOT NULL` cuando uses funciones jsonb.
1282
+ - Evita aplicar funciones jsonb directamente sobre campos potencialmente nulos.
1283
+
1284
+ bad_example: |
1285
+ SELECT jsonb_each(b.jsonb_data)
1286
+
1287
+ good_example: |
1288
+ SELECT jsonb_each(b.jsonb_data)
1289
+ WHERE b.jsonb_data IS NOT NULL
1290
+
1291
+ RULE-JSONB-ARRAY-PROTECT-001:
1292
+ description: "Antes de aplicar jsonb_array_elements() sobre una clave de tipo JSONB, verificar que dicha clave exista y sea de tipo 'array'."
1293
+ example_good: >
1294
+ b.jsonb_data ? 'items'
1295
+ AND jsonb_typeof(b.jsonb_data->'items') = 'array'
1296
+ AND jsonb_array_elements(b.jsonb_data->'items')
1297
+ example_bad: >
1298
+ jsonb_array_elements(b.jsonb_data->'items')
1299
+
1300
+ - rule_id: RULE-AGG-JSON-ORDER
1301
+ description: >
1302
+ Al usar jsonb_agg sobre un campo JSON construido en un LATERAL (ej. j.comi),
1303
+ no se puede ordenar por columnas internas del subquery (ej. j.anio), ya que no existen en el scope externo.
1304
+ Siempre ordenar usando expresiones sobre el JSON (ej. (j.comi->>'anio')::INT).
1305
+ bad_example: |
1306
+ jsonb_agg(j.comi ORDER BY j.anio DESC)
1307
+ good_example: |
1308
+ jsonb_agg(j.comi ORDER BY (j.comi->>'anio')::INT DESC)
1309
+
1310
+ RULE-GRAIN-001 (no mezclar granos en JOIN):
1311
+ • Define el grano de cada CTE (ej.: rut, tender_id, product_type).
1312
+ • Nunca hagas JOIN entre CTEs de granos distintos usando subconsultas con LIMIT 1 para obtener una “llave”.
1313
+ • Si necesitas métricas de rut en un agregado por product_type, primero une por rut (o agrega ambas fuentes al mismo grano) y recién después agrupa por product_type.
1314
+
1315
+ Anti-patrón a prohibir: JOIN ... ON key = (SELECT ... LIMIT 1)
1316
+ Patrón correcto: incluye la clave real (p.ej., rut) en el CTE de detalle (creditos) y JOIN ... ON j.rut = c.rut antes de GROUP BY product_type.
1317
+
1318
+ {
1319
+ "rule_id": "RULE-UNION-001",
1320
+ "description": "Evita errores de UNION/UNION ALL por desalineación de columnas entre subconsultas.",
1321
+ "applies_to": "sql_bcu",
1322
+ "rule": "Todas las subconsultas que participan en un UNION o UNION ALL deben tener exactamente el mismo número de columnas, con los mismos nombres (o alias) y en el mismo orden. Si una de las subconsultas agrega columnas adicionales como ROW_NUMBER, esas columnas deben ser excluidas explícitamente en el SELECT final para que ambas subconsultas coincidan.",
1323
+ "examples": {
1324
+ "incorrect": [
1325
+ "SELECT a, b FROM tabla1 UNION ALL SELECT a, b, c FROM tabla2",
1326
+ "SELECT * FROM (SELECT a, b, ROW_NUMBER() OVER (...) AS rn FROM x) x WHERE rn <= 10 UNION SELECT a, b FROM y"
1327
+ ],
1328
+ "correct": [
1329
+ "SELECT a, b FROM tabla1 UNION ALL SELECT a, b FROM tabla2",
1330
+ "SELECT a, b FROM (SELECT a, b, ROW_NUMBER() OVER (...) AS rn FROM x) x WHERE rn <= 10"
1331
+ ]
1332
+ },
1333
+ "fix_suggestion": "Asegúrate de que todas las subconsultas dentro del UNION seleccionen explícitamente las mismas columnas. Si se usa ROW_NUMBER u otras funciones analíticas, excluye esas columnas en el SELECT exterior antes del UNION."
1334
+ }
1335
+
1336
+ - No usar guiones (-) en los alias de columnas SQL.
1337
+ Usar guion bajo (_) en su lugar para evitar errores de sintaxis.