From 354470829255215395529ac94ab448ce8e2f614b Mon Sep 17 00:00:00 2001 From: Andrew Svirin Date: Tue, 5 Oct 2021 16:28:07 +0300 Subject: [PATCH 1/3] Add handling of command SHOW FULL COLUMNS FOR {table_name} Handling multiple databases for create queries, then sql is CREATE TABLE `database`.`table` and in same script is `information_schema`.`table` Fixing PDO::getAttribute() missing method Handling Multiple insert sql. --- src/FakePdoStatementTrait.php | 39 ++++++++- src/FakePdoTrait.php | 12 ++- src/Parser/CreateTableParser.php | 8 +- src/Parser/InsertMultipleParser.php | 79 ++++++++++++++++++ src/Parser/SQLParser.php | 10 ++- src/Parser/ShowParser.php | 45 +++++++++- src/Processor/ShowColumnsProcessor.php | 111 +++++++++++++++++++++++++ src/Query/ShowColumnsQuery.php | 34 ++++++++ 8 files changed, 327 insertions(+), 11 deletions(-) create mode 100644 src/Parser/InsertMultipleParser.php create mode 100644 src/Processor/ShowColumnsProcessor.php create mode 100644 src/Query/ShowColumnsQuery.php diff --git a/src/FakePdoStatementTrait.php b/src/FakePdoStatementTrait.php index e8d2967c..d4e905d9 100644 --- a/src/FakePdoStatementTrait.php +++ b/src/FakePdoStatementTrait.php @@ -137,12 +137,18 @@ public function universalExecute(?array $params = null) $create_queries = (new Parser\CreateTableParser())->parse($sql); foreach ($create_queries as $create_query) { + if (strpos($create_query->name, '.')) { + list($databaseName, $tableName) = explode('.', $create_query->name, 2); + } else { + $databaseName = $this->conn->getDatabaseName(); + $tableName = $create_query->name; + } $this->conn->getServer()->addTableDefinition( - $this->conn->getDatabaseName(), - $create_query->name, + $databaseName, + $tableName, Processor\CreateProcessor::makeTableDefinition( $create_query, - $this->conn->getDatabaseName() + $databaseName ) ); } @@ -150,6 +156,22 @@ public function universalExecute(?array $params = null) return true; } + // Check that there are multiple INSERT commands in the sql. + $insertPos1 = stripos($sql, 'INSERT INTO'); + $insertPos2 = strripos($sql, 'INSERT INTO'); + if (false !== $insertPos1 && $insertPos1 !== $insertPos2) { + $insert_queries = (new Parser\InsertMultipleParser())->parse($sql); + foreach ($insert_queries as $insert_query) { + $this->affectedRows += Processor\InsertProcessor::process( + $this->conn, + new Processor\Scope($this->boundValues), + $insert_query + ); + } + + return true; + } + //echo "\n" . $sql . "\n"; try { @@ -284,6 +306,17 @@ function ($row) { ); break; + case Query\ShowColumnsQuery::class: + $this->result = self::processResult( + $this->conn, + Processor\ShowColumnsProcessor::process( + $this->conn, + new Processor\Scope(array_merge($params ?? [], $this->boundValues)), + $parsed_query + ) + ); + break; + default: throw new \UnexpectedValueException('Unsupported operation type ' . $sql); } diff --git a/src/FakePdoTrait.php b/src/FakePdoTrait.php index 46bb44c6..c7953138 100644 --- a/src/FakePdoTrait.php +++ b/src/FakePdoTrait.php @@ -52,7 +52,7 @@ public function __construct(string $dsn, string $username = '', string $passwd = $dsn = \Nyholm\Dsn\DsnParser::parse($dsn); $host = $dsn->getHost(); - if (preg_match('/dbname=([a-zA-Z0-9_]+);/', $host, $matches)) { + if (preg_match('/dbname=([a-zA-Z0-9_]+)(?:;|$)/', $host, $matches)) { $this->databaseName = $matches[1]; } @@ -87,6 +87,16 @@ public function setAttribute($key, $value) return true; } + public function getAttribute($key) + { + switch ($key) { + case \PDO::ATTR_CASE: + $value = $this->lowercaseResultKeys ? \PDO::CASE_LOWER : \PDO::CASE_UPPER; + } + + return $value; + } + public function getServer() : Server { return $this->server; diff --git a/src/Parser/CreateTableParser.php b/src/Parser/CreateTableParser.php index 6d24a881..e3fc6091 100644 --- a/src/Parser/CreateTableParser.php +++ b/src/Parser/CreateTableParser.php @@ -218,7 +218,13 @@ private static function parseCreateTable(array $tokens, string $sql) : CreateQue \array_shift($tokens); } - $t = \array_shift($tokens); + // Extract [{database}.]{table} + if ($tokens[1] === '.') { + $t = \array_shift($tokens) . \array_shift($tokens) . \array_shift($tokens); + } else { + $t = \array_shift($tokens); + } + $name = static::decodeIdentifier($t); if (static::nextTokenIs($tokens, 'LIKE')) { diff --git a/src/Parser/InsertMultipleParser.php b/src/Parser/InsertMultipleParser.php new file mode 100644 index 00000000..7af8c59c --- /dev/null +++ b/src/Parser/InsertMultipleParser.php @@ -0,0 +1,79 @@ + + */ + public function parse(string $sql): array + { + return self::walk($this->splitStatements($sql)); + } + + /** + * @var list + */ + private $tokens = []; + + /** + * @var array + */ + private $sourceMap = []; + + /** + * @return non-empty-list + */ + private function splitStatements(string $sql): array + { + $re_split_sql = '% + # Match an SQL record ending with ";" + \s* # Discard leading whitespace. + ( # $1: Trimmed non-empty SQL record. + (?: # Group for content alternatives. + \'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\' # Either a single quoted string, + | "[^"\\\\]*(?:\\\\.[^"\\\\]*)*" # or a double quoted string, + | /\*[^*]*\*+(?:[^*/][^*]*\*+)*/ # or a multi-line comment, + | \#.* # or a # single line comment, + | --.* # or a -- single line comment, + | [^"\';#] # or one non-["\';#-] + )+ # One or more content alternatives + (?:;|$) # Record end is a ; or string end. + ) # End $1: Trimmed SQL record. + %xs'; + + if (preg_match_all($re_split_sql, $sql, $matches)) { + $statements = $matches[1]; + } + + return $statements ?? []; + } + + /** + * @param array $statements + * + * @return array + */ + private static function walk(array $statements) + { + $result = []; + + foreach ($statements as $statement) { + $statement = trim($statement); + if (false === stripos($statement, 'INSERT INTO')) { + continue; + } + $statement = rtrim($statement, ';'); + + $result[] = SQLParser::parse($statement); + } + + return $result; + } +} diff --git a/src/Parser/SQLParser.php b/src/Parser/SQLParser.php index b30b3ed7..5e304084 100644 --- a/src/Parser/SQLParser.php +++ b/src/Parser/SQLParser.php @@ -9,7 +9,9 @@ InsertQuery, UpdateQuery, DropTableQuery, - ShowTablesQuery}; + ShowTablesQuery, + ShowColumnsQuery +}; final class SQLParser { @@ -141,11 +143,11 @@ final class SQLParser 'TABLES' => true, ]; - /** @var array */ + /** @var array */ private static $cache = []; /** - * @return SelectQuery|InsertQuery|UpdateQuery|TruncateQuery|DeleteQuery|DropTableQuery|ShowTablesQuery|ShowIndexQuery + * @return SelectQuery|InsertQuery|UpdateQuery|TruncateQuery|DeleteQuery|DropTableQuery|ShowTablesQuery|ShowIndexQuery|ShowColumnsQuery */ public static function parse(string $sql) { @@ -157,7 +159,7 @@ public static function parse(string $sql) } /** - * @return SelectQuery|InsertQuery|UpdateQuery|TruncateQuery|DeleteQuery|DropTableQuery|ShowTablesQuery|ShowIndexQuery + * @return SelectQuery|InsertQuery|UpdateQuery|TruncateQuery|DeleteQuery|DropTableQuery|ShowTablesQuery|ShowIndexQuery|ShowColumnsQuery */ private static function parseImpl(string $sql) { diff --git a/src/Parser/ShowParser.php b/src/Parser/ShowParser.php index 15d7eb95..86e90059 100644 --- a/src/Parser/ShowParser.php +++ b/src/Parser/ShowParser.php @@ -2,9 +2,10 @@ namespace Vimeo\MysqlEngine\Parser; +use Vimeo\MysqlEngine\Query\ShowColumnsQuery; use Vimeo\MysqlEngine\Query\ShowIndexQuery; -use Vimeo\MysqlEngine\TokenType; use Vimeo\MysqlEngine\Query\ShowTablesQuery; +use Vimeo\MysqlEngine\TokenType; /** * Very limited parser for SHOW TABLES LIKE 'foo' @@ -47,6 +48,12 @@ public function parse() $this->pointer++; + // For case with TABLES and COLUMNS could be optinaly used argument FULL. + if ($this->tokens[$this->pointer]->value === 'FULL') { + $isFull = true; + $this->pointer++; + } + switch ($this->tokens[$this->pointer]->value) { case 'TABLES': return $this->parseShowTables(); @@ -54,6 +61,8 @@ public function parse() case 'INDEXES': case 'KEYS': return $this->parseShowIndex(); + case 'COLUMNS': + return $this->parseShowColumns($isFull ?? false); default: throw new ParserException("Parser error: expected SHOW TABLES"); } @@ -64,7 +73,7 @@ private function parseShowTables(): ShowTablesQuery $this->pointer++; if ($this->tokens[$this->pointer]->value !== 'LIKE') { - throw new ParserException("Parser error: expected SHOW TABLES LIKE"); + throw new ParserException("Parser error: expected SHOW [FULL] TABLES LIKE"); } $this->pointer++; @@ -102,6 +111,38 @@ private function parseShowIndex(): ShowIndexQuery list($this->pointer, $expression) = $expression_parser->buildWithPointer(); $query->whereClause = $expression; } + + return $query; + } + + private function parseShowColumns(bool $isFull): ShowColumnsQuery + { + $this->pointer++; + + if ($this->tokens[$this->pointer]->value !== 'FROM') { + throw new ParserException("Parser error: expected SHOW [FULL] COLUMNS FROM"); + } + + $this->pointer++; + + $token = $this->tokens[$this->pointer]; + if ($token->type !== TokenType::IDENTIFIER) { + throw new ParserException("Expected table name after FROM"); + } + + $query = new ShowColumnsQuery($token->value, $this->sql); + $query->isFull = $isFull; + $this->pointer++; + + if ($this->pointer < count($this->tokens)) { + if ($this->tokens[$this->pointer]->value !== 'WHERE') { + throw new ParserException("Parser error: expected SHOW [FULL] COLUMNS FROM [TABLE_NAME] WHERE"); + } + $expression_parser = new ExpressionParser($this->tokens, $this->pointer); + list($this->pointer, $expression) = $expression_parser->buildWithPointer(); + $query->whereClause = $expression; + } + return $query; } } diff --git a/src/Processor/ShowColumnsProcessor.php b/src/Processor/ShowColumnsProcessor.php new file mode 100644 index 00000000..441b30fe --- /dev/null +++ b/src/Processor/ShowColumnsProcessor.php @@ -0,0 +1,111 @@ +table); + $table_definition = $conn->getServer()->getTableDefinition( + $database, + $table + ); + if (!$table_definition) { + return new QueryResult([], []); + } + $columns = [ + 'Field' => new Column\Varchar(255), + 'Type' => new Column\Varchar(255), + 'Collation' => new Column\Varchar(255), + 'Null' => new Column\Enum(['NO', 'YES']), + 'Key' => new Column\Enum(['PRI']), + 'Default' => new Column\Varchar(255), + 'Extra' => new Column\Enum(['auto_increment']), + 'Privilegies' => new Column\Varchar(255), + 'Comment' => new Column\Varchar(255), + ]; + $rows = []; + foreach ($table_definition->columns as $name => $column) { + $rows[] = [ + 'Field' => $name, + 'Type' => self::resolveType($column), + 'Collation' => self::resolveCollation($column), + 'Null' => $column->isNullable() ? 'YES' : 'NO', + 'Key' => in_array($name, $table_definition->primaryKeyColumns) ? 'PRI' : '', + 'Default' => $column->getDefault(), + 'Extra' => self::resolveExtra($column), + 'Privilegies' => 'select,insert,update,references', + 'Comment' => '', + ]; + } + $result = self::applyWhere($conn, $scope, $stmt->whereClause, new QueryResult($rows, $columns)); + + $rows = array_merge($result->rows); + $columns = $result->columns; + if (!$stmt->isFull) { + $allowedColumns = [ + 'Field', + 'Type', + 'Null', + 'Key', + 'Default', + 'Extra', + ]; + $columns = array_intersect_key($columns, array_flip($allowedColumns)); + } + + return new QueryResult(array_merge($result->rows), $result->columns); + } + + private static function resolveType(Column $column): string + { + if ($column instanceof Column\Varchar) { + $type = 'varchar(255)'; + } elseif ($column instanceof Column\IntColumn) { + $type = 'int(11)'; + } elseif ($column instanceof Column\DateTime) { + $type = 'datetime'; + } else { + throw new \UnexpectedValueException('Column type not specified.'); + } + + return $type; + } + + private static function resolveCollation(Column $column): string + { + if (is_subclass_of($column, Column\CharacterColumn::class)) { + $collation = $column->getCollation(); + } + + return $collation ?? ''; + } + + private static function resolveDefault(Column $column): ?string + { + if ($column instanceof Column\Defaultable) { + $default = $column->getDefault(); + } + + return $default ?? null; + } + + private static function resolveExtra(Column $column): string + { + if ($column instanceof Column\IntegerColumn) { + $extra = $column->isAutoIncrement() ? 'auto_increment' : ''; + } + + return $extra ?? ''; + } +} diff --git a/src/Query/ShowColumnsQuery.php b/src/Query/ShowColumnsQuery.php new file mode 100644 index 00000000..c8835afb --- /dev/null +++ b/src/Query/ShowColumnsQuery.php @@ -0,0 +1,34 @@ +table = $table; + $this->sql = $sql; + } +} From 138dc7c0565e26bc841c5886c8cf55941252558a Mon Sep 17 00:00:00 2001 From: Andrew Svirin Date: Wed, 6 Oct 2021 21:46:24 +0300 Subject: [PATCH 2/3] Add handler for Alter table auto increment --- src/FakePdoStatementTrait.php | 18 +++++ src/FakePdoTrait.php | 2 + src/Parser/AlterTableParser.php | 89 ++++++++++++++++++++++ src/Parser/SQLParser.php | 33 +++++--- src/Query/AlterTableAutoincrementQuery.php | 27 +++++++ 5 files changed, 159 insertions(+), 10 deletions(-) create mode 100644 src/Parser/AlterTableParser.php create mode 100644 src/Query/AlterTableAutoincrementQuery.php diff --git a/src/FakePdoStatementTrait.php b/src/FakePdoStatementTrait.php index d4e905d9..55fd2d5b 100644 --- a/src/FakePdoStatementTrait.php +++ b/src/FakePdoStatementTrait.php @@ -172,6 +172,10 @@ public function universalExecute(?array $params = null) return true; } + if(false !== stripos($sql, 'SET')){ + return true; + } + //echo "\n" . $sql . "\n"; try { @@ -304,6 +308,7 @@ function ($row) { $parsed_query ) ); + break; case Query\ShowColumnsQuery::class: @@ -315,6 +320,19 @@ function ($row) { $parsed_query ) ); + + break; + + case Query\AlterTableAutoincrementQuery::class: + [$databaseName, $tableName] = Processor\Processor::parseTableName($this->conn, $parsed_query->table); + $td = $this->conn->getServer()->getTableDefinition($databaseName, $tableName); + + foreach ($td->columns as $columnName => $column) { + if ($column instanceof Schema\Column\IntegerColumn && $column->isAutoIncrement()) { + $td->autoIncrementOffsets[$columnName] = $parsed_query->value - 1; + } + } + break; default: diff --git a/src/FakePdoTrait.php b/src/FakePdoTrait.php index c7953138..4af65fec 100644 --- a/src/FakePdoTrait.php +++ b/src/FakePdoTrait.php @@ -92,6 +92,8 @@ public function getAttribute($key) switch ($key) { case \PDO::ATTR_CASE: $value = $this->lowercaseResultKeys ? \PDO::CASE_LOWER : \PDO::CASE_UPPER; + case \PDO::ATTR_SERVER_VERSION: + $value = '5.7.0'; } return $value; diff --git a/src/Parser/AlterTableParser.php b/src/Parser/AlterTableParser.php new file mode 100644 index 00000000..6374e16a --- /dev/null +++ b/src/Parser/AlterTableParser.php @@ -0,0 +1,89 @@ + + */ + private $tokens; + + /** + * @var string + */ + private $sql; + + /** + * @param array $tokens + */ + public function __construct(array $tokens, string $sql) + { + $this->tokens = $tokens; + $this->sql = $sql; + } + + /** + * @return AlterTableAutoincrementQuery + * @throws ParserException + */ + public function parse() + { + if ($this->tokens[$this->pointer]->value !== 'ALTER') { + throw new ParserException("Parser error: expected ALTER"); + } + + $this->pointer++; + + if ($this->tokens[$this->pointer]->value !== 'TABLE') { + throw new ParserException("Parser error: expected ALTER TABLE"); + } + + $this->pointer++; + + if ($this->tokens[$this->pointer]->type !== TokenType::IDENTIFIER) { + throw new ParserException("Expected table name after TABLE"); + } + $table = $this->tokens[$this->pointer]->value; + + $this->pointer++; + + switch ($this->tokens[$this->pointer]->value) { + case 'AUTO_INCREMENT': + return $this->parseAlterTableAutoIncrement($table); + } + } + + private function parseAlterTableAutoIncrement(string $table): AlterTableAutoincrementQuery + { + $this->pointer++; + + if ($this->tokens[$this->pointer]->value !== '=') { + throw new ParserException("Parser error: expected ALTER TABLE {table} AUTO_INCREMENT="); + } + + $this->pointer++; + + if ($this->tokens[$this->pointer]->type !== TokenType::NUMERIC_CONSTANT) { + throw new ParserException("Expected numeric after ="); + } + + $token = $this->tokens[$this->pointer] ?? null; + + return new AlterTableAutoincrementQuery($table, $token->value, $this->sql); + } +} diff --git a/src/Parser/SQLParser.php b/src/Parser/SQLParser.php index 5e304084..df6fb2ce 100644 --- a/src/Parser/SQLParser.php +++ b/src/Parser/SQLParser.php @@ -2,7 +2,8 @@ namespace Vimeo\MysqlEngine\Parser; use Vimeo\MysqlEngine\TokenType; -use Vimeo\MysqlEngine\Query\{SelectQuery, +use Vimeo\MysqlEngine\Query\{AlterTableAutoincrementQuery, + SelectQuery, DeleteQuery, ShowIndexQuery, TruncateQuery, @@ -10,8 +11,7 @@ UpdateQuery, DropTableQuery, ShowTablesQuery, - ShowColumnsQuery -}; + ShowColumnsQuery}; final class SQLParser { @@ -159,7 +159,7 @@ public static function parse(string $sql) } /** - * @return SelectQuery|InsertQuery|UpdateQuery|TruncateQuery|DeleteQuery|DropTableQuery|ShowTablesQuery|ShowIndexQuery|ShowColumnsQuery + * @return SelectQuery|InsertQuery|UpdateQuery|TruncateQuery|DeleteQuery|DropTableQuery|ShowTablesQuery|ShowIndexQuery|ShowColumnsQuery|AlterTableAutoincrementQuery */ private static function parseImpl(string $sql) { @@ -171,10 +171,20 @@ private static function parseImpl(string $sql) $tokens = \array_slice($tokens, 1, $close - 1); $token = $tokens[0]; } - if ($token->type !== TokenType::CLAUSE) { + if ($token->type === TokenType::CLAUSE) { + $command = $token->value; + } elseif ($token->type === TokenType::IDENTIFIER) { + $nextToken = $tokens[1]; + if ($nextToken->type === TokenType::RESERVED) { + $command = $token->value . ' ' . $nextToken->value; + } else { + throw new ParserException("Unexpected {$token->value}"); + } + } else { throw new ParserException("Unexpected {$token->value}"); } - switch ($token->value) { + + switch ($command) { case 'SELECT': $select = new SelectParser(0, $tokens, $sql); return $select->parse(); @@ -191,11 +201,14 @@ private static function parseImpl(string $sql) $truncate = new TruncateParser($tokens, $sql); return $truncate->parse(); case 'DROP': - $truncate = new DropParser($tokens, $sql); - return $truncate->parse(); + $drop = new DropParser($tokens, $sql); + return $drop->parse(); case 'SHOW': - $truncate = new ShowParser($tokens, $sql); - return $truncate->parse(); + $show = new ShowParser($tokens, $sql); + return $show->parse(); + case 'ALTER TABLE': + $alter = new AlterTableParser($tokens, $sql); + return $alter->parse(); default: throw new ParserException("Unexpected {$token->value}"); } diff --git a/src/Query/AlterTableAutoincrementQuery.php b/src/Query/AlterTableAutoincrementQuery.php new file mode 100644 index 00000000..0abaeaf0 --- /dev/null +++ b/src/Query/AlterTableAutoincrementQuery.php @@ -0,0 +1,27 @@ +table = $table; + $this->value = $value; + $this->sql = $sql; + } +} From befaa865351e26bab1f28cab347e5cb106bd1ff1 Mon Sep 17 00:00:00 2001 From: Andrew Svirin Date: Thu, 7 Oct 2021 10:32:36 +0300 Subject: [PATCH 3/3] Fix typo --- src/Processor/ShowColumnsProcessor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Processor/ShowColumnsProcessor.php b/src/Processor/ShowColumnsProcessor.php index 441b30fe..cde9561a 100644 --- a/src/Processor/ShowColumnsProcessor.php +++ b/src/Processor/ShowColumnsProcessor.php @@ -31,7 +31,7 @@ public static function process( 'Key' => new Column\Enum(['PRI']), 'Default' => new Column\Varchar(255), 'Extra' => new Column\Enum(['auto_increment']), - 'Privilegies' => new Column\Varchar(255), + 'Privileges' => new Column\Varchar(255), 'Comment' => new Column\Varchar(255), ]; $rows = []; @@ -44,7 +44,7 @@ public static function process( 'Key' => in_array($name, $table_definition->primaryKeyColumns) ? 'PRI' : '', 'Default' => $column->getDefault(), 'Extra' => self::resolveExtra($column), - 'Privilegies' => 'select,insert,update,references', + 'Privileges' => 'select,insert,update,references', 'Comment' => '', ]; }