(statement: string)
| 89 | * @throws Error if numbered/named parameters are not sequential starting from 1 |
| 90 | */ |
| 91 | export function countParameters(statement: string): number { |
| 92 | const style = detectParameterStyle(statement); |
| 93 | // Strip comments and strings to avoid matching parameters inside them |
| 94 | const cleanedSQL = stripCommentsAndStrings(statement); |
| 95 | |
| 96 | switch (style) { |
| 97 | case "numbered": { |
| 98 | // Extract all $N parameters and get unique indices |
| 99 | const matches = cleanedSQL.match(/\$\d+/g); |
| 100 | if (!matches) return 0; |
| 101 | const numbers = matches.map((m) => parseInt(m.slice(1), 10)); |
| 102 | const uniqueIndices = Array.from(new Set(numbers)).sort((a, b) => a - b); |
| 103 | |
| 104 | // Validate parameters are sequential starting from 1 |
| 105 | const maxIndex = Math.max(...uniqueIndices); |
| 106 | for (let i = 1; i <= maxIndex; i++) { |
| 107 | if (!uniqueIndices.includes(i)) { |
| 108 | throw new Error( |
| 109 | `Non-sequential numbered parameters detected. Found placeholders: ${uniqueIndices.map(n => `$${n}`).join(', ')}. ` + |
| 110 | `Parameters must be sequential starting from $1 (missing $${i}).` |
| 111 | ); |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | return maxIndex; |
| 116 | } |
| 117 | case "named": { |
| 118 | // Extract all @pN parameters and get unique indices |
| 119 | const matches = cleanedSQL.match(/@p\d+/g); |
| 120 | if (!matches) return 0; |
| 121 | const numbers = matches.map((m) => parseInt(m.slice(2), 10)); |
| 122 | const uniqueIndices = Array.from(new Set(numbers)).sort((a, b) => a - b); |
| 123 | |
| 124 | // Validate parameters are sequential starting from 1 |
| 125 | const maxIndex = Math.max(...uniqueIndices); |
| 126 | for (let i = 1; i <= maxIndex; i++) { |
| 127 | if (!uniqueIndices.includes(i)) { |
| 128 | throw new Error( |
| 129 | `Non-sequential named parameters detected. Found placeholders: ${uniqueIndices.map(n => `@p${n}`).join(', ')}. ` + |
| 130 | `Parameters must be sequential starting from @p1 (missing @p${i}).` |
| 131 | ); |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | return maxIndex; |
| 136 | } |
| 137 | case "positional": { |
| 138 | // Count question marks (positional parameters don't have this issue) |
| 139 | return (cleanedSQL.match(/\?/g) || []).length; |
| 140 | } |
| 141 | default: |
| 142 | return 0; |
| 143 | } |
| 144 | } |
| 145 | |
| 146 | /** |
| 147 | * Validate that parameter definitions match the SQL statement |
no test coverage detected