stateFieldsToWhere = ({
  fields,
  state,
  approximate = true,
  table,
  prefix = "",
}) => {
  let qstate = {};
  const orFields = [];
  Object.entries(state || {}).forEach(([k, v]) => {
    if (typeof v === "undefined") return;
    if (k === "_fts" || (table?.name && k === `_fts_${table.santized_name}`)) {
      const scState = getState();
      const language = scState.pg_ts_config;
      const use_websearch = scState.getConfig("search_use_websearch", false);
      qstate["_fts"] = {
        searchTerm: v.replace(/\0/g, ""),
        fields,
        language,
        use_websearch,
        table: prefix
          ? prefix.replaceAll(".", "")
          : table
            ? table.name
            : undefined,
        schema: db.isSQLite ? undefined : db.getTenantSchema(),
      };
      return;
    }
    if (k === "_or_field") {
      if (Array.isArray(v)) orFields.push(...v);
      else orFields.push(v);
      return;
    }

    const field = fields.find((fld) => fld.name === k);
    if (k === "_relation_path_" || k === "_inbound_relation_path_")
      handleRelationPath(
        typeof v === "string" ? stringToQuery(v) : v,
        qstate,
        table
      );
    else if (k.startsWith(".")) {
      const queryObj = parseRelationPath(k);
      queryObj.srcId = v;
      handleRelationPath(queryObj, qstate, table);
    } else if (k.startsWith("_fromdate_")) {
      const datefield = db.sqlsanitize(k.replace("_fromdate_", ""));
      const dfield = fields.find((fld) => fld.name === datefield);
      if (dfield)
        addOrCreateList(qstate, datefield, {
          gt: new Date(v),
          equal: true,
          day_only: dfield.attributes?.day_only,
        });
    } else if (k.startsWith("_todate_")) {
      const datefield = db.sqlsanitize(k.replace("_todate_", ""));
      const dfield = fields.find((fld) => fld.name === datefield);
      if (dfield)
        addOrCreateList(qstate, datefield, {
          lt: new Date(v),
          equal: true,
          day_only: dfield.attributes?.day_only,
        });
    } else if (k.startsWith("_fromneqdate_")) {
      const datefield = db.sqlsanitize(k.replace("_fromneqdate_", ""));
      const dfield = fields.find((fld) => fld.name === datefield);
      if (dfield)
        addOrCreateList(qstate, datefield, {
          gt: new Date(v),
          day_only: dfield.attributes?.day_only,
        });
    } else if (k.startsWith("_toneqdate_")) {
      const datefield = db.sqlsanitize(k.replace("_toneqdate_", ""));
      const dfield = fields.find((fld) => fld.name === datefield);
      if (dfield)
        addOrCreateList(qstate, datefield, {
          lt: new Date(v),
          day_only: dfield.attributes?.day_only,
        });
    } else if (k.startsWith("_gte_")) {
      const datefield = db.sqlsanitize(k.replace("_gte_", ""));
      const dfield = fields.find((fld) => fld.name === datefield);
      if (dfield) addOrCreateList(qstate, datefield, { gt: v, equal: true });
    } else if (k.startsWith("_lte_")) {
      const datefield = db.sqlsanitize(k.replace("_lte_", ""));
      const dfield = fields.find((fld) => fld.name === datefield);
      if (dfield) addOrCreateList(qstate, datefield, { lt: v, equal: true });
    } else if (k.startsWith("_gt_")) {
      const datefield = db.sqlsanitize(k.replace("_gt_", ""));
      const dfield = fields.find((fld) => fld.name === datefield);
      if (dfield) addOrCreateList(qstate, datefield, { gt: v });
    } else if (k.startsWith("_lt_")) {
      const datefield = db.sqlsanitize(k.replace("_lt_", ""));
      const dfield = fields.find((fld) => fld.name === datefield);
      if (dfield) addOrCreateList(qstate, datefield, { lt: v });
    } else if (k.startsWith("_not_")) {
      const notfield = db.sqlsanitize(k.replace("_not_", ""));
      const nfield = fields.find((fld) => fld.name === notfield);
      if (nfield) {
        if (!qstate.not) qstate.not = {};
        qstate.not[notfield] = v;
      }
    } else if (field && field.type.name === "String" && v && v.slugify) {
      qstate[k] = v;
    } else if (Array.isArray(v) && field && field.type && field.type.read) {
      qstate[k] = { or: v.map(field.type.read) };
    } else if (
      Array.isArray(v) &&
      field?.is_fkey &&
      field?.reftype === "Integer"
    ) {
      qstate[k] = { or: v.map((v) => (v && !isNaN(+v) ? +v : v)) };
    } else if (
      field &&
      field.type.name === "String" &&
      !(field.attributes && field.attributes.options) &&
      approximate &&
      !field.attributes?.exact_search_only
    ) {
      qstate[k] = { ilike: v };
    } else if (field && field.type.name === "Bool" && state[k] === "?") {
      // omit
    } else if (typeof v === "object" && v && field?.type?.name === "JSON") {
      let json = {};
      if (Object.values(v).length === 1 && Object.values(v)[0] === "") return;
      Object.entries(v).forEach(([kj, vj]) => {
        if (vj === "") return;
        if (kj.endsWith("__lte")) {
          json[kj.replace("__lte", "")] = {
            lte: +vj,
            ...(json[kj.replace("__lte", "")] || {}),
          };
        } else if (kj.endsWith("__gte")) {
          json[kj.replace("__gte", "")] = {
            gte: +vj,
            ...(json[kj.replace("__gte", "")] || {}),
          };
        } else {
          json[kj] = vj;

          if (field.attributes?.hasSchema) {
            const s = field.attributes.schema.find(
              (f) => f.key === Object.keys(v)[0]
            );
            if (s?.type === "String") {
              json[kj] = { ilike: vj };
            }
          }
        }
      });

      qstate[k] = [
        ...(qstate[k] ? [qstate[k]] : []),
        {
          json,
        },
      ];
    } else if (typeof v === "object" && field) {
      qstate[k] = v;
    } else if (field && field.type && field.type.read)
      qstate[k] = Array.isArray(v)
        ? { or: v.map(field.type.read) }
        : field.type.read(v);
    else if (field) qstate[k] = v;
    else if (k.split("->").length === 3) {
      const [jFieldNm, throughPart, finalPart] = k.split(".");
      const [thoughTblNm, throughField] = throughPart.split("->");
      const [jtNm, lblField] = finalPart.split("->");
      const jtTbl = Table.findOne(jtNm);
      let where = { [db.sqlsanitize(lblField)]: v };
      qstate[jFieldNm] = [
        ...(qstate[jFieldNm] ? [qstate[jFieldNm]] : []),
        {
          // where jFieldNm in (select id from jtnm where lblField=v)
          inSelect: {
            table: db.sqlsanitize(thoughTblNm),
            tenant: db.isSQLite ? undefined : db.getTenantSchema(),
            field: db.sqlsanitize(throughField),
            valField: jtTbl?.pk_name || "id",
            through: db.sqlsanitize(jtNm),
            where,
          },
        },
      ];
    } else if (k.includes("->")) {
      // jFieldNm.jtnm->lblField
      // where jFieldNm in (select id from jtnm where lblField=v)
      const [jFieldNm, krest] = k.split(".");
      const [jtNm, lblField] = krest.split("->");
      let where = { [db.sqlsanitize(lblField)]: v };
      const jTable = Table.findOne({ name: jtNm });
      const lblFld = (jTable.fields || []).find((f) => f.name === lblField);
      if (
        lblFld &&
        lblFld.type?.name === "String" &&
        !lblFld.attributes?.options
      )
        where = { [db.sqlsanitize(lblField)]: { ilike: v } };

      qstate[jFieldNm] = [
        ...(qstate[jFieldNm] ? [qstate[jFieldNm]] : []),
        {
          // where jFieldNm in (select id from jtnm where lblField=v)
          inSelect: {
            table: db.sqlsanitize(jtNm),
            tenant: db.isSQLite ? undefined : db.getTenantSchema(),
            field: jTable.pk_name,
            where,
          },
        },
      ];
    } else if (k.includes(".")) {
      const kpath = k.split(".");
      if (kpath.length === 3) {
        const [jtNm, jFieldNm, lblField] = kpath;
        let isString = false;
        const labelField = Table.findOne({ name: jtNm })?.getField?.(lblField);
        if (labelField)
          isString =
            labelField.type?.name === "String" &&
            !labelField.attributes?.exact_search_only;

        const pk = table ? table.pk_name : "id";
        qstate[pk] = [
          ...(qstate[pk] ? [qstate[pk]] : []),
          {
            // where id in (select jFieldNm from jtnm where lblField=v)
            inSelect: {
              table: db.sqlsanitize(jtNm),
              tenant: db.isSQLite ? undefined : db.getTenantSchema(),
              field: db.sqlsanitize(jFieldNm),
              where: {
                [db.sqlsanitize(lblField)]:
                  isString && approximate ? { ilike: v } : v,
              },
            },
          },
        ];
      } else if (kpath.length === 4) {
        const [jtNm, jFieldNm, tblName, lblField] = kpath;
        const pk = table ? table.pk_name : "id";
        qstate[pk] = [
          ...(qstate[pk] ? [qstate[pk]] : []),
          {
            // where id in (select ss1.id from jtNm ss1 join tblName ss2 on ss2.id = ss1.jFieldNm where ss2.lblField=v)
            inSelect: {
              table: db.sqlsanitize(jtNm),
              tenant: db.isSQLite ? undefined : db.getTenantSchema(),
              field: db.sqlsanitize(jFieldNm),
              valField: Table.findOne(jtNm)?.pk_name || "id",
              through: db.sqlsanitize(tblName),
              through_pk: Table.findOne(tblName)?.pk_name || "id",
              where: { [db.sqlsanitize(lblField)]: v },
            },
          },
        ];
      }
    }
  });
  if (orFields.length === 1) {
    const orKey = orFields[0];
    const orVal = qstate[orKey];
    delete qstate[orKey];
    return { or: [{ [orKey]: orVal }, qstate] };
  } else
    orFields.forEach((orField) => {
      if (typeof qstate[orField] === "undefined") return;
      if (!qstate.or) qstate.or = [];
      qstate.or.push({ [orField]: qstate[orField] });
      delete qstate[orField];
    });

  return qstate;
}
@function
@param {object} opts
@param {Field[]} opts.fields
@param {object} opts.state
@param {string} [opts.prefix = ""]
@param {boolean} [opts.approximate = true]
@param {Table} opts.table
@returns {object}