* Renders a `FilterQuery` predicate into a SQL fragment (without the `WHERE` keyword and * without table-alias prefixes) suitable for inlining into a partial-index DDL statement. * Used by `DatabaseTable.addIndex` when the user passes an object `where` on `@Index` / * `@Unique`. Strings are
(entityName: EntityName<T>, where: string | FilterQuery<T>)
| 2968 | * `@Unique`. Strings are returned unchanged. |
| 2969 | */ |
| 2970 | renderPartialIndexWhere<T extends object>(entityName: EntityName<T>, where: string | FilterQuery<T>): string { |
| 2971 | if (typeof where === 'string') { |
| 2972 | return where; |
| 2973 | } |
| 2974 | |
| 2975 | const name = Utils.className(entityName); |
| 2976 | |
| 2977 | if (where == null || (Utils.isPlainObject(where) && Object.keys(where as Dictionary).length === 0)) { |
| 2978 | throw new Error(`Cannot render partial-index predicate for entity '${name}': \`where\` is empty.`); |
| 2979 | } |
| 2980 | |
| 2981 | const alias = '__p'; |
| 2982 | const qb = this.createQueryBuilder(entityName, undefined, undefined, undefined, undefined, alias); |
| 2983 | qb.where(where as any); |
| 2984 | const sql = qb.getFormattedQuery(); |
| 2985 | |
| 2986 | // Relation traversal produces join clauses whose aliased identifiers can't be inlined |
| 2987 | // into a CREATE INDEX ... WHERE clause — reject with a clear error rather than emitting broken DDL. |
| 2988 | if (/\bjoin\b/i.test(sql.split(/\bwhere\b/i)[0])) { |
| 2989 | throw new Error( |
| 2990 | `Cannot render partial-index predicate for entity '${name}': \`where\` may not traverse relations.`, |
| 2991 | ); |
| 2992 | } |
| 2993 | |
| 2994 | // Anchor at end-of-string only — the synthetic QB has no top-level order by / limit / |
| 2995 | // group by / having / offset, so any such keyword inside the captured predicate is |
| 2996 | // inside a subquery and must not terminate the match. |
| 2997 | const match = /\bwhere\s+([\s\S]+)$/i.exec(sql); |
| 2998 | |
| 2999 | if (!match) { |
| 3000 | throw new Error(`Failed to render partial-index predicate for entity '${name}': ${sql}`); |
| 3001 | } |
| 3002 | |
| 3003 | const quote = (s: string) => this.platform.quoteIdentifier(s); |
| 3004 | const aliasPrefix = new RegExp(`${quote(alias).replace(/[[\]]/g, '\\$&')}\\.`, 'g'); |
| 3005 | const stripped = match[1].replace(aliasPrefix, '').trim(); |
| 3006 | |
| 3007 | // Any qualified column reference remaining after the alias strip points at another table or |
| 3008 | // subquery and can't be inlined into a CREATE INDEX ... WHERE predicate. Covers both |
| 3009 | // QB-generated sub-aliases (quoted, e.g. `"e0"."col"`) and raw fragments with bare refs |
| 3010 | // (e.g. `raw('other_table.col = 1')`). String literals are erased first so dots inside |
| 3011 | // them (e.g. JSON path operands like `'$.path'`) don't trip the guard. |
| 3012 | // Both patterns use a `(?!\s*\()` lookahead so schema-qualified function calls |
| 3013 | // (`pg_catalog.lower(name)`, `"public".my_func(col)`) are accepted — only `<id>.<id>` not |
| 3014 | // followed by `(` is treated as a cross-table column reference. |
| 3015 | const withoutStrings = stripped.replace(/'(?:[^']|'')*'/g, "''"); |
| 3016 | const quotedIdent = String.raw`(?:"(?:[^"]|"")+"|\`(?:[^\`]|\`\`)+\`|\[(?:[^\]]|\]\])+\])`; |
| 3017 | const anyIdent = `(?:${quotedIdent}|[A-Za-z_]\\w*)`; |
| 3018 | const quotedCrossRef = new RegExp(`${quotedIdent}\\s*\\.\\s*${anyIdent}(?!\\s*\\()`); |
| 3019 | const bareCrossRef = /\b[A-Za-z_]\w*\s*\.\s*[A-Za-z_]\w*\b(?!\s*\()/; |
| 3020 | if (quotedCrossRef.test(withoutStrings) || bareCrossRef.test(withoutStrings)) { |
| 3021 | throw new Error( |
| 3022 | `Cannot render partial-index predicate for entity '${name}': \`where\` references another table or subquery which cannot be inlined into a CREATE INDEX ... WHERE clause.`, |
| 3023 | ); |
| 3024 | } |
| 3025 | |
| 3026 | return stripped; |
| 3027 | } |
nothing calls this directly
no test coverage detected