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; + } +}