Пример #1
0
 protected function _partConfig(Db_Query $query, Db_Query_Part $part)
 {
     $cfg = array();
     $fields = $part->getFields();
     $objectConfig = Db_Object_Config::getInstance($part->getObject());
     $mainPanel = new stdClass();
     $mainPanel->xtype = 'panel';
     $mainPanel->border = false;
     ksort($fields);
     foreach ($fields as $name => $config) {
         $obj = new stdClass();
         $obj->xtype = 'reportfield';
         $obj->valueSelected = $config['select'];
         $obj->valueTitle = $config['title'];
         $obj->valueAlias = $config['alias'];
         $obj->valueField = $name;
         $obj->valueIsLink = false;
         $obj->valueSelectSub = $config['selectSub'];
         $obj->valueObject = $part->getObject();
         $obj->valuePartId = $part->getId();
         $obj->valueSubObject = '';
         $obj->valueSubObjectTtile = '';
         if ($config['isLink'] && !$objectConfig->isDictionaryLink($name)) {
             $obj->valueIsLink = true;
             $child = $query->findChild($part->getId(), $name);
             if ($child !== false) {
                 $linked = $child->getObject();
             } else {
                 $linked = $objectConfig->getLinkedObject($name);
             }
             if ($linked) {
                 $obj->valueSubObject = $linked;
                 $obj->valueSubObjectTtile = Db_Object_Config::getInstance($linked)->get('title');
             }
         }
         $cfg[] = $obj;
     }
     $mainPanel->items = $cfg;
     return array('items' => $mainPanel, 'objectcfg' => array('join' => $part->joinType, 'title' => $objectConfig->get('title'), 'object' => $part->getObject(), 'childField' => $part->getChildField()));
 }
Пример #2
0
 /**
  * Split shard partitin to new shards. It takes the full table or single partition
  * and split it to multiple partitions according to the number of provided 'parts'.
  * When doing initioa split 'shard' may be ommited however 'fields' shall be provided
  * @method split
  * @static
  * @param {Q_Tree} $config Contains all necessary information for split procedure in the following format:
  * @example
  *	{
  *		"plugin": "PLUGINNAME", // the name of plugin - shall be used by app
  *		"connection": "CONNECTIONNAME", // connection - shall be registered with plugin
  *		"table": "TABLENAME", // the table to shard
  *		"class": "CLASSNAME", // the class which is stored in the table
  *		"fields": {"FIELDNAME": "HASH", "FIELDNAME": "HASH", ...}, // Optional. Used only when starting sharding
  *			"shard": "SHARDNAME" // Optionsl. The shard to split. If no shards defined or SHARDNAME does not exist the script will fail
  *			// "parts" can be either array of connections or object {"SHARDNAME": connection, ...}
  *		"parts": {
  *			"SHARDNAME": {
  *				"prefix": "PREFIX",
  *				"dsn": "DSN",
  *					...
  *			},
  *			"SHARDNAME": {
  *				"prefix": "PREFIX",
  *				"dsn": "DSN",
  *					"username": "******",
  *					"password": "******",
  *					"driver_options": {
  *						"3": 2
  *					}
  *			},
  *			...
  *		}
  *	}
  *
  * @return {boolean} Weather php part of the process completed successfuly
  */
 static function split($config)
 {
     // all input data shall be provided
     // for future extension plugin/connection/table/class are considered unrelated
     if (!($plugin = $config->get('plugin', false))) {
         echo "Plugin name is not defined\n";
         return false;
     }
     // plugin shall be registered!
     if (!Q_Config::get('Q', 'pluginInfo', $plugin, false)) {
         echo "Plugin '{$plugin}' is not registered in the platform\n";
         return false;
     }
     if (!($connection = $config->get('connection', false))) {
         echo "Connection '{$connection}' is not defined\n";
         return false;
     }
     // connection shall exist and be registered with plugin!
     if (!Q_Config::get('Db', 'connections', $connection, false)) {
         echo "Connection '{$connection}' does not exist\n";
         return false;
     }
     if (!in_array($connection, Q_Config::get('Q', 'pluginInfo', $plugin, 'connections', array()))) {
         echo "Connection '{$connection}' is not registered for plugin '{$plugin}'\n";
         return false;
     }
     if (!($class = $config->get('class', false))) {
         echo "Class name is not defined\n";
         return false;
     }
     if (!($table = $config->get('table', false))) {
         echo "Table name is not defined\n";
         return false;
     }
     if (!($shard = $config->get('shard', false)) && Q_Config::get('Db', 'connections', $connection, 'shards', false)) {
         echo "Shard to partition is not defined\n";
         return false;
     }
     if (!($parts = $config->get('parts', false))) {
         echo "New parts are not defined\n";
         return false;
     }
     if ($node = $config->get('node', null)) {
         $nodeInternal = Q_Config::expect('Q', 'nodeInternal');
         $node = array("http://{$nodeInternal['host']}:{$nodeInternal['port']}/Q_Utils/query", $node);
     }
     // now we shall distinguish if table is already sharded or not
     if ($shard === false) {
         if (!($fields = $config->get('fields', false))) {
             echo "To start sharding you shall define 'fields' parameter\n";
             return false;
         }
     }
     // weather provided split config is mapped or not
     $split_mapped = array_keys($parts) !== range(0, count($parts) - 1);
     // set up config for shards if it does not exist yet
     if ($shard === false) {
         $partition = array();
         foreach ($fields as $name => $hash) {
             if (empty($hash)) {
                 $hash = 'md5';
             }
             $part = explode('%', $hash);
             $hash = $part[0];
             $len = isset($part[1]) ? $part[1] : Db_Query::HASH_LEN;
             // "0" has the lowest ascii code for both md5 and normalize
             //	$partition[] = $hash === 'md5' ? str_pad('', $len, "0", STR_PAD_LEFT) : str_pad('', $len, " ", STR_PAD_LEFT);
             $partition[] = str_pad('', $len, "0", STR_PAD_LEFT);
         }
         $shard = join('.', $partition);
         if (Q_Config::get('Db', 'connections', $connection, 'indexes', $table, false)) {
             echo "Shards are not defined but indexes for table '{$table}' are defined in local config\n";
             return false;
         }
         // Let's merge in dummy shards section - shard with name '' is handled as single table
         Q_Config::merge(array('Db' => array('connections' => array($connection => array("shards" => array(), "indexes" => array($table => array("fields" => $fields, "partition" => $split_mapped ? array($shard => '') : array($shard))))))));
         $shard_name = '';
     }
     // get partition information
     if (!($partition = Q_Config::get('Db', 'connections', $connection, 'indexes', $table, 'partition', false))) {
         echo "Upps, cannot get shards partitioning\n";
         return false;
     }
     // weather main config is mapped or not
     // also $points contains the partitioning array without mapping
     $points = ($mapped = array_keys($partition) !== range(0, count($partition) - 1)) ? array_keys($partition) : $partition;
     $i = array_search($shard, $points);
     $next = isset($points[++$i]) ? $points[$i] : null;
     $fields = Q_Config::expect('Db', 'connections', $connection, 'indexes', $table, 'fields');
     // now $shard and $next contain boundaries for data to split
     // $points contain partitioning array without mapping - array
     // $parts contains split parts (shards) definition - array or object ($split_mapped)
     // $partition contains current partitioning - array or object ($mapped)
     // $fields contains field names and hashes
     // time to calculate new split point(s)
     if (!isset($shard_name)) {
         $shard_name = $mapped ? $partition[$shard] : $shard;
     }
     $shard_db = $class::db();
     $pdo = $shard_db->reallyConnect($shard_name);
     $shard_table = $class::table();
     $shard_table = str_replace('{$dbname}', $shard_db->dbname, $shard_table);
     $shard_table = str_replace('{$prefix}', $shard_db->prefix, $shard_table);
     // verify if current shard is updated to latest version
     $current_version = $shard_db->select('version', "{$shard_db->prefix}Q_plugin")->where(array("plugin" => $plugin))->fetchAll(PDO::FETCH_ASSOC);
     if (!empty($current_version)) {
         $current_version = $current_version[0]['version'];
         $version = Q_Config::get('Q', "pluginInfo", $plugin, 'version', null);
         if (Q::compareVersion($current_version, $version) < 0) {
             echo "Please, update plugin '{$plugin}' to version '{$version}' (currently {$current_version})\n";
             return false;
         }
     } else {
         echo "Cannot get installed version of plugin '{$plugin}'\n";
         return false;
     }
     // We'll limit search with shard boundaries using latin1 string comparison
     $lower = join(explode('.', $shard));
     $upper = isset($next) ? join(explode('.', $next)) : null;
     $normalize = false;
     $where = $group = $order = array();
     foreach (array_keys($fields) as $i => $field) {
         $hash = !empty($fields[$field]) ? $fields[$field] : 'md5';
         $part = explode('%', $hash);
         $normalize = $normalize || ($hash = strtoupper($part[0])) === 'NORMALIZE';
         $len = isset($part[1]) ? $part[1] : Db_Query::HASH_LEN;
         $group[] = $field;
         $order[] = "CAST({$hash}({$field}) AS CHAR({$len}))";
     }
     // if any field uses 'normalize' hash
     // the original shard shall have MySQL NORMALIZE() function defined
     // MySQL version of NORMALIZE handles only 255 chars and does not add md5 hash
     // (see Db_Utils::normalize)
     if ($normalize) {
         try {
             $pdo->exec("DROP FUNCTION IF EXISTS NORMALIZE;");
             $pdo->exec("CREATE FUNCTION NORMALIZE(s CHAR(255))\n\t\t\t\t\t\tRETURNS CHAR(255) DETERMINISTIC\n\t\t\t\t\t\tBEGIN\n\t\t\t\t\t    \tDECLARE res CHAR(255) DEFAULT '';\n\t\t\t\t\t  \t\tDECLARE t CHAR(1);\n\t\t\t\t\t    \tWHILE LENGTH(s) > 0 DO\n\t\t\t\t\t        \tSET t = LOWER(LEFT(s, 1));\n\t\t\t\t\t    \t    SET s = SUBSTRING(s FROM 2);\n\t\t\t\t\t        \tIF t REGEXP '[^A-Za-z0-9]' THEN\n\t\t\t\t\t            \tSET t = '_';\n\t\t\t\t\t        \tEND IF;\n\t\t\t\t\t        \tSET res = CONCAT(res, t);\n\t\t\t\t\t    \tEND WHILE;\n\t\t\t\t\t    \tRETURN res;\n\t\t\t\t\t\tEND");
         } catch (Exception $e) {
             //echo "ERROR: {$e->getMessage()}\n";
             echo "Please, make sure that db user for shard '{$shard_name}' has 'CREATE ROUTINE' permission\n";
             return false;
         }
     }
     $order = join(', ', $order);
     $group = join(', ', $group);
     $where = "(STRCMP(CONCAT({$order}), '{$lower}') >= 0)" . (isset($upper) ? " AND (STRCMP(CONCAT({$order}), '{$upper}') < 0)" : "");
     $count = reset($pdo->query("SELECT COUNT(*) FROM {$shard_table} WHERE {$where}")->fetchAll(PDO::FETCH_NUM));
     if (empty($count)) {
         echo "Failed to connect to shard '{$shard_name}'\n";
         return false;
     }
     $count = reset($count);
     if ($count == 0) {
         echo "Cannot split empty shard!\n";
         return false;
     }
     // if only one new shard provided script will copy data and cnange config
     if (($num_shards = count($parts)) < 1) {
         echo "Please, provide at least one new shard";
         return false;
     }
     $break = round($count / $num_shards);
     // if split config is not mapped and current config is mapped we shall convert split
     //  config to mapped
     $new_partition = $mapped || $split_mapped ? array($shard => $split_mapped ? reset(array_keys($parts)) : $shard) : array($shard);
     $new_shards = array($split_mapped ? reset(array_keys($parts)) : $shard => reset($parts));
     $i = 0;
     foreach (array_slice($parts, 1) as $name => $dsn) {
         $offset = $break * ++$i;
         $split = reset($pdo->query("SELECT {$group} FROM {$shard_table} WHERE {$where} ORDER BY {$order} LIMIT {$offset}, 1")->fetchAll(PDO::FETCH_ASSOC));
         foreach ($fields as $field => $hash) {
             $split[$field] = Db_Query::hashed($split[$field], $hash);
         }
         $split = join('.', $split);
         if ($mapped || $split_mapped) {
             $new_partition[$split] = $split_mapped ? $name : $split;
         } else {
             $new_partition[] = $split;
         }
         $new_shards[$new_name = $split_mapped ? $name : $split] = $dsn;
         if (Q_Config::get('Db', 'connections', $connection, 'shards', $new_name, false)) {
             echo "WARNING!!! Shard already exists: '{$new_name}'\n";
         }
     }
     Q_Config::merge(array('Db' => array('connections' => array($connection => array("shards" => $new_shards)))));
     // if split config is mapped and current config is not we shall convert app config to mapped
     if ($split_mapped && !$mapped) {
         $partition = array();
         foreach ($points as $point) {
             $partition[$point] = $point;
         }
         Q_Config::set('Db', 'connections', $connection, 'indexes', $table, 'partition', $partition);
         $mapped = true;
     }
     // TODO: verify if new shards sizes are approx. equal
     // Verify versions of existing shards and
     // Install pligin schema to new shards
     Q_Plugin::installSchema(Q_PLUGINS_DIR . DS . $plugin, $plugin, 'plugin', $connection, array('sql' => array($connection => array('enabled' => true))));
     // make sure 'upcoming' config is loaded
     $configFiles = Q_Config::get('Q', 'configFiles', array());
     // 'local/Q/bootstrap.json' should be loaded already but we'll better check
     if (!in_array('Q/config/bootstrap.json', $configFiles)) {
         echo "Config file 'Q/config/bootstrap.json' shall be loaded via 'Q/configFiles key'\non every Q server - check 'platform/config/Q.json'\n";
         return false;
     }
     $upcoming_file = Q_Config::get('Q', 'internal', 'sharding', 'upcoming', 'Db/config/upcoming.json');
     //if (!unlink ($upcoming_file)) {
     //	echo "Please, manually remove file '$upcoming_file' and start this script again.\n";
     //	return false;
     //}
     if (!in_array($upcoming_file, $configFiles)) {
         // add upcoming.json to config
         if (!Q_Config::setOnServer('Q/config/bootstrap.json', array('Q' => array('configFiles' => array($upcoming_file))))) {
             echo "Failed to update 'local/Q/bootstrap.json'\n";
             return false;
         }
     }
     // Now after some short time all workers (php and node) will be ready for splitting
     // We'll let node server to wait necessary amount of time.
     $res = Q_Utils::queryInternal('Db/Shards', array('Q/method' => 'split', 'shard' => $shard_name, 'shards' => Q::json_encode($new_shards), 'part' => $shard, 'table' => $table, 'dbTable' => $shard_table, 'class' => $class, 'plugin' => $plugin, 'connection' => $connection, 'where' => $where, 'parts' => Q::json_encode(array('partition' => $new_partition, 'fields' => $fields))), $node);
     if ($res) {
         echo "Split process for shard '{$shard_name}' ({$shard}) has started\nPlease, monitor node.js console for important messages and process status\n";
         return true;
     }
     echo "Failed to start split process at node server\n";
     return false;
 }
Пример #3
0
 /**
  * Analyzes the query's criteria and decides where to execute the query.
  * Here is sample shards config:
  * 
  * **NOTE:** *"fields" shall be an object with keys as fields names and values containing hash definition
  * 		in the format "type%length" where type is one of 'md5' or 'normalize' and length is hash length
  * 		hash definition can be empty string or false. In such case 'md5%7' is used*
  *
  * **NOTE:** *"partition" can be an array. In such case shards shall be named after partition points*
  *
  *
  *	"Streams": {
  *		"prefix": "streams_",
  *		"dsn": "mysql:host=127.0.0.1;dbname=DBNAME",
  *		"username": "******",
  *		"password": "******",
  *		"driver_options": {
  *			"3": 2
  *		},
  *		"shards": {
  *			"alpha": {
  *				"prefix": "alpha_",
  *				"dsn": "mysql:host=127.0.0.1;dbname=SHARDDBNAME",
  *				"username": "******",
  *				"password": "******",
  *				"driver_options": {
  *					"3": 2
  *				}
  *			},
  *			"betta": {
  *				"prefix": "betta_",
  *				"dsn": "mysql:host=127.0.0.1;dbname=SHARDDBNAME",
  *				"username": "******",
  *				"password": "******",
  *				"driver_options": {
  *					"3": 2
  *				}
  *			},
  *			"gamma": {
  *				"prefix": "gamma_",
  *				"dsn": "mysql:host=127.0.0.1;dbname=SHARDDBNAME",
  *				"username": "******",
  *				"password": "******",
  *				"driver_options": {
  *					"3": 2
  *				}
  *			},
  *			"delta": {
  *				"prefix": "delta_",
  *				"dsn": "mysql:host=127.0.0.1;dbname=SHARDDBNAME",
  *				"username": "******",
  *				"password": "******",
  *				"driver_options": {
  *					"3": 2
  *				}
  *			}
  *		},
  *		"indexes": {
  *			"Stream": {
  *				"fields": {"publisherId": "md5", "name": "normalize"},
  *				"partition": {
  *					"0000000.       ": "alpha",
  *					"0000000.sample_": "betta",
  *					"4000000.       ": "gamma",
  *					"4000000.sample_": "delta",
  *					"8000000.       ": "alpha",
  *					"8000000.sample_": "betta",
  *					"c000000.       ": "gamma",
  *					"c000000.sample_": "delta"
  *				}
  *			}
  *		}
  *	}
  *
  * @method shard
  * @param {array} [$upcoming=null] Temporary config to use in sharding. Used during shard split process only
  * @param {array} [$criteria=null] Rarely used unless testing what shards the query would be executed on. Overrides the sharding criteria for the query.
  * @return {array} Returns an array of ($shardName => $query) pairs, where $shardName
  *  can be the name of a shard, '' for just the main shard, or "*" to have the query run on all the shards.
  */
 function shard($upcoming = null, $criteria = null)
 {
     if (isset($criteria)) {
         $this->criteria = $criteria;
     }
     $index = $this->shardIndex();
     if (!$index) {
         return array("" => $this);
     }
     if (empty($this->criteria)) {
         return array("*" => $this);
     }
     if (empty($index['fields'])) {
         throw new Exception("Db_Query: index for {$this->className} should have at least one field");
     }
     if (!isset($index['partition'])) {
         return array("" => $this);
     }
     $hashed = array();
     $fields = array_keys($index['fields']);
     foreach ($fields as $i => $field) {
         if (!isset($this->criteria[$field])) {
             // not enough information to target the query
             return array("*" => $this);
         }
         $value = $this->criteria[$field];
         $hash = !empty($index['fields'][$field]) ? $index['fields'][$field] : 'md5';
         $parts = explode('%', $hash);
         $hash = $parts[0];
         $len = isset($parts[1]) ? $parts[1] : self::HASH_LEN;
         if (is_array($value)) {
             $arr = array();
             foreach ($value as $v) {
                 $arr[] = self::applyHash($v, $hash, $len);
             }
             $hashed[$i] = $arr;
         } else {
             if ($value instanceof Db_Range) {
                 if ($hash !== 'normalize') {
                     throw new Exception("Db_Query: ranges don't work with {$hash} hash");
                 }
                 $hashed_min = self::applyHash($value->min, $hash, $len);
                 $hashed_max = self::applyHash($value->max, $hash, $len);
                 $hashed[$i] = new Db_Range($hashed_min, $value->includeMin, $value->includeMax, $hashed_max);
             } else {
                 $hashed[$i] = self::applyHash($value, $hash, $len);
             }
         }
     }
     if (array_keys($index['partition']) === range(0, count($index['partition']) - 1)) {
         // $index['partition'] is simple array, name the shards after the partition points
         self::$mapping = array_combine($index['partition'], $index['partition']);
     } else {
         self::$mapping = $index['partition'];
     }
     return $this->shard_internal($index, $hashed);
 }
Пример #4
0
 /**
  * set default params
  *
  * @param array $default
  */
 public static function setDefault(array $default)
 {
     self::$_default = array_merge(self::$_default, $default);
 }