Merge branch 'rfe1517'

This commit is contained in:
Madhura Jayaratne 2015-07-14 10:47:48 +05:30
commit ee1b179fcb
13 changed files with 216 additions and 57 deletions

View File

@ -2308,6 +2308,29 @@ class PMA_Table
}
$columns = $this->_dbi->fetchResult($sql, 'Field', 'Expression');
return $columns;
} else {
$createTable = $this->showCreate();
if ($createTable) {
$parser = new SqlParser\Parser($createTable);
/**
* @var CreateStatement $stmt
*/
$stmt = $parser->statements[0];
$fields = SqlParser\Utils\Table::getFields($stmt);
if ($column != null) {
$expression = isset($fields[$column]['expr']) ?
substr($fields[$column]['expr'], 1, -1) : '';
return array($column => $expression);
}
$ret = array();
foreach ($fields as $field => $options) {
if (isset($options['expr'])) {
$ret[$field] = substr($options['expr'], 1, -1);
}
}
return $ret;
}
}
}
@ -2316,7 +2339,7 @@ class PMA_Table
*/
public function showCreate()
{
return $GLOBALS['dbi']->fetchValue(
return $this->_dbi->fetchValue(
'SHOW CREATE TABLE ' . PMA_Util::backquote($this->_db_name) . '.'
. PMA_Util::backquote($this->_name),
0, 1

View File

@ -4839,7 +4839,8 @@ class PMA_Util
public static function isVirtualColumnsSupported()
{
$serverType = self::getServerType();
return $serverType == 'MySQL' && PMA_MYSQL_INT_VERSION > 50705;
return $serverType == 'MySQL' && PMA_MYSQL_INT_VERSION >= 50705
|| ($serverType == 'MariaDB' && PMA_MYSQL_INT_VERSION >= 50200);
}
}

View File

@ -57,4 +57,16 @@ abstract class Component
// abstract.
return null;
}
/**
* Builds the string representation of a component of this type.
*
* @see static::build
*
* @return string
*/
public function __toString()
{
return static::build($this);
}
}

View File

@ -205,7 +205,7 @@ class Expression extends Component
// We don't check to see if `$prev` is `true` (open bracket
// was found before) because the brackets count is one (the
// only bracket we found is this one).
if (($brackets === 1) && (empty($ret->function)) && ($prev !== null) && ($prev !== true)) {
if ((empty($ret->function)) && ($prev !== null) && ($prev !== true)) {
// A function name was previously found and now an open
// bracket, so this is a function call.
$ret->function = $prev;
@ -213,7 +213,15 @@ class Expression extends Component
$isExpr = true;
} elseif ($token->value === ')') {
--$brackets;
if ($brackets < 0) {
if ($brackets === 0) {
if (!empty($options['bracketsDelimited'])) {
// The current token is the last brackets, the next
// one will be outside.
$ret->expr .= $token->token;
++$list->idx;
break;
}
} elseif ($brackets < 0) {
$parser->error('Unexpected bracket.', $token);
$brackets = 0;
}
@ -268,11 +276,12 @@ class Expression extends Component
} else {
// Parsing aliases without `AS` keyword.
// Example: SELECT 'foo' `bar`
if ($brackets === 0) {
if (($brackets === 0) && (empty($options['noAlias']))) {
if (($token->type === Token::TYPE_NONE) || ($token->type === Token::TYPE_STRING)
|| (($token->type === Token::TYPE_SYMBOL) && ($token->flags & Token::FLAG_SYMBOL_BACKTICK))
) {
$ret->alias = $token->value;
continue;
}
}
}

View File

@ -47,6 +47,14 @@ class FieldDefinition extends Component
'COMMENT' => array(5, 'var'),
'COLUMN_FORMAT' => array(6, 'var'),
'ON UPDATE' => array(7, 'var'),
// MariaDB options.
'GENERATED ALWAYS' => 1,
'AS' => array(2, 'expr', array('bracketsDelimited' => true)),
'VIRTUAL' => 3,
'PERSISTENT' => 3,
// 'UNIQUE' => 4, // common
// 'UNIQUE KEY' => 4, // common
);
/**

View File

@ -72,8 +72,27 @@ class OptionsArray extends Component
*/
$lastOptionId = 0;
/**
* Counts brackets.
* @var int $brackets
*/
$brackets = 0;
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 ---------------------[ option ]----------------------> 1
*
* 1 -------------------[ = (optional) ]------------------> 2
*
* 2 ----------------------[ value ]----------------------> 0
*
* @var int
*/
$state = 0;
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
@ -86,23 +105,36 @@ class OptionsArray extends Component
break;
}
// Skipping whitespaces and comments.
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
// Skipping comments.
if ($token->type === Token::TYPE_COMMENT) {
continue;
}
// Skipping whitespace if not parsing value.
if (($token->type === Token::TYPE_WHITESPACE) && ($brackets === 0)) {
continue;
}
if ($lastOption === null) {
if (isset($options[strtoupper($token->token)])) {
$lastOption = $options[strtoupper($token->token)];
$lastOptionId = is_array($lastOption) ? $lastOption[0] : $lastOption;
$upper = strtoupper($token->token);
if (isset($options[$upper])) {
$lastOption = $options[$upper];
$lastOptionId = is_array($lastOption) ?
$lastOption[0] : $lastOption;
$state = 0;
// Checking for option conflicts.
// For example, in `SELECT` statements the keywords `ALL` and `DISTINCT`
// conflict and if used together, they produce an invalid query.
// Usually, tokens can be identified in the array by the option ID,
// but if conflicts occur, a generated option ID is used.
// The first pseudo duplicate ID is the maximum value of the real
// options (e.g. if there are 5 options, the first fake ID is 6).
// For example, in `SELECT` statements the keywords `ALL`
// and `DISTINCT` conflict and if used together, they
// produce an invalid query.
//
// Usually, tokens can be identified in the array by the
// option ID, but if conflicts occur, a generated option ID
// is used.
//
// The first pseudo duplicate ID is the maximum value of the
// real options (e.g. if there are 5 options, the first
// fake ID is 6).
if (isset($ret->options[$lastOptionId])) {
$parser->error('This option conflicts with \'' . $ret->options[$lastOptionId] . '\'.', $token);
$lastOptionId = $lastAssignedId++;
@ -113,33 +145,81 @@ class OptionsArray extends Component
}
}
if (is_array($lastOption)) {
if (empty($ret->options[$lastOptionId])) {
if ($state === 0) {
if (!is_array($lastOption)) {
// This is a just keyword option without any value.
// This is the beginning and the end of it.
$ret->options[$lastOptionId] = $token->value;
$lastOption = null;
$state = 0;
} elseif (($lastOption[1] === 'var') || ($lastOption[1] === 'var=')) {
// This is a keyword that is followed by a value.
// This is only the beginning. The value is parsed in state
// 1 and 2. State 1 is used to skip the first equals sign
// and state 2 to parse the actual value.
$ret->options[$lastOptionId] = array(
// @var string The name of the option.
'name' => $token->value,
// @var bool Whether it contains an equal sign.
// This is used by the builder to rebuild it.
'equal' => $lastOption[1] === 'var=',
// @var string Raw value.
'value' => '',
// @var string Processed value.
'value_' => '',
);
$state = 1;
} elseif ($lastOption[1] === 'expr') {
// This is a keyword that is followed by an expression.
// The expression is used by the specialized parser.
// Skipping this option in order to parse the expression.
++$list->idx;
$ret->options[$lastOptionId] = array(
// @var string The name of the option.
'name' => $token->value,
// @var Expression The parsed expression.
'value' => null,
);
$state = 1;
}
} elseif ($state === 1) {
$state = 2;
if ($token->value === '=') {
$ret->options[$lastOptionId]['equal'] = true;
continue;
}
}
// This is outside the `elseif` group above because the change might
// change this iteration.
if ($state === 2) {
if ($lastOption[1] === 'expr') {
$ret->options[$lastOptionId]['value'] = Expression::parse(
$parser,
$list,
empty($lastOption[2]) ? array() : $lastOption[2]
);
$lastOption = null;
$state = 0;
} else {
if ($token->value !== '=') {
if ($token->value === '(') {
++$brackets;
} elseif ($token->value === ')') {
--$brackets;
} else {
// Raw and processed value.
$ret->options[$lastOptionId]['value'] .= $token->token;
$ret->options[$lastOptionId]['value_'] .= $token->value;
}
if ($brackets === 0) {
$lastOption = null;
}
if ($token->token === '(') {
++$brackets;
} elseif ($token->token === ')') {
--$brackets;
}
// Raw value.
$ret->options[$lastOptionId]['value'] .= $token->token;
// Processed value.
$ret->options[$lastOptionId]['value_'] .= $token->value;
// Checking if we finished parsing.
if ($brackets === 0) {
$lastOption = null;
}
}
} else {
$ret->options[$lastOptionId] = $token->value;
$lastOption = null;
}
}
@ -161,12 +241,14 @@ class OptionsArray extends Component
}
$options = array();
foreach ($component->options as $option) {
if (is_array($option)) {
if (!is_array($option)) {
$options[] = $option;
} else {
$options[] = $option['name']
. (!empty($option['equal']) ? '=' : ' ')
. $option['value'];
} else {
$options[] = $option;
. ((string) $option['value']);
// If `$option['value']` happens to be a component, the magic
// method will build it automatically.
}
}
return implode(' ', $options);

View File

@ -63,7 +63,7 @@ class ContextMySql50000 extends Context
'MIGRATE' => 1, 'PARTIAL' => 1, 'PREPARE' => 1, 'PROFILE' => 1, 'RECOVER' => 1,
'RESTORE' => 1, 'RETURNS' => 1, 'ROUTINE' => 1, 'SESSION' => 1, 'STORAGE' => 1,
'STRIPED' => 1, 'SUBJECT' => 1, 'SUSPEND' => 1, 'UNICODE' => 1, 'UNKNOWN' => 1,
'UPGRADE' => 1, 'USE_FRM' => 1,
'UPGRADE' => 1, 'USE_FRM' => 1, 'VIRTUAL' => 1,
'CASCADED' => 1, 'CHECKSUM' => 1, 'DUMPFILE' => 1, 'EXTENDED' => 1,
'FUNCTION' => 1, 'GEOMETRY' => 1, 'INNOBASE' => 1, 'LANGUAGE' => 1,
'MAX_ROWS' => 1, 'MIN_ROWS' => 1, 'NATIONAL' => 1, 'NVARCHAR' => 1,
@ -77,8 +77,8 @@ class ContextMySql50000 extends Context
'VARIABLES' => 1,
'BERKELEYDB' => 1, 'COMPRESSED' => 1, 'CONCURRENT' => 1, 'CONNECTION' => 1,
'CONSISTENT' => 1, 'DEALLOCATE' => 1, 'IDENTIFIED' => 1, 'MASTER_SSL' => 1,
'NDBCLUSTER' => 1, 'PRIVILEGES' => 1, 'REPEATABLE' => 1, 'ROW_FORMAT' => 1,
'SQL_THREAD' => 1, 'TABLESPACE' => 1,
'NDBCLUSTER' => 1, 'PERSISTENT' => 1, 'PRIVILEGES' => 1, 'REPEATABLE' => 1,
'ROW_FORMAT' => 1, 'SQL_THREAD' => 1, 'TABLESPACE' => 1,
'FRAC_SECOND' => 1, 'MASTER_HOST' => 1, 'MASTER_PORT' => 1, 'MASTER_USER' => 1,
'PROCESSLIST' => 1, 'RAID_CHUNKS' => 1, 'REPLICATION' => 1, 'SQL_TSI_DAY' => 1,
'TRANSACTION' => 1, 'UNCOMMITTED' => 1,
@ -151,6 +151,7 @@ class ContextMySql50000 extends Context
'CHARACTER SET' => 7, 'IF NOT EXISTS' => 7,
'DATA DIRECTORY' => 7,
'DEFAULT CHARSET' => 7, 'DEFAULT COLLATE' => 7, 'INDEX DIRECTORY' => 7,
'GENERATED ALWAYS' => 7,
'DEFAULT CHARACTER SET' => 7,
'XML' => 9,

View File

@ -67,7 +67,8 @@ class ContextMySql50100 extends Context
'OPTIONS' => 1, 'PARTIAL' => 1, 'PLUGINS' => 1, 'PREPARE' => 1, 'PROFILE' => 1,
'REBUILD' => 1, 'RECOVER' => 1, 'RESTORE' => 1, 'RETURNS' => 1, 'ROUTINE' => 1,
'SESSION' => 1, 'STORAGE' => 1, 'STRIPED' => 1, 'SUBJECT' => 1, 'SUSPEND' => 1,
'UNICODE' => 1, 'UNKNOWN' => 1, 'UPGRADE' => 1, 'USE_FRM' => 1, 'WRAPPER' => 1,
'UNICODE' => 1, 'UNKNOWN' => 1, 'UPGRADE' => 1, 'USE_FRM' => 1, 'VIRTUAL' => 1,
'WRAPPER' => 1,
'CASCADED' => 1, 'CHECKSUM' => 1, 'DATAFILE' => 1, 'DUMPFILE' => 1,
'EXTENDED' => 1, 'FUNCTION' => 1, 'GEOMETRY' => 1, 'INNOBASE' => 1,
'LANGUAGE' => 1, 'MAXVALUE' => 1, 'MAX_ROWS' => 1, 'MAX_SIZE' => 1,
@ -83,9 +84,9 @@ class ContextMySql50100 extends Context
'UNINSTALL' => 1, 'VARIABLES' => 1,
'BERKELEYDB' => 1, 'COMPLETION' => 1, 'COMPRESSED' => 1, 'CONCURRENT' => 1,
'CONNECTION' => 1, 'CONSISTENT' => 1, 'DEALLOCATE' => 1, 'IDENTIFIED' => 1,
'MASTER_SSL' => 1, 'NDBCLUSTER' => 1, 'PARTITIONS' => 1, 'PRIVILEGES' => 1,
'REORGANISE' => 1, 'REORGANIZE' => 1, 'REPEATABLE' => 1, 'ROW_FORMAT' => 1,
'SQL_THREAD' => 1, 'TABLESPACE' => 1,
'MASTER_SSL' => 1, 'NDBCLUSTER' => 1, 'PARTITIONS' => 1, 'PERSISTENT' => 1,
'PRIVILEGES' => 1, 'REORGANISE' => 1, 'REORGANIZE' => 1, 'REPEATABLE' => 1,
'ROW_FORMAT' => 1, 'SQL_THREAD' => 1, 'TABLESPACE' => 1,
'EXTENT_SIZE' => 1, 'FRAC_SECOND' => 1, 'MASTER_HOST' => 1, 'MASTER_PORT' => 1,
'MASTER_USER' => 1, 'PROCESSLIST' => 1, 'RAID_CHUNKS' => 1, 'REPLICATION' => 1,
'SQL_TSI_DAY' => 1, 'TRANSACTION' => 1, 'UNCOMMITTED' => 1,
@ -162,6 +163,7 @@ class ContextMySql50100 extends Context
'CHARACTER SET' => 7, 'IF NOT EXISTS' => 7,
'DATA DIRECTORY' => 7,
'DEFAULT CHARSET' => 7, 'DEFAULT COLLATE' => 7, 'INDEX DIRECTORY' => 7,
'GENERATED ALWAYS' => 7,
'DEFAULT CHARACTER SET' => 7,
'XML' => 9,

View File

@ -67,7 +67,8 @@ class ContextMySql50500 extends Context
'NO_WAIT' => 1, 'OPTIONS' => 1, 'PARTIAL' => 1, 'PLUGINS' => 1, 'PREPARE' => 1,
'PROFILE' => 1, 'REBUILD' => 1, 'RECOVER' => 1, 'RESTORE' => 1, 'RETURNS' => 1,
'ROUTINE' => 1, 'SESSION' => 1, 'STORAGE' => 1, 'SUBJECT' => 1, 'SUSPEND' => 1,
'UNICODE' => 1, 'UNKNOWN' => 1, 'UPGRADE' => 1, 'USE_FRM' => 1, 'WRAPPER' => 1,
'UNICODE' => 1, 'UNKNOWN' => 1, 'UPGRADE' => 1, 'USE_FRM' => 1, 'VIRTUAL' => 1,
'WRAPPER' => 1,
'CASCADED' => 1, 'CHECKSUM' => 1, 'DATAFILE' => 1, 'DUMPFILE' => 1,
'EXTENDED' => 1, 'FUNCTION' => 1, 'GEOMETRY' => 1, 'INNOBASE' => 1,
'LANGUAGE' => 1, 'MAX_ROWS' => 1, 'MAX_SIZE' => 1, 'MIN_ROWS' => 1,
@ -82,9 +83,9 @@ class ContextMySql50500 extends Context
'TEMPTABLE' => 1, 'UNDEFINED' => 1, 'UNINSTALL' => 1, 'VARIABLES' => 1,
'COMPLETION' => 1, 'COMPRESSED' => 1, 'CONCURRENT' => 1, 'CONNECTION' => 1,
'CONSISTENT' => 1, 'DEALLOCATE' => 1, 'IDENTIFIED' => 1, 'MASTER_SSL' => 1,
'NDBCLUSTER' => 1, 'PARTITIONS' => 1, 'PRIVILEGES' => 1, 'REORGANIZE' => 1,
'REPEATABLE' => 1, 'ROW_FORMAT' => 1, 'SQL_THREAD' => 1, 'TABLESPACE' => 1,
'TABLE_NAME' => 1,
'NDBCLUSTER' => 1, 'PARTITIONS' => 1, 'PERSISTENT' => 1, 'PRIVILEGES' => 1,
'REORGANIZE' => 1, 'REPEATABLE' => 1, 'ROW_FORMAT' => 1, 'SQL_THREAD' => 1,
'TABLESPACE' => 1, 'TABLE_NAME' => 1,
'COLUMN_NAME' => 1, 'CURSOR_NAME' => 1, 'EXTENT_SIZE' => 1, 'FRAC_SECOND' => 1,
'MASTER_HOST' => 1, 'MASTER_PORT' => 1, 'MASTER_USER' => 1, 'MYSQL_ERRNO' => 1,
'PROCESSLIST' => 1, 'REPLICATION' => 1, 'SCHEMA_NAME' => 1, 'SQL_TSI_DAY' => 1,
@ -167,6 +168,7 @@ class ContextMySql50500 extends Context
'CHARACTER SET' => 7, 'IF NOT EXISTS' => 7,
'DATA DIRECTORY' => 7,
'DEFAULT CHARSET' => 7, 'DEFAULT COLLATE' => 7, 'INDEX DIRECTORY' => 7,
'GENERATED ALWAYS' => 7,
'DEFAULT CHARACTER SET' => 7,
'XML' => 9,

View File

@ -68,7 +68,7 @@ class ContextMySql50600 extends Context
'PLUGINS' => 1, 'PREPARE' => 1, 'PROFILE' => 1, 'REBUILD' => 1, 'RECOVER' => 1,
'RESTORE' => 1, 'RETURNS' => 1, 'ROUTINE' => 1, 'SESSION' => 1, 'STORAGE' => 1,
'SUBJECT' => 1, 'SUSPEND' => 1, 'UNICODE' => 1, 'UNKNOWN' => 1, 'UPGRADE' => 1,
'USE_FRM' => 1, 'WRAPPER' => 1,
'USE_FRM' => 1, 'VIRTUAL' => 1, 'WRAPPER' => 1,
'CASCADED' => 1, 'CHECKSUM' => 1, 'DATAFILE' => 1, 'DUMPFILE' => 1,
'EXCHANGE' => 1, 'EXTENDED' => 1, 'FUNCTION' => 1, 'GEOMETRY' => 1,
'LANGUAGE' => 1, 'MAX_ROWS' => 1, 'MAX_SIZE' => 1, 'MIN_ROWS' => 1,
@ -83,9 +83,9 @@ class ContextMySql50600 extends Context
'UNDEFINED' => 1, 'UNINSTALL' => 1, 'VARIABLES' => 1,
'COMPLETION' => 1, 'COMPRESSED' => 1, 'CONCURRENT' => 1, 'CONNECTION' => 1,
'CONSISTENT' => 1, 'DEALLOCATE' => 1, 'IDENTIFIED' => 1, 'MASTER_SSL' => 1,
'NDBCLUSTER' => 1, 'PARTITIONS' => 1, 'PLUGIN_DIR' => 1, 'PRIVILEGES' => 1,
'REORGANIZE' => 1, 'REPEATABLE' => 1, 'ROW_FORMAT' => 1, 'SQL_THREAD' => 1,
'TABLESPACE' => 1, 'TABLE_NAME' => 1,
'NDBCLUSTER' => 1, 'PARTITIONS' => 1, 'PERSISTENT' => 1, 'PLUGIN_DIR' => 1,
'PRIVILEGES' => 1, 'REORGANIZE' => 1, 'REPEATABLE' => 1, 'ROW_FORMAT' => 1,
'SQL_THREAD' => 1, 'TABLESPACE' => 1, 'TABLE_NAME' => 1,
'COLUMN_NAME' => 1, 'CURSOR_NAME' => 1, 'DIAGNOSTICS' => 1, 'EXTENT_SIZE' => 1,
'MASTER_HOST' => 1, 'MASTER_PORT' => 1, 'MASTER_USER' => 1, 'MYSQL_ERRNO' => 1,
'PROCESSLIST' => 1, 'REPLICATION' => 1, 'SCHEMA_NAME' => 1, 'SQL_TSI_DAY' => 1,
@ -173,6 +173,7 @@ class ContextMySql50600 extends Context
'CHARACTER SET' => 7, 'IF NOT EXISTS' => 7,
'DATA DIRECTORY' => 7,
'DEFAULT CHARSET' => 7, 'DEFAULT COLLATE' => 7, 'INDEX DIRECTORY' => 7,
'GENERATED ALWAYS' => 7,
'DEFAULT CHARACTER SET' => 7,
'XML' => 9,

View File

@ -85,9 +85,9 @@ class ContextMySql50700 extends Context
'UNDEFINED' => 1, 'UNINSTALL' => 1, 'VARIABLES' => 1,
'COMPLETION' => 1, 'COMPRESSED' => 1, 'CONCURRENT' => 1, 'CONNECTION' => 1,
'CONSISTENT' => 1, 'DEALLOCATE' => 1, 'IDENTIFIED' => 1, 'MASTER_SSL' => 1,
'NDBCLUSTER' => 1, 'PARTITIONS' => 1, 'PLUGIN_DIR' => 1, 'PRIVILEGES' => 1,
'REORGANIZE' => 1, 'REPEATABLE' => 1, 'ROW_FORMAT' => 1, 'SQL_THREAD' => 1,
'TABLESPACE' => 1, 'TABLE_NAME' => 1, 'VALIDATION' => 1,
'NDBCLUSTER' => 1, 'PARTITIONS' => 1, 'PERSISTENT' => 1, 'PLUGIN_DIR' => 1,
'PRIVILEGES' => 1, 'REORGANIZE' => 1, 'REPEATABLE' => 1, 'ROW_FORMAT' => 1,
'SQL_THREAD' => 1, 'TABLESPACE' => 1, 'TABLE_NAME' => 1, 'VALIDATION' => 1,
'COLUMN_NAME' => 1, 'COMPRESSION' => 1, 'CURSOR_NAME' => 1, 'DIAGNOSTICS' => 1,
'EXTENT_SIZE' => 1, 'MASTER_HOST' => 1, 'MASTER_PORT' => 1, 'MASTER_USER' => 1,
'MYSQL_ERRNO' => 1, 'NONBLOCKING' => 1, 'PROCESSLIST' => 1, 'REPLICATION' => 1,
@ -181,6 +181,7 @@ class ContextMySql50700 extends Context
'CHARACTER SET' => 7, 'IF NOT EXISTS' => 7,
'DATA DIRECTORY' => 7,
'DEFAULT CHARSET' => 7, 'DEFAULT COLLATE' => 7, 'INDEX DIRECTORY' => 7,
'GENERATED ALWAYS' => 7,
'DEFAULT CHARACTER SET' => 7,
'XML' => 9,

View File

@ -95,7 +95,7 @@ abstract class Statement
}
/**
* Builds the statement.
* Builds the string representation of this statement.
*
* @return string
*/
@ -292,4 +292,16 @@ abstract class Statement
{
}
/**
* Builds the string representation of this statement.
*
* @see static::build
*
* @return string
*/
public function __toString()
{
return static::build($this);
}
}

View File

@ -122,6 +122,11 @@ class Table
$ret[$field->name]['on_update_current_timestamp'] = true;
}
}
if (($option = $field->options->has('AS'))) {
$ret[$field->name]['generated'] = true;
$ret[$field->name]['expr'] = $option->expr;
}
}
}