picked_fields_to_query = (columns, fields, layout, req, table) => {
  let joinFields = {};
  let aggregations = {};
  let freeVars = new Set(); // for join fields
  const locale = req?.getLocale?.();

  (columns || []).forEach((column) => {
    if (column.type === "JoinField") {
      if (column.join_field && column.join_field.split) {
        if (column.join_field.includes("->")) {
          const [relation, target] = column.join_field.split("->");
          const [ontable, ref] = relation.split(".");
          const targetNm = validSqlId(
            `${ref}_${ontable.replaceAll(" ", "").toLowerCase()}_${target}`
          );
          column.targetNm = targetNm;
          joinFields[targetNm] = {
            ref,
            target,
            ontable,
          };
        } else {
          const kpath = column.join_field.split(".");
          let jfKey;
          if (kpath.length === 2) {
            const [refNm, targetNm] = kpath;
            jfKey = `${refNm}_${targetNm}`;
            joinFields[jfKey] = {
              ref: refNm,
              target: targetNm,
            };
          } else if (kpath.length === 3) {
            const [refNm, through, targetNm] = kpath;
            jfKey = `${refNm}_${through}_${targetNm}`;
            joinFields[jfKey] = {
              ref: refNm,
              target: targetNm,
              through,
            };
          } else if (kpath.length === 4) {
            const [refNm, through1, through2, targetNm] = kpath;
            jfKey = `${refNm}_${through1}_${through2}_${targetNm}`;
            joinFields[jfKey] = {
              ref: refNm,
              target: targetNm,
              through: [through1, through2],
            };
          }
          const targetField = table ? table.getField(column.join_field) : null;
          if (locale && targetField?.attributes?.localized_by?.[locale]) {
            joinFields[jfKey].target =
              targetField?.attributes?.localized_by?.[locale];
          }
        }
      } else {
        throw new InvalidConfiguration(
          `Join field is specified as column but no join field is chosen`
        );
      }
    } else if (column.type === "FormulaValue") {
      freeVars = new Set([...freeVars, ...freeVariables(column.formula)]);
    } else if (column.type === "ViewLink") {
      if (column.view_label_formula || column.isFormula?.label)
        freeVars = new Set([
          ...freeVars,
          ...freeVariables(column.view_label || column.label),
        ]);
      if (column.extra_state_fml)
        freeVars = new Set([
          ...freeVars,
          ...freeVariables(column.extra_state_fml),
        ]);
      if (column.view && column.view.split) {
        const [vtype, vrest] = column.view.split(":");
        if (vtype === "ParentShow") {
          const [pviewnm, ptbl, pfld] = vrest.split(".");
          const field = fields.find((f) => f.name === pfld);
          if (field && field.attributes.summary_field)
            joinFields[`summary_field_${ptbl.toLowerCase()}`] = {
              ref: pfld,
              target: field.attributes.summary_field,
            };
        }
      }
    } else if (column.type === "Aggregation") {
      if (column.agg_relation && column.agg_relation.split) {
        let table, fld, through;
        if (column.agg_relation.includes("->")) {
          let restpath;
          [through, restpath] = column.agg_relation.split("->");
          [table, fld] = restpath.split(".");
        } else {
          [table, fld] = column.agg_relation.split(".");
        }

        //console.log(column);
        const field = column.agg_field.split("@")[0];
        let targetNm = validSqlId(
          db.sqlsanitize(
            (
              column.stat.replace(" ", "") +
                "_" +
                table +
                "_" +
                fld +
                "_" +
                field +
                "_" +
                column.aggwhere || ""
            ).toLowerCase()
          )
        );
        // postgres fields have a max len
        if (targetNm.length > 58) {
          targetNm = targetNm
            .split("")
            .filter((c, i) => i % 2 == 0)
            .join("");
        }
        column.targetNm = targetNm;
        aggregations[targetNm] = {
          table,
          ref: fld,
          where: column.aggwhere
            ? jsexprToWhere(column.aggwhere, req ? { user: req.user } : {})
            : undefined,
          field,
          aggregate: column.stat,
          through,
        };
      }
    } else if (column.type === "Link") {
      if (column.link_text_formula)
        freeVars = new Set([...freeVars, ...freeVariables(column.link_text)]);
      if (column.link_url_formula)
        freeVars = new Set([...freeVars, ...freeVariables(column.link_url)]);
    } else if (column.type === "Action" && column.action_label_formula) {
      freeVars = new Set([...freeVars, ...freeVariables(column.action_label)]);
    }
    if (column.showif)
      freeVars = new Set([...freeVars, ...freeVariables(column.showif)]);
  });
  if (layout) {
    traverseSync(layout, {
      view(v) {
        if (v.extra_state_fml)
          freeVars = new Set([
            ...freeVars,
            ...freeVariables(v.extra_state_fml),
          ]);
      },
      view_link(v) {
        if (v.extra_state_fml)
          freeVars = new Set([
            ...freeVars,
            ...freeVariables(v.extra_state_fml),
          ]);
        if (v.isFormula?.label && typeof v.view_label === "string")
          freeVars = new Set([...freeVars, ...freeVariables(v.view_label)]);
      },
      link(v) {
        if (v?.isFormula?.text && typeof v.text === "string")
          freeVars = new Set([...freeVars, ...freeVariables(v.text)]);
        if (v?.isFormula?.url && typeof v.url === "string")
          freeVars = new Set([...freeVars, ...freeVariables(v.url)]);
      },
      image(v) {
        if (v?.isFormula?.alt && typeof v.alt === "string")
          freeVars = new Set([...freeVars, ...freeVariables(v.alt)]);
      },
      card(v) {
        if (v?.isFormula?.title && typeof v.title === "string")
          freeVars = new Set([...freeVars, ...freeVariables(v.title)]);
        if (v?.isFormula?.url && typeof v.url === "string")
          freeVars = new Set([...freeVars, ...freeVariables(v.url)]);
      },
      tabs(v) {
        (v.titles || []).forEach((t) => {
          if (typeof t === "string")
            freeVars = new Set([
              ...freeVars,
              ...freeVariablesInInterpolation(t),
            ]);
        });
        (v.showif || []).forEach((t) => {
          freeVars = new Set([...freeVars, ...freeVariablesInInterpolation(t)]);
        });
      },
      blank(v) {
        if (v?.isFormula?.text && typeof v.contents === "string")
          freeVars = new Set([...freeVars, ...freeVariables(v.contents)]);
        if (v.isHTML)
          freeVars = new Set([
            ...freeVars,
            ...freeVariablesInInterpolation(v.contents),
          ]);
      },
      container(v) {
        if (v.showIfFormula)
          freeVars = new Set([...freeVars, ...freeVariables(v.showIfFormula)]);
        if (v.isFormula?.bgColor)
          freeVars = new Set([...freeVars, ...freeVariables(v.bgColor)]);
        if (v.isFormula?.url)
          freeVars = new Set([...freeVars, ...freeVariables(v.url)]);
        if (v.isFormula?.customClass)
          freeVars = new Set([...freeVars, ...freeVariables(v.customClass)]);
        if (v.isFormula?.customId)
          freeVars = new Set([...freeVars, ...freeVariables(v.customId)]);
      },
    });
  }
  if (layout?.besides && layout?.list_columns) {
    layout?.besides.forEach((s) => {
      if (s.showif)
        freeVars = new Set([...freeVars, ...freeVariables(s.showif)]);
    });
  }
  add_free_variables_to_joinfields(freeVars, joinFields, fields);
  return { joinFields, aggregations };
}
Picked fields to query
@function
@param {object[]} columns
@param {Field[]} fields
@param layout
@throws {InvalidConfiguration}
@returns {object}