     1	// Cross-table search helper functions that enable filtering across related database tables
     2	
     3	/**
     4	 * Validates that a field exists and is indexed for cross-table search
     5	 *
     6	 * @async
     7	 * @param {Object} scopes - All registered hooked-api scopes containing schema and relationship definitions
     8	 * @param {Object} log - Logger instance for debug and trace output
     9	 * @param {string} targetScopeName - The scope containing the field to validate
    10	 * @param {string} fieldName - The field name to check
    11	 * @param {Set<string>} [searchedScopes=new Set()] - Internal parameter to prevent circular references
    12	 * @returns {Promise<void>}
    13	 * @throws {Error} If scope not found, field not found, field not indexed, or circular reference detected
    14	   *
    15	   * @example
    16	   * // Input: Validate indexed field
    17	   * // Schema has: companies.name with indexed: true
    18	   * await validateCrossTableField(scopes, log, 'companies', 'name');
    19	   *
    20	   * // Output: Validation succeeds (no error thrown)
    21	   *
    22	   * @example
    23	   * // Input: Non-indexed field
    24	   * // Schema has: companies.internal_code without indexed property
    25	   * try {
    26	   *   await validateCrossTableField('companies', 'internal_code');
    27	   * } catch (error) {
    28	   *   console.log(error.message);
    29	   *   // "Field 'companies.internal_code' is not indexed. Add 'indexed: true' to allow cross-table search"
    30	   * }
    31	   *
    32	   * @description
    33	   * Used by:
    34	   * - buildJoinChain calls this to ensure target fields are properly indexed
    35	   * - analyzeRequiredIndexes uses this to identify missing indexes
    36	   *
    37	   * Purpose:
    38	   * - Cross-table searches can be slow without proper indexes
    39	   * - Forces developers to explicitly mark searchable fields with indexed: true
    40	   * - Provides clear error messages when configuration is missing
    41	   *
    42	   * Data flow:
    43	   * - Called during query building to validate field configuration
    44	   * - Ensures performance by requiring indexes on searchable fields
    45	   * - Part of the validation phase before expensive JOIN operations
    46	   */
    47	export const validateCrossTableField = async (scopes, log, targetScopeName, fieldName, searchedScopes = new Set()) => {
    48	  log.trace('[VALIDATE] Starting validateCrossTableField:', { targetScopeName, fieldName, searchedScopes: Array.from(searchedScopes) })
    49	
    50	  if (searchedScopes.has(targetScopeName)) {
    51	    throw new Error(`Circular reference detected: ${targetScopeName} -> ${Array.from(searchedScopes).join(' -> ')}`)
    52	  }
    53	
    54	  log.trace('[VALIDATE] Getting target schema for:', targetScopeName)
    55	  let targetSchemaInstance
    56	  try {
    57	    targetSchemaInstance = scopes[targetScopeName].vars.schemaInfo.schemaInstance
    58	    log.trace('[VALIDATE] Got target schema:', { scopeName: targetScopeName, schemaKeys: Object.keys(targetSchemaInstance || {}) })
    59	  } catch (error) {
    60	    log.trace('[VALIDATE] Error getting target schema:', error.message)
    61	    throw new Error(`Target scope '${targetScopeName}' not found`)
    62	  }
    63	
    64	  if (!targetSchemaInstance) {
    65	    log.trace('[VALIDATE] Target schema is null/undefined')
    66	    throw new Error(`Target scope '${targetScopeName}' has no schema`)
    67	  }
    68	
    69	  const fieldDef = targetSchemaInstance.structure[fieldName]
    70	  log.trace('[VALIDATE] Field lookup result:', { fieldName, fieldExists: !!fieldDef })
    71	  if (!fieldDef) {
    72	    throw new Error(`Field '${fieldName}' not found in scope '${targetScopeName}'`)
    73	  }
    74	
    75	  log.trace('[VALIDATE] Checking indexed status:', { fieldName, indexed: fieldDef.indexed })
    76	  if (!fieldDef.indexed) {
    77	    throw new Error(`Field '${targetScopeName}.${fieldName}' is not indexed. Add 'indexed: true' to allow cross-table search`)
    78	  }
    79	
    80	  log.trace('[VALIDATE] Validation successful for field:', `${targetScopeName}.${fieldName}`)
    81	}
    82	
    83	/**
    84	 * Builds a chain of SQL JOINs to reach a field in a related table
    85	 *
    86	 * @async
    87	 * @param {Object} scopes - All registered hooked-api scopes containing schema and relationship definitions
    88	 * @param {Object} log - Logger instance for debug and trace output
    89	 * @param {string} fromScopeName - Starting scope (e.g., 'articles')
    90	 * @param {string} targetPath - Dot-separated path to target field (e.g., 'author.company.name')
    91	 * @param {Set<string>} [searchedScopes=new Set()] - Internal parameter to prevent circular references
    92	 * @returns {Promise<Object>} JOIN information for query builder
    93	 * @throws {Error} If path is invalid or relationships not properly configured
    94	   *
    95	   * @example
    96	   * // Input: Simple belongsTo relationship
    97	   * // articles table has author_id field, authors schema defines belongsTo relationship
    98	   * const joinInfo = await buildJoinChain('articles', 'author.name');
    99	   *
   100	   * // Output: Single JOIN configuration
   101	   * // {
   102	   * //   joinAlias: 'articles_to_authors_authors',
   103	   * //   targetTableName: 'authors',
   104	   * //   sourceField: 'author_id',
   105	   * //   targetField: 'name',
   106	   * //   joinCondition: 'articles.author_id = articles_to_authors_authors.id',
   107	   * //   isOneToMany: false,
   108	   * //   isPolymorphic: false
   109	   * // }
   110	   *
   111	   * @example
   112	   * // Input: Multi-level path through relationships
   113	   * // articles → authors → companies
   114	   * const joinInfo = await buildJoinChain('articles', 'author.company.name');
   115	   *
   116	   * // Output: Multi-level JOIN chain
   117	   * // {
   118	   * //   targetTableName: 'companies',
   119	   * //   joinAlias: 'authors_to_companies_companies',
   120	   * //   joinCondition: 'articles.author_id = articles_to_authors_authors.id AND articles_to_authors_authors.company_id = authors_to_companies_companies.id',
   121	   * //   targetField: 'name',
   122	   * //   isOneToMany: false,
   123	   * //   isMultiLevel: true,
   124	   * //   joinChain: [
   125	   * //     {
   126	   * //       targetTableName: 'authors',
   127	   * //       joinAlias: 'articles_to_authors_authors',
   128	   * //       joinCondition: 'articles.author_id = articles_to_authors_authors.id',
   129	   * //       isOneToMany: false,
   130	   * //       relationshipField: 'author_id',
   131	   * //       relationshipType: 'belongsTo'
   132	   * //     },
   133	   * //     {
   134	   * //       targetTableName: 'companies',
   135	   * //       joinAlias: 'authors_to_companies_companies',
   136	   * //       joinCondition: 'articles_to_authors_authors.company_id = authors_to_companies_companies.id',
   137	   * //       isOneToMany: false,
   138	   * //       relationshipField: 'company_id',
   139	   * //       relationshipType: 'belongsTo'
   140	   * //     }
   141	   * //   ]
   142	   * // }
   143	   *
   144	   * @example
   145	   * // Input: One-to-many relationship (hasMany)
   146	   * // authors have many articles
   147	   * const joinInfo = await buildJoinChain('authors', 'articles.title');
   148	   *
   149	   * // Output: JOIN with isOneToMany flag
   150	   * // {
   151	   * //   joinAlias: 'authors_to_articles_articles',
   152	   * //   targetTableName: 'articles',
   153	   * //   sourceField: 'author_id',
   154	   * //   targetField: 'title',
   155	   * //   joinCondition: 'authors.id = authors_to_articles_articles.author_id',
   156	   * //   isOneToMany: true,
   157	   * //   isPolymorphic: false
   158	   * // }
   159	   *
   160	   * @description
   161	   * Used by:
   162	   * - rest-api-knex-plugin's dataQuery method when processing cross-table filters
   163	   * - Called for each filter that references a related table field
   164	   *
   165	   * Purpose:
   166	   * - Automatically constructs complex SQL JOINs from simple dot notation
   167	   * - Supports many-to-one (belongsTo), one-to-many (hasMany), and many-to-many relationships
   168	   * - Handles polymorphic relationships and multi-level paths
   169	   *
   170	   * Data flow:
   171	   * 1. Filter parser identifies cross-table reference (contains dots)
   172	   * 2. buildJoinChain analyzes relationship path segment by segment
   173	   * 3. For each segment, finds the appropriate relationship definition
   174	   * 4. Constructs JOIN conditions based on foreign keys
   175	   * 5. Returns complete JOIN information for SQL query builder
   176	   * 6. Query builder adds these JOINs to enable filtering on related data
   177	   */
   178	export const buildJoinChain = async (scopes, log, fromScopeName, targetPath, searchedScopes = new Set()) => {
   179	  log.trace('[BUILD-JOIN] Starting buildJoinChain:', { fromScopeName, targetPath })
   180	
   181	  const pathSegments = targetPath.split('.')
   182	  if (pathSegments.length < 2) {
   183	    throw new Error(`Invalid cross-table path '${targetPath}'. Must contain at least 'scope.field'`)
   184	  }
   185	
   186	  const targetFieldName = pathSegments[pathSegments.length - 1]
   187	  const relationshipPath = pathSegments.slice(0, -1)
   188	
   189	  log.trace('[BUILD-JOIN] Parsed path:', { relationshipPath, targetFieldName })
   190	
   191	  const joinChain = []
   192	  let currentScope = fromScopeName
   193	
   194	  for (let i = 0; i < relationshipPath.length; i++) {
   195	    const targetScope = relationshipPath[i]
   196	
   197	    log.trace('[BUILD-JOIN] Looking for direct relationship:', { from: currentScope, to: targetScope })
   198	
   199	    if (searchedScopes.has(currentScope)) {
   200	      throw new Error(`Circular reference detected in path: ${Array.from(searchedScopes).join(' -> ')} -> ${currentScope}`)
   201	    }
   202	    searchedScopes.add(currentScope)
   203	
   204	    const schemaInfo = scopes[currentScope].vars.schemaInfo
   205	    const currentSchema = schemaInfo.schemaInstance
   206	    const currentRelationships = schemaInfo.schemaRelationships
   207	
   208	    let foundRelationship = null
   209	    let relationshipType = null
   210	    let relationshipField = null
   211	    let relationshipDef = null
   212	
   213	    if (currentRelationships) {
   214	      for (const [relName, relDef] of Object.entries(currentRelationships)) {
   215	        if (relDef.type === 'hasMany' && relDef.target === targetScope) {
   216	          if (relDef.via) {
   217	            const targetRelationships = scopes[targetScope].vars.schemaInfo.schemaRelationships
   218	            const viaRel = targetRelationships?.[relDef.via]
   219	
   220	            if (viaRel?.belongsToPolymorphic) {
   221	              const { typeField, idField } = viaRel.belongsToPolymorphic
   222	              foundRelationship = targetScope
   223	              relationshipType = 'hasManyPolymorphic'
   224	              relationshipField = { typeField, idField, via: relDef.via }
   225	              relationshipDef = relDef
   226	              log.trace('[BUILD-JOIN] Found polymorphic hasMany relationship:', {
   227	                relName, targetScope, via: relDef.via, typeField, idField
   228	              })
   229	              break
   230	            }
   231	          }
   232	
   233	          if (relDef.through) {
   234	            foundRelationship = targetScope
   235	            relationshipType = 'manyToMany'
   236	            relationshipField = {
   237	              through: relDef.through,
   238	              foreignKey: relDef.foreignKey,
   239	              otherKey: relDef.otherKey
   240	            }
   241	            relationshipDef = relDef
   242	            log.trace('[BUILD-JOIN] Found many-to-many relationship:', {
   243	              relName,
   244	              targetScope,
   245	              through: relDef.through,
   246	              foreignKey: relDef.foreignKey,
   247	              otherKey: relDef.otherKey
   248	            })
   249	            break
   250	          }
   251	
   252	          foundRelationship = targetScope
   253	          relationshipType = 'hasMany'
   254	          relationshipField = relDef.foreignKey
   255	          if (!relationshipField) {
   256	            log.error('[BUILD-JOIN] Missing foreignKey in hasMany relationship:', { relName, currentScope })
   257	            throw new Error(`Missing foreignKey in hasMany relationship '${relName}' for scope '${currentScope}'`)
   258	          }
   259	          relationshipDef = relDef
   260	          log.trace('[BUILD-JOIN] Found hasMany relationship:', { relName, targetScope, foreignKey: relationshipField })
   261	          break
   262	        }
   263	      }
   264	    }
   265	
   266	    if (!foundRelationship) {
   267	      for (const [fieldName, fieldDef] of Object.entries(currentSchema.structure)) {
   268	        if (fieldDef.belongsTo === targetScope) {
   269	          foundRelationship = targetScope
   270	          relationshipType = 'belongsTo'
   271	          relationshipField = fieldName
   272	          relationshipDef = fieldDef
   273	          log.trace('[BUILD-JOIN] Found belongsTo relationship:', { fieldName, targetScope })
   274	          break
   275	        }
   276	      }
   277	    }
   278	
   279	    if (!foundRelationship) {
   280	      throw new Error(
   281	          `No searchable relationship from '${currentScope}' to '${targetScope}'. `
   282	      )
   283	    }
   284	
   285	    const sourceSchema = scopes[currentScope].vars.schemaInfo.schemaInstance
   286	    const sourceTableName = scopes[currentScope].vars.schemaInfo.tableName
   287	    const targetSchemaInstance = scopes[targetScope].vars.schemaInfo.schemaInstance
   288	    const targetTableName = scopes[targetScope].vars.schemaInfo.tableName
   289	
   290	    if (relationshipType === 'manyToMany') {
   291	      const { through, foreignKey, otherKey } = relationshipField
   292	      const pivotTableName = scopes[through].vars.schemaInfo.tableName || through
   293	      const previousAlias = i === 0 ? sourceTableName : joinChain[i - 1].joinAlias
   294	
   295	      const pivotAlias = `${currentScope}_to_${through}_${through}`
   296	      const pivotJoinCondition = `${previousAlias}.id = ${pivotAlias}.${foreignKey}`
   297	
   298	      joinChain.push({
   299	        targetTableName: pivotTableName,
   300	        joinAlias: pivotAlias,
   301	        joinCondition: pivotJoinCondition,
   302	        isOneToMany: true,
   303	        isPolymorphic: false,
   304	        relationshipField: foreignKey,
   305	        relationshipType: 'manyToMany_pivot',
   306	        targetScopeName: through
   307	      })
   308	
   309	      const targetAlias = `${through}_to_${targetScope}_${targetScope}`
   310	      const targetJoinCondition = `${pivotAlias}.${otherKey} = ${targetAlias}.id`
   311	
   312	      joinChain.push({
   313	        targetTableName,
   314	        joinAlias: targetAlias,
   315	        joinCondition: targetJoinCondition,
   316	        isOneToMany: false,
   317	        isPolymorphic: false,
   318	        relationshipField: otherKey,
   319	        relationshipType: 'manyToMany_target',
   320	        targetScopeName: targetScope
   321	      })
   322	    } else {
   323	      const joinAlias = `${currentScope}_to_${targetScope}_${targetScope}`
   324	
   325	      let joinCondition
   326	      if (relationshipType === 'hasManyPolymorphic') {
   327	        const { typeField, idField } = relationshipField
   328	        const previousAlias = i === 0 ? sourceTableName : joinChain[i - 1].joinAlias
   329	        joinCondition = `${joinAlias}.${typeField} = '${currentScope}' AND ${joinAlias}.${idField} = ${previousAlias}.id`
   330	      } else if (relationshipType === 'hasMany') {
   331	        const previousAlias = i === 0 ? sourceTableName : joinChain[i - 1].joinAlias
   332	        joinCondition = `${previousAlias}.id = ${joinAlias}.${relationshipField}`
   333	      } else {
   334	        const previousAlias = i === 0 ? sourceTableName : joinChain[i - 1].joinAlias
   335	        joinCondition = `${previousAlias}.${relationshipField} = ${joinAlias}.id`
   336	      }
   337	
   338	      joinChain.push({
   339	        targetTableName,
   340	        joinAlias,
   341	        joinCondition,
   342	        isOneToMany: relationshipType === 'hasMany' || relationshipType === 'hasManyPolymorphic',
   343	        isPolymorphic: relationshipType === 'hasManyPolymorphic',
   344	        relationshipField,
   345	        relationshipType,
   346	        targetScopeName: targetScope
   347	      })
   348	    }
   349	
   350	    currentScope = targetScope
   351	  }
   352	
   353	  const finalScope = relationshipPath[relationshipPath.length - 1]
   354	  await validateCrossTableField(scopes, log, finalScope, targetFieldName, searchedScopes)
   355	
   356	  if (joinChain.length === 0) {
   357	    throw new Error(`Invalid path '${targetPath}': no relationships to traverse`)
   358	  }
   359	
   360	  const lastJoin = joinChain[joinChain.length - 1]
   361	
   362	  if (joinChain.length > 1) {
   363	    return {
   364	      targetTableName: lastJoin.targetTableName,
   365	      joinAlias: lastJoin.joinAlias,
   366	      joinCondition: joinChain.map(j => j.joinCondition).join(' AND '),
   367	      targetField: targetFieldName,
   368	      isOneToMany: joinChain.some(j => j.isOneToMany),
   369	      isMultiLevel: true,
   370	      joinChain,
   371	      targetScopeName: finalScope
   372	    }
   373	  } else {
   374	    return {
   375	      joinAlias: lastJoin.joinAlias,
   376	      targetTableName: lastJoin.targetTableName,
   377	      sourceField: lastJoin.relationshipField,
   378	      targetField: targetFieldName,
   379	      joinCondition: lastJoin.joinCondition,
   380	      isOneToMany: lastJoin.isOneToMany,
   381	      isPolymorphic: lastJoin.isPolymorphic,
   382	      targetScopeName: lastJoin.targetScopeName || finalScope
   383	    }
   384	  }
   385	}
   386	
   387	/**
   388	 * Analyzes a search schema to identify which fields need database indexes
   389	 *
   390	 * @param {Object} scopes - All registered hooked-api scopes containing schema and relationship definitions
   391	 * @param {Object} log - Logger instance for debug and trace output
   392	 * @param {string} scopeName - The scope being analyzed
   393	 * @param {Object} searchSchema - Search schema definition with filter fields
   394	 * @returns {Array<Object>} Array of required indexes with scope, field, and reason
   395	   *
   396	   * @example
   397	   * // Input: Search schema with cross-table references
   398	   * const searchSchema = {
   399	   *   authorName: {
   400	   *     type: 'string',
   401	   *     actualField: 'authors.name',
   402	   *     filterOperator: 'like'
   403	   *   },
   404	   *   companyName: {
   405	   *     type: 'string',
   406	   *     actualField: 'authors.company.name'
   407	   *   },
   408	   *   search: {
   409	   *     type: 'string',
   410	   *     oneOf: ['title', 'authors.name', 'authors.company.name']
   411	   *   }
   412	   * };
   413	   *
   414	   * const requiredIndexes = analyzeRequiredIndexes('articles', searchSchema);
   415	   *
   416	   * // Output: List of fields that need indexes
   417	   * // [
   418	   * //   { scope: 'authors', field: 'name', reason: 'Cross-table search from articles.authorName' },
   419	   * //   { scope: 'authors', field: 'company', reason: 'Cross-table search from articles.companyName' },
   420	   * //   { scope: 'authors', field: 'name', reason: 'Cross-table oneOf search from articles.search' },
   421	   * //   { scope: 'authors', field: 'company', reason: 'Cross-table oneOf search from articles.search' }
   422	   * // ]
   423	   *
   424	   * @example
   425	   * // Input: No cross-table references
   426	   * const searchSchema = {
   427	   *   title: { type: 'string' },
   428	   *   status: { type: 'string' }
   429	   * };
   430	   *
   431	   * const requiredIndexes = analyzeRequiredIndexes('articles', searchSchema);
   432	   *
   433	   * // Output: Empty array - no cross-table indexes needed
   434	   * // []
   435	   *
   436	   * @description
   437	   * Used by:
   438	   * - Called during API initialization to identify missing indexes
   439	   * - Used by developers to understand performance requirements
   440	   *
   441	   * Purpose:
   442	   * - Cross-table searches require indexes for acceptable performance
   443	   * - Helps identify configuration issues before they cause slow queries
   444	   * - Provides clear documentation of index requirements
   445	   *
   446	   * Data flow:
   447	   * - Runs during schema compilation phase
   448	   * - Analyzes search schemas to find cross-table references
   449	   * - Output can be used to create indexes manually or automatically
   450	   * - Prevents performance issues before queries are executed
   451	   */
   452	export const analyzeRequiredIndexes = (scopes, log, scopeName, schemaInfo) => {
   453	  const requiredIndexes = []
   454	
   455	  const schemaToAnalyze = schemaInfo.searchSchemaStructure
   456	
   457	  Object.entries(schemaToAnalyze).forEach(([filterKey, fieldDef]) => {
   458	    if (fieldDef.actualField && fieldDef.actualField.includes('.')) {
   459	      const [targetScopeName, targetFieldName] = fieldDef.actualField.split('.')
   460	      requiredIndexes.push({
   461	        scope: targetScopeName,
   462	        field: targetFieldName,
   463	        reason: `Cross-table search from ${scopeName}.${filterKey}`
   464	      })
   465	    }
   466	
   467	    if (fieldDef.oneOf && Array.isArray(fieldDef.oneOf)) {
   468	      fieldDef.oneOf.forEach(field => {
   469	        if (field.includes('.')) {
   470	          const [targetScopeName, targetFieldName] = field.split('.')
   471	          requiredIndexes.push({
   472	            scope: targetScopeName,
   473	            field: targetFieldName,
   474	            reason: `Cross-table oneOf search from ${scopeName}.${filterKey}`
   475	          })
   476	        }
   477	      })
   478	    }
   479	  })
   480	
   481	  return requiredIndexes
   482	}
   483	
   484	/**
   485	 * Creates database indexes for fields identified by analyzeRequiredIndexes
   486	 *
   487	 * @async
   488	 * @param {Object} scopes - All registered hooked-api scopes containing schema and relationship definitions
   489	 * @param {Object} log - Logger instance for debug and trace output
   490	 * @param {Array<Object>} requiredIndexes - Index requirements from analyzeRequiredIndexes
   491	 * @param {Object} knex - Knex database connection instance
   492	 * @returns {Promise<Array<Object>>} Array of successfully created indexes
   493	   *
   494	   * @example
   495	   * // Input: Required indexes from analysis
   496	   * const requiredIndexes = [
   497	   *   { scope: 'authors', field: 'name', reason: 'Cross-table search' },
   498	   *   { scope: 'companies', field: 'name', reason: 'Cross-table search' }
   499	   * ];
   500	   *
   501	   * const createdIndexes = await createRequiredIndexes(requiredIndexes, knex);
   502	   *
   503	   * // Output: Successfully created indexes
   504	   * // [
   505	   * //   { tableName: 'authors', field: 'name', indexName: 'idx_authors_name_search' },
   506	   * //   { tableName: 'companies', field: 'name', indexName: 'idx_companies_name_search' }
   507	   * // ]
   508	   *
   509	   * // Database effect: CREATE INDEX idx_authors_name_search ON authors(name);
   510	   * // Database effect: CREATE INDEX idx_companies_name_search ON companies(name);
   511	   *
   512	   * @example
   513	   * // Input: Some indexes already exist
   514	   * const requiredIndexes = [
   515	   *   { scope: 'authors', field: 'name', reason: 'Cross-table search' },  // Already exists
   516	   *   { scope: 'authors', field: 'email', reason: 'Cross-table search' }  // New
   517	   * ];
   518	   *
   519	   * const createdIndexes = await createRequiredIndexes(requiredIndexes, knex);
   520	   *
   521	   * // Output: Only newly created indexes
   522	   * // [
   523	   * //   { tableName: 'authors', field: 'email', indexName: 'idx_authors_email_search' }
   524	   * // ]
   525	   *
   526	   * @description
   527	   * Used by:
   528	   * - Can be called during database migration or setup
   529	   * - Used by developers to automatically create performance indexes
   530	   *
   531	   * Purpose:
   532	   * - Automates index creation based on search schema requirements
   533	   * - Ensures consistent index naming across the application
   534	   * - Safely handles cases where indexes already exist
   535	   *
   536	   * Data flow:
   537	   * - Typically runs during database setup or migration
   538	   * - Creates indexes identified by analyzeRequiredIndexes
   539	   * - Improves query performance for cross-table searches
   540	   * - Part of the database optimization phase
   541	   */
   542	export const createRequiredIndexes = async (scopes, log, requiredIndexes, knex) => {
   543	  const createdIndexes = []
   544	
   545	  for (const indexInfo of requiredIndexes) {
   546	    const { scope, field } = indexInfo
   547	    const tableName = scopes[scope].vars.schemaInfo.tableName
   548	    const indexName = `idx_${tableName}_${field}_search`
   549	
   550	    try {
   551	      const hasIndex = await knex.schema.hasIndex(tableName, [field])
   552	      if (!hasIndex) {
   553	        await knex.schema.table(tableName, table => {
   554	          table.index([field], indexName)
   555	        })
   556	        createdIndexes.push({ tableName, field, indexName })
   557	        log.info(`Created index: ${indexName} on ${tableName}.${field}`)
   558	      }
   559	    } catch (error) {
   560	      log.warn(`Failed to create index on ${tableName}.${field}:`, error.message)
   561	    }
   562	  }
   563	
   564	  return createdIndexes
   565	}
