import { SortDir } from "@open-mercato/shared/lib/query/types";
import { resolveEntityTableName, resolveRegisteredEntityTableName } from "@open-mercato/shared/lib/query/engine";
import { isOrmBackedSystemEntityId } from "@open-mercato/shared/lib/data/engine";
import { sql } from "kysely";
import { readCoverageSnapshot, refreshCoverageSnapshot } from "./coverage.js";
import { createProfiler, shouldEnableProfiler } from "@open-mercato/shared/lib/profiler";
import { decryptIndexDocCustomFields } from "@open-mercato/shared/lib/encryption/indexDoc";
import { parseBooleanToken, parseBooleanWithDefault } from "@open-mercato/shared/lib/boolean";
import {
  applyJoinFilters,
  normalizeFilters,
  partitionFilters,
  resolveJoins
} from "@open-mercato/shared/lib/query/join-utils";
import { resolveSearchConfig } from "@open-mercato/shared/lib/search/config";
import { tokenizeText } from "@open-mercato/shared/lib/search/tokenize";
import { runBeforeQueryPipeline, runAfterQueryPipeline } from "@open-mercato/shared/lib/query/query-extension-runner";
import { resolveEncryptedSortFields, resolveEncryptedSortMaxRows, sortRowsInMemory } from "@open-mercato/shared/lib/query/encrypted-sort";
import { mapWithConcurrency } from "@open-mercato/shared/lib/query/bounded-decrypt";
import { createLogger } from "@open-mercato/shared/lib/logger";
const logger = createLogger("query_index").child({ component: "engine" });
const DECRYPT_CONCURRENCY = 8;
function buildFilterableCustomFieldJoins(sources) {
  if (!sources || sources.length === 0) return [];
  return sources.flatMap((source, index) => {
    if (!source.join) return [];
    const alias = typeof source.alias === "string" && source.alias.trim().length > 0 ? source.alias.trim() : `cfs_${index}`;
    return [{
      alias,
      table: source.table,
      entityId: source.entityId,
      from: { field: source.join.fromField },
      to: { field: source.join.toField },
      type: source.join.type === "inner" ? "inner" : "left"
    }];
  });
}
function resolveBooleanEnv(names, defaultValue) {
  for (const name of names) {
    const raw = process.env[name];
    if (raw !== void 0) return parseBooleanWithDefault(raw, defaultValue);
  }
  return defaultValue;
}
function coerceSortDirection(dir) {
  return String(dir ?? SortDir.Asc).toLowerCase() === SortDir.Desc ? SortDir.Desc : SortDir.Asc;
}
function resolveDebugVerbosity() {
  const queryIndexDebug = process.env.OM_QUERY_INDEX_DEBUG;
  if (queryIndexDebug !== void 0) {
    return parseBooleanToken(queryIndexDebug) ?? false;
  }
  const level = (process.env.LOG_VERBOSITY ?? process.env.LOG_LEVEL ?? "").toLowerCase();
  if (["debug", "trace", "silly"].includes(level)) return true;
  return false;
}
function createSearchAliasMinter() {
  let seq = 0;
  return () => `st_${seq++}`;
}
function createQueryProfiler(entity) {
  const enabled = shouldEnableProfiler(entity);
  return createProfiler({
    scope: "query_engine",
    target: entity,
    label: `query_engine:${entity}`,
    loggerLabel: "[qe:profile]",
    enabled
  });
}
class HybridQueryEngine {
  constructor(em, fallback, eventBusResolver, vectorServiceResolver, encryptionResolver) {
    this.em = em;
    this.fallback = fallback;
    this.eventBusResolver = eventBusResolver;
    this.vectorServiceResolver = vectorServiceResolver;
    this.encryptionResolver = encryptionResolver;
    this.customFieldKeysCache = /* @__PURE__ */ new Map();
    this.columnCache = /* @__PURE__ */ new Map();
    this.customEntityCache = /* @__PURE__ */ new Map();
    this.debugVerbosity = null;
    this.sqlDebugEnabled = null;
    this.forcePartialIndexEnabled = null;
    this.autoReindexEnabled = null;
    this.coverageOptimizationEnabled = null;
    this.pendingCoverageRefreshKeys = /* @__PURE__ */ new Set();
    const coverageTtl = Number.parseInt(process.env.QUERY_INDEX_COVERAGE_CACHE_MS ?? "", 10);
    this.coverageStatsTtlMs = Number.isFinite(coverageTtl) && coverageTtl >= 0 ? coverageTtl : 5 * 60 * 1e3;
    const cfTtl = Number.parseInt(process.env.QUERY_INDEX_CF_KEYS_CACHE_MS ?? "", 10);
    this.customFieldKeysTtlMs = Number.isFinite(cfTtl) && cfTtl >= 0 ? cfTtl : 5 * 60 * 1e3;
  }
  getEncryptionService() {
    try {
      return this.encryptionResolver?.() ?? null;
    } catch {
      return null;
    }
  }
  getDb() {
    const emAny = this.em;
    if (typeof emAny.getKysely === "function") return emAny.getKysely();
    throw new Error("HybridQueryEngine requires an EntityManager exposing getKysely() (MikroORM v7)");
  }
  async query(entity, opts = {}) {
    const ext = opts.extensions;
    let hybridExtCtx = null;
    const noopDi = { resolve: (_name) => {
      throw new Error("No DI context");
    } };
    if (ext) {
      hybridExtCtx = {
        entity: String(entity),
        engine: "hybrid",
        tenantId: opts.tenantId ?? "",
        organizationId: opts.organizationId,
        userId: ext.userId,
        em: this.em,
        container: ext.container,
        userFeatures: ext.userFeatures
      };
      const diCtx = ext.resolve ? { resolve: ext.resolve } : noopDi;
      const beforeResult = await runBeforeQueryPipeline(opts, hybridExtCtx, diCtx);
      if (beforeResult.blocked) {
        throw new Error(beforeResult.errorMessage ?? "Query blocked by extension subscriber");
      }
      opts = beforeResult.query;
    }
    const { extensions: _stripExt, ...coreOpts } = opts;
    opts = coreOpts;
    const providedProfiler = opts.profiler;
    const profiler = providedProfiler && providedProfiler.enabled ? providedProfiler : createQueryProfiler(String(entity));
    profiler.mark("query:init");
    let profileClosed = false;
    const finishProfile = (meta) => {
      if (!profiler.enabled || profileClosed) return;
      profileClosed = true;
      profiler.end(meta);
    };
    const applyAfterExtensions = async (queryResult) => {
      if (!ext || !hybridExtCtx) return queryResult;
      const diCtx = ext.resolve ? { resolve: ext.resolve } : noopDi;
      return await runAfterQueryPipeline(
        queryResult,
        opts,
        hybridExtCtx,
        diCtx
      );
    };
    try {
      const debugEnabled = this.isDebugVerbosity();
      if (debugEnabled) this.debug("query:start", { entity });
      const isCustom = opts.forceCustomEntityStorage === true || await this.isCustomEntity(entity);
      if (isCustom) {
        if (debugEnabled) this.debug("query:custom-entity", { entity });
        const section = profiler.section("custom_entity");
        try {
          const result2 = await this.queryCustomEntity(entity, opts);
          section.end({ mode: "custom_entity" });
          finishProfile({
            result: "custom_entity",
            total: Array.isArray(result2.items) ? result2.items.length : void 0
          });
          return await applyAfterExtensions(result2);
        } catch (err) {
          section.end({ error: err instanceof Error ? err.message : String(err) });
          throw err;
        }
      }
      const db = this.getDb();
      profiler.mark("query:db_ready");
      const baseTable = resolveEntityTableName(this.em, entity);
      profiler.mark("query:base_table_resolved");
      const searchConfig = resolveSearchConfig();
      const orgScope = this.resolveOrganizationScope(opts);
      const searchEnabled = searchConfig.enabled && await this.tableExists("search_tokens");
      const baseExists = await profiler.measure("base_table_exists", () => this.tableExists(baseTable));
      if (!baseExists) {
        if (debugEnabled) this.debug("query:fallback:missing-base", { entity, baseTable });
        const fallbackResult = await this.fallback.query(entity, opts);
        finishProfile({ result: "fallback", reason: "missing_base" });
        return await applyAfterExtensions(fallbackResult);
      }
      if (opts.omitAutomaticTenantOrgScope === true) {
        if (debugEnabled) this.debug("query:fallback:omit-automatic-scope", { entity });
        const fallbackResult = await this.fallback.query(entity, opts);
        finishProfile({ result: "fallback", reason: "omit_automatic_tenant_org_scope" });
        return await applyAfterExtensions(fallbackResult);
      }
      const normalizedFilters = normalizeFilters(opts.filters);
      const cfFilters = normalizedFilters.filter((filter) => filter.field.startsWith("cf:") || filter.field.startsWith("l10n:"));
      const coverageScope = this.resolveCoverageSnapshotScope(opts);
      const wantsCf = (opts.fields || []).some((field) => typeof field === "string" && (field.startsWith("cf:") || field.startsWith("l10n:"))) || cfFilters.length > 0 || Array.isArray(opts.includeCustomFields) && opts.includeCustomFields.length > 0;
      if (debugEnabled) {
        this.debug("query:config", {
          entity,
          wantsCustomFields: wantsCf,
          customFieldSources: Array.isArray(opts.customFieldSources) ? opts.customFieldSources.map((src) => src?.entityId) : void 0,
          fields: opts.fields
        });
      }
      let partialIndexWarning = null;
      let entityHasActiveCustomFields = true;
      if (wantsCf) {
        entityHasActiveCustomFields = await this.entityHasActiveCustomFields(entity, opts.tenantId ?? null);
        const hasIndexRows = await profiler.measure(
          "index_any_rows",
          () => this.indexAnyRows(entity),
          (value) => ({ hasIndexRows: value })
        );
        if (!hasIndexRows) {
          if (debugEnabled) this.debug("query:fallback:no-index", { entity });
          const fallbackResult = await this.fallback.query(entity, opts);
          finishProfile({ result: "fallback", reason: "no_index_rows" });
          return await applyAfterExtensions(fallbackResult);
        }
        if (entityHasActiveCustomFields) {
          const gap = await profiler.measure(
            "resolve_coverage_gap",
            () => this.resolveCoverageGap(entity, opts, coverageScope),
            (value) => value ? {
              scope: value.scope,
              baseCount: value.stats?.baseCount ?? null,
              indexedCount: value.stats?.indexedCount ?? null
            } : { scope: null }
          );
          if (gap) {
            if (!opts.skipAutoReindex) {
              this.scheduleAutoReindex(entity, opts, gap.stats, coverageScope?.organizationId ?? null);
            }
            const force = this.isForcePartialIndexEnabled();
            if (!force) {
              if (gap.stats) {
                logger.warn("Partial index coverage detected; falling back to basic engine", { entity, baseCount: gap.stats.baseCount, indexedCount: gap.stats.indexedCount, scope: gap.scope });
                if (debugEnabled) this.debug("query:fallback:partial-coverage", { entity, baseCount: gap.stats.baseCount, indexedCount: gap.stats.indexedCount, scope: gap.scope });
              } else {
                logger.warn("Partial index coverage detected; falling back to basic engine", { entity });
                if (debugEnabled) this.debug("query:fallback:partial-coverage", { entity });
              }
              const fallbackResult = await this.fallback.query(entity, opts);
              const resultWithWarning = {
                ...fallbackResult,
                meta: {
                  ...fallbackResult.meta ?? {},
                  partialIndexWarning: {
                    entity,
                    entityLabel: this.resolveEntityLabel(entity),
                    baseCount: gap.stats?.baseCount ?? null,
                    indexedCount: gap.stats?.indexedCount ?? null,
                    scope: gap.stats ? gap.scope : void 0
                  }
                }
              };
              finishProfile({
                result: "fallback",
                reason: "partial_index",
                scope: gap.scope,
                baseCount: gap.stats?.baseCount ?? null,
                indexedCount: gap.stats?.indexedCount ?? null
              });
              return await applyAfterExtensions(resultWithWarning);
            }
            if (gap.stats) {
              logger.warn("Partial index coverage detected; forcing query index usage due to FORCE_QUERY_INDEX_ON_PARTIAL_INDEXES", { entity, baseCount: gap.stats.baseCount, indexedCount: gap.stats.indexedCount, scope: gap.scope });
              if (debugEnabled) this.debug("query:partial-coverage:forced", { entity, baseCount: gap.stats.baseCount, indexedCount: gap.stats.indexedCount, scope: gap.scope });
            } else {
              logger.warn("Partial index coverage detected; forcing query index usage due to FORCE_QUERY_INDEX_ON_PARTIAL_INDEXES", { entity });
              if (debugEnabled) this.debug("query:partial-coverage:forced", { entity });
            }
            partialIndexWarning = {
              entity,
              entityLabel: this.resolveEntityLabel(entity),
              baseCount: gap.stats?.baseCount ?? null,
              indexedCount: gap.stats?.indexedCount ?? null,
              scope: gap.stats ? gap.scope : void 0
            };
          }
        } else if (debugEnabled) {
          this.debug("query:coverage:skip-no-custom-fields", { entity });
        }
      }
      const qualify = (col) => `b.${col}`;
      const columns = await this.getBaseColumnsForEntity(entity);
      const hasOrganizationColumn = await this.columnExists(baseTable, "organization_id");
      const hasTenantColumn = await this.columnExists(baseTable, "tenant_id");
      const hasDeletedColumn = await this.columnExists(baseTable, "deleted_at");
      if (!opts.tenantId) throw new Error("QueryEngine: tenantId is required");
      const resolvedJoinsConfig = resolveJoins(
        baseTable,
        [...opts.joins ?? [], ...buildFilterableCustomFieldJoins(opts.customFieldSources)],
        (entityId) => resolveEntityTableName(this.em, entityId)
      );
      const joinMap = /* @__PURE__ */ new Map();
      const aliasTables = /* @__PURE__ */ new Map();
      aliasTables.set("b", baseTable);
      aliasTables.set("base", baseTable);
      aliasTables.set(baseTable, baseTable);
      for (const join of resolvedJoinsConfig) {
        joinMap.set(join.alias, join);
        aliasTables.set(join.alias, join.table);
      }
      const { baseFilters, joinFilters } = partitionFilters(baseTable, normalizedFilters, joinMap);
      const searchRuntimeBase = {
        enabled: false,
        config: searchConfig,
        organizationScope: orgScope,
        tenantId: opts.tenantId ?? null,
        mintAlias: createSearchAliasMinter()
      };
      const indexSources = [{ alias: "ei", entityId: entity, recordIdColumn: "b.id" }];
      let preparedCfSources = [];
      const shouldAttachCustomSources = Array.isArray(opts.customFieldSources) && opts.customFieldSources.length > 0 && (wantsCf || searchEnabled);
      if (shouldAttachCustomSources) {
        preparedCfSources = this.prepareCustomFieldSources(opts.customFieldSources ?? []);
        for (const source of preparedCfSources) {
          indexSources.push({ alias: source.indexAlias, entityId: source.entityId, recordIdColumn: `${source.alias}.${source.recordIdColumn}` });
        }
      }
      const searchSources = indexSources.map((src) => ({ entity: String(src.entityId), recordIdColumn: src.recordIdColumn })).filter((src) => src.recordIdColumn && src.entity);
      const hasSearchTokens = searchEnabled && searchSources.length ? await this.searchSourcesHaveTokens(searchSources, opts.tenantId ?? null, orgScope) : false;
      const searchRuntime = { ...searchRuntimeBase, searchSources, enabled: searchEnabled && hasSearchTokens };
      const joinSearchAvailability = /* @__PURE__ */ new Map();
      const searchFilters = normalizeFilters(opts.filters).filter((filter) => filter.op === "like" || filter.op === "ilike");
      if (searchFilters.length) {
        this.logSearchDebug("search:init", {
          entity,
          baseTable,
          tenantId: opts.tenantId ?? null,
          organizationScope: orgScope,
          fields: searchFilters.map((filter) => String(filter.field)),
          searchEnabled,
          hasSearchTokens,
          searchSources,
          searchConfig: {
            enabled: searchConfig.enabled,
            minTokenLength: searchConfig.minTokenLength,
            enablePartials: searchConfig.enablePartials,
            hashAlgorithm: searchConfig.hashAlgorithm,
            blocklistedFields: searchConfig.blocklistedFields
          }
        });
        if (!searchEnabled) this.logSearchDebug("search:disabled", { entity, baseTable });
        else if (!hasSearchTokens) this.logSearchDebug("search:no-search-tokens", {
          entity,
          baseTable,
          tenantId: opts.tenantId ?? null,
          organizationScope: orgScope,
          searchSources
        });
      }
      const hasNonBaseSearchSource = searchSources.some(
        (src) => src.entity !== String(entity) || src.recordIdColumn !== "b.id"
      );
      if (!partialIndexWarning && Array.isArray(opts.customFieldSources) && opts.customFieldSources.length > 0 && this.isForcePartialIndexEnabled()) {
        const seen = /* @__PURE__ */ new Set([entity]);
        for (const source of opts.customFieldSources) {
          const targetEntity = source?.entityId ? String(source.entityId) : null;
          if (!targetEntity || seen.has(targetEntity)) continue;
          seen.add(targetEntity);
          const sourceHasCustomFields = await this.entityHasActiveCustomFields(targetEntity, opts.tenantId ?? null);
          if (!sourceHasCustomFields) {
            if (debugEnabled) this.debug("query:coverage:skip-no-custom-fields", { entity: targetEntity });
            continue;
          }
          const sourceTable = source.table ?? resolveEntityTableName(this.em, targetEntity);
          try {
            const gap = await profiler.measure(
              "resolve_coverage_gap",
              () => this.resolveCoverageGap(targetEntity, opts, coverageScope, sourceTable),
              (value) => value ? {
                entity: targetEntity,
                scope: value.scope,
                baseCount: value.stats?.baseCount ?? null,
                indexedCount: value.stats?.indexedCount ?? null
              } : { entity: targetEntity, scope: null }
            );
            if (!gap) continue;
            if (!opts.skipAutoReindex) {
              this.scheduleAutoReindex(targetEntity, opts, gap.stats, coverageScope?.organizationId ?? null);
            }
            partialIndexWarning = {
              entity: targetEntity,
              entityLabel: this.resolveEntityLabel(targetEntity),
              baseCount: gap.stats?.baseCount ?? null,
              indexedCount: gap.stats?.indexedCount ?? null,
              scope: gap.stats ? gap.scope : void 0
            };
            if (debugEnabled) {
              if (gap.stats) this.debug("query:partial-coverage:forced", { entity: targetEntity, baseCount: gap.stats.baseCount, indexedCount: gap.stats.indexedCount, scope: gap.scope });
              else this.debug("query:partial-coverage:forced", { entity: targetEntity });
            }
            break;
          } catch (err) {
            if (debugEnabled) this.debug("query:partial-coverage:check-failed", { entity: targetEntity, error: err instanceof Error ? err.message : err });
          }
        }
      }
      if (!partialIndexWarning && wantsCf && entityHasActiveCustomFields && this.isForcePartialIndexEnabled() && opts.tenantId) {
        try {
          await this.indexCoverageStats(entity, opts, coverageScope);
          const globalStats = await this.indexCoverageStats(entity, opts, coverageScope);
          if (globalStats) {
            const globalBase = globalStats.baseCount;
            const globalIndexed = globalStats.indexedCount;
            const globalGap = globalBase > 0 && globalIndexed < globalBase || globalIndexed > globalBase;
            if (globalGap) {
              logger.warn("Partial index coverage detected at global scope; forcing query index usage due to FORCE_QUERY_INDEX_ON_PARTIAL_INDEXES", { entity, baseCount: globalBase, indexedCount: globalIndexed, scope: "global" });
              if (debugEnabled) this.debug("query:partial-coverage:forced", { entity, baseCount: globalBase, indexedCount: globalIndexed, scope: "global" });
              partialIndexWarning = {
                entity,
                entityLabel: this.resolveEntityLabel(entity),
                baseCount: globalBase,
                indexedCount: globalIndexed,
                scope: "global"
              };
            }
          }
        } catch (err) {
          if (debugEnabled) this.debug("query:partial-coverage:global-check-failed", { entity, error: err instanceof Error ? err.message : err });
        }
      }
      const resolveBaseColumn = (field) => {
        if (columns.has(field)) return field;
        if (field === "organization_id" && columns.has("id")) return "id";
        return null;
      };
      const fallbackOrgId = opts.organizationId ?? (Array.isArray(opts.organizationIds) && opts.organizationIds.length === 1 ? opts.organizationIds[0] : null);
      const encSvc = this.getEncryptionService();
      const resolvedSorts = [];
      for (const sort of opts.sort || []) {
        const field = String(sort.field);
        if (field.startsWith("cf:")) {
          resolvedSorts.push({ ...sort, field });
        } else {
          const baseField = resolveBaseColumn(field);
          if (baseField) resolvedSorts.push({ ...sort, field: baseField });
        }
      }
      const encryptedSortFields = await resolveEncryptedSortFields(
        encSvc,
        entity,
        resolvedSorts.filter((sort) => !sort.field.startsWith("cf:")).map((sort) => sort.field),
        opts.tenantId ?? null,
        fallbackOrgId
      );
      const requiresPlaintextSort = encryptedSortFields.size > 0;
      const applyBaseScope = (q) => {
        let next = q;
        if (orgScope && hasOrganizationColumn) {
          next = this.applyOrganizationScope(next, qualify("organization_id"), orgScope);
        }
        if (hasTenantColumn) {
          next = next.where(qualify("tenant_id"), "=", opts.tenantId);
        }
        if (!opts.withDeleted && hasDeletedColumn) {
          next = next.where(qualify("deleted_at"), "is", null);
        }
        return next;
      };
      const applyEntityIndexesJoin = (q) => {
        return q.leftJoin("entity_indexes as ei", (jb) => {
          let jc = jb.on("ei.entity_type", "=", String(entity)).onRef("ei.entity_id", "=", sql`(${sql.ref(qualify("id"))}::text)`);
          if (hasOrganizationColumn) {
            jc = jc.onRef("ei.organization_id", "=", qualify("organization_id")).on("ei.organization_id", "is not", null);
          }
          if (hasTenantColumn) {
            jc = jc.onRef("ei.tenant_id", "=", qualify("tenant_id")).on("ei.tenant_id", "is not", null);
          }
          if (!opts.withDeleted) {
            jc = jc.on("ei.deleted_at", "is", null);
          }
          return jc;
        });
      };
      const applyCustomFieldSourceJoins = (q) => {
        let next = q;
        for (const source of preparedCfSources) {
          const join = (opts.customFieldSources ?? []).find((s) => s && (s.alias ?? void 0) === source.alias)?.join;
          if (!join) continue;
          const joinType = (join.type ?? "left") === "inner" ? "innerJoin" : "leftJoin";
          next = next[joinType](`${source.table} as ${source.alias}`, (jb) => jb.onRef(`${source.alias}.${join.toField}`, "=", qualify(join.fromField)));
          next = next.leftJoin(`entity_indexes as ${source.indexAlias}`, (jb) => {
            let jc = jb.on(`${source.indexAlias}.entity_type`, "=", String(source.entityId)).onRef(`${source.indexAlias}.entity_id`, "=", sql`(${sql.ref(`${source.alias}.${source.recordIdColumn}`)}::text)`);
            const orgRef = source.organizationField ? `${source.alias}.${source.organizationField}` : columns.has("organization_id") ? qualify("organization_id") : null;
            if (orgRef) {
              jc = jc.onRef(`${source.indexAlias}.organization_id`, "=", orgRef).on(`${source.indexAlias}.organization_id`, "is not", null);
            }
            const tenantRef = source.tenantField ? `${source.alias}.${source.tenantField}` : columns.has("tenant_id") ? qualify("tenant_id") : null;
            if (tenantRef) {
              jc = jc.onRef(`${source.indexAlias}.tenant_id`, "=", tenantRef).on(`${source.indexAlias}.tenant_id`, "is not", null);
            }
            if (!opts.withDeleted) jc = jc.on(`${source.indexAlias}.deleted_at`, "is", null);
            return jc;
          });
        }
        return next;
      };
      const applyCfFilters = (q) => {
        let next = q;
        for (const filter of cfFilters) {
          next = this.applyCfFilterAcrossSources(
            next,
            filter.field,
            filter.op,
            filter.value,
            indexSources,
            searchRuntime
          );
        }
        return next;
      };
      const regularBaseFilters = baseFilters.filter((filter) => !filter.orGroup);
      const orGroupFilters = baseFilters.filter((filter) => filter.orGroup);
      const applyRegularBaseFilters = (q) => {
        let next = q;
        for (const filter of regularBaseFilters) {
          const fieldName = String(filter.field);
          const baseField = resolveBaseColumn(fieldName);
          if (!baseField) {
            next = this.applyIndexDocFilterFromAlias(
              next,
              "ei",
              entity,
              fieldName,
              filter.op,
              filter.value,
              "b.id",
              searchRuntime
            );
            continue;
          }
          const column = qualify(baseField);
          next = this.applyColumnFilter(next, column, filter, {
            ...searchRuntime,
            entity,
            field: fieldName,
            recordIdColumn: "b.id"
          });
        }
        return next;
      };
      const applyOrGroupedBaseFilters = (q) => {
        if (orGroupFilters.length === 0) return q;
        const groups = /* @__PURE__ */ new Map();
        for (const filter of orGroupFilters) {
          if (!filter.orGroup) continue;
          const existing = groups.get(filter.orGroup) ?? [];
          existing.push(filter);
          groups.set(filter.orGroup, existing);
        }
        const groupList = Array.from(groups.values()).filter((g) => g.length > 0);
        if (groupList.length === 0) return q;
        return q.where((eb) => eb.or(
          groupList.map((groupFilters) => {
            const parts = groupFilters.map(
              (filter) => this.buildBaseFilterExpression(eb, filter, resolveBaseColumn, qualify, entity, searchRuntime)
            );
            return parts.length === 1 ? parts[0] : eb.and(parts);
          })
        ));
      };
      const applyAliasScopes = async (target, aliasName) => {
        let next = target;
        const tableName = aliasTables.get(aliasName);
        if (!tableName) return next;
        if (orgScope && await this.columnExists(tableName, "organization_id")) {
          next = this.applyOrganizationScope(next, `${aliasName}.organization_id`, orgScope);
        }
        if (opts.tenantId && await this.columnExists(tableName, "tenant_id")) {
          next = next.where(`${aliasName}.tenant_id`, "=", opts.tenantId);
        }
        if (!opts.withDeleted && await this.columnExists(tableName, "deleted_at")) {
          next = next.where(`${aliasName}.deleted_at`, "is", null);
        }
        return next;
      };
      const applyJoinFilterOpFn = (target, column, op, value) => {
        switch (op) {
          case "eq":
            return target.where(column, "=", value);
          case "ne":
            return target.where(column, "!=", value);
          case "gt":
            return target.where(column, ">", value);
          case "gte":
            return target.where(column, ">=", value);
          case "lt":
            return target.where(column, "<", value);
          case "lte":
            return target.where(column, "<=", value);
          case "in":
            return target.where(column, "in", this.toArray(value));
          case "nin":
            return target.where(column, "not in", this.toArray(value));
          case "like":
            return target.where(column, "like", value);
          case "ilike":
            return target.where(column, "ilike", value);
          case "exists":
            return value ? target.where(column, "is not", null) : target.where(column, "is", null);
          default:
            return target;
        }
      };
      const applyJoinSearchFilterOp = async (target, filter, _qualified, join) => {
        if (!searchEnabled || !join.entityId) return false;
        if (!["like", "ilike"].includes(filter.op)) return false;
        if (typeof filter.value !== "string" || filter.value.trim().length === 0) return false;
        let searchAvailable = joinSearchAvailability.get(join.entityId);
        if (searchAvailable === void 0) {
          searchAvailable = await this.hasSearchTokens(String(join.entityId), opts.tenantId ?? null, orgScope);
          joinSearchAvailability.set(join.entityId, searchAvailable);
        }
        if (!searchAvailable) return false;
        const tokens = tokenizeText(String(filter.value), searchConfig);
        if (!tokens.hashes.length) return false;
        return this.applySearchTokens(target, {
          entity: String(join.entityId),
          field: filter.column,
          hashes: tokens.hashes,
          recordIdColumn: `${join.alias}.id`,
          tenantId: opts.tenantId ?? null,
          organizationScope: orgScope,
          mintAlias: searchRuntime.mintAlias
        });
      };
      const applyQueryShape = async (q) => {
        let next = applyBaseScope(q);
        next = applyEntityIndexesJoin(next);
        next = applyCustomFieldSourceJoins(next);
        next = applyCfFilters(next);
        next = applyRegularBaseFilters(next);
        next = applyOrGroupedBaseFilters(next);
        next = await applyJoinFilters({
          db,
          baseTable,
          builder: next,
          joinMap,
          joinFilters,
          aliasTables,
          qualifyBase: (column) => qualify(column),
          applyAliasScope: async (target, alias) => applyAliasScopes(target, alias),
          applyFilterOp: (target, column, op, value) => applyJoinFilterOpFn(target, column, op, value),
          applyJoinFilterOp: async (target, filter, qualified, join) => {
            const applied = await applyJoinSearchFilterOp(target, filter, qualified, join);
            return { applied, builder: target };
          },
          columnExists: (tbl, column) => this.columnExists(tbl, column)
        });
        return next;
      };
      const hasCustomFieldFilters = cfFilters.length > 0;
      const canOptimizeCount = !hasCustomFieldFilters && !hasNonBaseSearchSource;
      const selectFieldSet = new Set(opts.fields && opts.fields.length ? opts.fields.map(String) : Array.from(columns.keys()));
      if (requiresPlaintextSort) {
        for (const field of encryptedSortFields) selectFieldSet.add(field);
      }
      if (opts.includeCustomFields === true) {
        const entityIds = Array.from(new Set(indexSources.map((src) => String(src.entityId))));
        try {
          const resolvedKeys = await this.resolveAvailableCustomFieldKeys(entityIds, opts.tenantId ?? null);
          resolvedKeys.forEach((key) => selectFieldSet.add(`cf:${key}`));
          if (this.isDebugVerbosity()) this.debug("query:cf:resolved-keys", { entity, keys: resolvedKeys });
        } catch (err) {
          logger.warn("Failed to resolve custom field keys", { entity, err });
        }
      } else if (Array.isArray(opts.includeCustomFields)) {
        opts.includeCustomFields.map((key) => String(key)).forEach((key) => selectFieldSet.add(`cf:${key}`));
      }
      const selectFields = Array.from(selectFieldSet);
      const applySelection = (q, fieldsOverride) => {
        let next = q;
        const fields = fieldsOverride ?? selectFields;
        for (const field of fields) {
          const fieldName = String(field);
          if (fieldName.startsWith("cf:")) {
            const alias = this.sanitize(fieldName);
            const jsonExpr = this.buildCfJsonExprSql(fieldName, indexSources);
            const exprRaw = jsonExpr ?? sql`NULL::jsonb`;
            next = next.select(exprRaw.as(alias));
          } else if (columns.has(fieldName)) {
            next = next.select(`${qualify(fieldName)} as ${fieldName}`);
          }
        }
        return next;
      };
      const applySort = (q) => {
        let next = q;
        if (requiresPlaintextSort) return next;
        for (const s of resolvedSorts) {
          const fieldName = String(s.field);
          if (fieldName.startsWith("cf:")) {
            const textExpr = this.buildCfTextExprSql(fieldName, indexSources);
            if (textExpr) {
              const direction = sql.raw(coerceSortDirection(s.dir));
              next = next.orderBy(sql`${textExpr} ${direction}`);
            }
          } else {
            const baseField = resolveBaseColumn(fieldName);
            if (!baseField) continue;
            next = next.orderBy(qualify(baseField), s.dir ?? SortDir.Asc);
          }
        }
        return next;
      };
      const page = opts.page?.page ?? 1;
      const pageSize = opts.page?.pageSize ?? 20;
      const sqlDebugEnabled = this.isSqlDebugEnabled();
      let total;
      if (canOptimizeCount) {
        const optimizedRoot = db.selectFrom(`${baseTable} as b`);
        let countCore = applyBaseScope(optimizedRoot);
        countCore = applyRegularBaseFilters(countCore);
        countCore = applyOrGroupedBaseFilters(countCore);
        countCore = await applyJoinFilters({
          db,
          baseTable,
          builder: countCore,
          joinMap,
          joinFilters,
          aliasTables,
          qualifyBase: (column) => qualify(column),
          applyAliasScope: async (target, alias) => applyAliasScopes(target, alias),
          applyFilterOp: (target, column, op, value) => applyJoinFilterOpFn(target, column, op, value),
          applyJoinFilterOp: async (target, filter, qualified, join) => {
            const applied = await applyJoinSearchFilterOp(target, filter, qualified, join);
            return { applied, builder: target };
          },
          columnExists: (tbl, column) => this.columnExists(tbl, column)
        });
        const sub = countCore.select(sql.ref(qualify("id")).as("id")).groupBy(qualify("id")).as("sq");
        const countQuery = db.selectFrom(sub).select(sql`count(*)`.as("count"));
        if (debugEnabled && sqlDebugEnabled) {
          const compiled = countQuery.compile();
          this.debug("query:sql:count", { entity, sql: compiled.sql, bindings: compiled.parameters });
        }
        const countRow = await this.captureSqlTiming(
          "query:sql:count",
          entity,
          () => countQuery.executeTakeFirst(),
          { optimized: true },
          profiler
        );
        total = this.parseCount(countRow);
      } else {
        const countRoot = db.selectFrom(`${baseTable} as b`);
        const countBuilder = (await applyQueryShape(countRoot)).select(sql`count(distinct ${sql.ref(qualify("id"))})`.as("count"));
        if (debugEnabled && sqlDebugEnabled) {
          const compiled = countBuilder.compile();
          this.debug("query:sql:count", { entity, sql: compiled.sql, bindings: compiled.parameters });
        }
        const countRow = await this.captureSqlTiming(
          "query:sql:count",
          entity,
          () => countBuilder.executeTakeFirst(),
          { optimized: false },
          profiler
        );
        total = this.parseCount(countRow);
      }
      const dekKeyCache = /* @__PURE__ */ new Map();
      const decryptRow = async (item) => {
        let next = item;
        if (encSvc?.decryptEntityPayload) {
          const decrypt = encSvc.decryptEntityPayload.bind(encSvc);
          try {
            const decrypted = await decrypt(
              entity,
              next,
              next?.tenant_id ?? next?.tenantId ?? opts.tenantId ?? null,
              next?.organization_id ?? next?.organizationId ?? fallbackOrgId ?? null
            );
            next = { ...next, ...decrypted };
          } catch (err) {
            logger.error("Error decrypting entity payload", { err });
          }
        }
        if (encSvc) {
          try {
            next = await decryptIndexDocCustomFields(
              next,
              {
                tenantId: next?.tenant_id ?? next?.tenantId ?? opts.tenantId ?? null,
                organizationId: next?.organization_id ?? next?.organizationId ?? null
              },
              encSvc,
              dekKeyCache
            );
          } catch {
          }
        }
        return next;
      };
      let items;
      let encryptedSortRowCapWarning;
      if (requiresPlaintextSort) {
        const cap = resolveEncryptedSortMaxRows();
        const phase1Root = db.selectFrom(`${baseTable} as b`);
        let phase1 = await applyQueryShape(phase1Root);
        const scopeFieldNames = [
          hasTenantColumn ? "tenant_id" : null,
          hasOrganizationColumn ? "organization_id" : null
        ].filter((field) => field !== null);
        const sortFieldNames = Array.from(/* @__PURE__ */ new Set(["id", ...scopeFieldNames, ...resolvedSorts.map((s) => String(s.field))]));
        phase1 = applySelection(phase1, sortFieldNames);
        if (cap !== null) {
          phase1 = phase1.limit(cap).orderBy(qualify("id"), "asc");
        }
        if (debugEnabled && sqlDebugEnabled) {
          const compiled = phase1.compile();
          this.debug("query:sql:data:phase1", { entity, sql: compiled.sql, bindings: compiled.parameters });
        }
        const candidateRows = await this.captureSqlTiming(
          "query:sql:data:phase1",
          entity,
          () => phase1.execute(),
          { phase: 1 },
          profiler
        );
        const decryptedCandidates = await mapWithConcurrency(candidateRows, DECRYPT_CONCURRENCY, decryptRow);
        const orderedCandidates = sortRowsInMemory(decryptedCandidates, resolvedSorts);
        const pageIds = orderedCandidates.slice((page - 1) * pageSize, page * pageSize).map((row) => row.id);
        if (cap !== null && total > cap) {
          encryptedSortRowCapWarning = {
            entity,
            sortFields: resolvedSorts.map((s) => String(s.field)),
            maxRows: cap,
            totalMatched: total
          };
        }
        if (pageIds.length === 0) {
          items = [];
        } else {
          const phase2Root = db.selectFrom(`${baseTable} as b`);
          let phase2 = await applyQueryShape(phase2Root);
          phase2 = applySelection(phase2);
          phase2 = phase2.where(qualify("id"), "in", pageIds);
          if (debugEnabled && sqlDebugEnabled) {
            const compiled = phase2.compile();
            this.debug("query:sql:data:phase2", { entity, sql: compiled.sql, bindings: compiled.parameters });
          }
          const pageRowsRaw = await this.captureSqlTiming(
            "query:sql:data:phase2",
            entity,
            () => phase2.execute(),
            { phase: 2 },
            profiler
          );
          const decryptedPageRows = await mapWithConcurrency(pageRowsRaw, DECRYPT_CONCURRENCY, decryptRow);
          const byId = new Map(decryptedPageRows.map((row) => [String(row.id), row]));
          items = pageIds.map((id) => byId.get(String(id))).filter((row) => row != null);
        }
      } else {
        const dataRoot = db.selectFrom(`${baseTable} as b`);
        let dataBuilder = await applyQueryShape(dataRoot);
        dataBuilder = applySelection(dataBuilder);
        dataBuilder = applySort(dataBuilder);
        dataBuilder = dataBuilder.limit(pageSize).offset((page - 1) * pageSize);
        if (debugEnabled && sqlDebugEnabled) {
          const compiled = dataBuilder.compile();
          this.debug("query:sql:data", { entity, sql: compiled.sql, bindings: compiled.parameters, page, pageSize });
        }
        const itemsRaw = await this.captureSqlTiming(
          "query:sql:data",
          entity,
          () => dataBuilder.execute(),
          { page, pageSize },
          profiler
        );
        items = await mapWithConcurrency(itemsRaw, DECRYPT_CONCURRENCY, decryptRow);
      }
      if (debugEnabled) this.debug("query:complete", { entity, total, items: items.length });
      const typedItems = items;
      let result = { items: typedItems, page, pageSize, total };
      if (partialIndexWarning || encryptedSortRowCapWarning) {
        const meta = {};
        if (partialIndexWarning) meta.partialIndexWarning = partialIndexWarning;
        if (encryptedSortRowCapWarning) meta.encryptedSortRowCapWarning = encryptedSortRowCapWarning;
        result.meta = meta;
      }
      result = await applyAfterExtensions(result);
      finishProfile({
        result: "ok",
        total,
        page,
        pageSize,
        itemCount: Array.isArray(items) ? items.length : void 0,
        partialIndexWarning: partialIndexWarning ? true : false
      });
      return result;
    } catch (err) {
      finishProfile({ result: "error", error: err instanceof Error ? err.message : String(err) });
      throw err;
    }
  }
  prepareCustomFieldSources(sources) {
    const prepared = [];
    sources.forEach((source, index) => {
      if (!source) return;
      const joinTable = source.table ?? resolveEntityTableName(this.em, source.entityId);
      const alias = source.alias ?? `cfs_${index}`;
      if (!source.join) {
        throw new Error(`QueryEngine: customFieldSources entry for ${String(source.entityId)} requires a join configuration`);
      }
      prepared.push({
        alias,
        indexAlias: `ei_${alias}`,
        entityId: source.entityId,
        recordIdColumn: source.recordIdColumn ?? "id",
        organizationField: source.organizationField,
        tenantField: source.tenantField,
        table: joinTable
      });
    });
    return prepared;
  }
  async isCustomEntity(entity) {
    const cached = this.customEntityCache.get(entity);
    if (cached !== void 0) return cached;
    let result = false;
    try {
      if (isOrmBackedSystemEntityId(this.em, entity)) {
        result = false;
      } else {
        const db = this.getDb();
        const row = await db.selectFrom("custom_entities").select("id").where("entity_id", "=", entity).where("is_active", "=", true).executeTakeFirst();
        if (row) {
          result = true;
        } else if (resolveRegisteredEntityTableName(this.em, entity) !== null) {
          result = false;
        } else {
          result = await this.hasCustomEntityStorageRows(entity);
        }
      }
    } catch {
      result = false;
    }
    this.customEntityCache.set(entity, result);
    return result;
  }
  async hasCustomEntityStorageRows(entity) {
    try {
      const db = this.getDb();
      const row = await db.selectFrom("custom_entities_storage").select(sql`1`.as("one")).where("entity_type", "=", entity).limit(1).executeTakeFirst();
      return !!row;
    } catch {
      return false;
    }
  }
  /**
   * Adds a WHERE EXISTS / OR WHERE EXISTS subquery that matches
   * `search_tokens` for the supplied (entity, field) against the
   * provided record id column.
   *
   * Returns true when the sub-query was applied (i.e. tokens were
   * non-empty). Caller is responsible for the calling context
   * (direct where vs. inside `eb.or([...])`).
   */
  applySearchTokens(q, opts) {
    if (!opts.hashes.length) {
      this.logSearchDebug("search:skip-no-hashes", {
        entity: opts.entity,
        field: opts.field,
        tenantId: opts.tenantId ?? null,
        organizationScope: opts.organizationScope
      });
      return false;
    }
    const alias = opts.mintAlias();
    this.logSearchDebug("search:apply-search-tokens", {
      entity: opts.entity,
      field: opts.field,
      alias,
      tokenCount: opts.hashes.length,
      tenantId: opts.tenantId ?? null,
      organizationScope: opts.organizationScope,
      combineWith: opts.combineWith ?? "and"
    });
    const engine = this;
    const buildSub = (eb) => {
      let sub = eb.selectFrom(`search_tokens as ${alias}`).select(sql`1`.as("one")).where(`${alias}.entity_type`, "=", opts.entity).where(`${alias}.field`, "=", opts.field).where(sql`${sql.ref(`${alias}.entity_id`)} = ${sql.ref(opts.recordIdColumn)}::text`).where(`${alias}.token_hash`, "in", opts.hashes).groupBy([`${alias}.entity_id`, `${alias}.field`]).having(sql`count(distinct ${sql.ref(`${alias}.token_hash`)}) >= ${opts.hashes.length}`);
      if (opts.tenantId !== void 0) {
        sub = sub.where(sql`${sql.ref(`${alias}.tenant_id`)} is not distinct from ${opts.tenantId ?? null}`);
      }
      if (opts.organizationScope) {
        sub = engine.applyOrganizationScope(sub, `${alias}.organization_id`, opts.organizationScope);
      }
      return sub;
    };
    if (opts.combineWith === "or") {
      ;
      q.__pendingOrExists = buildSub(q);
      return true;
    }
    ;
    q.__applied = true;
    const built = buildSub(q);
    if (typeof q.where === "function") {
      ;
      q = q.where((eb) => eb.exists(built));
    }
    return true;
  }
  /** SQL fragment for `cf:<key>` (or legacy bare key) as JSON across a single alias. */
  jsonbSqlAlias(alias, key) {
    if (key.startsWith("cf:")) {
      const bare = key.slice(3);
      return sql`coalesce(${sql.ref(alias + ".doc")} -> ${key}, ${sql.ref(alias + ".doc")} -> ${bare})`;
    }
    return sql`${sql.ref(alias + ".doc")} -> ${key}`;
  }
  /** SQL fragment for `cf:<key>` (or legacy bare key) as text across a single alias. */
  cfTextExprAlias(alias, key) {
    if (key.startsWith("cf:")) {
      const bare = key.slice(3);
      return sql`coalesce((${sql.ref(alias + ".doc")} ->> ${key}), (${sql.ref(alias + ".doc")} ->> ${bare}))`;
    }
    return sql`(${sql.ref(alias + ".doc")} ->> ${key})`;
  }
  /** Build JSON/text SQL expressions across multiple index alias sources (coalesce over them). */
  buildCfJsonExprSql(key, sources) {
    if (!sources.length) return null;
    const parts = sources.map((src) => this.jsonbSqlAlias(src.alias, key));
    if (parts.length === 1) return parts[0];
    return sql`coalesce(${sql.join(parts, sql`, `)})`;
  }
  buildCfTextExprSql(key, sources) {
    if (!sources.length) return null;
    const parts = sources.map((src) => this.cfTextExprAlias(src.alias, key));
    if (parts.length === 1) return parts[0];
    return sql`coalesce(${sql.join(parts, sql`, `)})`;
  }
  applyCfFilterAcrossSources(builder, key, op, value, sources, search) {
    if (!sources.length) return builder;
    if ((op === "like" || op === "ilike") && search?.enabled && typeof value === "string") {
      const tokens = tokenizeText(String(value), search.config);
      const hashes = tokens.hashes;
      if (hashes.length) {
        const applied = this.applyMultiSourceSearchExists(builder, sources, key, hashes, search);
        this.logSearchDebug("search:cf-filter-across", {
          entity: sources.map((src) => src.entityId),
          field: key,
          tokens: tokens.tokens,
          hashes,
          applied,
          tenantId: search.tenantId ?? null,
          organizationScope: search.organizationScope
        });
        if (applied.builder !== builder) return applied.builder;
      } else {
        this.logSearchDebug("search:cf-skip-empty-hashes", {
          entity: sources.map((src) => src.entityId),
          field: key,
          value
        });
      }
      return builder;
    }
    const textExpr = this.buildCfTextExprSql(key, sources);
    const jsonExpr = this.buildCfJsonExprSql(key, sources);
    if (!textExpr || !jsonExpr) return builder;
    const arrContains = (val) => sql`${jsonExpr} @> ${JSON.stringify([val])}::jsonb`;
    switch (op) {
      case "eq":
        return builder.where((eb) => eb.or([
          sql`${textExpr} = ${value}`,
          arrContains(value)
        ]));
      case "ne":
        return builder.where(sql`${textExpr} <> ${value}`);
      case "in": {
        const values = this.toArray(value);
        return builder.where((eb) => eb.or(
          values.flatMap((val) => [
            sql`${textExpr} = ${val}`,
            arrContains(val)
          ])
        ));
      }
      case "nin": {
        const values = this.toArray(value);
        return builder.where(sql`${textExpr} not in (${sql.join(values.map((v) => sql`${v}`), sql`, `)})`);
      }
      case "like":
        return builder.where(sql`${textExpr} like ${value}`);
      case "ilike":
        return builder.where(sql`${textExpr} ilike ${value}`);
      case "exists":
        return value ? builder.where(sql`${textExpr} is not null`) : builder.where(sql`${textExpr} is null`);
      case "gt":
      case "gte":
      case "lt":
      case "lte": {
        const operator = sql.raw(op === "gt" ? ">" : op === "gte" ? ">=" : op === "lt" ? "<" : "<=");
        return builder.where(sql`${textExpr} ${operator} ${value}`);
      }
      default:
        return builder;
    }
  }
  /** Apply a search-token EXISTS subquery across multiple sources (OR-joined). */
  applyMultiSourceSearchExists(builder, sources, key, hashes, search) {
    if (!sources.length || !hashes.length) return { builder, applied: false };
    const next = builder.where((eb) => eb.or(
      sources.map(
        (source) => eb.exists(this.buildSearchTokensSub(eb, {
          entity: String(source.entityId),
          field: key,
          hashes,
          recordIdColumn: `${source.alias}.entity_id`,
          tenantId: search.tenantId ?? null,
          organizationScope: search.organizationScope ?? null,
          mintAlias: search.mintAlias
        }))
      )
    ));
    return { builder: next, applied: true };
  }
  /** Construct a search-token EXISTS subquery using the given ExpressionBuilder. */
  buildSearchTokensSub(eb, opts) {
    const alias = opts.mintAlias();
    let sub = eb.selectFrom(`search_tokens as ${alias}`).select(sql`1`.as("one")).where(`${alias}.entity_type`, "=", opts.entity).where(`${alias}.field`, "=", opts.field).where(sql`${sql.ref(`${alias}.entity_id`)} = ${sql.ref(opts.recordIdColumn)}::text`).where(`${alias}.token_hash`, "in", opts.hashes).groupBy([`${alias}.entity_id`, `${alias}.field`]).having(sql`count(distinct ${sql.ref(`${alias}.token_hash`)}) >= ${opts.hashes.length}`);
    if (opts.tenantId !== void 0) {
      sub = sub.where(sql`${sql.ref(`${alias}.tenant_id`)} is not distinct from ${opts.tenantId ?? null}`);
    }
    if (opts.organizationScope) {
      sub = this.applyOrganizationScope(sub, `${alias}.organization_id`, opts.organizationScope);
    }
    return sub;
  }
  applyCfFilterFromAlias(q, alias, entityType, key, op, value, search) {
    const textExpr = this.cfTextExprAlias(alias, key);
    const arrExpr = sql`(${sql.ref(alias + ".doc")} -> ${key})`;
    const arrContains = (val) => sql`${arrExpr} @> ${JSON.stringify([val])}::jsonb`;
    if ((op === "like" || op === "ilike") && search?.enabled && typeof value === "string") {
      const tokens = tokenizeText(String(value), search.config);
      const hashes = tokens.hashes;
      if (hashes.length) {
        const applied = q.where((eb) => eb.exists(this.buildSearchTokensSub(eb, {
          entity: entityType,
          field: key,
          hashes,
          recordIdColumn: `${alias}.entity_id`,
          tenantId: search.tenantId ?? null,
          organizationScope: search.organizationScope ?? null,
          mintAlias: search.mintAlias
        })));
        this.logSearchDebug("search:cf-filter", {
          entity: entityType,
          field: key,
          tokens: tokens.tokens,
          hashes,
          applied: true,
          tenantId: search.tenantId ?? null,
          organizationScope: search.organizationScope
        });
        return applied;
      } else {
        this.logSearchDebug("search:cf-skip-empty-hashes", { entity: entityType, field: key, value });
      }
      return q;
    }
    switch (op) {
      case "eq":
        return q.where((eb) => eb.or([
          sql`${textExpr} = ${value}`,
          arrContains(value)
        ]));
      case "ne":
        return q.where(sql`${textExpr} <> ${value}`);
      case "in": {
        const vals = this.toArray(value);
        return q.where((eb) => eb.or(
          vals.flatMap((val) => [
            sql`${textExpr} = ${val}`,
            arrContains(val)
          ])
        ));
      }
      case "nin": {
        const vals = this.toArray(value);
        return q.where(sql`${textExpr} not in (${sql.join(vals.map((v) => sql`${v}`), sql`, `)})`);
      }
      case "like":
        return q.where(sql`${textExpr} like ${value}`);
      case "ilike":
        return q.where(sql`${textExpr} ilike ${value}`);
      case "exists":
        return value ? q.where(sql`${textExpr} is not null`) : q.where(sql`${textExpr} is null`);
      case "gt":
      case "gte":
      case "lt":
      case "lte": {
        const operator = sql.raw(op === "gt" ? ">" : op === "gte" ? ">=" : op === "lt" ? "<" : "<=");
        return q.where(sql`${textExpr} ${operator} ${value}`);
      }
      default:
        return q;
    }
  }
  applyIndexDocFilterFromAlias(q, alias, entityType, key, op, value, recordIdColumn, search) {
    const textExpr = sql`(${sql.ref(alias + ".doc")} ->> ${key})`;
    if ((op === "like" || op === "ilike") && search?.enabled && typeof value === "string") {
      const tokens = tokenizeText(String(value), search.config);
      const hashes = tokens.hashes;
      if (hashes.length) {
        const applied = q.where((eb) => eb.exists(this.buildSearchTokensSub(eb, {
          entity: entityType,
          field: key,
          hashes,
          recordIdColumn,
          tenantId: search.tenantId ?? null,
          organizationScope: search.organizationScope ?? null,
          mintAlias: search.mintAlias
        })));
        this.logSearchDebug("search:index-doc-filter", {
          entity: entityType,
          field: key,
          tokens: tokens.tokens,
          hashes,
          applied: true,
          tenantId: search.tenantId ?? null,
          organizationScope: search.organizationScope
        });
        return applied;
      } else {
        this.logSearchDebug("search:index-doc-skip-empty-hashes", { entity: entityType, field: key, value });
      }
      return q;
    }
    switch (op) {
      case "eq":
        return q.where(sql`${textExpr} = ${value}`);
      case "ne":
        return q.where(sql`${textExpr} <> ${value}`);
      case "in": {
        const vals = this.toArray(value);
        return q.where(sql`${textExpr} in (${sql.join(vals.map((v) => sql`${v}`), sql`, `)})`);
      }
      case "nin": {
        const vals = this.toArray(value);
        return q.where(sql`${textExpr} not in (${sql.join(vals.map((v) => sql`${v}`), sql`, `)})`);
      }
      case "like":
        return q.where(sql`${textExpr} like ${value}`);
      case "ilike":
        return q.where(sql`${textExpr} ilike ${value}`);
      case "exists":
        return value ? q.where(sql`${textExpr} is not null`) : q.where(sql`${textExpr} is null`);
      case "gt":
      case "gte":
      case "lt":
      case "lte": {
        const operator = sql.raw(op === "gt" ? ">" : op === "gte" ? ">=" : op === "lt" ? "<" : "<=");
        return q.where(sql`${textExpr} ${operator} ${value}`);
      }
      default:
        return q;
    }
  }
  /**
   * Build a single OR-group base filter expression as a Kysely predicate
   * (no side effects on the outer builder).
   */
  buildBaseFilterExpression(eb, filter, resolveBaseColumn, qualify, entity, searchRuntime) {
    const fieldName = String(filter.field);
    const baseField = resolveBaseColumn(fieldName);
    if (!baseField) {
      return this.buildIndexDocFilterExpression(eb, "ei", entity, fieldName, filter.op, filter.value, "b.id", searchRuntime);
    }
    if ((filter.op === "like" || filter.op === "ilike") && searchRuntime?.enabled && typeof filter.value === "string") {
      const tokens = tokenizeText(String(filter.value), searchRuntime.config);
      if (tokens.hashes.length) {
        const sources = (searchRuntime.searchSources && searchRuntime.searchSources.length ? searchRuntime.searchSources : [{ entity: String(entity), recordIdColumn: "b.id" }]).filter((src) => src.recordIdColumn && src.entity);
        if (sources.length) {
          return eb.or(
            sources.map(
              (src) => eb.exists(this.buildSearchTokensSub(eb, {
                entity: src.entity,
                field: fieldName,
                hashes: tokens.hashes,
                recordIdColumn: src.recordIdColumn,
                tenantId: searchRuntime.tenantId ?? null,
                organizationScope: searchRuntime.organizationScope ?? null,
                mintAlias: searchRuntime.mintAlias
              }))
            )
          );
        }
      }
      return sql`true`;
    }
    return this.buildColumnFilterExpression(eb, qualify(baseField), filter.op, filter.value);
  }
  buildColumnFilterExpression(eb, column, op, value) {
    switch (op) {
      case "eq":
        return eb(column, "=", value);
      case "ne":
        return eb(column, "!=", value);
      case "gt":
        return eb(column, ">", value);
      case "gte":
        return eb(column, ">=", value);
      case "lt":
        return eb(column, "<", value);
      case "lte":
        return eb(column, "<=", value);
      case "in":
        return eb(column, "in", this.toArray(value));
      case "nin":
        return eb(column, "not in", this.toArray(value));
      case "like":
        return eb(column, "like", value);
      case "ilike":
        return eb(column, "ilike", value);
      case "exists":
        return eb(column, value ? "is not" : "is", null);
      default:
        return sql`true`;
    }
  }
  buildIndexDocFilterExpression(eb, alias, _entity, key, op, value, _recordIdColumn, _search) {
    const textExpr = sql`(${sql.ref(alias + ".doc")} ->> ${key})`;
    switch (op) {
      case "eq":
        return sql`${textExpr} = ${value}`;
      case "ne":
        return sql`${textExpr} <> ${value}`;
      case "gt":
      case "gte":
      case "lt":
      case "lte": {
        const operator = sql.raw(op === "gt" ? ">" : op === "gte" ? ">=" : op === "lt" ? "<" : "<=");
        return sql`${textExpr} ${operator} ${value}`;
      }
      case "like":
        return sql`${textExpr} like ${value}`;
      case "ilike":
        return sql`${textExpr} ilike ${value}`;
      case "in": {
        const vals = this.toArray(value);
        return sql`${textExpr} in (${sql.join(vals.map((v) => sql`${v}`), sql`, `)})`;
      }
      case "nin": {
        const vals = this.toArray(value);
        return sql`${textExpr} not in (${sql.join(vals.map((v) => sql`${v}`), sql`, `)})`;
      }
      case "exists":
        return value ? sql`${textExpr} is not null` : sql`${textExpr} is null`;
      default:
        return sql`true`;
    }
  }
  async queryCustomEntity(entity, opts = {}) {
    const db = this.getDb();
    const alias = "ce";
    const orgScope = this.resolveOrganizationScope(opts);
    if (!opts.tenantId) throw new Error("QueryEngine: tenantId is required");
    const searchConfig = resolveSearchConfig();
    const searchEnabled = searchConfig.enabled && await this.tableExists("search_tokens");
    const hasSearchTokens = searchEnabled ? await this.hasSearchTokens(entity, opts.tenantId ?? null, orgScope) : false;
    const searchRuntime = {
      enabled: searchEnabled && hasSearchTokens,
      config: searchConfig,
      organizationScope: orgScope,
      tenantId: opts.tenantId ?? null,
      mintAlias: createSearchAliasMinter()
    };
    const normalizedFilters = normalizeFilters(opts.filters);
    const applyScope = (q) => {
      let next = q.where(`${alias}.entity_type`, "=", entity).where(`${alias}.tenant_id`, "=", opts.tenantId);
      if (orgScope) {
        next = this.applyOrganizationScope(next, `${alias}.organization_id`, orgScope);
      }
      if (!opts.withDeleted) next = next.where(`${alias}.deleted_at`, "is", null);
      for (const filter of normalizedFilters) {
        if (filter.field.startsWith("cf:")) {
          next = this.applyCfFilterFromAlias(next, alias, entity, filter.field, filter.op, filter.value, searchRuntime);
          continue;
        }
        const column = this.resolveCustomEntityColumn(alias, String(filter.field));
        if (column) {
          next = this.applyColumnFilter(next, column, filter, {
            ...searchRuntime,
            entity,
            field: String(filter.field),
            recordIdColumn: `${alias}.entity_id`
          });
          continue;
        }
        const docExpr = sql`(${sql.ref(alias + ".doc")} ->> ${String(filter.field)})`;
        next = this.applyColumnFilter(next, docExpr, filter, {
          ...searchRuntime,
          entity,
          field: String(filter.field),
          recordIdColumn: `${alias}.entity_id`
        });
      }
      return next;
    };
    const cfKeys = /* @__PURE__ */ new Set();
    for (const f of opts.fields || []) {
      if (typeof f === "string" && f.startsWith("cf:")) cfKeys.add(f.slice(3));
      else if (typeof f === "string" && f.startsWith("l10n:")) cfKeys.add(f);
    }
    for (const filter of normalizedFilters) {
      if (typeof filter.field === "string" && filter.field.startsWith("cf:")) cfKeys.add(filter.field.slice(3));
      else if (typeof filter.field === "string" && filter.field.startsWith("l10n:")) cfKeys.add(filter.field);
    }
    if (opts.includeCustomFields === true) {
      try {
        const rows = await db.selectFrom("custom_field_defs").select("key").where("entity_id", "=", entity).where("is_active", "=", true).where("tenant_id", "=", opts.tenantId).execute();
        for (const row of rows) {
          const key = row.key;
          if (typeof key === "string") cfKeys.add(key);
          else if (key != null) cfKeys.add(String(key));
        }
      } catch {
      }
    } else if (Array.isArray(opts.includeCustomFields)) {
      for (const k of opts.includeCustomFields) cfKeys.add(k);
    }
    const applySelection = (q) => {
      let next = q;
      const requested = opts.fields && opts.fields.length ? opts.fields : ["id"];
      for (const field of requested) {
        const f = String(field);
        if (f.startsWith("cf:")) {
          const aliasName = this.sanitize(f);
          next = next.select(this.jsonbSqlAlias(alias, f).as(aliasName));
        } else if (f === "id") {
          next = next.select(`${alias}.entity_id as id`);
        } else if (f === "created_at" || f === "updated_at" || f === "deleted_at") {
          next = next.select(`${alias}.${f} as ${f}`);
        } else {
          const expr = sql`(${sql.ref(alias + ".doc")} ->> ${f})`;
          next = next.select(expr.as(f));
        }
      }
      for (const key of cfKeys) {
        const aliasName = this.sanitize(`cf:${key}`);
        next = next.select(this.jsonbSqlAlias(alias, `cf:${key}`).as(aliasName));
      }
      return next;
    };
    const applySort = (q) => {
      let next = q;
      for (const s of opts.sort || []) {
        if (s.field.startsWith("cf:")) {
          const key = s.field.slice(3);
          const aliasName = this.sanitize(`cf:${key}`);
          next = next.orderBy(aliasName, s.dir ?? SortDir.Asc);
        } else if (s.field === "id") {
          next = next.orderBy(`${alias}.entity_id`, s.dir ?? SortDir.Asc);
        } else if (s.field === "created_at" || s.field === "updated_at" || s.field === "deleted_at") {
          next = next.orderBy(`${alias}.${s.field}`, s.dir ?? SortDir.Asc);
        } else {
          const direction = sql.raw(coerceSortDirection(s.dir));
          next = next.orderBy(sql`(${sql.ref(alias + ".doc")} ->> ${s.field}) ${direction}`);
        }
      }
      return next;
    };
    const page = opts.page?.page ?? 1;
    const pageSize = opts.page?.pageSize ?? 20;
    const root = db.selectFrom(`custom_entities_storage as ${alias}`);
    const countQuery = applyScope(root).select(sql`count(distinct ${sql.ref(`${alias}.entity_id`)})`.as("count"));
    const countRow = await countQuery.executeTakeFirst();
    const total = this.parseCount(countRow);
    let dataQuery = applyScope(db.selectFrom(`custom_entities_storage as ${alias}`));
    dataQuery = applySelection(dataQuery);
    dataQuery = applySort(dataQuery);
    dataQuery = dataQuery.limit(pageSize).offset((page - 1) * pageSize);
    const items = await dataQuery.execute();
    return { items, page, pageSize, total };
  }
  async tableExists(table) {
    const db = this.getDb();
    const exists = await db.selectFrom("information_schema.tables").select(sql`1`.as("one")).where("table_name", "=", table).executeTakeFirst();
    return !!exists;
  }
  async hasSearchTokens(entity, tenantId, orgScope) {
    try {
      const db = this.getDb();
      let query = db.selectFrom("search_tokens").select(sql`1`.as("one")).where("entity_type", "=", entity);
      if (tenantId !== void 0) {
        query = query.where(sql`tenant_id is not distinct from ${tenantId}`);
      }
      if (orgScope) {
        query = this.applyOrganizationScope(query, "search_tokens.organization_id", orgScope);
      }
      const row = await query.limit(1).executeTakeFirst();
      return !!row;
    } catch (err) {
      this.logSearchDebug("search:has-tokens-error", {
        entity,
        tenantId,
        organizationScope: orgScope,
        error: err instanceof Error ? err.message : String(err)
      });
      return false;
    }
  }
  async searchSourcesHaveTokens(sources, tenantId, orgScope) {
    for (const source of sources) {
      const ok = await this.hasSearchTokens(source.entity, tenantId, orgScope);
      this.logSearchDebug("search:source-has-tokens", {
        entity: source.entity,
        recordIdColumn: source.recordIdColumn,
        tenantId,
        organizationScope: orgScope,
        hasTokens: ok
      });
      if (ok) return true;
    }
    return false;
  }
  async resolveAvailableCustomFieldKeys(entityIds, tenantId) {
    if (!entityIds.length) return [];
    const cacheKey = this.customFieldKeysCacheKey(entityIds, tenantId);
    const now = Date.now();
    const cached = this.customFieldKeysCache.get(cacheKey);
    if (cached && cached.expiresAt > now) return cached.value.slice();
    const db = this.getDb();
    const rows = await db.selectFrom("custom_field_defs").select("key").where("entity_id", "in", entityIds).where("is_active", "=", true).where((eb) => eb.or([
      eb("tenant_id", "=", tenantId),
      eb("tenant_id", "is", null)
    ])).execute();
    const keys = /* @__PURE__ */ new Set();
    for (const row of rows) {
      const key = row.key;
      if (typeof key === "string" && key.trim().length) keys.add(key.trim());
      else if (key != null) keys.add(String(key));
    }
    const result = Array.from(keys);
    if (this.customFieldKeysTtlMs > 0) {
      this.customFieldKeysCache.set(cacheKey, { expiresAt: now + this.customFieldKeysTtlMs, value: result });
    }
    return result.slice();
  }
  async entityHasActiveCustomFields(entityId, tenantId) {
    try {
      const keys = await this.resolveAvailableCustomFieldKeys([entityId], tenantId);
      return keys.length > 0;
    } catch (err) {
      if (this.isDebugVerbosity()) {
        this.debug("query:cf:check-error", {
          entity: entityId,
          tenantId: tenantId ?? null,
          error: err instanceof Error ? err.message : err
        });
      }
      return true;
    }
  }
  customFieldKeysCacheKey(entityIds, tenantId) {
    const sorted = entityIds.slice().sort((a, b) => a.localeCompare(b)).join(",");
    return `${tenantId ?? "__none__"}|${sorted}`;
  }
  resolveVectorService() {
    if (!this.vectorServiceResolver) return null;
    try {
      return this.vectorServiceResolver() ?? null;
    } catch {
      return null;
    }
  }
  resolveEntityLabel(entity) {
    return entity;
  }
  async indexAnyRows(entity) {
    const db = this.getDb();
    const coverage = await db.selectFrom("entity_index_coverage").select(sql`1`.as("one")).where("entity_type", "=", entity).where("indexed_count", ">", 0).executeTakeFirst();
    if (coverage) return true;
    const exists = await db.selectFrom("entity_indexes").select("entity_id").where("entity_type", "=", entity).executeTakeFirst();
    return !!exists;
  }
  async getStoredCoverageSnapshot(entity, tenantId, organizationId, withDeleted) {
    try {
      const db = this.getDb();
      const scope = {
        entityType: entity,
        tenantId,
        organizationId,
        withDeleted
      };
      const row = await readCoverageSnapshot(db, scope);
      if (row && this.isCoverageSnapshotFresh(row)) {
        return { baseCount: row.baseCount, indexedCount: row.indexedCount };
      }
      if (this.isCoverageOptimizationEnabled()) {
        this.scheduleCoverageRefresh(entity, tenantId, organizationId, withDeleted);
        if (!row) return null;
        return { baseCount: row.baseCount, indexedCount: row.indexedCount };
      }
      await refreshCoverageSnapshot(this.em, scope);
      const refreshed = await readCoverageSnapshot(db, scope);
      if (!refreshed) return null;
      return { baseCount: refreshed.baseCount, indexedCount: refreshed.indexedCount };
    } catch (err) {
      if (this.isDebugVerbosity()) {
        this.debug("coverage:snapshot:read-error", {
          entity,
          tenantId,
          organizationId,
          withDeleted,
          error: err instanceof Error ? err.message : err
        });
      }
      return null;
    }
  }
  isCoverageSnapshotFresh(row) {
    if (this.coverageStatsTtlMs <= 0) return false;
    if (!row) return false;
    const refreshedAt = row.refreshed_at instanceof Date ? row.refreshed_at : row.refreshed_at ? new Date(row.refreshed_at) : null;
    const refreshedAtMs = refreshedAt?.getTime();
    if (!refreshedAtMs || !Number.isFinite(refreshedAtMs)) return false;
    return Date.now() - refreshedAtMs <= this.coverageStatsTtlMs;
  }
  scheduleAutoReindex(entity, opts, stats, organizationIdOverride) {
    if (!this.isAutoReindexEnabled()) return;
    const bus = this.resolveEventBus();
    if (!bus) return;
    const payload = {
      entityType: entity,
      tenantId: opts.tenantId ?? null,
      organizationId: organizationIdOverride ?? opts.organizationId ?? null,
      force: false
    };
    const context = stats ? { entity, tenantId: payload.tenantId, organizationId: payload.organizationId, baseCount: stats.baseCount, indexedCount: stats.indexedCount } : { entity, tenantId: payload.tenantId, organizationId: payload.organizationId };
    void Promise.resolve().then(async () => {
      try {
        await bus.emitEvent("query_index.reindex", payload, { persistent: true });
        if (this.isDebugVerbosity()) this.debug("query:auto-reindex:scheduled", context);
      } catch (err) {
        logger.warn("Failed to schedule auto reindex", { ...context, err });
      }
    });
  }
  scheduleCoverageRefresh(entity, tenantId, organizationId, withDeleted) {
    const bus = this.resolveEventBus();
    if (!bus) return;
    const key = [entity, tenantId ?? "__tenant__", organizationId ?? "__org__", withDeleted ? "1" : "0"].join("|");
    if (this.pendingCoverageRefreshKeys.has(key)) return;
    this.pendingCoverageRefreshKeys.add(key);
    void Promise.resolve().then(async () => {
      try {
        await bus.emitEvent("query_index.coverage.refresh", {
          entityType: entity,
          tenantId: tenantId ?? null,
          organizationId: organizationId ?? null,
          withDeleted,
          delayMs: 0
        });
        if (this.isDebugVerbosity()) {
          this.debug("coverage:refresh:scheduled", {
            entity,
            tenantId: tenantId ?? null,
            organizationId: organizationId ?? null,
            withDeleted
          });
        }
      } catch (err) {
        if (this.isDebugVerbosity()) {
          this.debug("coverage:refresh:failed", {
            entity,
            tenantId: tenantId ?? null,
            organizationId: organizationId ?? null,
            withDeleted,
            error: err instanceof Error ? err.message : err
          });
        }
      }
    }).finally(() => {
      this.pendingCoverageRefreshKeys.delete(key);
    });
  }
  resolveEventBus() {
    if (!this.eventBusResolver) return null;
    try {
      const bus = this.eventBusResolver();
      return bus ?? null;
    } catch {
      return null;
    }
  }
  isAutoReindexEnabled() {
    if (this.autoReindexEnabled != null) return this.autoReindexEnabled;
    const raw = (process.env.SCHEDULE_AUTO_REINDEX ?? process.env.QUERY_INDEX_AUTO_REINDEX ?? "").trim().toLowerCase();
    if (!raw) {
      this.autoReindexEnabled = true;
      return true;
    }
    const parsed = parseBooleanToken(raw);
    this.autoReindexEnabled = parsed === null ? true : parsed;
    return this.autoReindexEnabled;
  }
  isCoverageOptimizationEnabled() {
    if (this.coverageOptimizationEnabled != null) return this.coverageOptimizationEnabled;
    const raw = (process.env.OPTIMIZE_INDEX_COVERAGE_STATS ?? "").trim().toLowerCase();
    if (!raw) {
      this.coverageOptimizationEnabled = false;
      return false;
    }
    this.coverageOptimizationEnabled = parseBooleanToken(raw) === true;
    return this.coverageOptimizationEnabled;
  }
  async columnExists(table, column) {
    const key = `${table}.${column}`;
    if (this.columnCache.has(key)) {
      const cached = this.columnCache.get(key);
      if (cached === true) return true;
      this.columnCache.delete(key);
    }
    const db = this.getDb();
    const exists = await db.selectFrom("information_schema.columns").select(sql`1`.as("one")).where("table_name", "=", table).where("column_name", "=", column).executeTakeFirst();
    const present = !!exists;
    if (present) this.columnCache.set(key, true);
    else this.columnCache.delete(key);
    return present;
  }
  async getBaseColumnsForEntity(entity) {
    const db = this.getDb();
    const table = resolveEntityTableName(this.em, entity);
    const rows = await db.selectFrom("information_schema.columns").select(["column_name", "data_type"]).where("table_name", "=", table).execute();
    const map = /* @__PURE__ */ new Map();
    for (const r of rows) map.set(r.column_name, r.data_type);
    return map;
  }
  resolveOrganizationScope(opts) {
    if (opts.organizationIds !== void 0) {
      const raw = (opts.organizationIds ?? []).map((id) => typeof id === "string" ? id.trim() : id);
      const includeNull = raw.some((id) => id == null || id === "");
      const ids = raw.filter((id) => typeof id === "string" && id.length > 0);
      const unique = Array.from(new Set(ids));
      return { ids: unique, includeNull };
    }
    if (typeof opts.organizationId === "string" && opts.organizationId.trim().length > 0) {
      return { ids: [opts.organizationId], includeNull: false };
    }
    return null;
  }
  resolveCoverageSnapshotScope(opts) {
    const tenantId = opts.tenantId ?? null;
    const orgScope = this.resolveOrganizationScope(opts);
    if (!orgScope) return { tenantId, organizationId: null };
    if (orgScope.includeNull) {
      if (orgScope.ids.length === 0) return { tenantId, organizationId: null };
      return null;
    }
    if (orgScope.ids.length === 1) return { tenantId, organizationId: orgScope.ids[0] };
    if (orgScope.ids.length === 0) return { tenantId, organizationId: null };
    return null;
  }
  applyOrganizationScope(q, column, scope) {
    if (scope.ids.length === 0 && !scope.includeNull) {
      return q.where(sql`1 = 0`);
    }
    return q.where((eb) => {
      const parts = [];
      if (scope.ids.length > 0) parts.push(eb(column, "in", scope.ids));
      if (scope.includeNull) parts.push(eb(column, "is", null));
      if (parts.length === 1) return parts[0];
      return eb.or(parts);
    });
  }
  normalizeFilters(filters) {
    if (!filters) return [];
    const normalizeField = (k) => k.startsWith("cf_") ? `cf:${k.slice(3)}` : k;
    if (Array.isArray(filters)) {
      return filters.map((filter) => ({
        field: normalizeField(String(filter.field)),
        op: filter.op,
        value: filter.value
      }));
    }
    const out = [];
    const obj = filters;
    const add = (field, op, value) => out.push({ field, op, value });
    for (const [rawKey, rawVal] of Object.entries(obj)) {
      const field = normalizeField(rawKey);
      if (rawVal !== null && typeof rawVal === "object" && !Array.isArray(rawVal)) {
        for (const [opKey, opVal] of Object.entries(rawVal)) {
          switch (opKey) {
            case "$eq":
              add(field, "eq", opVal);
              break;
            case "$ne":
              add(field, "ne", opVal);
              break;
            case "$gt":
              add(field, "gt", opVal);
              break;
            case "$gte":
              add(field, "gte", opVal);
              break;
            case "$lt":
              add(field, "lt", opVal);
              break;
            case "$lte":
              add(field, "lte", opVal);
              break;
            case "$in":
              add(field, "in", opVal);
              break;
            case "$nin":
              add(field, "nin", opVal);
              break;
            case "$like":
              add(field, "like", opVal);
              break;
            case "$ilike":
              add(field, "ilike", opVal);
              break;
            case "$exists":
              add(field, "exists", opVal);
              break;
          }
        }
      } else {
        add(field, "eq", rawVal);
      }
    }
    return out;
  }
  sanitize(s) {
    return s.replace(/[^a-zA-Z0-9_]/g, "_");
  }
  toArray(value) {
    if (Array.isArray(value)) return value;
    if (value === void 0) return [];
    return [value];
  }
  parseCount(row) {
    if (row && typeof row === "object" && "count" in row) {
      const value = row.count;
      if (typeof value === "number") return value;
      if (typeof value === "string") {
        const parsed = Number(value);
        return Number.isNaN(parsed) ? 0 : parsed;
      }
      if (typeof value === "bigint") return Number(value);
    }
    return 0;
  }
  logSearchDebug(event, payload) {
    if (!this.isDebugVerbosity()) return;
    logger.debug("Search debug event", { event, payload });
  }
  applyColumnFilter(q, column, filter, search) {
    if ((filter.op === "like" || filter.op === "ilike") && search?.enabled && typeof filter.value === "string") {
      const tokens = tokenizeText(String(filter.value), search.config);
      const hashes = tokens.hashes;
      if (hashes.length) {
        const sources = (search.searchSources && search.searchSources.length ? search.searchSources : [{ entity: search.entity, recordIdColumn: search.recordIdColumn ?? "" }]).filter((src) => src.recordIdColumn && src.entity);
        if (sources.length) {
          const engine = this;
          q = q.where((eb) => eb.or(
            sources.map((src) => eb.exists(engine.buildSearchTokensSub(eb, {
              entity: src.entity,
              field: search.field,
              hashes,
              recordIdColumn: src.recordIdColumn,
              tenantId: search.tenantId ?? null,
              organizationScope: search.organizationScope ?? null,
              mintAlias: search.mintAlias
            })))
          ));
          this.logSearchDebug("search:filter", {
            entity: search.entity,
            field: search.field,
            tokens: tokens.tokens,
            hashes,
            applied: true,
            tenantId: search.tenantId ?? null,
            organizationScope: search.organizationScope,
            sources: sources.map((src) => ({ entity: src.entity, recordIdColumn: src.recordIdColumn }))
          });
          return q;
        }
      } else {
        this.logSearchDebug("search:skip-empty-hashes", {
          entity: search.entity,
          field: search.field,
          value: filter.value
        });
      }
      return q;
    }
    const col = column;
    switch (filter.op) {
      case "eq":
        return q.where(col, "=", filter.value);
      case "ne":
        return q.where(col, "!=", filter.value);
      case "gt":
      case "gte":
      case "lt":
      case "lte": {
        const operator = filter.op === "gt" ? ">" : filter.op === "gte" ? ">=" : filter.op === "lt" ? "<" : "<=";
        return q.where(col, operator, filter.value);
      }
      case "in":
        return q.where(col, "in", this.toArray(filter.value));
      case "nin":
        return q.where(col, "not in", this.toArray(filter.value));
      case "like":
        return q.where(col, "like", filter.value);
      case "ilike":
        return q.where(col, "ilike", filter.value);
      case "exists":
        return filter.value ? q.where(col, "is not", null) : q.where(col, "is", null);
      default:
        return q;
    }
  }
  resolveCustomEntityColumn(alias, field) {
    if (field === "id") return `${alias}.entity_id`;
    if (field === "organization_id" || field === "organizationId") return `${alias}.organization_id`;
    if (field === "tenant_id" || field === "tenantId") return `${alias}.tenant_id`;
    if (field === "created_at" || field === "updated_at" || field === "deleted_at") return `${alias}.${field}`;
    return null;
  }
  isDebugVerbosity() {
    if (this.debugVerbosity != null) return this.debugVerbosity;
    this.debugVerbosity = resolveDebugVerbosity();
    return this.debugVerbosity;
  }
  isSqlDebugEnabled() {
    if (this.sqlDebugEnabled != null) return this.sqlDebugEnabled;
    this.sqlDebugEnabled = resolveBooleanEnv(["QUERY_ENGINE_DEBUG_SQL"], false);
    return this.sqlDebugEnabled;
  }
  isForcePartialIndexEnabled() {
    if (this.forcePartialIndexEnabled != null) return this.forcePartialIndexEnabled;
    this.forcePartialIndexEnabled = resolveBooleanEnv(["FORCE_QUERY_INDEX_ON_PARTIAL_INDEXES"], false);
    return this.forcePartialIndexEnabled;
  }
  async resolveCoverageGap(entity, opts, coverageScope, _sourceTable) {
    const scope = coverageScope ?? this.resolveCoverageSnapshotScope(opts);
    if (!scope) return null;
    const tenantId = scope.tenantId;
    const organizationId = scope.organizationId;
    const withDeleted = !!opts.withDeleted;
    const snapshot = await this.getStoredCoverageSnapshot(entity, tenantId, organizationId, withDeleted);
    if (!snapshot) {
      this.scheduleCoverageRefresh(entity, tenantId, organizationId, withDeleted);
      return { stats: void 0, scope: "scoped" };
    }
    const baseCount = snapshot.baseCount;
    const indexCount = snapshot.indexedCount;
    const hasGap = baseCount > 0 && indexCount < baseCount;
    if (hasGap || indexCount > baseCount) return { stats: snapshot, scope: "scoped" };
    return null;
  }
  // Backward-compatible hook for tests that mock coverage stats
  async indexCoverageStats(entity, opts, coverageScope) {
    const gap = await this.resolveCoverageGap(entity, opts, coverageScope);
    return gap?.stats ?? null;
  }
  async captureSqlTiming(label, entity, execute, extra, profiler) {
    const shouldDebug = this.isSqlDebugEnabled() && this.isDebugVerbosity();
    const shouldProfile = profiler?.enabled === true;
    if (!shouldDebug && !shouldProfile) return Promise.resolve(execute());
    const startedAt = process.hrtime.bigint();
    try {
      return await Promise.resolve(execute());
    } finally {
      const elapsedMs = Number(process.hrtime.bigint() - startedAt) / 1e6;
      const context = { entity, durationMs: Math.round(elapsedMs * 1e3) / 1e3 };
      if (extra) Object.assign(context, extra);
      if (shouldProfile) profiler.record(label, context.durationMs, extra);
      if (shouldDebug) this.debug(`${label}:timing`, context);
    }
  }
  debug(message, context) {
    if (!this.isDebugVerbosity()) return;
    if (!this.isSqlDebugEnabled()) return;
    if (context) logger.debug(message, context);
    else logger.debug(message);
  }
}
export {
  HybridQueryEngine,
  coerceSortDirection
};
//# sourceMappingURL=engine.js.map
