<?php
/**
 * This file is a port of the Lexer & TokensList classes from the PHPMyAdmin/sql-parser library.
 *
 * @package wp-sqlite-integration
 * @see https://github.com/phpmyadmin/sql-parser
 */

/**
 * Performs lexical analysis over a SQL statement and splits it in multiple tokens.
 */
class WP_SQLite_Lexer {

	/**
	 * The maximum length of a keyword.
	 */
	const KEYWORD_MAX_LENGTH = 30;

	/**
	 * The maximum length of a label.
	 *
	 * Ref: https://dev.mysql.com/doc/refman/5.7/en/statement-labels.html
	 */
	const LABEL_MAX_LENGTH = 16;

	/**
	 * The maximum length of an operator.
	 */
	const OPERATOR_MAX_LENGTH = 4;

	/**
	 * A list of methods that are used in lexing the SQL query.
	 *
	 * @var string[]
	 */
	const PARSER_METHODS = array(
		// It is best to put the parsers in order of their complexity
		// (ascending) and their occurrence rate (descending).
		//
		// Conflicts:
		//
		// 1. `parse_delimiter`, `parse_unknown`, `parse_keyword`, `parse_number`
		// They fight over delimiter. The delimiter may be a keyword, a
		// number or almost any character which makes the delimiter one of
		// the first tokens that must be parsed.
		//
		// 1. `parse_number` and `parse_operator`
		// They fight over `+` and `-`.
		//
		// 2. `parse_comment` and `parse_operator`
		// They fight over `/` (as in ```/*comment*/``` or ```a / b```)
		//
		// 3. `parse_bool` and `parse_keyword`
		// They fight over `TRUE` and `FALSE`.
		//
		// 4. `parse_keyword` and `parse_unknown`
		// They fight over words. `parse_unknown` does not know about
		// keywords.

		'parse_delimiter',
		'parse_whitespace',
		'parse_number',
		'parse_comment',
		'parse_operator',
		'parse_bool',
		'parse_string',
		'parse_symbol',
		'parse_keyword',
		'parse_label',
		'parse_unknown',
	);


	/**
	 * A list of keywords that indicate that the function keyword
	 * is not used as a function.
	 *
	 * @var string[]
	 */
	const KEYWORD_NAME_INDICATORS = array(
		'FROM',
		'SET',
		'WHERE',
	);

	/**
	 * A list of operators that indicate that the function keyword
	 * is not used as a function.
	 *
	 * @var string[]
	 */
	const OPERATOR_NAME_INDICATORS = array(
		',',
		'.',
	);

	/**
	 * The string to be parsed.
	 *
	 * @var string
	 */
	public $str = '';

	/**
	 * The length of `$str`.
	 *
	 * By storing its length, a lot of time is saved, because parsing methods
	 * would call `strlen` everytime.
	 *
	 * @var int
	 */
	public $string_length = 0;

	/**
	 * The index of the last parsed character.
	 *
	 * @var int
	 */
	public $last = 0;

	/**
	 * The default delimiter. This is used, by default, in all new instances.
	 *
	 * @var string
	 */
	public static $default_delimiter = ';';

	/**
	 * Statements delimiter.
	 * This may change during lexing.
	 *
	 * @var string
	 */
	public $delimiter;

	/**
	 * The length of the delimiter.
	 *
	 * Because `parse_delimiter` can be called a lot, it would perform a lot of
	 * calls to `strlen`, which might affect performance when the delimiter is
	 * big.
	 *
	 * @var int
	 */
	public $delimiter_length;

	/**
	 * List of operators and their flags.
	 *
	 * @var array<string, int>
	 */
	public static $operators = array(

		/*
		 * Some operators (*, =) may have ambiguous flags, because they depend on
		 * the context they are being used in.
		 * For example: 1. SELECT * FROM table; # SQL specific (wildcard)
		 *                 SELECT 2 * 3;        # arithmetic
		 *              2. SELECT * FROM table WHERE foo = 'bar';
		 *                 SET @i = 0;
		 */

		// @see WP_SQLite_Token::FLAG_OPERATOR_ARITHMETIC
		'%'   => 1,
		'*'   => 1,
		'+'   => 1,
		'-'   => 1,
		'/'   => 1,

		// @see WP_SQLite_Token::FLAG_OPERATOR_LOGICAL
		'!'   => 2,
		'!='  => 2,
		'&&'  => 2,
		'<'   => 2,
		'<='  => 2,
		'<=>' => 2,
		'<>'  => 2,
		'='   => 2,
		'>'   => 2,
		'>='  => 2,
		'||'  => 2,

		// @see WP_SQLite_Token::FLAG_OPERATOR_BITWISE
		'&'   => 4,
		'<<'  => 4,
		'>>'  => 4,
		'^'   => 4,
		'|'   => 4,
		'~'   => 4,

		// @see WP_SQLite_Token::FLAG_OPERATOR_ASSIGNMENT
		':='  => 8,

		// @see WP_SQLite_Token::FLAG_OPERATOR_SQL
		'('   => 16,
		')'   => 16,
		'.'   => 16,
		','   => 16,
		';'   => 16,
	);

	/**
	 * List of keywords.
	 *
	 * The value associated to each keyword represents its flags.
	 *
	 * @see WP_SQLite_Token::FLAG_KEYWORD_RESERVED
	 *      WP_SQLite_Token::FLAG_KEYWORD_COMPOSED
	 *      WP_SQLite_Token::FLAG_KEYWORD_DATA_TYPE
	 *      ∂WP_SQLite_Token::FLAG_KEYWORD_KEY
	 *      WP_SQLite_Token::FLAG_KEYWORD_FUNCTION
	 *
	 * @var array<string,int>
	 */
	public static $keywords = array(
		'AT'                                => 1,
		'DO'                                => 1,
		'IO'                                => 1,
		'NO'                                => 1,
		'XA'                                => 1,
		'ANY'                               => 1,
		'CPU'                               => 1,
		'END'                               => 1,
		'IPC'                               => 1,
		'NDB'                               => 1,
		'NEW'                               => 1,
		'ONE'                               => 1,
		'ROW'                               => 1,
		'XID'                               => 1,
		'BOOL'                              => 1,
		'BYTE'                              => 1,
		'CODE'                              => 1,
		'CUBE'                              => 1,
		'DATA'                              => 1,
		'DISK'                              => 1,
		'ENDS'                              => 1,
		'FAST'                              => 1,
		'FILE'                              => 1,
		'FULL'                              => 1,
		'HASH'                              => 1,
		'HELP'                              => 1,
		'HOST'                              => 1,
		'LAST'                              => 1,
		'LESS'                              => 1,
		'LIST'                              => 1,
		'LOGS'                              => 1,
		'MODE'                              => 1,
		'NAME'                              => 1,
		'NEXT'                              => 1,
		'NONE'                              => 1,
		'ONLY'                              => 1,
		'OPEN'                              => 1,
		'PAGE'                              => 1,
		'PORT'                              => 1,
		'PREV'                              => 1,
		'ROWS'                              => 1,
		'SLOW'                              => 1,
		'SOME'                              => 1,
		'STOP'                              => 1,
		'THAN'                              => 1,
		'TYPE'                              => 1,
		'VIEW'                              => 1,
		'WAIT'                              => 1,
		'WORK'                              => 1,
		'X509'                              => 1,
		'AFTER'                             => 1,
		'BEGIN'                             => 1,
		'BLOCK'                             => 1,
		'BTREE'                             => 1,
		'CACHE'                             => 1,
		'CHAIN'                             => 1,
		'CLOSE'                             => 1,
		'ERROR'                             => 1,
		'EVENT'                             => 1,
		'EVERY'                             => 1,
		'FIRST'                             => 1,
		'FIXED'                             => 1,
		'FLUSH'                             => 1,
		'FOUND'                             => 1,
		'HOSTS'                             => 1,
		'LEVEL'                             => 1,
		'LOCAL'                             => 1,
		'LOCKS'                             => 1,
		'MERGE'                             => 1,
		'MUTEX'                             => 1,
		'NAMES'                             => 1,
		'NCHAR'                             => 1,
		'NEVER'                             => 1,
		'OWNER'                             => 1,
		'PHASE'                             => 1,
		'PROXY'                             => 1,
		'QUERY'                             => 1,
		'QUICK'                             => 1,
		'RELAY'                             => 1,
		'RESET'                             => 1,
		'RTREE'                             => 1,
		'SHARE'                             => 1,
		'SLAVE'                             => 1,
		'START'                             => 1,
		'SUPER'                             => 1,
		'SWAPS'                             => 1,
		'TYPES'                             => 1,
		'UNTIL'                             => 1,
		'VALUE'                             => 1,
		'ACTION'                            => 1,
		'ALWAYS'                            => 1,
		'BACKUP'                            => 1,
		'BINLOG'                            => 1,
		'CIPHER'                            => 1,
		'CLIENT'                            => 1,
		'COMMIT'                            => 1,
		'ENABLE'                            => 1,
		'ENGINE'                            => 1,
		'ERRORS'                            => 1,
		'ESCAPE'                            => 1,
		'EVENTS'                            => 1,
		'EXPIRE'                            => 1,
		'EXPORT'                            => 1,
		'FAULTS'                            => 1,
		'FIELDS'                            => 1,
		'FILTER'                            => 1,
		'GLOBAL'                            => 1,
		'GRANTS'                            => 1,
		'IMPORT'                            => 1,
		'ISSUER'                            => 1,
		'LEAVES'                            => 1,
		'MASTER'                            => 1,
		'MEDIUM'                            => 1,
		'MEMORY'                            => 1,
		'MODIFY'                            => 1,
		'NUMBER'                            => 1,
		'OFFSET'                            => 1,
		'PARSER'                            => 1,
		'PLUGIN'                            => 1,
		'RELOAD'                            => 1,
		'REMOVE'                            => 1,
		'REPAIR'                            => 1,
		'RESUME'                            => 1,
		'ROLLUP'                            => 1,
		'SERVER'                            => 1,
		'SIGNED'                            => 1,
		'SIMPLE'                            => 1,
		'SOCKET'                            => 1,
		'SONAME'                            => 1,
		'SOUNDS'                            => 1,
		'SOURCE'                            => 1,
		'STARTS'                            => 1,
		'STATUS'                            => 1,
		'STRING'                            => 1,
		'TABLES'                            => 1,
		'ACCOUNT'                           => 1,
		'ANALYSE'                           => 1,
		'CHANGED'                           => 1,
		'CHANNEL'                           => 1,
		'COLUMNS'                           => 1,
		'COMMENT'                           => 1,
		'COMPACT'                           => 1,
		'CONTEXT'                           => 1,
		'CURRENT'                           => 1,
		'DEFINER'                           => 1,
		'DISABLE'                           => 1,
		'DISCARD'                           => 1,
		'DYNAMIC'                           => 1,
		'ENGINES'                           => 1,
		'EXECUTE'                           => 1,
		'FOLLOWS'                           => 1,
		'GENERAL'                           => 1,
		'HANDLER'                           => 1,
		'INDEXES'                           => 1,
		'INSTALL'                           => 1,
		'INVOKER'                           => 1,
		'LOGFILE'                           => 1,
		'MIGRATE'                           => 1,
		'NO_WAIT'                           => 1,
		'OPTIONS'                           => 1,
		'PARTIAL'                           => 1,
		'PERSIST'                           => 1,
		'PLUGINS'                           => 1,
		'PREPARE'                           => 1,
		'PROFILE'                           => 1,
		'REBUILD'                           => 1,
		'RECOVER'                           => 1,
		'RESTORE'                           => 1,
		'RETURNS'                           => 1,
		'ROUTINE'                           => 1,
		'SESSION'                           => 1,
		'STACKED'                           => 1,
		'STORAGE'                           => 1,
		'SUBJECT'                           => 1,
		'SUSPEND'                           => 1,
		'UNICODE'                           => 1,
		'UNKNOWN'                           => 1,
		'UPGRADE'                           => 1,
		'USE_FRM'                           => 1,
		'WITHOUT'                           => 1,
		'WRAPPER'                           => 1,
		'CASCADED'                          => 1,
		'CHECKSUM'                          => 1,
		'DATAFILE'                          => 1,
		'DUMPFILE'                          => 1,
		'EXCHANGE'                          => 1,
		'EXTENDED'                          => 1,
		'FUNCTION'                          => 1,
		'LANGUAGE'                          => 1,
		'MAX_ROWS'                          => 1,
		'MAX_SIZE'                          => 1,
		'MIN_ROWS'                          => 1,
		'NATIONAL'                          => 1,
		'NVARCHAR'                          => 1,
		'PRECEDES'                          => 1,
		'PRESERVE'                          => 1,
		'PROFILES'                          => 1,
		'REDOFILE'                          => 1,
		'RELAYLOG'                          => 1,
		'ROLLBACK'                          => 1,
		'SCHEDULE'                          => 1,
		'SECURITY'                          => 1,
		'SEQUENCE'                          => 1,
		'SHUTDOWN'                          => 1,
		'SNAPSHOT'                          => 1,
		'SWITCHES'                          => 1,
		'TRIGGERS'                          => 1,
		'UNDOFILE'                          => 1,
		'WARNINGS'                          => 1,
		'AGGREGATE'                         => 1,
		'ALGORITHM'                         => 1,
		'COMMITTED'                         => 1,
		'DIRECTORY'                         => 1,
		'DUPLICATE'                         => 1,
		'EXPANSION'                         => 1,
		'INVISIBLE'                         => 1,
		'IO_THREAD'                         => 1,
		'ISOLATION'                         => 1,
		'NODEGROUP'                         => 1,
		'PACK_KEYS'                         => 1,
		'READ_ONLY'                         => 1,
		'REDUNDANT'                         => 1,
		'SAVEPOINT'                         => 1,
		'SQL_CACHE'                         => 1,
		'TEMPORARY'                         => 1,
		'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,
		'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,
		'SCHEMA_NAME'                       => 1,
		'SQL_TSI_DAY'                       => 1,
		'TRANSACTION'                       => 1,
		'UNCOMMITTED'                       => 1,
		'CATALOG_NAME'                      => 1,
		'CLASS_ORIGIN'                      => 1,
		'DEFAULT_AUTH'                      => 1,
		'DES_KEY_FILE'                      => 1,
		'INITIAL_SIZE'                      => 1,
		'MASTER_DELAY'                      => 1,
		'MESSAGE_TEXT'                      => 1,
		'PARTITIONING'                      => 1,
		'PERSIST_ONLY'                      => 1,
		'RELAY_THREAD'                      => 1,
		'SERIALIZABLE'                      => 1,
		'SQL_NO_CACHE'                      => 1,
		'SQL_TSI_HOUR'                      => 1,
		'SQL_TSI_WEEK'                      => 1,
		'SQL_TSI_YEAR'                      => 1,
		'SUBPARTITION'                      => 1,
		'COLUMN_FORMAT'                     => 1,
		'INSERT_METHOD'                     => 1,
		'MASTER_SSL_CA'                     => 1,
		'RELAY_LOG_POS'                     => 1,
		'SQL_TSI_MONTH'                     => 1,
		'SUBPARTITIONS'                     => 1,
		'AUTO_INCREMENT'                    => 1,
		'AVG_ROW_LENGTH'                    => 1,
		'KEY_BLOCK_SIZE'                    => 1,
		'MASTER_LOG_POS'                    => 1,
		'MASTER_SSL_CRL'                    => 1,
		'MASTER_SSL_KEY'                    => 1,
		'RELAY_LOG_FILE'                    => 1,
		'SQL_TSI_MINUTE'                    => 1,
		'SQL_TSI_SECOND'                    => 1,
		'TABLE_CHECKSUM'                    => 1,
		'USER_RESOURCES'                    => 1,
		'AUTOEXTEND_SIZE'                   => 1,
		'CONSTRAINT_NAME'                   => 1,
		'DELAY_KEY_WRITE'                   => 1,
		'FILE_BLOCK_SIZE'                   => 1,
		'MASTER_LOG_FILE'                   => 1,
		'MASTER_PASSWORD'                   => 1,
		'MASTER_SSL_CERT'                   => 1,
		'PARSE_GCOL_EXPR'                   => 1,
		'REPLICATE_DO_DB'                   => 1,
		'SQL_AFTER_GTIDS'                   => 1,
		'SQL_TSI_QUARTER'                   => 1,
		'SUBCLASS_ORIGIN'                   => 1,
		'MASTER_SERVER_ID'                  => 1,
		'REDO_BUFFER_SIZE'                  => 1,
		'SQL_BEFORE_GTIDS'                  => 1,
		'STATS_PERSISTENT'                  => 1,
		'UNDO_BUFFER_SIZE'                  => 1,
		'CONSTRAINT_SCHEMA'                 => 1,
		'GROUP_REPLICATION'                 => 1,
		'IGNORE_SERVER_IDS'                 => 1,
		'MASTER_SSL_CAPATH'                 => 1,
		'MASTER_SSL_CIPHER'                 => 1,
		'RETURNED_SQLSTATE'                 => 1,
		'SQL_BUFFER_RESULT'                 => 1,
		'STATS_AUTO_RECALC'                 => 1,
		'CONSTRAINT_CATALOG'                => 1,
		'MASTER_RETRY_COUNT'                => 1,
		'MASTER_SSL_CRLPATH'                => 1,
		'MAX_STATEMENT_TIME'                => 1,
		'REPLICATE_DO_TABLE'                => 1,
		'SQL_AFTER_MTS_GAPS'                => 1,
		'STATS_SAMPLE_PAGES'                => 1,
		'REPLICATE_IGNORE_DB'               => 1,
		'MASTER_AUTO_POSITION'              => 1,
		'MASTER_CONNECT_RETRY'              => 1,
		'MAX_QUERIES_PER_HOUR'              => 1,
		'MAX_UPDATES_PER_HOUR'              => 1,
		'MAX_USER_CONNECTIONS'              => 1,
		'REPLICATE_REWRITE_DB'              => 1,
		'REPLICATE_IGNORE_TABLE'            => 1,
		'MASTER_HEARTBEAT_PERIOD'           => 1,
		'REPLICATE_WILD_DO_TABLE'           => 1,
		'MAX_CONNECTIONS_PER_HOUR'          => 1,
		'REPLICATE_WILD_IGNORE_TABLE'       => 1,

		'AS'                                => 3,
		'BY'                                => 3,
		'IS'                                => 3,
		'ON'                                => 3,
		'OR'                                => 3,
		'TO'                                => 3,
		'ADD'                               => 3,
		'ALL'                               => 3,
		'AND'                               => 3,
		'ASC'                               => 3,
		'DEC'                               => 3,
		'DIV'                               => 3,
		'FOR'                               => 3,
		'GET'                               => 3,
		'NOT'                               => 3,
		'OUT'                               => 3,
		'SQL'                               => 3,
		'SSL'                               => 3,
		'USE'                               => 3,
		'XOR'                               => 3,
		'BOTH'                              => 3,
		'CALL'                              => 3,
		'CASE'                              => 3,
		'DESC'                              => 3,
		'DROP'                              => 3,
		'DUAL'                              => 3,
		'EACH'                              => 3,
		'ELSE'                              => 3,
		'EXIT'                              => 3,
		'FROM'                              => 3,
		'INT1'                              => 3,
		'INT2'                              => 3,
		'INT3'                              => 3,
		'INT4'                              => 3,
		'INT8'                              => 3,
		'INTO'                              => 3,
		'JOIN'                              => 3,
		'KEYS'                              => 3,
		'KILL'                              => 3,
		'LIKE'                              => 3,
		'LOAD'                              => 3,
		'LOCK'                              => 3,
		'LONG'                              => 3,
		'LOOP'                              => 3,
		'NULL'                              => 3,
		'OVER'                              => 3,
		'READ'                              => 3,
		'SHOW'                              => 3,
		'THEN'                              => 3,
		'TRUE'                              => 3,
		'UNDO'                              => 3,
		'WHEN'                              => 3,
		'WITH'                              => 3,
		'ALTER'                             => 3,
		'CHECK'                             => 3,
		'CROSS'                             => 3,
		'FALSE'                             => 3,
		'FETCH'                             => 3,
		'FORCE'                             => 3,
		'GRANT'                             => 3,
		'GROUP'                             => 3,
		'INNER'                             => 3,
		'INOUT'                             => 3,
		'LEAVE'                             => 3,
		'LIMIT'                             => 3,
		'LINES'                             => 3,
		'ORDER'                             => 3,
		'OUTER'                             => 3,
		'PURGE'                             => 3,
		'RANGE'                             => 3,
		'READS'                             => 3,
		'RLIKE'                             => 3,
		'TABLE'                             => 3,
		'UNION'                             => 3,
		'USAGE'                             => 3,
		'USING'                             => 3,
		'WHERE'                             => 3,
		'WHILE'                             => 3,
		'WRITE'                             => 3,
		'BEFORE'                            => 3,
		'CHANGE'                            => 3,
		'COLUMN'                            => 3,
		'CREATE'                            => 3,
		'CURSOR'                            => 3,
		'DELETE'                            => 3,
		'ELSEIF'                            => 3,
		'EXCEPT'                            => 3,
		'FLOAT4'                            => 3,
		'FLOAT8'                            => 3,
		'HAVING'                            => 3,
		'IGNORE'                            => 3,
		'INFILE'                            => 3,
		'LINEAR'                            => 3,
		'OPTION'                            => 3,
		'REGEXP'                            => 3,
		'RENAME'                            => 3,
		'RETURN'                            => 3,
		'REVOKE'                            => 3,
		'SELECT'                            => 3,
		'SIGNAL'                            => 3,
		'STORED'                            => 3,
		'UNLOCK'                            => 3,
		'UPDATE'                            => 3,
		'ANALYZE'                           => 3,
		'BETWEEN'                           => 3,
		'CASCADE'                           => 3,
		'COLLATE'                           => 3,
		'DECLARE'                           => 3,
		'DELAYED'                           => 3,
		'ESCAPED'                           => 3,
		'EXPLAIN'                           => 3,
		'FOREIGN'                           => 3,
		'ITERATE'                           => 3,
		'LEADING'                           => 3,
		'NATURAL'                           => 3,
		'OUTFILE'                           => 3,
		'PRIMARY'                           => 3,
		'RELEASE'                           => 3,
		'REQUIRE'                           => 3,
		'SCHEMAS'                           => 3,
		'TRIGGER'                           => 3,
		'VARYING'                           => 3,
		'VIRTUAL'                           => 3,
		'CONTINUE'                          => 3,
		'DAY_HOUR'                          => 3,
		'DESCRIBE'                          => 3,
		'DISTINCT'                          => 3,
		'ENCLOSED'                          => 3,
		'MAXVALUE'                          => 3,
		'MODIFIES'                          => 3,
		'OPTIMIZE'                          => 3,
		'RESIGNAL'                          => 3,
		'RESTRICT'                          => 3,
		'SPECIFIC'                          => 3,
		'SQLSTATE'                          => 3,
		'STARTING'                          => 3,
		'TRAILING'                          => 3,
		'UNSIGNED'                          => 3,
		'ZEROFILL'                          => 3,
		'CONDITION'                         => 3,
		'DATABASES'                         => 3,
		'GENERATED'                         => 3,
		'INTERSECT'                         => 3,
		'MIDDLEINT'                         => 3,
		'PARTITION'                         => 3,
		'PRECISION'                         => 3,
		'PROCEDURE'                         => 3,
		'RECURSIVE'                         => 3,
		'SENSITIVE'                         => 3,
		'SEPARATOR'                         => 3,
		'ACCESSIBLE'                        => 3,
		'ASENSITIVE'                        => 3,
		'CONSTRAINT'                        => 3,
		'DAY_MINUTE'                        => 3,
		'DAY_SECOND'                        => 3,
		'OPTIONALLY'                        => 3,
		'READ_WRITE'                        => 3,
		'REFERENCES'                        => 3,
		'SQLWARNING'                        => 3,
		'TERMINATED'                        => 3,
		'YEAR_MONTH'                        => 3,
		'DISTINCTROW'                       => 3,
		'HOUR_MINUTE'                       => 3,
		'HOUR_SECOND'                       => 3,
		'INSENSITIVE'                       => 3,
		'MASTER_BIND'                       => 3,
		'LOW_PRIORITY'                      => 3,
		'SQLEXCEPTION'                      => 3,
		'VARCHARACTER'                      => 3,
		'DETERMINISTIC'                     => 3,
		'HIGH_PRIORITY'                     => 3,
		'MINUTE_SECOND'                     => 3,
		'STRAIGHT_JOIN'                     => 3,
		'IO_AFTER_GTIDS'                    => 3,
		'SQL_BIG_RESULT'                    => 3,
		'DAY_MICROSECOND'                   => 3,
		'IO_BEFORE_GTIDS'                   => 3,
		'OPTIMIZER_COSTS'                   => 3,
		'HOUR_MICROSECOND'                  => 3,
		'SQL_SMALL_RESULT'                  => 3,
		'MINUTE_MICROSECOND'                => 3,
		'NO_WRITE_TO_BINLOG'                => 3,
		'SECOND_MICROSECOND'                => 3,
		'SQL_CALC_FOUND_ROWS'               => 3,
		'MASTER_SSL_VERIFY_SERVER_CERT'     => 3,

		'NO SQL'                            => 7,
		'GROUP BY'                          => 7,
		'NOT NULL'                          => 7,
		'ORDER BY'                          => 7,
		'SET NULL'                          => 7,
		'AND CHAIN'                         => 7,
		'FULL JOIN'                         => 7,
		'IF EXISTS'                         => 7,
		'LEFT JOIN'                         => 7,
		'LESS THAN'                         => 7,
		'LOAD DATA'                         => 7,
		'NO ACTION'                         => 7,
		'ON DELETE'                         => 7,
		'ON UPDATE'                         => 7,
		'UNION ALL'                         => 7,
		'CROSS JOIN'                        => 7,
		'ESCAPED BY'                        => 7,
		'FOR UPDATE'                        => 7,
		'INNER JOIN'                        => 7,
		'LINEAR KEY'                        => 7,
		'NO RELEASE'                        => 7,
		'OR REPLACE'                        => 7,
		'RIGHT JOIN'                        => 7,
		'ENCLOSED BY'                       => 7,
		'LINEAR HASH'                       => 7,
		'ON SCHEDULE'                       => 7,
		'STARTING BY'                       => 7,
		'AND NO CHAIN'                      => 7,
		'CONTAINS SQL'                      => 7,
		'FOR EACH ROW'                      => 7,
		'NATURAL JOIN'                      => 7,
		'PARTITION BY'                      => 7,
		'SET PASSWORD'                      => 7,
		'SQL SECURITY'                      => 7,
		'CHARACTER SET'                     => 7,
		'IF NOT EXISTS'                     => 7,
		'TERMINATED BY'                     => 7,
		'DATA DIRECTORY'                    => 7,
		'READS SQL DATA'                    => 7,
		'UNION DISTINCT'                    => 7,
		'DEFAULT CHARSET'                   => 7,
		'DEFAULT COLLATE'                   => 7,
		'FULL OUTER JOIN'                   => 7,
		'INDEX DIRECTORY'                   => 7,
		'LEFT OUTER JOIN'                   => 7,
		'SUBPARTITION BY'                   => 7,
		'DISABLE ON SLAVE'                  => 7,
		'GENERATED ALWAYS'                  => 7,
		'RIGHT OUTER JOIN'                  => 7,
		'MODIFIES SQL DATA'                 => 7,
		'NATURAL LEFT JOIN'                 => 7,
		'START TRANSACTION'                 => 7,
		'LOCK IN SHARE MODE'                => 7,
		'NATURAL RIGHT JOIN'                => 7,
		'SELECT TRANSACTION'                => 7,
		'DEFAULT CHARACTER SET'             => 7,
		'ON COMPLETION PRESERVE'            => 7,
		'NATURAL LEFT OUTER JOIN'           => 7,
		'NATURAL RIGHT OUTER JOIN'          => 7,
		'WITH CONSISTENT SNAPSHOT'          => 7,
		'ON COMPLETION NOT PRESERVE'        => 7,

		'BIT'                               => 9,
		'XML'                               => 9,
		'ENUM'                              => 9,
		'JSON'                              => 9,
		'TEXT'                              => 9,
		'ARRAY'                             => 9,
		'SERIAL'                            => 9,
		'BOOLEAN'                           => 9,
		'DATETIME'                          => 9,
		'GEOMETRY'                          => 9,
		'MULTISET'                          => 9,
		'MULTILINEPOINT'                    => 9,
		'MULTILINEPOLYGON'                  => 9,

		'INT'                               => 11,
		'SET'                               => 11,
		'BLOB'                              => 11,
		'REAL'                              => 11,
		'FLOAT'                             => 11,
		'BIGINT'                            => 11,
		'DOUBLE'                            => 11,
		'DECIMAL'                           => 11,
		'INTEGER'                           => 11,
		'NUMERIC'                           => 11,
		'TINYINT'                           => 11,
		'VARCHAR'                           => 11,
		'LONGBLOB'                          => 11,
		'LONGTEXT'                          => 11,
		'SMALLINT'                          => 11,
		'TINYBLOB'                          => 11,
		'TINYTEXT'                          => 11,
		'CHARACTER'                         => 11,
		'MEDIUMINT'                         => 11,
		'VARBINARY'                         => 11,
		'MEDIUMBLOB'                        => 11,
		'MEDIUMTEXT'                        => 11,

		'BINARY VARYING'                    => 15,

		'KEY'                               => 19,
		'INDEX'                             => 19,
		'UNIQUE'                            => 19,
		'SPATIAL'                           => 19,
		'FULLTEXT'                          => 19,

		'INDEX KEY'                         => 23,
		'UNIQUE KEY'                        => 23,
		'FOREIGN KEY'                       => 23,
		'PRIMARY KEY'                       => 23,
		'SPATIAL KEY'                       => 23,
		'FULLTEXT KEY'                      => 23,
		'UNIQUE INDEX'                      => 23,
		'SPATIAL INDEX'                     => 23,
		'FULLTEXT INDEX'                    => 23,

		'X'                                 => 33,
		'Y'                                 => 33,
		'LN'                                => 33,
		'PI'                                => 33,
		'ABS'                               => 33,
		'AVG'                               => 33,
		'BIN'                               => 33,
		'COS'                               => 33,
		'COT'                               => 33,
		'DAY'                               => 33,
		'ELT'                               => 33,
		'EXP'                               => 33,
		'HEX'                               => 33,
		'LOG'                               => 33,
		'MAX'                               => 33,
		'MD5'                               => 33,
		'MID'                               => 33,
		'MIN'                               => 33,
		'NOW'                               => 33,
		'OCT'                               => 33,
		'ORD'                               => 33,
		'POW'                               => 33,
		'SHA'                               => 33,
		'SIN'                               => 33,
		'STD'                               => 33,
		'SUM'                               => 33,
		'TAN'                               => 33,
		'ACOS'                              => 33,
		'AREA'                              => 33,
		'ASIN'                              => 33,
		'ATAN'                              => 33,
		'CAST'                              => 33,
		'CEIL'                              => 33,
		'CONV'                              => 33,
		'HOUR'                              => 33,
		'LOG2'                              => 33,
		'LPAD'                              => 33,
		'RAND'                              => 33,
		'RPAD'                              => 33,
		'SHA1'                              => 33,
		'SHA2'                              => 33,
		'SIGN'                              => 33,
		'SQRT'                              => 33,
		'SRID'                              => 33,
		'ST_X'                              => 33,
		'ST_Y'                              => 33,
		'TRIM'                              => 33,
		'USER'                              => 33,
		'UUID'                              => 33,
		'WEEK'                              => 33,
		'ASCII'                             => 33,
		'ASWKB'                             => 33,
		'ASWKT'                             => 33,
		'ATAN2'                             => 33,
		'COUNT'                             => 33,
		'CRC32'                             => 33,
		'FIELD'                             => 33,
		'FLOOR'                             => 33,
		'INSTR'                             => 33,
		'LCASE'                             => 33,
		'LEAST'                             => 33,
		'LOG10'                             => 33,
		'LOWER'                             => 33,
		'LTRIM'                             => 33,
		'MONTH'                             => 33,
		'POWER'                             => 33,
		'QUOTE'                             => 33,
		'ROUND'                             => 33,
		'RTRIM'                             => 33,
		'SLEEP'                             => 33,
		'SPACE'                             => 33,
		'UCASE'                             => 33,
		'UNHEX'                             => 33,
		'UPPER'                             => 33,
		'ASTEXT'                            => 33,
		'BIT_OR'                            => 33,
		'BUFFER'                            => 33,
		'CONCAT'                            => 33,
		'DECODE'                            => 33,
		'ENCODE'                            => 33,
		'EQUALS'                            => 33,
		'FORMAT'                            => 33,
		'IFNULL'                            => 33,
		'ISNULL'                            => 33,
		'LENGTH'                            => 33,
		'LOCATE'                            => 33,
		'MINUTE'                            => 33,
		'NULLIF'                            => 33,
		'POINTN'                            => 33,
		'SECOND'                            => 33,
		'STDDEV'                            => 33,
		'STRCMP'                            => 33,
		'SUBSTR'                            => 33,
		'WITHIN'                            => 33,
		'ADDDATE'                           => 33,
		'ADDTIME'                           => 33,
		'AGAINST'                           => 33,
		'BIT_AND'                           => 33,
		'BIT_XOR'                           => 33,
		'CEILING'                           => 33,
		'CHARSET'                           => 33,
		'CROSSES'                           => 33,
		'CURDATE'                           => 33,
		'CURTIME'                           => 33,
		'DAYNAME'                           => 33,
		'DEGREES'                           => 33,
		'ENCRYPT'                           => 33,
		'EXTRACT'                           => 33,
		'GLENGTH'                           => 33,
		'ISEMPTY'                           => 33,
		'IS_IPV4'                           => 33,
		'IS_IPV6'                           => 33,
		'IS_UUID'                           => 33,
		'QUARTER'                           => 33,
		'RADIANS'                           => 33,
		'REVERSE'                           => 33,
		'SOUNDEX'                           => 33,
		'ST_AREA'                           => 33,
		'ST_SRID'                           => 33,
		'SUBDATE'                           => 33,
		'SUBTIME'                           => 33,
		'SYSDATE'                           => 33,
		'TOUCHES'                           => 33,
		'TO_DAYS'                           => 33,
		'VAR_POP'                           => 33,
		'VERSION'                           => 33,
		'WEEKDAY'                           => 33,
		'ASBINARY'                          => 33,
		'CENTROID'                          => 33,
		'COALESCE'                          => 33,
		'COMPRESS'                          => 33,
		'CONTAINS'                          => 33,
		'DATEDIFF'                          => 33,
		'DATE_ADD'                          => 33,
		'DATE_SUB'                          => 33,
		'DISJOINT'                          => 33,
		'DISTANCE'                          => 33,
		'ENDPOINT'                          => 33,
		'ENVELOPE'                          => 33,
		'GET_LOCK'                          => 33,
		'GREATEST'                          => 33,
		'ISCLOSED'                          => 33,
		'ISSIMPLE'                          => 33,
		'JSON_SET'                          => 33,
		'MAKEDATE'                          => 33,
		'MAKETIME'                          => 33,
		'MAKE_SET'                          => 33,
		'MBREQUAL'                          => 33,
		'OVERLAPS'                          => 33,
		'PASSWORD'                          => 33,
		'POSITION'                          => 33,
		'ST_ASWKB'                          => 33,
		'ST_ASWKT'                          => 33,
		'ST_UNION'                          => 33,
		'TIMEDIFF'                          => 33,
		'TRUNCATE'                          => 33,
		'VARIANCE'                          => 33,
		'VAR_SAMP'                          => 33,
		'YEARWEEK'                          => 33,
		'ANY_VALUE'                         => 33,
		'BENCHMARK'                         => 33,
		'BIT_COUNT'                         => 33,
		'COLLATION'                         => 33,
		'CONCAT_WS'                         => 33,
		'DAYOFWEEK'                         => 33,
		'DAYOFYEAR'                         => 33,
		'DIMENSION'                         => 33,
		'FROM_DAYS'                         => 33,
		'GEOMETRYN'                         => 33,
		'INET_ATON'                         => 33,
		'INET_NTOA'                         => 33,
		'JSON_KEYS'                         => 33,
		'JSON_TYPE'                         => 33,
		'LOAD_FILE'                         => 33,
		'MBRCOVERS'                         => 33,
		'MBREQUALS'                         => 33,
		'MBRWITHIN'                         => 33,
		'MONTHNAME'                         => 33,
		'NUMPOINTS'                         => 33,
		'ROW_COUNT'                         => 33,
		'ST_ASTEXT'                         => 33,
		'ST_BUFFER'                         => 33,
		'ST_EQUALS'                         => 33,
		'ST_LENGTH'                         => 33,
		'ST_POINTN'                         => 33,
		'ST_WITHIN'                         => 33,
		'SUBSTRING'                         => 33,
		'TO_BASE64'                         => 33,
		'UPDATEXML'                         => 33,
		'BIT_LENGTH'                        => 33,
		'CONVERT_TZ'                        => 33,
		'CONVEXHULL'                        => 33,
		'DAYOFMONTH'                        => 33,
		'EXPORT_SET'                        => 33,
		'FOUND_ROWS'                        => 33,
		'GET_FORMAT'                        => 33,
		'INET6_ATON'                        => 33,
		'INET6_NTOA'                        => 33,
		'INTERSECTS'                        => 33,
		'JSON_ARRAY'                        => 33,
		'JSON_DEPTH'                        => 33,
		'JSON_MERGE'                        => 33,
		'JSON_QUOTE'                        => 33,
		'JSON_VALID'                        => 33,
		'MBRTOUCHES'                        => 33,
		'NAME_CONST'                        => 33,
		'PERIOD_ADD'                        => 33,
		'STARTPOINT'                        => 33,
		'STDDEV_POP'                        => 33,
		'ST_CROSSES'                        => 33,
		'ST_GEOHASH'                        => 33,
		'ST_ISEMPTY'                        => 33,
		'ST_ISVALID'                        => 33,
		'ST_TOUCHES'                        => 33,
		'TO_SECONDS'                        => 33,
		'UNCOMPRESS'                        => 33,
		'UUID_SHORT'                        => 33,
		'WEEKOFYEAR'                        => 33,
		'AES_DECRYPT'                       => 33,
		'AES_ENCRYPT'                       => 33,
		'BIN_TO_UUID'                       => 33,
		'CHAR_LENGTH'                       => 33,
		'DATE_FORMAT'                       => 33,
		'DES_DECRYPT'                       => 33,
		'DES_ENCRYPT'                       => 33,
		'FIND_IN_SET'                       => 33,
		'FROM_BASE64'                       => 33,
		'GEOMFROMWKB'                       => 33,
		'GTID_SUBSET'                       => 33,
		'JSON_INSERT'                       => 33,
		'JSON_LENGTH'                       => 33,
		'JSON_OBJECT'                       => 33,
		'JSON_PRETTY'                       => 33,
		'JSON_REMOVE'                       => 33,
		'JSON_SEARCH'                       => 33,
		'LINEFROMWKB'                       => 33,
		'MBRCONTAINS'                       => 33,
		'MBRDISJOINT'                       => 33,
		'MBROVERLAPS'                       => 33,
		'MICROSECOND'                       => 33,
		'PERIOD_DIFF'                       => 33,
		'POLYFROMWKB'                       => 33,
		'SEC_TO_TIME'                       => 33,
		'STDDEV_SAMP'                       => 33,
		'STR_TO_DATE'                       => 33,
		'ST_ASBINARY'                       => 33,
		'ST_CENTROID'                       => 33,
		'ST_CONTAINS'                       => 33,
		'ST_DISJOINT'                       => 33,
		'ST_DISTANCE'                       => 33,
		'ST_ENDPOINT'                       => 33,
		'ST_ENVELOPE'                       => 33,
		'ST_ISCLOSED'                       => 33,
		'ST_ISSIMPLE'                       => 33,
		'ST_OVERLAPS'                       => 33,
		'ST_SIMPLIFY'                       => 33,
		'ST_VALIDATE'                       => 33,
		'SYSTEM_USER'                       => 33,
		'TIME_FORMAT'                       => 33,
		'TIME_TO_SEC'                       => 33,
		'UUID_TO_BIN'                       => 33,
		'COERCIBILITY'                      => 33,
		'EXTERIORRING'                      => 33,
		'EXTRACTVALUE'                      => 33,
		'GEOMETRYTYPE'                      => 33,
		'GEOMFROMTEXT'                      => 33,
		'GROUP_CONCAT'                      => 33,
		'IS_FREE_LOCK'                      => 33,
		'IS_USED_LOCK'                      => 33,
		'JSON_EXTRACT'                      => 33,
		'JSON_REPLACE'                      => 33,
		'JSON_UNQUOTE'                      => 33,
		'LINEFROMTEXT'                      => 33,
		'MBRCOVEREDBY'                      => 33,
		'MLINEFROMWKB'                      => 33,
		'MPOLYFROMWKB'                      => 33,
		'OCTET_LENGTH'                      => 33,
		'OLD_PASSWORD'                      => 33,
		'POINTFROMWKB'                      => 33,
		'POLYFROMTEXT'                      => 33,
		'RANDOM_BYTES'                      => 33,
		'RELEASE_LOCK'                      => 33,
		'SESSION_USER'                      => 33,
		'ST_ASGEOJSON'                      => 33,
		'ST_DIMENSION'                      => 33,
		'ST_GEOMETRYN'                      => 33,
		'ST_NUMPOINTS'                      => 33,
		'TIMESTAMPADD'                      => 33,
		'CONNECTION_ID'                     => 33,
		'FROM_UNIXTIME'                     => 33,
		'GTID_SUBTRACT'                     => 33,
		'INTERIORRINGN'                     => 33,
		'JSON_CONTAINS'                     => 33,
		'MBRINTERSECTS'                     => 33,
		'MLINEFROMTEXT'                     => 33,
		'MPOINTFROMWKB'                     => 33,
		'MPOLYFROMTEXT'                     => 33,
		'NUMGEOMETRIES'                     => 33,
		'POINTFROMTEXT'                     => 33,
		'ST_CONVEXHULL'                     => 33,
		'ST_DIFFERENCE'                     => 33,
		'ST_INTERSECTS'                     => 33,
		'ST_STARTPOINT'                     => 33,
		'TIMESTAMPDIFF'                     => 33,
		'WEIGHT_STRING'                     => 33,
		'IS_IPV4_COMPAT'                    => 33,
		'IS_IPV4_MAPPED'                    => 33,
		'LAST_INSERT_ID'                    => 33,
		'MPOINTFROMTEXT'                    => 33,
		'POLYGONFROMWKB'                    => 33,
		'ST_GEOMFROMWKB'                    => 33,
		'ST_LINEFROMWKB'                    => 33,
		'ST_POLYFROMWKB'                    => 33,
		'UNIX_TIMESTAMP'                    => 33,
		'GEOMCOLLFROMWKB'                   => 33,
		'MASTER_POS_WAIT'                   => 33,
		'POLYGONFROMTEXT'                   => 33,
		'ST_EXTERIORRING'                   => 33,
		'ST_GEOMETRYTYPE'                   => 33,
		'ST_GEOMFROMTEXT'                   => 33,
		'ST_INTERSECTION'                   => 33,
		'ST_LINEFROMTEXT'                   => 33,
		'ST_MAKEENVELOPE'                   => 33,
		'ST_MLINEFROMWKB'                   => 33,
		'ST_MPOLYFROMWKB'                   => 33,
		'ST_POINTFROMWKB'                   => 33,
		'ST_POLYFROMTEXT'                   => 33,
		'SUBSTRING_INDEX'                   => 33,
		'CHARACTER_LENGTH'                  => 33,
		'GEOMCOLLFROMTEXT'                  => 33,
		'GEOMETRYFROMTEXT'                  => 33,
		'JSON_MERGE_PATCH'                  => 33,
		'NUMINTERIORRINGS'                  => 33,
		'ST_INTERIORRINGN'                  => 33,
		'ST_MLINEFROMTEXT'                  => 33,
		'ST_MPOINTFROMWKB'                  => 33,
		'ST_MPOLYFROMTEXT'                  => 33,
		'ST_NUMGEOMETRIES'                  => 33,
		'ST_POINTFROMTEXT'                  => 33,
		'ST_SYMDIFFERENCE'                  => 33,
		'JSON_ARRAY_APPEND'                 => 33,
		'JSON_ARRAY_INSERT'                 => 33,
		'JSON_STORAGE_FREE'                 => 33,
		'JSON_STORAGE_SIZE'                 => 33,
		'LINESTRINGFROMWKB'                 => 33,
		'MULTIPOINTFROMWKB'                 => 33,
		'RELEASE_ALL_LOCKS'                 => 33,
		'ST_LATFROMGEOHASH'                 => 33,
		'ST_MPOINTFROMTEXT'                 => 33,
		'ST_POLYGONFROMWKB'                 => 33,
		'JSON_CONTAINS_PATH'                => 33,
		'MULTIPOINTFROMTEXT'                => 33,
		'ST_BUFFER_STRATEGY'                => 33,
		'ST_DISTANCE_SPHERE'                => 33,
		'ST_GEOMCOLLFROMTXT'                => 33,
		'ST_GEOMCOLLFROMWKB'                => 33,
		'ST_GEOMFROMGEOJSON'                => 33,
		'ST_LONGFROMGEOHASH'                => 33,
		'ST_POLYGONFROMTEXT'                => 33,
		'JSON_MERGE_PRESERVE'               => 33,
		'MULTIPOLYGONFROMWKB'               => 33,
		'ST_GEOMCOLLFROMTEXT'               => 33,
		'ST_GEOMETRYFROMTEXT'               => 33,
		'ST_NUMINTERIORRINGS'               => 33,
		'ST_POINTFROMGEOHASH'               => 33,
		'UNCOMPRESSED_LENGTH'               => 33,
		'MULTIPOLYGONFROMTEXT'              => 33,
		'ST_LINESTRINGFROMWKB'              => 33,
		'ST_MULTIPOINTFROMWKB'              => 33,
		'ST_MULTIPOINTFROMTEXT'             => 33,
		'MULTILINESTRINGFROMWKB'            => 33,
		'ST_MULTIPOLYGONFROMWKB'            => 33,
		'MULTILINESTRINGFROMTEXT'           => 33,
		'ST_MULTIPOLYGONFROMTEXT'           => 33,
		'GEOMETRYCOLLECTIONFROMWKB'         => 33,
		'ST_MULTILINESTRINGFROMWKB'         => 33,
		'GEOMETRYCOLLECTIONFROMTEXT'        => 33,
		'ST_MULTILINESTRINGFROMTEXT'        => 33,
		'VALIDATE_PASSWORD_STRENGTH'        => 33,
		'WAIT_FOR_EXECUTED_GTID_SET'        => 33,
		'ST_GEOMETRYCOLLECTIONFROMWKB'      => 33,
		'ST_GEOMETRYCOLLECTIONFROMTEXT'     => 33,
		'WAIT_UNTIL_SQL_THREAD_AFTER_GTIDS' => 33,

		'IF'                                => 35,
		'IN'                                => 35,
		'MOD'                               => 35,
		'LEFT'                              => 35,
		'MATCH'                             => 35,
		'RIGHT'                             => 35,
		'EXISTS'                            => 35,
		'INSERT'                            => 35,
		'REPEAT'                            => 35,
		'SCHEMA'                            => 35,
		'VALUES'                            => 35,
		'CONVERT'                           => 35,
		'DEFAULT'                           => 35,
		'REPLACE'                           => 35,
		'DATABASE'                          => 35,
		'UTC_DATE'                          => 35,
		'UTC_TIME'                          => 35,
		'LOCALTIME'                         => 35,
		'CURRENT_DATE'                      => 35,
		'CURRENT_TIME'                      => 35,
		'CURRENT_USER'                      => 35,
		'UTC_TIMESTAMP'                     => 35,
		'LOCALTIMESTAMP'                    => 35,
		'CURRENT_TIMESTAMP'                 => 35,

		'NOT IN'                            => 39,

		'DATE'                              => 41,
		'TIME'                              => 41,
		'YEAR'                              => 41,
		'POINT'                             => 41,
		'POLYGON'                           => 41,
		'TIMESTAMP'                         => 41,
		'LINESTRING'                        => 41,
		'MULTIPOINT'                        => 41,
		'MULTIPOLYGON'                      => 41,
		'MULTILINESTRING'                   => 41,
		'GEOMETRYCOLLECTION'                => 41,

		'CHAR'                              => 43,
		'BINARY'                            => 43,
		'INTERVAL'                          => 43,
	);

	/**
	 * All data type options.
	 *
	 * @var array<string, int|array<int, int|string>>
	 */
	public static $data_type_options = array(
		'BINARY'        => 1,
		'CHARACTER SET' => array(
			2,
			'var',
		),
		'CHARSET'       => array(
			2,
			'var',
		),
		'COLLATE'       => array(
			3,
			'var',
		),
		'UNSIGNED'      => 4,
		'ZEROFILL'      => 5,
	);

	/**
	 * All field options.
	 *
	 * @var array<string, bool|int|array<int, int|string|array<string, bool>>>
	 */
	public static $field_options = array(

		/*
		 * Tells the `OptionsArray` to not sort the options.
		 * See the note below.
		 */
		'_UNSORTED'        => true,

		'NOT NULL'         => 1,
		'NULL'             => 1,
		'DEFAULT'          => array(
			2,
			'expr',
			array( 'breakOnAlias' => true ),
		),

		// Following are not according to grammar, but MySQL happily accepts these at any location.
		'CHARSET'          => array(
			2,
			'var',
		),
		'COLLATE'          => array(
			3,
			'var',
		),
		'AUTO_INCREMENT'   => 3,
		'PRIMARY'          => 4,
		'PRIMARY KEY'      => 4,
		'UNIQUE'           => 4,
		'UNIQUE KEY'       => 4,
		'COMMENT'          => array(
			5,
			'var',
		),
		'COLUMN_FORMAT'    => array(
			6,
			'var',
		),
		'ON UPDATE'        => array(
			7,
			'expr',
		),

		// Generated columns options.
		'GENERATED ALWAYS' => 8,
		'AS'               => array(
			9,
			'expr',
			array( 'parenthesesDelimited' => true ),
		),
		'VIRTUAL'          => 10,
		'PERSISTENT'       => 11,
		'STORED'           => 11,
		'CHECK'            => array(
			12,
			'expr',
			array( 'parenthesesDelimited' => true ),
		),
		'INVISIBLE'        => 13,
		'ENFORCED'         => 14,
		'NOT'              => 15,
		'COMPRESSED'       => 16,

		/*
		 * Common entries.
		 *
		 * NOTE: Some of the common options are not in the same order which
		 * causes troubles when checking if the options are in the right order.
		 * I should find a way to define multiple sets of options and make the
		 * parser select the right set.
		 *
		 * 'UNIQUE'                        => 4,
		 * 'UNIQUE KEY'                    => 4,
		 * 'COMMENT'                       => [5, 'var'],
		 * 'NOT NULL'                      => 1,
		 * 'NULL'                          => 1,
		 * 'PRIMARY'                       => 4,
		 * 'PRIMARY KEY'                   => 4,
		 */
	);

	/**
	 * Quotes mode.
	 *
	 * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_ansi_quotes
	 * @link https://mariadb.com/kb/en/sql-mode/#ansi_quotes
	 */
	const SQL_MODE_ANSI_QUOTES = 2;

	/**
	 * The array of tokens.
	 *
	 * @var stdClass[]
	 */
	public $tokens = array();

	/**
	 * The count of tokens.
	 *
	 * @var int
	 */
	public $tokens_count = 0;

	/**
	 * The index of the next token to be returned.
	 *
	 * @var int
	 */
	public $tokens_index = 0;

	/**
	 * The object constructor.
	 *
	 * @param string $str       The query to be lexed.
	 * @param string $delimiter The delimiter to be used.
	 */
	public function __construct( $str, $delimiter = null ) {
		$this->str = $str;
		// `strlen` is used instead of `mb_strlen` because the lexer needs to parse each byte of the input.
		$this->string_length = strlen( $str );

		// Setting the delimiter.
		$this->set_delimiter( ! empty( $delimiter ) ? $delimiter : static::$default_delimiter );

		$this->lex();
	}

	/**
	 * Sets the delimiter.
	 *
	 * @param string $delimiter The new delimiter.
	 *
	 * @return void
	 */
	public function set_delimiter( $delimiter ) {
		$this->delimiter        = $delimiter;
		$this->delimiter_length = strlen( $delimiter );
	}

	/**
	 * Parses the string and extracts lexemes.
	 *
	 * @return void
	 */
	public function lex() {
		/*
		 * TODO: Sometimes, static::parse* functions make unnecessary calls to
		 * is* functions. For a better performance, some rules can be deduced
		 * from context.
		 * For example, in `parse_bool` there is no need to compare the token
		 * every time with `true` and `false`. The first step would be to
		 * compare with 'true' only and just after that add another letter from
		 * context and compare again with `false`.
		 * Another example is `parse_comment`.
		 */

		/**
		 * Last processed token.
		 *
		 * @var WP_SQLite_Token
		 */
		$last_token = null;

		for ( $this->last = 0, $last_idx = 0; $this->last < $this->string_length; $last_idx = ++$this->last ) {
			/**
			 * The new token.
			 *
			 * @var WP_SQLite_Token
			 */
			$token = null;

			foreach ( self::PARSER_METHODS as $method ) {
				$token = $this->$method();

				if ( $token ) {
					break;
				}
			}

			if ( null === $token ) {
				$token = new WP_SQLite_Token( $this->str[ $this->last ] );
				$this->error( 'Unexpected character.', $this->str[ $this->last ], $this->last );
			} elseif (
				null !== $last_token
				&& WP_SQLite_Token::TYPE_SYMBOL === $token->type
				&& $token->flags & WP_SQLite_Token::FLAG_SYMBOL_VARIABLE
				&& (
					WP_SQLite_Token::TYPE_STRING === $last_token->type
					|| (
						WP_SQLite_Token::TYPE_SYMBOL === $last_token->type
						&& $last_token->flags & WP_SQLite_Token::FLAG_SYMBOL_BACKTICK
					)
				)
			) {
				// Handles ```... FROM 'user'@'%' ...```.
				$last_token->token .= $token->token;
				$last_token->type   = WP_SQLite_Token::TYPE_SYMBOL;
				$last_token->flags  = WP_SQLite_Token::FLAG_SYMBOL_USER;
				$last_token->value .= '@' . $token->value;
				continue;
			} elseif (
				null !== $last_token
				&& WP_SQLite_Token::TYPE_KEYWORD === $token->type
				&& WP_SQLite_Token::TYPE_OPERATOR === $last_token->type
				&& '.' === $last_token->value
			) {
				// Handles ```... tbl.FROM ...```. In this case, FROM is not a reserved word.
				$token->type  = WP_SQLite_Token::TYPE_NONE;
				$token->flags = 0;
				$token->value = $token->token;
			}

			$token->position = $last_idx;

			$this->tokens[ $this->tokens_count++ ] = $token;

			// Handling delimiters.
			if ( WP_SQLite_Token::TYPE_NONE === $token->type && 'DELIMITER' === $token->value ) {
				if ( $this->last + 1 >= $this->string_length ) {
					$this->error( 'Expected whitespace(s) before delimiter.', '', $this->last + 1 );
					continue;
				}

				/*
				 * Skipping last R (from `delimiteR`) and whitespaces between
				 * the keyword `DELIMITER` and the actual delimiter.
				 */
				$pos   = ++$this->last;
				$token = $this->parse_whitespace();

				if ( null !== $token ) {
					$token->position                       = $pos;
					$this->tokens[ $this->tokens_count++ ] = $token;
				}

				// Preparing the token that holds the new delimiter.
				if ( $this->last + 1 >= $this->string_length ) {
					$this->error( 'Expected delimiter.', '', $this->last + 1 );
					continue;
				}

				$pos = $this->last + 1;

				// Parsing the delimiter.
				$this->delimiter  = null;
				$delimiter_length = 0;
				while (
					++$this->last < $this->string_length
					&& ! static::is_whitespace( $this->str[ $this->last ] )
					&& $delimiter_length < 15
				) {
					$this->delimiter .= $this->str[ $this->last ];
					++$delimiter_length;
				}

				if ( empty( $this->delimiter ) ) {
					$this->error( 'Expected delimiter.', '', $this->last );
					$this->delimiter = ';';
				}

				--$this->last;

				// Saving the delimiter and its token.
				$this->delimiter_length                = strlen( $this->delimiter );
				$token                                 = new WP_SQLite_Token( $this->delimiter, WP_SQLite_Token::TYPE_DELIMITER );
				$token->position                       = $pos;
				$this->tokens[ $this->tokens_count++ ] = $token;
			}

			$last_token = $token;
		}

		// Adding a final delimiter to mark the ending.
		$this->tokens[ $this->tokens_count++ ] = new WP_SQLite_Token( null, WP_SQLite_Token::TYPE_DELIMITER );

		$this->solve_ambiguity_on_star_operator();
		$this->solve_ambiguity_on_function_keywords();
	}

	/**
	 * Resolves the ambiguity when dealing with the "*" operator.
	 *
	 * In SQL statements, the "*" operator can be an arithmetic operator (like in 2*3) or an SQL wildcard (like in
	 * SELECT a.* FROM ...). To solve this ambiguity, the solution is to find the next token, excluding whitespaces and
	 * comments, right after the "*" position. The "*" is for sure an SQL wildcard if the next token found is any of:
	 * - "FROM" (the FROM keyword like in "SELECT * FROM...");
	 * - "USING" (the USING keyword like in "DELETE table_name.* USING...");
	 * - "," (a comma separator like in "SELECT *, field FROM...");
	 * - ")" (a closing parenthesis like in "COUNT(*)").
	 * This methods will change the flag of the "*" tokens when any of those condition above is true. Otherwise, the
	 * default flag (arithmetic) will be kept.
	 *
	 * @return void
	 */
	private function solve_ambiguity_on_star_operator() {
		$i_bak = $this->tokens_index;
		while ( true ) {
			$star_token = $this->tokens_get_next_of_type_and_value( WP_SQLite_Token::TYPE_OPERATOR, '*' );
			if ( null === $star_token ) {
				break;
			}
			// tokens_get_next() already gets rid of whitespaces and comments.
			$next = $this->tokens_get_next();

			if ( null === $next ) {
				continue;
			}

			if (
				( WP_SQLite_Token::TYPE_KEYWORD !== $next->type || ! in_array( $next->value, array( 'FROM', 'USING' ), true ) )
				&& ( WP_SQLite_Token::TYPE_OPERATOR !== $next->type || ! in_array( $next->value, array( ',', ')' ), true ) )
			) {
				continue;
			}

			$star_token->flags = WP_SQLite_Token::FLAG_OPERATOR_SQL;
		}

		$this->tokens_index = $i_bak;
	}

	/**
	 * Resolves the ambiguity when dealing with the functions keywords.
	 *
	 * In SQL statements, the function keywords might be used as table names or columns names.
	 * To solve this ambiguity, the solution is to find the next token, excluding whitespaces and
	 * comments, right after the function keyword position. The function keyword is for sure used
	 * as column name or table name if the next token found is any of:
	 *
	 * - "FROM" (the FROM keyword like in "SELECT Country x, AverageSalary avg FROM...");
	 * - "WHERE" (the WHERE keyword like in "DELETE FROM emp x WHERE x.salary = 20");
	 * - "SET" (the SET keyword like in "UPDATE Country x, City y set x.Name=x.Name");
	 * - "," (a comma separator like 'x,' in "UPDATE Country x, City y set x.Name=x.Name");
	 * - "." (a dot separator like in "x.asset_id FROM (SELECT evt.asset_id FROM evt)".
	 * - "NULL" (when used as a table alias like in "avg.col FROM (SELECT ev.col FROM ev) avg").
	 *
	 * This method will change the flag of the function keyword tokens when any of those
	 * condition above is true. Otherwise, the
	 * default flag (function keyword) will be kept.
	 *
	 * @return void
	 */
	private function solve_ambiguity_on_function_keywords() {
		$i_bak            = $this->tokens_index;
		$keyword_function = WP_SQLite_Token::TYPE_KEYWORD | WP_SQLite_Token::FLAG_KEYWORD_FUNCTION;
		while ( true ) {
			$keyword_token = $this->tokens_get_next_of_type_and_flag( WP_SQLite_Token::TYPE_KEYWORD, $keyword_function );
			if ( null === $keyword_token ) {
				break;
			}
			$next = $this->tokens_get_next();
			if (
				( WP_SQLite_Token::TYPE_KEYWORD !== $next->type
					|| ! in_array( $next->value, self::KEYWORD_NAME_INDICATORS, true )
				)
				&& ( WP_SQLite_Token::TYPE_OPERATOR !== $next->type
					|| ! in_array( $next->value, self::OPERATOR_NAME_INDICATORS, true )
				)
				&& ( null !== $next->value )
			) {
				continue;
			}

			$keyword_token->type    = WP_SQLite_Token::TYPE_NONE;
			$keyword_token->flags   = WP_SQLite_Token::TYPE_NONE;
			$keyword_token->keyword = $keyword_token->value;
		}

		$this->tokens_index = $i_bak;
	}

	/**
	 * Creates a new error log.
	 *
	 * @param string $msg  The error message.
	 * @param string $str  The character that produced the error.
	 * @param int    $pos  The position of the character.
	 * @param int    $code The code of the error.
	 *
	 * @throws Exception The error log.
	 * @return void
	 */
	public function error( $msg, $str = '', $pos = 0, $code = 0 ) {
		throw new Exception(
			print_r(
				array(
					'query'    => $this->str,
					'message'  => $msg,
					'str'      => $str,
					'position' => $pos,
					'code'     => $code,
				),
				true
			)
		);
	}

	/**
	 * Parses a keyword.
	 *
	 * @return WP_SQLite_Token|null
	 */
	public function parse_keyword() {
		$token = '';

		/**
		 * Value to be returned.
		 *
		 * @var WP_SQLite_Token
		 */
		$ret = null;

		// The value of `$this->last` where `$token` ends in `$this->str`.
		$i_end = $this->last;

		// Whether last parsed character is a whitespace.
		$last_space = false;

		for ( $j = 1; $j < static::KEYWORD_MAX_LENGTH && $this->last < $this->string_length; ++$j, ++$this->last ) {
			$last_space = false;
			// Composed keywords shouldn't have more than one whitespace between keywords.
			if ( static::is_whitespace( $this->str[ $this->last ] ) ) {
				if ( $last_space ) {
					--$j; // The size of the keyword didn't increase.
					continue;
				}

				$last_space = true;
			}

			$token .= $this->str[ $this->last ];
			$flags  = static::is_keyword( $token );

			if ( ( $this->last + 1 !== $this->string_length && ! static::is_separator( $this->str[ $this->last + 1 ] ) ) || ! $flags ) {
				continue;
			}

			$ret   = new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_KEYWORD, $flags );
			$i_end = $this->last;

			/*
			 * We don't break so we find longest keyword.
			 * For example, `OR` and `ORDER` have a common prefix `OR`.
			 * If we stopped at `OR`, the parsing would be invalid.
			 */
		}

		$this->last = $i_end;

		return $ret;
	}

	/**
	 * Parses a label.
	 *
	 * @return WP_SQLite_Token|null
	 */
	public function parse_label() {
		$token = '';

		/**
		 * Value to be returned.
		 *
		 * @var WP_SQLite_Token
		 */
		$ret = null;

		// The value of `$this->last` where `$token` ends in `$this->str`.
		$i_end = $this->last;
		for ( $j = 1; $j < static::LABEL_MAX_LENGTH && $this->last < $this->string_length; ++$j, ++$this->last ) {
			if ( ':' === $this->str[ $this->last ] && $j > 1 ) {
				// End of label.
				$token .= $this->str[ $this->last ];
				$ret    = new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_LABEL );
				$i_end  = $this->last;
				break;
			}

			if ( static::is_whitespace( $this->str[ $this->last ] ) && $j > 1 ) {
				/*
				 * Whitespace between label and `:`.
				 * The size of the keyword didn't increase.
				 */
				--$j;
			} elseif ( static::is_separator( $this->str[ $this->last ] ) ) {
				// Any other separator.
				break;
			}

			$token .= $this->str[ $this->last ];
		}

		$this->last = $i_end;

		return $ret;
	}

	/**
	 * Parses an operator.
	 *
	 * @return WP_SQLite_Token|null
	 */
	public function parse_operator() {
		$token = '';

		/**
		 * Value to be returned.
		 *
		 * @var WP_SQLite_Token
		 */
		$ret = null;

		// The value of `$this->last` where `$token` ends in `$this->str`.
		$i_end = $this->last;

		for ( $j = 1; $j < static::OPERATOR_MAX_LENGTH && $this->last < $this->string_length; ++$j, ++$this->last ) {
			$token .= $this->str[ $this->last ];
			$flags  = static::is_operator( $token );

			if ( ! $flags ) {
				continue;
			}

			$ret   = new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_OPERATOR, $flags );
			$i_end = $this->last;
		}

		$this->last = $i_end;

		return $ret;
	}

	/**
	 * Parses a whitespace.
	 *
	 * @return WP_SQLite_Token|null
	 */
	public function parse_whitespace() {
		$token = $this->str[ $this->last ];

		if ( ! static::is_whitespace( $token ) ) {
			return null;
		}

		while ( ++$this->last < $this->string_length && static::is_whitespace( $this->str[ $this->last ] ) ) {
			$token .= $this->str[ $this->last ];
		}

		--$this->last;

		return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_WHITESPACE );
	}

	/**
	 * Parses a comment.
	 *
	 * @return WP_SQLite_Token|null
	 */
	public function parse_comment() {
		$i_bak = $this->last;
		$token = $this->str[ $this->last ];

		// Bash style comments (#comment\n).
		if ( static::is_comment( $token ) ) {
			while ( ++$this->last < $this->string_length && "\n" !== $this->str[ $this->last ] ) {
				$token .= $this->str[ $this->last ];
			}

			// Include trailing \n as whitespace token.
			if ( $this->last < $this->string_length ) {
				--$this->last;
			}

			return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_COMMENT, WP_SQLite_Token::FLAG_COMMENT_BASH );
		}

		// C style comments (/*comment*\/).
		if ( ++$this->last < $this->string_length ) {
			$token .= $this->str[ $this->last ];
			if ( static::is_comment( $token ) ) {
				// There might be a conflict with "*" operator here, when string is "*/*".
				// This can occurs in the following statements:
				// - "SELECT */* comment */ FROM ..."
				// - "SELECT 2*/* comment */3 AS `six`;".
				$next = $this->last + 1;
				if ( ( $next < $this->string_length ) && '*' === $this->str[ $next ] ) {
					// Conflict in "*/*": first "*" was not for ending a comment.
					// Stop here and let other parsing method define the true behavior of that first star.
					$this->last = $i_bak;

					return null;
				}

				$flags = WP_SQLite_Token::FLAG_COMMENT_C;

				// This comment already ended. It may be a part of a previous MySQL specific command.
				if ( '*/' === $token ) {
					return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_COMMENT, $flags );
				}

				// Checking if this is a MySQL-specific command.
				if ( $this->last + 1 < $this->string_length && '!' === $this->str[ $this->last + 1 ] ) {
					$flags |= WP_SQLite_Token::FLAG_COMMENT_MYSQL_CMD;
					$token .= $this->str[ ++$this->last ];

					while (
						++$this->last < $this->string_length
						&& $this->str[ $this->last ] >= '0'
						&& $this->str[ $this->last ] <= '9'
					) {
						$token .= $this->str[ $this->last ];
					}

					--$this->last;

					// We split this comment and parse only its beginning here.
					return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_COMMENT, $flags );
				}

				// Parsing the comment.
				while (
					++$this->last < $this->string_length
					&& ( '*' !== $this->str[ $this->last - 1 ] || '/' !== $this->str[ $this->last ] )
				) {
					$token .= $this->str[ $this->last ];
				}

				// Adding the ending.
				if ( $this->last < $this->string_length ) {
					$token .= $this->str[ $this->last ];
				}

				return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_COMMENT, $flags );
			}
		}

		// SQL style comments (-- comment\n).
		if ( ++$this->last < $this->string_length ) {
			$token .= $this->str[ $this->last ];
			$end    = false;
		} else {
			--$this->last;
			$end = true;
		}

		if ( static::is_comment( $token, $end ) ) {
			// Checking if this comment did not end already (```--\n```).
			if ( "\n" !== $this->str[ $this->last ] ) {
				while ( ++$this->last < $this->string_length && "\n" !== $this->str[ $this->last ] ) {
					$token .= $this->str[ $this->last ];
				}
			}

			// Include trailing \n as whitespace token.
			if ( $this->last < $this->string_length ) {
				--$this->last;
			}

			return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_COMMENT, WP_SQLite_Token::FLAG_COMMENT_SQL );
		}

		$this->last = $i_bak;

		return null;
	}

	/**
	 * Parses a boolean.
	 *
	 * @return WP_SQLite_Token|null
	 */
	public function parse_bool() {
		if ( $this->last + 3 >= $this->string_length ) {
			// At least `min(strlen('TRUE'), strlen('FALSE'))` characters are required.
			return null;
		}

		$i_bak = $this->last;
		$token = $this->str[ $this->last ] . $this->str[ ++$this->last ]
		. $this->str[ ++$this->last ] . $this->str[ ++$this->last ]; // _TRUE_ or _FALS_e.

		if ( static::is_bool( $token ) ) {
			return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_BOOL );
		}

		if ( ++$this->last < $this->string_length ) {
			$token .= $this->str[ $this->last ]; // fals_E_.
			if ( static::is_bool( $token ) ) {
				return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_BOOL, 1 );
			}
		}

		$this->last = $i_bak;

		return null;
	}

	/**
	 * Parses a number.
	 *
	 * @return WP_SQLite_Token|null
	 */
	public function parse_number() {
		/*
		 * A rudimentary state machine is being used to parse numbers due to
		 * the various forms of their notation.
		 *
		 * Below are the states of the machines and the conditions to change
		 * the state.
		 *
		 *      1 --------------------[ + or - ]-------------------> 1
		 *      1 -------------------[ 0x or 0X ]------------------> 2
		 *      1 --------------------[ 0 to 9 ]-------------------> 3
		 *      1 -----------------------[ . ]---------------------> 4
		 *      1 -----------------------[ b ]---------------------> 7
		 *
		 *      2 --------------------[ 0 to F ]-------------------> 2
		 *
		 *      3 --------------------[ 0 to 9 ]-------------------> 3
		 *      3 -----------------------[ . ]---------------------> 4
		 *      3 --------------------[ e or E ]-------------------> 5
		 *
		 *      4 --------------------[ 0 to 9 ]-------------------> 4
		 *      4 --------------------[ e or E ]-------------------> 5
		 *
		 *      5 ---------------[ + or - or 0 to 9 ]--------------> 6
		 *
		 *      7 -----------------------[ ' ]---------------------> 8
		 *
		 *      8 --------------------[ 0 or 1 ]-------------------> 8
		 *      8 -----------------------[ ' ]---------------------> 9
		 *
		 * State 1 may be reached by negative numbers.
		 * State 2 is reached only by hex numbers.
		 * State 4 is reached only by float numbers.
		 * State 5 is reached only by numbers in approximate form.
		 * State 7 is reached only by numbers in bit representation.
		 *
		 * Valid final states are: 2, 3, 4 and 6. Any parsing that finished in a
		 * state other than these is invalid.
		 * Also, negative states are invalid states.
		 */
		$i_bak = $this->last;
		$token = '';
		$flags = 0;
		$state = 1;
		for ( ; $this->last < $this->string_length; ++$this->last ) {
			if ( 1 === $state ) {
				if ( '-' === $this->str[ $this->last ] ) {
					$flags |= WP_SQLite_Token::FLAG_NUMBER_NEGATIVE;
				} elseif (
					$this->last + 1 < $this->string_length
					&& '0' === $this->str[ $this->last ]
					&& 'x' === $this->str[ $this->last + 1 ]
				) {
					$token .= $this->str[ $this->last++ ];
					$state  = 2;
				} elseif ( $this->str[ $this->last ] >= '0' && $this->str[ $this->last ] <= '9' ) {
					$state = 3;
				} elseif ( '.' === $this->str[ $this->last ] ) {
					$state = 4;
				} elseif ( 'b' === $this->str[ $this->last ] ) {
					$state = 7;
				} elseif ( '+' !== $this->str[ $this->last ] ) {
					// `+` is a valid character in a number.
					break;
				}
			} elseif ( 2 === $state ) {
				$flags |= WP_SQLite_Token::FLAG_NUMBER_HEX;
				if (
					! (
						( $this->str[ $this->last ] >= '0' && $this->str[ $this->last ] <= '9' )
						|| ( $this->str[ $this->last ] >= 'A' && $this->str[ $this->last ] <= 'F' )
						|| ( $this->str[ $this->last ] >= 'a' && $this->str[ $this->last ] <= 'f' )
					)
				) {
					break;
				}
			} elseif ( 3 === $state ) {
				if ( '.' === $this->str[ $this->last ] ) {
					$state = 4;
				} elseif ( 'e' === $this->str[ $this->last ] || 'E' === $this->str[ $this->last ] ) {
					$state = 5;
				} elseif (
					( $this->str[ $this->last ] >= 'a' && $this->str[ $this->last ] <= 'z' )
					|| ( $this->str[ $this->last ] >= 'A' && $this->str[ $this->last ] <= 'Z' )
				) {
					// A number can't be directly followed by a letter.
					$state = -$state;
				} elseif ( $this->str[ $this->last ] < '0' || $this->str[ $this->last ] > '9' ) {
					// Just digits and `.`, `e` and `E` are valid characters.
					break;
				}
			} elseif ( 4 === $state ) {
				$flags |= WP_SQLite_Token::FLAG_NUMBER_FLOAT;
				if ( 'e' === $this->str[ $this->last ] || 'E' === $this->str[ $this->last ] ) {
					$state = 5;
				} elseif (
					( $this->str[ $this->last ] >= 'a' && $this->str[ $this->last ] <= 'z' )
					|| ( $this->str[ $this->last ] >= 'A' && $this->str[ $this->last ] <= 'Z' )
				) {
					// A number can't be directly followed by a letter.
					$state = -$state;
				} elseif ( $this->str[ $this->last ] < '0' || $this->str[ $this->last ] > '9' ) {
					// Just digits, `e` and `E` are valid characters.
					break;
				}
			} elseif ( 5 === $state ) {
				$flags |= WP_SQLite_Token::FLAG_NUMBER_APPROXIMATE;
				if (
					'+' === $this->str[ $this->last ] || '-' === $this->str[ $this->last ]
					|| ( $this->str[ $this->last ] >= '0' && $this->str[ $this->last ] <= '9' )
				) {
					$state = 6;
				} elseif (
					( $this->str[ $this->last ] >= 'a' && $this->str[ $this->last ] <= 'z' )
					|| ( $this->str[ $this->last ] >= 'A' && $this->str[ $this->last ] <= 'Z' )
				) {
					// A number can't be directly followed by a letter.
					$state = -$state;
				} else {
					break;
				}
			} elseif ( 6 === $state ) {
				if ( $this->str[ $this->last ] < '0' || $this->str[ $this->last ] > '9' ) {
					// Just digits are valid characters.
					break;
				}
			} elseif ( 7 === $state ) {
				$flags |= WP_SQLite_Token::FLAG_NUMBER_BINARY;
				if ( '\'' !== $this->str[ $this->last ] ) {
					break;
				}

				$state = 8;
			} elseif ( 8 === $state ) {
				if ( '\'' === $this->str[ $this->last ] ) {
					$state = 9;
				} elseif ( '0' !== $this->str[ $this->last ] && '1' !== $this->str[ $this->last ] ) {
					break;
				}
			} elseif ( 9 === $state ) {
				break;
			}

			$token .= $this->str[ $this->last ];
		}

		if ( 2 === $state || 3 === $state || ( '.' !== $token && 4 === $state ) || 6 === $state || 9 === $state ) {
			--$this->last;

			return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_NUMBER, $flags );
		}

		$this->last = $i_bak;

		return null;
	}

	/**
	 * Parses a string.
	 *
	 * @param string $quote Additional starting symbol.
	 *
	 * @return WP_SQLite_Token|null
	 */
	public function parse_string( $quote = '' ) {
		$token = $this->str[ $this->last ];
		$flags = static::is_string( $token );

		if ( ! $flags && $token !== $quote ) {
			return null;
		}

		$quote = $token;

		while ( ++$this->last < $this->string_length ) {
			if (
				$this->last + 1 < $this->string_length
				&& (
					( $this->str[ $this->last ] === $quote && $this->str[ $this->last + 1 ] === $quote )
					|| ( '\\' === $this->str[ $this->last ] && '`' !== $quote )
				)
			) {
				$token .= $this->str[ $this->last ] . $this->str[ ++$this->last ];
			} else {
				if ( $this->str[ $this->last ] === $quote ) {
					break;
				}

				$token .= $this->str[ $this->last ];
			}
		}

		if ( $this->last >= $this->string_length || $this->str[ $this->last ] !== $quote ) {
			$this->error(
				sprintf(
					'Ending quote %1$s was expected.',
					$quote
				),
				'',
				$this->last
			);
		} else {
			$token .= $this->str[ $this->last ];
		}

		return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_STRING, $flags );
	}

	/**
	 * Parses a symbol.
	 *
	 * @return WP_SQLite_Token|null
	 */
	public function parse_symbol() {
		$token = $this->str[ $this->last ];
		$flags = static::is_symbol( $token );

		if ( ! $flags ) {
			return null;
		}

		if ( $flags & WP_SQLite_Token::FLAG_SYMBOL_VARIABLE ) {
			if ( $this->last + 1 < $this->string_length && '@' === $this->str[ ++$this->last ] ) {
				// This is a system variable (e.g. `@@hostname`).
				$token .= $this->str[ $this->last++ ];
				$flags |= WP_SQLite_Token::FLAG_SYMBOL_SYSTEM;
			}
		} elseif ( $flags & WP_SQLite_Token::FLAG_SYMBOL_PARAMETER ) {
			if ( '?' !== $token && $this->last + 1 < $this->string_length ) {
				++$this->last;
			}
		} else {
			$token = '';
		}

		$str = null;

		if ( $this->last < $this->string_length ) {
			$str = $this->parse_string( '`' );

			if ( null === $str ) {
				$str = $this->parse_unknown();

				if ( null === $str && ! ( $flags & WP_SQLite_Token::FLAG_SYMBOL_PARAMETER ) ) {
					$this->error( 'Variable name was expected.', $this->str[ $this->last ], $this->last );
				}
			}
		}

		if ( null !== $str ) {
			$token .= $str->token;
		}

		return new WP_SQLite_Token( $token, WP_SQLite_Token::TYPE_SYMBOL, $flags );
	}

	/**
	 * Parses unknown parts of the query.
	 *
	 * @return WP_SQLite_Token|null
	 */
	public function parse_unknown() {
		$token = $this->str[ $this->last ];
		if ( static::is_separator( $token ) ) {
			return null;
		}

		while ( ++$this->last < $this->string_length && ! static::is_separator( $this->str[ $this->last ] ) ) {
			$token .= $this->str[ $this->last ];

			// Test if end of token equals the current delimiter. If so, remove it from the token.
			if ( str_ends_with( $token, $this->delimiter ) ) {
				$token       = substr( $token, 0, -$this->delimiter_length );
				$this->last -= $this->delimiter_length - 1;
				break;
			}
		}

		--$this->last;

		return new WP_SQLite_Token( $token );
	}

	/**
	 * Parses the delimiter of the query.
	 *
	 * @return WP_SQLite_Token|null
	 */
	public function parse_delimiter() {
		$index = 0;

		while ( $index < $this->delimiter_length && $this->last + $index < $this->string_length ) {
			if ( $this->delimiter[ $index ] !== $this->str[ $this->last + $index ] ) {
				return null;
			}

			++$index;
		}

		$this->last += $this->delimiter_length - 1;

		return new WP_SQLite_Token( $this->delimiter, WP_SQLite_Token::TYPE_DELIMITER );
	}

	/**
	 * Checks if the given string is a keyword.
	 *
	 * @param string $str         String to be checked.
	 * @param bool   $is_reserved Checks if the keyword is reserved.
	 *
	 * @return int|null
	 */
	public static function is_keyword( $str, $is_reserved = false ) {
		$str = strtoupper( $str );

		if ( isset( static::$keywords[ $str ] ) ) {
			if ( $is_reserved && ! ( static::$keywords[ $str ] & WP_SQLite_Token::FLAG_KEYWORD_RESERVED ) ) {
				return null;
			}

			return static::$keywords[ $str ];
		}

		return null;
	}

	/**
	 * Checks if the given string is an operator.
	 *
	 * @param string $str String to be checked.
	 *
	 * @return int|null The appropriate flag for the operator.
	 */
	public static function is_operator( $str ) {
		if ( ! isset( static::$operators[ $str ] ) ) {
			return null;
		}

		return static::$operators[ $str ];
	}

	/**
	 * Checks if the given character is a whitespace.
	 *
	 * @param string $str String to be checked.
	 *
	 * @return bool
	 */
	public static function is_whitespace( $str ) {
		return ( ' ' === $str ) || ( "\r" === $str ) || ( "\n" === $str ) || ( "\t" === $str );
	}

	/**
	 * Checks if the given string is the beginning of a whitespace.
	 *
	 * @param string $str String to be checked.
	 * @param mixed  $end Whether this is the end of the string.
	 *
	 * @return int|null The appropriate flag for the comment type.
	 */
	public static function is_comment( $str, $end = false ) {
		$string_length = strlen( $str );
		if ( 0 === $string_length ) {
			return null;
		}

		// If comment is Bash style (#).
		if ( '#' === $str[0] ) {
			return WP_SQLite_Token::FLAG_COMMENT_BASH;
		}

		// If comment is opening C style (/*), warning, it could be a MySQL command (/*!).
		if ( ( $string_length > 1 ) && ( '/' === $str[0] ) && ( '*' === $str[1] ) ) {
			return ( $string_length > 2 ) && ( '!' === $str[2] ) ?
				WP_SQLite_Token::FLAG_COMMENT_MYSQL_CMD : WP_SQLite_Token::FLAG_COMMENT_C;
		}

		// If comment is closing C style (*/), warning, it could conflicts with wildcard and a real opening C style.
		// It would looks like the following valid SQL statement: "SELECT */* comment */ FROM...".
		if ( ( $string_length > 1 ) && ( '*' === $str[0] ) && ( '/' === $str[1] ) ) {
			return WP_SQLite_Token::FLAG_COMMENT_C;
		}

		// If comment is SQL style (--\s?).
		if ( ( $string_length > 2 ) && ( '-' === $str[0] ) && ( '-' === $str[1] ) && static::is_whitespace( $str[2] ) ) {
			return WP_SQLite_Token::FLAG_COMMENT_SQL;
		}

		if ( ( 2 === $string_length ) && $end && ( '-' === $str[0] ) && ( '-' === $str[1] ) ) {
			return WP_SQLite_Token::FLAG_COMMENT_SQL;
		}

		return null;
	}

	/**
	 * Checks if the given string is a boolean value.
	 * This actually checks only for `TRUE` and `FALSE` because `1` or `0` are
	 * numbers and are parsed by specific methods.
	 *
	 * @param string $str String to be checked.
	 *
	 * @return bool
	 */
	public static function is_bool( $str ) {
		$str = strtoupper( $str );

		return ( 'TRUE' === $str ) || ( 'FALSE' === $str );
	}

	/**
	 * Checks if the given character can be a part of a number.
	 *
	 * @param string $str String to be checked.
	 *
	 * @return bool
	 */
	public static function is_number( $str ) {
		return ( $str >= '0' ) && ( $str <= '9' ) || ( '.' === $str )
			|| ( '-' === $str ) || ( '+' === $str ) || ( 'e' === $str ) || ( 'E' === $str );
	}

	/**
	 * Checks if the given character is the beginning of a symbol. A symbol
	 * can be either a variable or a field name.
	 *
	 * @param string $str String to be checked.
	 *
	 * @return int|null The appropriate flag for the symbol type.
	 */
	public static function is_symbol( $str ) {
		if ( 0 === strlen( $str ) ) {
			return null;
		}

		if ( '@' === $str[0] ) {
			return WP_SQLite_Token::FLAG_SYMBOL_VARIABLE;
		}

		if ( '`' === $str[0] ) {
			return WP_SQLite_Token::FLAG_SYMBOL_BACKTICK;
		}

		if ( ':' === $str[0] || '?' === $str[0] ) {
			return WP_SQLite_Token::FLAG_SYMBOL_PARAMETER;
		}

		return null;
	}

	/**
	 * Checks if the given character is the beginning of a string.
	 *
	 * @param string $str String to be checked.
	 *
	 * @return int|null The appropriate flag for the string type.
	 */
	public static function is_string( $str ) {
		if ( strlen( $str ) === 0 ) {
			return null;
		}

		if ( '\'' === $str[0] ) {
			return WP_SQLite_Token::FLAG_STRING_SINGLE_QUOTES;
		}

		if ( '"' === $str[0] ) {
			return WP_SQLite_Token::FLAG_STRING_DOUBLE_QUOTES;
		}

		return null;
	}

	/**
	 * Checks if the given character can be a separator for two lexeme.
	 *
	 * @param string $str String to be checked.
	 *
	 * @return bool
	 */
	public static function is_separator( $str ) {
		/*
		 * NOTES:   Only non alphanumeric ASCII characters may be separators.
		 * `~` is the last printable ASCII character.
		 */
		return ( $str <= '~' )
			&& ( '_' !== $str )
			&& ( '$' !== $str )
			&& ( ( $str < '0' ) || ( $str > '9' ) )
			&& ( ( $str < 'a' ) || ( $str > 'z' ) )
			&& ( ( $str < 'A' ) || ( $str > 'Z' ) );
	}

	/**
	 * Constructor.
	 *
	 * @param stdClass[] $tokens The initial array of tokens.
	 */
	public function tokens( array $tokens = array() ) {
		$this->tokens       = $tokens;
		$this->tokens_count = count( $tokens );
	}

	/**
	 * Gets the next token.
	 *
	 * @param int $type The type of the token.
	 * @param int $flag The flag of the token.
	 */
	public function tokens_get_next_of_type_and_flag( $type, $flag ) {
		for ( ; $this->tokens_index < $this->tokens_count; ++$this->tokens_index ) {
			if ( ( $this->tokens[ $this->tokens_index ]->type === $type ) && ( $this->tokens[ $this->tokens_index ]->flags === $flag ) ) {
				return $this->tokens[ $this->tokens_index++ ];
			}
		}

		return null;
	}

	/**
	 * Gets the next token.
	 *
	 * @param int    $type  The type of the token.
	 * @param string $value The value of the token.
	 *
	 * @return stdClass|null
	 */
	public function tokens_get_next_of_type_and_value( $type, $value ) {
		for ( ; $this->tokens_index < $this->tokens_count; ++$this->tokens_index ) {
			if ( ( $this->tokens[ $this->tokens_index ]->type === $type ) && ( $this->tokens[ $this->tokens_index ]->value === $value ) ) {
				return $this->tokens[ $this->tokens_index++ ];
			}
		}

		return null;
	}

	/**
	 * Gets the next token. Skips any irrelevant token (whitespaces and
	 * comments).
	 *
	 * @return stdClass|null
	 */
	public function tokens_get_next() {
		for ( ; $this->tokens_index < $this->tokens_count; ++$this->tokens_index ) {
			if (
				( WP_SQLite_Token::TYPE_WHITESPACE !== $this->tokens[ $this->tokens_index ]->type )
				&& ( WP_SQLite_Token::TYPE_COMMENT !== $this->tokens[ $this->tokens_index ]->type )
			) {
				return $this->tokens[ $this->tokens_index++ ];
			}
		}

		return null;
	}
}
