@open-mercato/core 0.6.7-develop.6785.1.1dd7cfac55 → 0.6.7-develop.6795.1.8a3f27921c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities.ids.generated.js +1 -0
  3. package/dist/generated/entities.ids.generated.js.map +2 -2
  4. package/dist/modules/auth/backend/settings/page.js +14 -9
  5. package/dist/modules/auth/backend/settings/page.js.map +2 -2
  6. package/dist/modules/customers/cli.js +17 -7
  7. package/dist/modules/customers/cli.js.map +2 -2
  8. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +229 -216
  9. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
  10. package/dist/modules/customers/search.js +50 -29
  11. package/dist/modules/customers/search.js.map +2 -2
  12. package/dist/modules/data_sync/api/mappings/[id]/route.js +22 -9
  13. package/dist/modules/data_sync/api/mappings/[id]/route.js.map +2 -2
  14. package/dist/modules/data_sync/api/mappings/route.js +13 -4
  15. package/dist/modules/data_sync/api/mappings/route.js.map +2 -2
  16. package/dist/modules/data_sync/api/options.js +7 -2
  17. package/dist/modules/data_sync/api/options.js.map +2 -2
  18. package/dist/modules/data_sync/api/run.js +7 -2
  19. package/dist/modules/data_sync/api/run.js.map +2 -2
  20. package/dist/modules/data_sync/api/runs/[id]/cancel.js +8 -3
  21. package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
  22. package/dist/modules/data_sync/api/runs/[id]/retry.js +7 -2
  23. package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
  24. package/dist/modules/data_sync/api/runs/[id]/route.js +8 -3
  25. package/dist/modules/data_sync/api/runs/[id]/route.js.map +2 -2
  26. package/dist/modules/data_sync/api/runs.js +7 -2
  27. package/dist/modules/data_sync/api/runs.js.map +2 -2
  28. package/dist/modules/data_sync/api/schedules/[id]/route.js +19 -6
  29. package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
  30. package/dist/modules/data_sync/api/schedules/route.js +13 -4
  31. package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
  32. package/dist/modules/data_sync/api/validate.js +9 -4
  33. package/dist/modules/data_sync/api/validate.js.map +2 -2
  34. package/dist/modules/design_system/acl.js +9 -0
  35. package/dist/modules/design_system/acl.js.map +7 -0
  36. package/dist/modules/design_system/backend/design-system/page.js +11 -0
  37. package/dist/modules/design_system/backend/design-system/page.js.map +7 -0
  38. package/dist/modules/design_system/backend/design-system/page.meta.js +15 -0
  39. package/dist/modules/design_system/backend/design-system/page.meta.js.map +7 -0
  40. package/dist/modules/design_system/gallery/components/CodeSnippet.js +52 -0
  41. package/dist/modules/design_system/gallery/components/CodeSnippet.js.map +7 -0
  42. package/dist/modules/design_system/gallery/components/EntryCard.js +63 -0
  43. package/dist/modules/design_system/gallery/components/EntryCard.js.map +7 -0
  44. package/dist/modules/design_system/gallery/components/GalleryShell.js +163 -0
  45. package/dist/modules/design_system/gallery/components/GalleryShell.js.map +7 -0
  46. package/dist/modules/design_system/gallery/components/VariantPreview.js +9 -0
  47. package/dist/modules/design_system/gallery/components/VariantPreview.js.map +7 -0
  48. package/dist/modules/design_system/gallery/entries/banners.js +186 -0
  49. package/dist/modules/design_system/gallery/entries/banners.js.map +7 -0
  50. package/dist/modules/design_system/gallery/entries/buttons.js +361 -0
  51. package/dist/modules/design_system/gallery/entries/buttons.js.map +7 -0
  52. package/dist/modules/design_system/gallery/entries/charts.js +457 -0
  53. package/dist/modules/design_system/gallery/entries/charts.js.map +7 -0
  54. package/dist/modules/design_system/gallery/entries/dates.js +306 -0
  55. package/dist/modules/design_system/gallery/entries/dates.js.map +7 -0
  56. package/dist/modules/design_system/gallery/entries/detail.js +358 -0
  57. package/dist/modules/design_system/gallery/entries/detail.js.map +7 -0
  58. package/dist/modules/design_system/gallery/entries/display.js +785 -0
  59. package/dist/modules/design_system/gallery/entries/display.js.map +7 -0
  60. package/dist/modules/design_system/gallery/entries/feedback.js +658 -0
  61. package/dist/modules/design_system/gallery/entries/feedback.js.map +7 -0
  62. package/dist/modules/design_system/gallery/entries/filters.js +505 -0
  63. package/dist/modules/design_system/gallery/entries/filters.js.map +7 -0
  64. package/dist/modules/design_system/gallery/entries/foundations.js +453 -0
  65. package/dist/modules/design_system/gallery/entries/foundations.js.map +7 -0
  66. package/dist/modules/design_system/gallery/entries/icons.js +140 -0
  67. package/dist/modules/design_system/gallery/entries/icons.js.map +7 -0
  68. package/dist/modules/design_system/gallery/entries/inputs.js +1217 -0
  69. package/dist/modules/design_system/gallery/entries/inputs.js.map +7 -0
  70. package/dist/modules/design_system/gallery/entries/messages.js +334 -0
  71. package/dist/modules/design_system/gallery/entries/messages.js.map +7 -0
  72. package/dist/modules/design_system/gallery/entries/navigation.js +475 -0
  73. package/dist/modules/design_system/gallery/entries/navigation.js.map +7 -0
  74. package/dist/modules/design_system/gallery/entries/notifications.js +257 -0
  75. package/dist/modules/design_system/gallery/entries/notifications.js.map +7 -0
  76. package/dist/modules/design_system/gallery/entries/overlays.js +576 -0
  77. package/dist/modules/design_system/gallery/entries/overlays.js.map +7 -0
  78. package/dist/modules/design_system/gallery/entries/scaffolding.js +378 -0
  79. package/dist/modules/design_system/gallery/entries/scaffolding.js.map +7 -0
  80. package/dist/modules/design_system/gallery/entries/schedule.js +297 -0
  81. package/dist/modules/design_system/gallery/entries/schedule.js.map +7 -0
  82. package/dist/modules/design_system/gallery/registry.js +139 -0
  83. package/dist/modules/design_system/gallery/registry.js.map +7 -0
  84. package/dist/modules/design_system/gallery/types.js +1 -0
  85. package/dist/modules/design_system/gallery/types.js.map +7 -0
  86. package/dist/modules/design_system/index.js +9 -0
  87. package/dist/modules/design_system/index.js.map +7 -0
  88. package/dist/modules/design_system/setup.js +14 -0
  89. package/dist/modules/design_system/setup.js.map +7 -0
  90. package/dist/modules/inbox_ops/search.js +15 -4
  91. package/dist/modules/inbox_ops/search.js.map +2 -2
  92. package/dist/modules/integrations/api/[id]/credentials/route.js +11 -5
  93. package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
  94. package/dist/modules/integrations/api/[id]/health/route.js +6 -3
  95. package/dist/modules/integrations/api/[id]/health/route.js.map +2 -2
  96. package/dist/modules/integrations/api/[id]/route.js +6 -3
  97. package/dist/modules/integrations/api/[id]/route.js.map +2 -2
  98. package/dist/modules/integrations/api/[id]/state/route.js +6 -3
  99. package/dist/modules/integrations/api/[id]/state/route.js.map +2 -2
  100. package/dist/modules/integrations/api/[id]/version/route.js +6 -3
  101. package/dist/modules/integrations/api/[id]/version/route.js.map +2 -2
  102. package/dist/modules/integrations/api/logs/route.js +6 -3
  103. package/dist/modules/integrations/api/logs/route.js.map +2 -2
  104. package/dist/modules/integrations/api/route.js +6 -3
  105. package/dist/modules/integrations/api/route.js.map +2 -2
  106. package/dist/modules/integrations/api/umes-read.js +2 -2
  107. package/dist/modules/integrations/api/umes-read.js.map +2 -2
  108. package/dist/modules/integrations/lib/organization-scope.js +2 -9
  109. package/dist/modules/integrations/lib/organization-scope.js.map +2 -2
  110. package/dist/modules/messages/search.js +10 -5
  111. package/dist/modules/messages/search.js.map +2 -2
  112. package/dist/modules/query_index/data/entities.js +2 -1
  113. package/dist/modules/query_index/data/entities.js.map +2 -2
  114. package/dist/modules/query_index/lib/engine.js +32 -50
  115. package/dist/modules/query_index/lib/engine.js.map +2 -2
  116. package/dist/modules/query_index/migrations/Migration20260731105052_query_index.js +17 -0
  117. package/dist/modules/query_index/migrations/Migration20260731105052_query_index.js.map +7 -0
  118. package/generated/entities.ids.generated.ts +1 -0
  119. package/package.json +16 -16
  120. package/src/modules/auth/backend/settings/page.tsx +17 -8
  121. package/src/modules/customers/cli.ts +17 -7
  122. package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +24 -13
  123. package/src/modules/customers/i18n/de.json +17 -0
  124. package/src/modules/customers/i18n/en.json +17 -0
  125. package/src/modules/customers/i18n/es.json +17 -0
  126. package/src/modules/customers/i18n/pl.json +17 -0
  127. package/src/modules/customers/search.ts +59 -27
  128. package/src/modules/data_sync/AGENTS.md +1 -0
  129. package/src/modules/data_sync/api/mappings/[id]/route.ts +22 -9
  130. package/src/modules/data_sync/api/mappings/route.ts +13 -4
  131. package/src/modules/data_sync/api/options.ts +7 -2
  132. package/src/modules/data_sync/api/run.ts +7 -2
  133. package/src/modules/data_sync/api/runs/[id]/cancel.ts +8 -3
  134. package/src/modules/data_sync/api/runs/[id]/retry.ts +7 -2
  135. package/src/modules/data_sync/api/runs/[id]/route.ts +8 -3
  136. package/src/modules/data_sync/api/runs.ts +7 -2
  137. package/src/modules/data_sync/api/schedules/[id]/route.ts +19 -6
  138. package/src/modules/data_sync/api/schedules/route.ts +13 -4
  139. package/src/modules/data_sync/api/validate.ts +9 -4
  140. package/src/modules/design_system/AGENTS.md +34 -0
  141. package/src/modules/design_system/acl.ts +5 -0
  142. package/src/modules/design_system/backend/design-system/page.meta.ts +11 -0
  143. package/src/modules/design_system/backend/design-system/page.tsx +14 -0
  144. package/src/modules/design_system/gallery/components/CodeSnippet.tsx +55 -0
  145. package/src/modules/design_system/gallery/components/EntryCard.tsx +95 -0
  146. package/src/modules/design_system/gallery/components/GalleryShell.tsx +206 -0
  147. package/src/modules/design_system/gallery/components/VariantPreview.tsx +15 -0
  148. package/src/modules/design_system/gallery/entries/banners.tsx +226 -0
  149. package/src/modules/design_system/gallery/entries/buttons.tsx +422 -0
  150. package/src/modules/design_system/gallery/entries/charts.tsx +518 -0
  151. package/src/modules/design_system/gallery/entries/dates.tsx +325 -0
  152. package/src/modules/design_system/gallery/entries/detail.tsx +403 -0
  153. package/src/modules/design_system/gallery/entries/display.tsx +893 -0
  154. package/src/modules/design_system/gallery/entries/feedback.tsx +746 -0
  155. package/src/modules/design_system/gallery/entries/filters.tsx +570 -0
  156. package/src/modules/design_system/gallery/entries/foundations.tsx +529 -0
  157. package/src/modules/design_system/gallery/entries/icons.tsx +168 -0
  158. package/src/modules/design_system/gallery/entries/inputs.tsx +1516 -0
  159. package/src/modules/design_system/gallery/entries/messages.tsx +339 -0
  160. package/src/modules/design_system/gallery/entries/navigation.tsx +573 -0
  161. package/src/modules/design_system/gallery/entries/notifications.tsx +285 -0
  162. package/src/modules/design_system/gallery/entries/overlays.tsx +677 -0
  163. package/src/modules/design_system/gallery/entries/scaffolding.tsx +430 -0
  164. package/src/modules/design_system/gallery/entries/schedule.tsx +307 -0
  165. package/src/modules/design_system/gallery/registry.ts +152 -0
  166. package/src/modules/design_system/gallery/types.ts +32 -0
  167. package/src/modules/design_system/i18n/.hardcoded-allowlist.json +71 -0
  168. package/src/modules/design_system/i18n/de.json +46 -0
  169. package/src/modules/design_system/i18n/en.json +46 -0
  170. package/src/modules/design_system/i18n/es.json +46 -0
  171. package/src/modules/design_system/i18n/pl.json +46 -0
  172. package/src/modules/design_system/index.ts +7 -0
  173. package/src/modules/design_system/setup.ts +12 -0
  174. package/src/modules/inbox_ops/i18n/de.json +3 -0
  175. package/src/modules/inbox_ops/i18n/en.json +3 -0
  176. package/src/modules/inbox_ops/i18n/es.json +3 -0
  177. package/src/modules/inbox_ops/i18n/pl.json +3 -0
  178. package/src/modules/inbox_ops/search.ts +20 -4
  179. package/src/modules/integrations/api/[id]/credentials/route.ts +11 -5
  180. package/src/modules/integrations/api/[id]/health/route.ts +6 -3
  181. package/src/modules/integrations/api/[id]/route.ts +6 -3
  182. package/src/modules/integrations/api/[id]/state/route.ts +6 -3
  183. package/src/modules/integrations/api/[id]/version/route.ts +6 -3
  184. package/src/modules/integrations/api/logs/route.ts +6 -3
  185. package/src/modules/integrations/api/route.ts +6 -3
  186. package/src/modules/integrations/api/umes-read.ts +2 -2
  187. package/src/modules/integrations/lib/organization-scope.ts +5 -25
  188. package/src/modules/messages/i18n/de.json +2 -0
  189. package/src/modules/messages/i18n/en.json +2 -0
  190. package/src/modules/messages/i18n/es.json +2 -0
  191. package/src/modules/messages/i18n/pl.json +2 -0
  192. package/src/modules/messages/search.ts +11 -5
  193. package/src/modules/query_index/data/entities.ts +7 -0
  194. package/src/modules/query_index/lib/engine.ts +40 -63
  195. package/src/modules/query_index/migrations/.snapshot-open-mercato.json +12 -0
  196. package/src/modules/query_index/migrations/Migration20260731105052_query_index.ts +35 -0
@@ -20,6 +20,14 @@ import {
20
20
  type ResolvedJoin,
21
21
  } from '@open-mercato/shared/lib/query/join-utils'
22
22
  import { resolveSearchConfig, type SearchConfig } from '@open-mercato/shared/lib/search/config'
23
+ import {
24
+ createSearchTokenAvailability,
25
+ isSearchFilterOp,
26
+ hasSearchFilter,
27
+ type SearchTokenAvailability,
28
+ type SearchTokenProbeDb,
29
+ type SearchTokenProbeQueryBuilder,
30
+ } from '@open-mercato/shared/lib/search/availability'
23
31
  import { tokenizeText } from '@open-mercato/shared/lib/search/tokenize'
24
32
  import { runBeforeQueryPipeline, runAfterQueryPipeline, type QueryExtensionContext } from '@open-mercato/shared/lib/query/query-extension-runner'
25
33
  import { warnOnCiphertextLikeFallback } from '@open-mercato/shared/lib/query/ciphertext-search-warning'
@@ -146,6 +154,7 @@ export class HybridQueryEngine implements QueryEngine {
146
154
  private autoReindexEnabled: boolean | null = null
147
155
  private coverageOptimizationEnabled: boolean | null = null
148
156
  private pendingCoverageRefreshKeys = new Set<string>()
157
+ private searchAvailabilityInstance: SearchTokenAvailability | null = null
149
158
 
150
159
  constructor(
151
160
  private em: EntityManager,
@@ -174,6 +183,22 @@ export class HybridQueryEngine implements QueryEngine {
174
183
  throw new Error('HybridQueryEngine requires an EntityManager exposing getKysely() (MikroORM v7)')
175
184
  }
176
185
 
186
+ private searchAvailability(): SearchTokenAvailability {
187
+ if (!this.searchAvailabilityInstance) {
188
+ this.searchAvailabilityInstance = createSearchTokenAvailability({
189
+ getDb: () => this.getDb() as unknown as SearchTokenProbeDb,
190
+ getConfig: resolveSearchConfig,
191
+ applyOrganizationScope: (query, column, scope) => this.applyOrganizationScope(
192
+ query as unknown as AnyBuilder,
193
+ column,
194
+ scope,
195
+ ) as unknown as SearchTokenProbeQueryBuilder,
196
+ logDebug: (event, payload) => this.logSearchDebug(event, payload),
197
+ })
198
+ }
199
+ return this.searchAvailabilityInstance
200
+ }
201
+
177
202
  async query<T = unknown>(entity: EntityId, opts: QueryOptions = {}): Promise<QueryResult<T>> {
178
203
  const ext: QueryExtensionsConfig | undefined = opts.extensions
179
204
  let hybridExtCtx: QueryExtensionContext | null = null
@@ -251,7 +276,7 @@ export class HybridQueryEngine implements QueryEngine {
251
276
  profiler.mark('query:base_table_resolved')
252
277
  const searchConfig = resolveSearchConfig()
253
278
  const orgScope = this.resolveOrganizationScope(opts)
254
- const searchEnabled = searchConfig.enabled && await this.tableExists('search_tokens')
279
+ const searchEnabled = await this.searchAvailability().staticEnabled()
255
280
 
256
281
  const baseExists = await profiler.measure('base_table_exists', () => this.tableExists(baseTable))
257
282
  if (!baseExists) {
@@ -416,16 +441,19 @@ export class HybridQueryEngine implements QueryEngine {
416
441
  const searchSources: SearchTokenSource[] = indexSources
417
442
  .map((src) => ({ entity: String(src.entityId), recordIdColumn: src.recordIdColumn }))
418
443
  .filter((src) => src.recordIdColumn && src.entity)
419
- const searchFilters = normalizedFilters.filter((filter) => filter.op === 'like' || filter.op === 'ilike')
420
- // Probe `search_tokens` only when this query actually searches. Every consumer of
444
+ const searchFilters = normalizedFilters.filter((filter) => isSearchFilterOp(filter.op))
445
+ const sourceSearchFilters = [
446
+ ...baseFilters,
447
+ ...normalizedFilters.filter((filter) => String(filter.field).startsWith('cf:')),
448
+ ].filter((filter) => isSearchFilterOp(filter.op))
449
+ // Probe `search_tokens` only when this query actually searches (#4723). Every consumer of
421
450
  // `searchRuntime.enabled` already sits behind a like/ilike guard, so on a plain list load the
422
451
  // answer is unused — and the probe is a `LIMIT 1` the planner can resolve as a seq scan over a
423
452
  // large `search_tokens`. The join path below already probes lazily for the same reason.
424
- const hasSearchTokens = searchEnabled && searchSources.length && searchFilters.length
425
- ? await this.searchSourcesHaveTokens(searchSources, opts.tenantId ?? null, orgScope)
453
+ const hasSearchTokens = searchEnabled && searchSources.length && sourceSearchFilters.length
454
+ ? await this.searchAvailability().anySourceHasTokens(searchSources, opts.tenantId ?? null, orgScope)
426
455
  : false
427
456
  const searchRuntime: SearchRuntime = { ...searchRuntimeBase, searchSources, enabled: searchEnabled && hasSearchTokens }
428
- const joinSearchAvailability = new Map<string, boolean>()
429
457
  if (searchFilters.length) {
430
458
  this.logSearchDebug('search:init', {
431
459
  entity,
@@ -476,9 +504,8 @@ export class HybridQueryEngine implements QueryEngine {
476
504
  const join = joinMap.get(alias)
477
505
  if (!join?.entityId) continue
478
506
  const hasJoinedTokens = searchEnabled
479
- ? await this.hasSearchTokens(String(join.entityId), opts.tenantId ?? null, orgScope)
507
+ ? await this.searchAvailability().hasTokens(String(join.entityId), opts.tenantId ?? null, orgScope)
480
508
  : false
481
- joinSearchAvailability.set(String(join.entityId), hasJoinedTokens)
482
509
  const fallbackFields = filters
483
510
  .filter((filter) => !hasJoinedTokens || typeof filter.value !== 'string' || tokenizeText(filter.value, searchConfig).hashes.length === 0)
484
511
  .map((filter) => filter.column)
@@ -784,11 +811,7 @@ export class HybridQueryEngine implements QueryEngine {
784
811
  if (!['like', 'ilike'].includes(filter.op)) return false
785
812
  if (typeof filter.value !== 'string' || filter.value.trim().length === 0) return false
786
813
 
787
- let searchAvailable = joinSearchAvailability.get(join.entityId)
788
- if (searchAvailable === undefined) {
789
- searchAvailable = await this.hasSearchTokens(String(join.entityId), opts.tenantId ?? null, orgScope)
790
- joinSearchAvailability.set(join.entityId, searchAvailable)
791
- }
814
+ const searchAvailable = await this.searchAvailability().hasTokens(String(join.entityId), opts.tenantId ?? null, orgScope)
792
815
  if (!searchAvailable) return false
793
816
 
794
817
  const tokens = tokenizeText(String(filter.value), searchConfig)
@@ -1683,13 +1706,11 @@ export class HybridQueryEngine implements QueryEngine {
1683
1706
  if (!opts.tenantId) throw new Error('QueryEngine: tenantId is required')
1684
1707
 
1685
1708
  const normalizedFilters = normalizeFilters(opts.filters)
1686
- const hasSearchFilter = normalizedFilters.some((filter) => filter.op === 'like' || filter.op === 'ilike')
1687
-
1688
1709
  const searchConfig = resolveSearchConfig()
1689
- const searchEnabled = searchConfig.enabled && await this.tableExists('search_tokens')
1690
- // Same gate as `query()`: without a like/ilike filter the probe's answer is never read.
1691
- const hasSearchTokens = searchEnabled && hasSearchFilter
1692
- ? await this.hasSearchTokens(entity, opts.tenantId ?? null, orgScope)
1710
+ const searchEnabled = await this.searchAvailability().staticEnabled()
1711
+ // Same gate as `query()` (#4723): without a like/ilike filter the probe's answer is never read.
1712
+ const hasSearchTokens = searchEnabled && hasSearchFilter(normalizedFilters)
1713
+ ? await this.searchAvailability().hasTokens(entity, opts.tenantId ?? null, orgScope)
1693
1714
  : false
1694
1715
  const searchRuntime: SearchRuntime = {
1695
1716
  enabled: searchEnabled && hasSearchTokens,
@@ -1829,50 +1850,6 @@ export class HybridQueryEngine implements QueryEngine {
1829
1850
  return !!exists
1830
1851
  }
1831
1852
 
1832
- private async hasSearchTokens(
1833
- entity: string,
1834
- tenantId: string | null,
1835
- orgScope?: { ids: string[]; includeNull: boolean } | null
1836
- ): Promise<boolean> {
1837
- try {
1838
- const db = this.getDb() as any
1839
- let query = db
1840
- .selectFrom('search_tokens')
1841
- .select(sql<number>`1`.as('one'))
1842
- .where('entity_type', '=', entity)
1843
- if (tenantId !== undefined) {
1844
- query = query.where(sql<boolean>`tenant_id is not distinct from ${tenantId}`)
1845
- }
1846
- if (orgScope) {
1847
- query = this.applyOrganizationScope(query, 'search_tokens.organization_id', orgScope)
1848
- }
1849
- const row = await query.limit(1).executeTakeFirst()
1850
- return !!row
1851
- } catch (err) {
1852
- this.logSearchDebug('search:has-tokens-error', {
1853
- entity, tenantId, organizationScope: orgScope,
1854
- error: err instanceof Error ? err.message : String(err),
1855
- })
1856
- return false
1857
- }
1858
- }
1859
-
1860
- private async searchSourcesHaveTokens(
1861
- sources: SearchTokenSource[],
1862
- tenantId: string | null,
1863
- orgScope?: { ids: string[]; includeNull: boolean } | null
1864
- ): Promise<boolean> {
1865
- for (const source of sources) {
1866
- const ok = await this.hasSearchTokens(source.entity, tenantId, orgScope)
1867
- this.logSearchDebug('search:source-has-tokens', {
1868
- entity: source.entity, recordIdColumn: source.recordIdColumn,
1869
- tenantId, organizationScope: orgScope, hasTokens: ok,
1870
- })
1871
- if (ok) return true
1872
- }
1873
- return false
1874
- }
1875
-
1876
1853
  private async resolveAvailableCustomFieldKeys(entityIds: string[], tenantId: string | null): Promise<string[]> {
1877
1854
  if (!entityIds.length) return []
1878
1855
  const cacheKey = this.customFieldKeysCacheKey(entityIds, tenantId)
@@ -1365,6 +1365,18 @@
1365
1365
  "primary": false,
1366
1366
  "unique": false
1367
1367
  },
1368
+ {
1369
+ "columnNames": [
1370
+ "entity_type",
1371
+ "tenant_id",
1372
+ "organization_id"
1373
+ ],
1374
+ "composite": true,
1375
+ "constraint": false,
1376
+ "keyName": "search_tokens_presence_idx",
1377
+ "primary": false,
1378
+ "unique": false
1379
+ },
1368
1380
  {
1369
1381
  "columnNames": [
1370
1382
  "id"
@@ -0,0 +1,35 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ // Availability-probe index (.ai/specs/2026-07-31-search-token-probe-index.md).
4
+ // The query engines gate like/ilike routing on "does (entity_type, tenant, org)
5
+ // have any search tokens?". Neither existing index serves that probe as an
6
+ // access path — lookup_idx buries tenant_id behind field/token_hash — so on a
7
+ // large search_tokens the planner degraded the probe to a seq scan and the MISS
8
+ // answer had to exhaust every entry for the entity type (p95 12.5 s in
9
+ // production, #4723). With this (entity_type, tenant_id, organization_id)
10
+ // prefix and the index-usable `= / IS NULL` predicates the availability
11
+ // resolver emits, both the hit AND the miss become a B-tree prefix seek.
12
+ //
13
+ // search_tokens is high-churn, so the index is built CONCURRENTLY to avoid
14
+ // blocking writes during the build. CREATE INDEX CONCURRENTLY cannot run
15
+ // inside a transaction, hence isTransactional() => false; the migration runner
16
+ // applies migrations one-by-one, so this opt-out is safe (same pattern as
17
+ // Migration20260611103000_query_index). Drop first so retrying a failed
18
+ // concurrent build removes PostgreSQL's invalid index stub instead of letting
19
+ // IF NOT EXISTS silently accept it.
20
+ export class Migration20260731105052_query_index extends Migration {
21
+
22
+ override isTransactional(): boolean {
23
+ return false;
24
+ }
25
+
26
+ override up(): void | Promise<void> {
27
+ this.addSql(`drop index concurrently if exists "search_tokens_presence_idx";`);
28
+ this.addSql(`create index concurrently "search_tokens_presence_idx" on "search_tokens" ("entity_type", "tenant_id", "organization_id");`);
29
+ }
30
+
31
+ override down(): void | Promise<void> {
32
+ this.addSql(`drop index concurrently if exists "search_tokens_presence_idx";`);
33
+ }
34
+
35
+ }