diff --git a/jetbrains-plugin/src/main/kotlin/com/mariadbprofiler/plugin/model/QueryEntry.kt b/jetbrains-plugin/src/main/kotlin/com/mariadbprofiler/plugin/model/QueryEntry.kt index 1d2184e..211955a 100644 --- a/jetbrains-plugin/src/main/kotlin/com/mariadbprofiler/plugin/model/QueryEntry.kt +++ b/jetbrains-plugin/src/main/kotlin/com/mariadbprofiler/plugin/model/QueryEntry.kt @@ -23,8 +23,9 @@ data class QueryEntry( * Query with `?` placeholders replaced by bound values. * * Only replaces `?` characters that appear **outside** single-quoted SQL - * string literals. Doubled single-quotes (`''`) inside a literal are - * treated as an escaped quote and do not toggle the "inside string" state. + * string literals. Handles both doubled single-quotes (`''`) and backslash + * escapes (`\'`, `\\`, etc.) inside literals, matching MySQL/MariaDB default + * behavior (when `NO_BACKSLASH_ESCAPES` is not set). * * Param values are wrapped in single quotes with internal single-quotes * doubled (`O'Brien` → `'O''Brien'`); NULL params are emitted bare. @@ -42,7 +43,17 @@ data class QueryEntry( val ch = query[i] if (inString) { - if (ch == '\'') { + if (ch == '\\') { + /* backslash escape: \' \\ etc. – copy both chars, stay in string */ + if (i + 1 < query.length) { + sb.append(ch) + sb.append(query[i + 1]) + i += 2 + continue + } + /* trailing backslash – just append it */ + sb.append(ch) + } else if (ch == '\'') { /* '' inside a literal is an escaped quote – stay in string */ if (i + 1 < query.length && query[i + 1] == '\'') { sb.append("''") diff --git a/jetbrains-plugin/src/test/kotlin/com/mariadbprofiler/plugin/model/QueryEntryTest.kt b/jetbrains-plugin/src/test/kotlin/com/mariadbprofiler/plugin/model/QueryEntryTest.kt index a7f7a1a..dba0d33 100644 --- a/jetbrains-plugin/src/test/kotlin/com/mariadbprofiler/plugin/model/QueryEntryTest.kt +++ b/jetbrains-plugin/src/test/kotlin/com/mariadbprofiler/plugin/model/QueryEntryTest.kt @@ -88,4 +88,110 @@ class QueryEntryTest { assertEquals(42, entry.backtrace[0].line) assertEquals("UserController.php:42", entry.sourceFile) } + + @Test + fun `boundQuery replaces placeholders with params`() { + val entry = QueryEntry( + query = "SELECT * FROM users WHERE name = ? AND age = ?", + params = listOf("John", "25") + ) + assertEquals("SELECT * FROM users WHERE name = 'John' AND age = '25'", entry.boundQuery) + } + + @Test + fun `boundQuery handles NULL params`() { + val entry = QueryEntry( + query = "SELECT * FROM users WHERE name = ? AND email = ?", + params = listOf("John", null) + ) + assertEquals("SELECT * FROM users WHERE name = 'John' AND email = NULL", entry.boundQuery) + } + + @Test + fun `boundQuery escapes single quotes in params`() { + val entry = QueryEntry( + query = "SELECT * FROM users WHERE name = ?", + params = listOf("O'Brien") + ) + assertEquals("SELECT * FROM users WHERE name = 'O''Brien'", entry.boundQuery) + } + + @Test + fun `boundQuery does not replace placeholders inside single-quoted strings`() { + val entry = QueryEntry( + query = "SELECT * FROM users WHERE name = 'test?' AND age = ?", + params = listOf("25") + ) + assertEquals("SELECT * FROM users WHERE name = 'test?' AND age = '25'", entry.boundQuery) + } + + @Test + fun `boundQuery handles doubled single-quotes inside strings`() { + val entry = QueryEntry( + query = "SELECT * FROM users WHERE name = 'O''Brien' AND age = ?", + params = listOf("25") + ) + assertEquals("SELECT * FROM users WHERE name = 'O''Brien' AND age = '25'", entry.boundQuery) + } + + @Test + fun `boundQuery handles backslash-escaped single quotes`() { + val entry = QueryEntry( + query = "SELECT * FROM users WHERE name = 'O\\'Brien' AND age = ?", + params = listOf("25") + ) + assertEquals("SELECT * FROM users WHERE name = 'O\\'Brien' AND age = '25'", entry.boundQuery) + } + + @Test + fun `boundQuery handles backslash-escaped backslashes`() { + val entry = QueryEntry( + query = "SELECT * FROM paths WHERE path = 'C:\\\\Users\\\\test' AND id = ?", + params = listOf("1") + ) + assertEquals("SELECT * FROM paths WHERE path = 'C:\\\\Users\\\\test' AND id = '1'", entry.boundQuery) + } + + @Test + fun `boundQuery handles mixed escape sequences`() { + val entry = QueryEntry( + query = "SELECT * FROM data WHERE value = 'test\\nline' AND name = ?", + params = listOf("John") + ) + assertEquals("SELECT * FROM data WHERE value = 'test\\nline' AND name = 'John'", entry.boundQuery) + } + + @Test + fun `boundQuery handles placeholder at end of backslash-escaped string`() { + val entry = QueryEntry( + query = "SELECT * FROM users WHERE name = 'can\\'t' AND status = ?", + params = listOf("active") + ) + assertEquals("SELECT * FROM users WHERE name = 'can\\'t' AND status = 'active'", entry.boundQuery) + } + + @Test + fun `boundQuery returns null when no params`() { + val entry = QueryEntry(query = "SELECT * FROM users") + assertEquals(null, entry.boundQuery) + } + + @Test + fun `boundQuery handles more placeholders than params`() { + val entry = QueryEntry( + query = "SELECT * FROM users WHERE name = ? AND age = ? AND email = ?", + params = listOf("John", "25") + ) + // Should replace first two, leave third as-is + assertEquals("SELECT * FROM users WHERE name = 'John' AND age = '25' AND email = ?", entry.boundQuery) + } + + @Test + fun `boundQuery handles complex nested strings`() { + val entry = QueryEntry( + query = "SELECT * FROM logs WHERE msg = 'User said \\'hello\\'' AND id = ?", + params = listOf("123") + ) + assertEquals("SELECT * FROM logs WHERE msg = 'User said \\'hello\\'' AND id = '123'", entry.boundQuery) + } }