From 1613777b368ec173a5fa9c5277c7313088e02054 Mon Sep 17 00:00:00 2001 From: Rob Smith Date: Sun, 9 Sep 2012 17:54:34 -0700 Subject: [PATCH] Use QDUI Query Rewriter which supports a much wider set of queries to explain. Fixes #17 --- lib/QueryExplain.php | 24 ++-- lib/QueryRewrite.php | 117 +++++++++++++++++++ lib/TestQueryRewrite.php | 246 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 376 insertions(+), 11 deletions(-) create mode 100644 lib/QueryRewrite.php create mode 100644 lib/TestQueryRewrite.php diff --git a/lib/QueryExplain.php b/lib/QueryExplain.php index eaeab15e..6c442a8d 100644 --- a/lib/QueryExplain.php +++ b/lib/QueryExplain.php @@ -43,6 +43,8 @@ */ require "QueryTableParser.php"; +require_once "QueryRewrite.php"; + class QueryExplain { private $get_connection_func; @@ -143,13 +145,10 @@ public function explain() { if (!isset($this->mysqli)) { return null; } - - if (!preg_match("/^\s*\(?\s*(EXPLAIN)?\s*SELECT/i", $this->query)) { - return null; - } - + try { $result = $this->explain_query($this->query); + if ($this->mysqli->errno) { return $this->mysqli->error . " (" . $this->mysqli->errno . ")"; } @@ -157,6 +156,7 @@ public function explain() { if (!$result) { return "unknown error getting explain plan\n"; } + return $this->result_as_table($result); } catch (Exception $e) { return $e->getMessage(); @@ -177,7 +177,7 @@ private function connect() { throw new Exception("Missing field {$r}"); } } - + try { $this->mysqli = new mysqli(); $this->mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, self::$CONNECT_TIMEOUT); @@ -202,11 +202,13 @@ private function connect() { * @return MySQLi_Result the result handle */ private function explain_query() { - if (preg_match("/^\s*EXPLAIN/i", $this->query)) - { - return $this->mysqli->query($this->query); - } - return $this->mysqli->query("EXPLAIN " . $this->query); + $Query = new QueryRewrite($this->query); + $explain = $Query->asExplain(); + + if (is_null($explain)) + return null; + + return $this->mysqli->query($explain); } /** diff --git a/lib/QueryRewrite.php b/lib/QueryRewrite.php new file mode 100644 index 00000000..5ca01283 --- /dev/null +++ b/lib/QueryRewrite.php @@ -0,0 +1,117 @@ +setQuery($sql); + } + + public function setQuery($sql = null) { + $this->type = self::UNKNOWN; + $this->sql = $sql; + if (is_null($this->sql)) + return; + // Remove comments + $this->sql = preg_replace(self::COMMENTS_C, '', $this->sql); + $this->sql = preg_replace(self::COMMENTS_HASH, '', $this->sql); + $this->sql = preg_replace(self::COMMENTS_SQL, '', $this->sql); + // Remove whitespace + $this->sql = trim($this->sql); + $this->sql = str_replace("\n", " ", $this->sql); + $this->figureOutType(); + } + + private function figureOutType(){ + if (preg_match('/^SELECT\s/i', $this->sql)) + $this->type = self::SELECT; + elseif (preg_match('/^DELETE\s+FROM\s/i', $this->sql)) + $this->type = self::DELETE; + elseif (preg_match('/^DELETE\s+'.self::TABLEREF.'\s+FROM\s/i', $this->sql)) + $this->type = self::DELETEMULTI; + elseif (preg_match('/^INSERT\s+INTO\s'.self::TABLEREF.'[\s\(]*SELECT\s+/i', $this->sql)) + $this->type = self::INSERTSELECT; + elseif (preg_match('/^INSERT\s+INTO\s/i', $this->sql)) + $this->type = self::INSERT; + elseif (preg_match('/^(.*)\s+UNION\s+(.*)$/i', $this->sql)) + $this->type = self::UNION; + elseif (preg_match('/^UPDATE\s/i', $this->sql)) + $this->type = self::UPDATE; + elseif (preg_match('/^ALTER\s/i', $this->sql)) + $this->type = self::ALTER; + elseif (preg_match('/^CREATE\s/i', $this->sql)) + $this->type = self::CREATE; + elseif (preg_match('/^DROP\s/i', $this->sql)) + $this->type = self::DROP; + else + $this->type = self::UNKNOWN; + } + + public function getType() { + return $this->type; + } + + public function toSelect() { + $select = ''; + switch ($this->type) { + case self::SELECT: + case self::UNION: + $select = $this->sql; + break; + case self::DELETE: + $select = preg_replace('/^DELETE\s+FROM\s/i', 'SELECT 0 FROM ', $this->sql); + break; + case self::DELETEMULTI: + $select = preg_replace('/^DELETE\s+'.self::TABLEREF.'\s+FROM\s/i', 'SELECT 0 FROM ', $this->sql); + break; + case self::UPDATE: + preg_match('/^UPDATE\s+(.*)\s+SET\s+(.*)\s+WHERE\s+(.*)$/i', $this->sql, $subpatterns); + $select = "SELECT {$subpatterns[2]} FROM {$subpatterns[1]} WHERE {$subpatterns[3]}"; + break; + case self::INSERTSELECT: + if (preg_match('/\(\s*(SELECT\s+.*)\)/', $this->sql, $subpatterns)) + $select = trim("{$subpatterns[1]}"); + else + preg_match('/^INSERT\s+INTO\s+\w+\s+(SELECT.*)/i', $this->sql, $subpatterns); + $select = trim("{$subpatterns[1]}"); + break; + default: + return null; + } + return trim($select); + } + + public function asExplain() { + $sql = $this->toSelect(); + if (is_null($sql)) + return null; + return "EXPLAIN $sql"; + } + + public function asExtendedExplain() { + $sql = $this->toSelect(); + if (is_null($sql)) + return null; + return "EXPLAIN EXTENDED $sql"; + } + } diff --git a/lib/TestQueryRewrite.php b/lib/TestQueryRewrite.php new file mode 100644 index 00000000..121d215f --- /dev/null +++ b/lib/TestQueryRewrite.php @@ -0,0 +1,246 @@ +_QueryRewrite = new QueryRewrite(); + } + + public function tearDown() { + unset($this->_QueryRewrite); + } + + public function testSelect() { + $sql = 'SELECT 123'; + $expectedType = QueryRewrite::SELECT; + $expectedSelect = 'SELECT 123'; + $expectedExplain = "EXPLAIN $expectedSelect"; + $expectedExtendedExplain = "EXPLAIN EXTENDED $expectedSelect"; + $this->_QueryRewrite->setQuery($sql); + $this->assertEquals($expectedType, $this->_QueryRewrite->getType()); + $this->assertEquals($expectedSelect, $this->_QueryRewrite->toSelect()); + $this->assertEquals($expectedExplain, $this->_QueryRewrite->asExplain()); + $this->assertEquals($expectedExtendedExplain, $this->_QueryRewrite->asExtendedExplain()); + } + + public function testSelectWithComments() { + $sql = '/* 123 * 456 */ SELECT 123 #214241*/'; + $expectedType = QueryRewrite::SELECT; + $expectedSelect = 'SELECT 123'; + $expectedExplain = "EXPLAIN $expectedSelect"; + $expectedExtendedExplain = "EXPLAIN EXTENDED $expectedSelect"; + $this->_QueryRewrite->setQuery($sql); + $this->assertEquals($expectedType, $this->_QueryRewrite->getType()); + $this->assertEquals($expectedSelect, $this->_QueryRewrite->toSelect()); + $this->assertEquals($expectedExplain, $this->_QueryRewrite->asExplain()); + $this->assertEquals($expectedExtendedExplain, $this->_QueryRewrite->asExtendedExplain()); + } + + public function testSelectWithWhitespace() { + $sql = "\t\t SELECT \t\t\t 123\t\t\t \t\t\t\n"; + $expectedType = QueryRewrite::SELECT; + $expectedSelect = "SELECT \t\t\t 123"; + $expectedExplain = "EXPLAIN $expectedSelect"; + $expectedExtendedExplain = "EXPLAIN EXTENDED $expectedSelect"; + $this->_QueryRewrite->setQuery($sql); + $this->assertEquals($expectedType, $this->_QueryRewrite->getType()); + $this->assertEquals($expectedSelect, $this->_QueryRewrite->toSelect()); + $this->assertEquals($expectedExplain, $this->_QueryRewrite->asExplain()); + $this->assertEquals($expectedExtendedExplain, $this->_QueryRewrite->asExtendedExplain()); + } + + public function testSelectBug1() { + $sql = "/* Script : /home/site/XXXXXXXXXX.php Utilisateur : prod_outils */ SELECT SQL_SMALL_RESULT DISTINCT ID_CLIENT +FROM DB.CLIENT CL +WHERE CL.TELEPHONE = '0000000000' +LIMIT 1 +"; + $expectedType = QueryRewrite::SELECT; + $expectedSelect = "SELECT SQL_SMALL_RESULT DISTINCT ID_CLIENT FROM DB.CLIENT CL WHERE CL.TELEPHONE = '0000000000' LIMIT 1"; + $expectedExplain = "EXPLAIN $expectedSelect"; + $expectedExtendedExplain = "EXPLAIN EXTENDED $expectedSelect"; + $this->_QueryRewrite->setQuery($sql); + $this->assertEquals($expectedType, $this->_QueryRewrite->getType()); + $this->assertEquals($expectedSelect, $this->_QueryRewrite->toSelect()); + $this->assertEquals($expectedExplain, $this->_QueryRewrite->asExplain()); + $this->assertEquals($expectedExtendedExplain, $this->_QueryRewrite->asExtendedExplain()); + } + + public function testSelectBug2() { + $sql = "SELECT id FROM ips WHERE ip='192.168.0.1' AND type=1 LIMIT 1"; + $expectedType = QueryRewrite::SELECT; + $expectedSelect = "SELECT id FROM ips WHERE ip='192.168.0.1' AND type=1 LIMIT 1"; + $expectedExplain = "EXPLAIN $expectedSelect"; + $expectedExtendedExplain = "EXPLAIN EXTENDED $expectedSelect"; + $this->_QueryRewrite->setQuery($sql); + $this->assertEquals($expectedType, $this->_QueryRewrite->getType()); + $this->assertEquals($expectedSelect, $this->_QueryRewrite->toSelect()); + $this->assertEquals($expectedExplain, $this->_QueryRewrite->asExplain()); + $this->assertEquals($expectedExtendedExplain, $this->_QueryRewrite->asExtendedExplain()); + } + + public function testSelectBug3() { + $sql = "INSERT INTO tbl_old( +SELECT * +FROM tbl +WHERE id =123 )"; + $expectedType = QueryRewrite::INSERTSELECT; + $expectedSelect = "SELECT * FROM tbl WHERE id =123"; + $expectedExplain = "EXPLAIN $expectedSelect"; + $expectedExtendedExplain = "EXPLAIN EXTENDED $expectedSelect"; + $this->_QueryRewrite->setQuery($sql); + $this->assertEquals($expectedType, $this->_QueryRewrite->getType()); + $this->assertEquals($expectedSelect, $this->_QueryRewrite->toSelect()); + $this->assertEquals($expectedExplain, $this->_QueryRewrite->asExplain()); + $this->assertEquals($expectedExtendedExplain, $this->_QueryRewrite->asExtendedExplain()); + } + + public function testConstructor() { + $sql = "SELECT id FROM ips WHERE ip='192.168.0.1' AND type=1 LIMIT 1"; + $expectedType = QueryRewrite::SELECT; + $expectedSelect = "SELECT id FROM ips WHERE ip='192.168.0.1' AND type=1 LIMIT 1"; + $expectedExplain = "EXPLAIN $expectedSelect"; + $expectedExtendedExplain = "EXPLAIN EXTENDED $expectedSelect"; + $QRW = new QueryRewrite($sql); + $this->assertEquals($expectedType, $QRW->getType()); + $this->assertEquals($expectedSelect, $QRW->toSelect()); + $this->assertEquals($expectedExplain, $QRW->asExplain()); + $this->assertEquals($expectedExtendedExplain, $QRW->asExtendedExplain()); + unset($QRW); + } + + public function testDelete() { + $sql = 'DELETE FROM table WHERE id = 123'; + $expectedType = QueryRewrite::DELETE; + $expectedSelect = 'SELECT 0 FROM table WHERE id = 123'; + $expectedExplain = "EXPLAIN $expectedSelect"; + $expectedExtendedExplain = "EXPLAIN EXTENDED $expectedSelect"; + $this->_QueryRewrite->setQuery($sql); + $this->assertEquals($expectedType, $this->_QueryRewrite->getType()); + $this->assertEquals($expectedSelect, $this->_QueryRewrite->toSelect()); + $this->assertEquals($expectedExplain, $this->_QueryRewrite->asExplain()); + $this->assertEquals($expectedExtendedExplain, $this->_QueryRewrite->asExtendedExplain()); + } + + public function testInsert() { + $sql = 'INSERT INTO table (col1, col2, col3) VALUES (1, 2, 3), (4, 5, 6)'; + $expectedType = QueryRewrite::INSERT; + $expectedSelect = NULL; + $expectedExplain = NULL; + $expectedExtendedExplain = NULL; + $this->_QueryRewrite->setQuery($sql); + $this->assertEquals($expectedType, $this->_QueryRewrite->getType()); + $this->assertEquals($expectedSelect, $this->_QueryRewrite->toSelect()); + $this->assertEquals($expectedExplain, $this->_QueryRewrite->asExplain()); + $this->assertEquals($expectedExtendedExplain, $this->_QueryRewrite->asExtendedExplain()); + } + + public function testInsertSelect() { + $sql = 'INSERT INTO table SELECT * FROM table2'; + $expectedType = QueryRewrite::INSERTSELECT; + $expectedSelect = 'SELECT * FROM table2'; + $expectedExplain = "EXPLAIN $expectedSelect"; + $expectedExtendedExplain = "EXPLAIN EXTENDED $expectedSelect"; + $this->_QueryRewrite->setQuery($sql); + $this->assertEquals($expectedType, $this->_QueryRewrite->getType()); + $this->assertEquals($expectedSelect, $this->_QueryRewrite->toSelect()); + $this->assertEquals($expectedExplain, $this->_QueryRewrite->asExplain()); + $this->assertEquals($expectedExtendedExplain, $this->_QueryRewrite->asExtendedExplain()); + } + + public function testInsertSelect1() { + $sql = 'INSERT INTO table (SELECT * FROM table2)'; + $expectedType = QueryRewrite::INSERTSELECT; + $expectedSelect = 'SELECT * FROM table2'; + $expectedExplain = "EXPLAIN $expectedSelect"; + $expectedExtendedExplain = "EXPLAIN EXTENDED $expectedSelect"; + $this->_QueryRewrite->setQuery($sql); + $this->assertEquals($expectedType, $this->_QueryRewrite->getType()); + $this->assertEquals($expectedSelect, $this->_QueryRewrite->toSelect()); + $this->assertEquals($expectedExplain, $this->_QueryRewrite->asExplain()); + $this->assertEquals($expectedExtendedExplain, $this->_QueryRewrite->asExtendedExplain()); + } + + public function testUpdate() { + $sql = 'UPDATE table SET col1 = 123 WHERE id = 456'; + $expectedType = QueryRewrite::UPDATE; + $expectedSelect = 'SELECT col1 = 123 FROM table WHERE id = 456'; + $expectedExplain = "EXPLAIN $expectedSelect"; + $expectedExtendedExplain = "EXPLAIN EXTENDED $expectedSelect"; + $this->_QueryRewrite->setQuery($sql); + $this->assertEquals($expectedType, $this->_QueryRewrite->getType()); + $this->assertEquals($expectedSelect, $this->_QueryRewrite->toSelect()); + $this->assertEquals($expectedExplain, $this->_QueryRewrite->asExplain()); + $this->assertEquals($expectedExtendedExplain, $this->_QueryRewrite->asExtendedExplain()); + } + + public function testAlter() { + $sql = 'ALTER TABLE MODIFY col1 INT NOT NULL DEFAULT 0'; + $expectedType = QueryRewrite::ALTER; + $expectedSelect = NULL; + $expectedExplain = NULL; + $expectedExtendedExplain = NULL; + $this->_QueryRewrite->setQuery($sql); + $this->assertEquals($expectedType, $this->_QueryRewrite->getType()); + $this->assertEquals($expectedSelect, $this->_QueryRewrite->toSelect()); + $this->assertEquals($expectedExplain, $this->_QueryRewrite->asExplain()); + $this->assertEquals($expectedExtendedExplain, $this->_QueryRewrite->asExtendedExplain()); + } + + public function testDrop() { + $sql = 'DROP TABLE table'; + $expectedType = QueryRewrite::DROP; + $expectedSelect = NULL; + $expectedExplain = NULL; + $expectedExtendedExplain = NULL; + $this->_QueryRewrite->setQuery($sql); + $this->assertEquals($expectedType, $this->_QueryRewrite->getType()); + $this->assertEquals($expectedSelect, $this->_QueryRewrite->toSelect()); + $this->assertEquals($expectedExplain, $this->_QueryRewrite->asExplain()); + $this->assertEquals($expectedExtendedExplain, $this->_QueryRewrite->asExtendedExplain()); + } + + public function testCreate() { + $sql = 'CREATE TABLE table (id INT)'; + $expectedType = QueryRewrite::CREATE; + $expectedSelect = NULL; + $expectedExplain = NULL; + $expectedExtendedExplain = NULL; + $this->_QueryRewrite->setQuery($sql); + $this->assertEquals($expectedType, $this->_QueryRewrite->getType()); + $this->assertEquals($expectedSelect, $this->_QueryRewrite->toSelect()); + $this->assertEquals($expectedExplain, $this->_QueryRewrite->asExplain()); + $this->assertEquals($expectedExtendedExplain, $this->_QueryRewrite->asExtendedExplain()); + } + + public function testDeleteMulti() { + $sql = 'DELETE table1 FROM table1 JOIN table2 ON table1.id = table2.id WHERE table1.col1 = 123 AND table2.col2 = 456 '; + $expectedType = QueryRewrite::DELETEMULTI; + $expectedSelect = 'SELECT 0 FROM table1 JOIN table2 ON table1.id = table2.id WHERE table1.col1 = 123 AND table2.col2 = 456'; + $expectedExplain = "EXPLAIN $expectedSelect"; + $expectedExtendedExplain = "EXPLAIN EXTENDED $expectedSelect"; + $this->_QueryRewrite->setQuery($sql); + $this->assertEquals($expectedType, $this->_QueryRewrite->getType()); + $this->assertEquals($expectedSelect, $this->_QueryRewrite->toSelect()); + $this->assertEquals($expectedExplain, $this->_QueryRewrite->asExplain()); + $this->assertEquals($expectedExtendedExplain, $this->_QueryRewrite->asExtendedExplain()); + } + + public function testUnion() { + $sql = '(SELECT 123) UNION (SELECT 456)'; + $expectedType = QueryRewrite::UNION; + $expectedSelect = '(SELECT 123) UNION (SELECT 456)'; + $expectedExplain = "EXPLAIN $expectedSelect"; + $expectedExtendedExplain = "EXPLAIN EXTENDED $expectedSelect"; + $this->_QueryRewrite->setQuery($sql); + $this->assertEquals($expectedType, $this->_QueryRewrite->getType()); + $this->assertEquals($expectedSelect, $this->_QueryRewrite->toSelect()); + $this->assertEquals($expectedExplain, $this->_QueryRewrite->asExplain()); + $this->assertEquals($expectedExtendedExplain, $this->_QueryRewrite->asExtendedExplain()); + } + + + }