Explorar o código

added route Notify; added table struct functions in pdo

Piotr Labudda %!s(int64=10) %!d(string=hai) anos
pai
achega
f81031c884
Modificáronse 2 ficheiros con 800 adicións e 0 borrados
  1. 394 0
      SE/se-lib/Core/Pdo.php
  2. 406 0
      SE/se-lib/Route/Notify.php

+ 394 - 0
SE/se-lib/Core/Pdo.php

@@ -24,6 +24,400 @@ class Core_Pdo extends PDO {
 		return $this->_zasob_id;
 		return $this->_zasob_id;
 	}
 	}
 
 
+	public function getTableStruct($tblName) {
+		$sth = $this->prepare("
+			-- show fields from {$tblName}
+			select cols.COLUMN_NAME as name
+				, cols.DATA_TYPE as type
+				, cols.COLUMN_TYPE as type_mysql
+				, if('YES' = cols.IS_NULLABLE, 1, 0) as is_nullable
+				, cols.COLUMN_DEFAULT as default_value
+				, if(cols.COLUMN_DEFAULT is null, 1, 0) as default_is_null
+				, cols.CHARACTER_MAXIMUM_LENGTH as max_length
+				, cols.NUMERIC_PRECISION as num_precision
+				, cols.NUMERIC_SCALE as num_scale
+				, cols.CHARACTER_SET_NAME as char_encoding -- latin2
+				, cols.COLLATION_NAME as char_collation -- latin2_general_ci
+				, cols.EXTRA as extra
+		--		, cols.*
+			from INFORMATION_SCHEMA.COLUMNS cols
+			where cols.TABLE_SCHEMA = :db_name
+				and cols.TABLE_NAME = :tbl_name
+		");
+		$sth->bindValue(':db_name', $this->getDatabaseName(), PDO::PARAM_STR);
+		$sth->bindValue(':tbl_name', $tblName, PDO::PARAM_STR);
+		$sth->execute();
+		$structRaw = $sth->fetchAll();
+		if (empty($structRaw)) throw new Exception("Empty struct for table '{$tblName}'");
+		foreach ($structRaw as $field) {
+			$struct[$field['name']] = $field;
+		}
+		return $struct;
+	}
+
+	public function assertTableStructXsd($tblName, $expectedStructXsd) {
+		throw new Exception("Unimplemented - TODO!");
+/*
+- `decimal(5,2)`:
+	<xsd:element name="A" type="decimal_5_2"/>
+
+	<xsd:simpleType name="decimal_5_2">
+		<xsd:restriction base="xsd:decimal">
+			<xsd:totalDigits value="5"/>
+			<xsd:fractionDigits value="2"/>
+		</xsd:restriction>
+	</xsd:simpleType>
+*/
+/* MySQL types:
+	int tinyint smallint mediumint bigint
+	decimal
+	float double real => double
+	date datetime timestamp time year
+	char varchar
+	text tinytext mediumtext longtext
+	enum
+	set
+
+	bit
+	boolean => `tinyint(1)` -- 0 or 1
+	serial  => `bigint(20) unsigned` and unique key
+	binary varbinary
+	blob tinyblob mediumblob longblob
+	geometry point linestring polygon multipoint multilinestring multipolygon geometrycollection
+*/
+	}
+
+	/*
+	 * TODO: keys name may be different - try to find and connect with given schema?
+	 * TODO: remove old uniq keys?
+	 */
+	public function assertTableStruct($tblName, $expectedStruct, $params = array()) {
+		// $expectedStruct['t1'] = array('type'=>'varchar', 'max_length'=>300, 'default_value'=>null)
+		$expectedStruct = $this->_fixExpectedStruct($expectedStruct);
+		//DBG::_(true, true, "fixedEpectedStruct", $expectedStruct, __CLASS__, __FUNCTION__, __LINE__);
+		//DBG::_(true, true, "fixedEpectedStruct", $this->showCreateStructMysql($tblName, $expectedStruct, $params), __CLASS__, __FUNCTION__, __LINE__);
+		$struct = $this->getTableStruct($tblName);
+		$expectedFields = array_keys($expectedStruct);
+		$currentFields = array_keys($struct);
+
+		$missingFields = array_diff($expectedFields, $currentFields);
+		DBG::_(true, true, "struct", $struct, __CLASS__, __FUNCTION__, __LINE__);
+		DBG::_(true, true, "missingFields", $missingFields, __CLASS__, __FUNCTION__, __LINE__);
+
+		foreach ($missingFields as $fldName => $expected) {
+			DBG::_(true, true, "TODO: add missing field[{$fldName}]:", $expected, __CLASS__, __FUNCTION__, __LINE__);
+			$sqlType = array();
+			$sqlType['type'] = $expected['type'];
+			$sqlType['null'] = ($expected['is_nullable'])? '' : 'NOT NULL';
+			$sqlType['default'] = ($expected['is_nullable'])? '' : 'NOT NULL';
+			$sqlAdd = "alter table CRM_NOTIFY add {$fldName} {$sqlType['type']} {$sqlType['null']} {$sqlType['default']}";
+			DBG::_(true, true, "TODO: sqlChange", $sqlAdd, __CLASS__, __FUNCTION__, __LINE__);
+		}
+
+		$toUpdateFields = array_intersect($expectedFields, $currentFields);
+		DBG::_(true, true, "toUpdateFields", $toUpdateFields, __CLASS__, __FUNCTION__, __LINE__);
+		foreach ($toUpdateFields as $fldName) {
+			$current = $struct[$fldName];
+			$expected = $expectedStruct[$fldName];
+			$needChange = false;
+			$toChange = array();
+			$toChange['type'] = $current['type'];
+			$toChange['null'] = ($current['is_nullable'])? '' : 'NOT NULL';
+			// TODO: cannot determine when default is NULL
+			$toChange['default'] = '';
+			if (!empty($current['default_value'])) $toChange['default'] = "DEFAULT '{$current['default_value']}'";
+			if (is_null($current['default_value'])) $toChange['default'] = "DEFAULT NULL";
+
+			DBG::_(true, true, "TODO: update field[{$fldName}]:", array('expected'=>$expected,'current'=>$current), __CLASS__, __FUNCTION__, __LINE__);
+			if ($current['type'] != $expected['type']) {
+				throw new Exception("Unimplemented change field type from {$current['type']} to {$expected['type']}");
+			}
+
+			if ($current['is_nullable'] != $expected['is_nullable']) {
+				if (!$current['is_nullable']) {
+					$toChange['null'] = '';
+				}
+			}
+			$sqlChange = "alter table CRM_NOTIFY change {$fldName} {$fldName} {$toChange['type']} {$toChange['null']} {$toChange['default']}";
+			if ($needChange) {
+				DBG::_(true, true, "TODO: sqlChange", $sqlChange, __CLASS__, __FUNCTION__, __LINE__);
+			}
+		}
+		if(0)foreach ($struct as $col) {
+			$fldName = $col['Field'];
+			if (!array_key_exists($fldName, $expectedStruct)) continue;// skip not defined cols
+			$expected = $expectedStruct[$fldName];
+			$toChange = array();
+			$toChange['need_change'] = false;
+			$toChange['type'] = $col['Type'];
+			$toChange['null'] = ('YES' == $col['Null'])? '' : 'NOT NULL';
+			// TODO: cannot determine when default is NULL
+			$toChange['default'] = (!is_null($col['Default']))? "DEFAULT {$col['Default']}" : '';
+
+			$expectedNullable = (array_key_exists('nullable', $expected) && $expected['nullable']);
+			$colNullable = ('YES' == $col['Null']);
+			if ($expectedNullable != $colNullable) {
+				$toChange['null'] = ($expectedNullable)? '' : 'NOT NULL';
+				$toChange['need_change'] = true;
+			}
+
+			// examples:
+			//   1 - NOT NULL
+			//   2 - NOT NULL DEFAULT ''
+			// x 3 - NOT NULL DEFAULT NULL
+			// x 4 - NULL
+			//   5 - NULL DEFAULT ''
+			//   6 - NULL DEFAULT NULL
+			//       1 == 2, 4 -> 6
+			$expectedHasDefault = array_key_exists('default', $expected);
+			$expectedDefaultValue = (array_key_exists('default', $expected))? $expected['default'] : "''";
+			$colDefault = $col['Default'];
+			if ($expectedDefault != $colDefault) {
+				$toChange['default'] = 'DEFAULT ' . ((is_null($expectedDefault))? 'NULL' : $expectedDefault);
+				$toChange['need_change'] = true;
+			}
+
+			// ALTER TABLE `CRM_NOTIFY` CHANGE `who` `who` VARCHAR(20) NOT NULL
+			// ALTER TABLE `CRM_NOTIFY` CHANGE `who` `who` VARCHAR(20) NULL
+			// ALTER TABLE `CRM_NOTIFY` CHANGE `who` `who` VARCHAR(20) NULL DEFAULT NULL
+			$sqlChange = "alter table CRM_NOTIFY change {$fldName} {$fldName} {$toChange['type']} {$toChange['null']} {$toChange['default']}";
+			if ($toChange['need_change']) {
+				DBG::_(true, true, "TODO: sqlChange", $sqlChange, __CLASS__, __FUNCTION__, __LINE__);
+			}
+		}
+	}
+
+	/* assert's that field struct has defined or throw exception if cannot set default value:
+			['type'] = 'int', 'varchar', ... (MySQL Types)
+			['is_nullable'] = true, false
+			['default_value'] = NULL or (string, numeric - based on type)
+			  default_value is not set when default_value == NULL and is_nullable == false
+			['default_is_null'] = true, false
+			['max_length'] = NULL, [0-...] // MySQL `CHARACTER_MAXIMUM_LENGTH`
+			['num_precision'] = NULL, [0-65]
+			['num_scale'] = NULL, [0-12]
+			['char_encoding'] = 'utf8', 'latin2', ...
+			['char_collation'] = 'utf8_general_ci', 'latin2_general_ci', ...
+			['extra'] = 'auto_increment', 'on update CURRENT_TIMESTAMP'
+			['values'] = null or array for enum and set
+
+	TODO: validate - wrong related values. ex: {type: 'int', char_collation: 'latin2'}
+	TODO: validate - not allowed value. ex: {type: 'xyz'}
+	*/
+	public function _fixExpectedStruct($expectedStruct) {
+		$fixedStruct = array();
+		foreach ($expectedStruct as $fldName => $expected) {
+			//DBG::_(true, true, "TODO: expected", $expected, __CLASS__, __FUNCTION__, __LINE__);
+			if (!array_key_exists('type', $expected)) throw new Exception("Undefined type for field '{$fldName}'");
+
+			if (!array_key_exists('is_nullable', $expected)) {
+				$expected['is_nullable'] = false;
+				if (array_key_exists('default_value', $expected) && null === $expected['default_value']) {
+					$expected['is_nullable'] = true;
+				}
+			}
+			if (!array_key_exists('default_is_null', $expected)) $expected['default_is_null'] = false;
+			if (!array_key_exists('default_value', $expected)) {
+				$expected['default_value'] = null;
+//				switch ($expected['type']) {
+//					case 'char':
+//					case 'varchar': $expected['default_value'] = ($expected['is_nullable'])? null : ''; break;
+//					case 'tinyint':
+//					case 'bigint':
+//					case 'int': $expected['default_value'] = ($expected['is_nullable'])? null : 0; break;
+//				}
+				if ($expected['is_nullable'] && null === $expected['default_value']) {
+					$expected['default_is_null'] = true;
+				}
+			}
+
+			if (!array_key_exists('max_length', $expected)) {
+				switch ($expected['type']) {
+					case 'char':
+					case 'varchar': $expected['max_length'] = 255; break;
+					case 'binary': $expected['max_length'] = 255; break;// bainary(0) is possible - why? Cannot store values.
+					case 'varbinary': $expected['max_length'] = 255; break;
+					case 'binary':
+					case 'varbinary':
+					case 'bit': throw new Exception("Undefined max_length for field '{$fldName}' with type '{$expected['type']}'");
+					//case 'blob': $expected['max_length'] = 65535; break;// is set by engine
+					//case 'tinyblob': $expected['max_length'] = 255; break;// is set by engine
+					//case 'mediumblob': $expected['max_length'] = 16777215; break;// is set by engine
+					//case 'longblob': $expected['max_length'] = 4294967295; break;// is set by engine
+					//case 'text': $expected['max_length'] = 65535; break;// is set by engine
+					//case 'tinytext': $expected['max_length'] = 255; break;// is set by engine
+					//case 'mediumtext': $expected['max_length'] = 16777215; break;// is set by engine
+					//case 'longtext': $expected['max_length'] = 4294967295; break;// is set by engine
+					//case 'enum': $expected['max_length'] = 255; break;// is set by engine
+					//case 'set': $expected['max_length'] = 255; break;// is set by engine
+					default: $expected['max_length'] = null;//throw new Exception("Undefined max_length for field '{$fldName}'");
+				}
+			}
+			if (!array_key_exists('num_precision', $expected)) {
+				switch ($expected['type']) {
+					case 'int': $expected['num_precision'] = 10; break;// int(11) - +1 in type definition
+					case 'tinyint': $expected['num_precision'] = 3; break;// int(4)
+					case 'smallint': $expected['num_precision'] = 5; break;// int(6)
+					case 'mediumint': $expected['num_precision'] = 7; break;// int(8)
+					case 'bigint': $expected['num_precision'] = 19; break;// int(20)
+					case 'decimal': $expected['num_precision'] = 10; break;// decimal(10,0)
+					default: $expected['num_precision'] = null;
+				}
+				//throw new Exception("Undefined num_precision for field '{$fldName}'");
+			}
+			if (!array_key_exists('num_scale', $expected)) {
+				switch ($expected['type']) {
+					case 'int': $expected['num_scale'] = 0; break;
+					case 'tinyint': $expected['num_scale'] = 0; break;
+					case 'smallint': $expected['num_scale'] = 0; break;
+					case 'mediumint': $expected['num_scale'] = 0; break;
+					case 'bigint': $expected['num_scale'] = 0; break;
+					case 'decimal': $expected['num_scale'] = 0; break;// ex.: decimal(3,2); decimal(24,0)
+					case 'double': $expected['num_scale'] = null; break;// ex.: double(10,4); double; double(17,0);
+					case 'float': $expected['num_scale'] = null; break;// ex.: float(6,2); float; float(17,0);
+					default: $expected['num_scale'] = null;
+				}
+				//throw new Exception("Undefined num_scale for field '{$fldName}'");
+			}
+			if (!array_key_exists('char_encoding', $expected)) {
+				switch ($expected['type']) {
+					case 'char':
+					case 'varchar': $expected['char_encoding'] = 'utf8'; break;
+					case 'enum':
+					case 'set': $expected['char_encoding'] = 'utf8'; break;
+					case 'text':
+					case 'tinytext':
+					case 'mediumtext':
+					case 'longtext': $expected['char_encoding'] = 'utf8'; break;
+					default: $expected['char_encoding'] = null;
+				}
+				//throw new Exception("Undefined char_encoding for field '{$fldName}'");
+			}
+			if (!array_key_exists('char_collation', $expected)) {
+				switch ($expected['type']) {
+					case 'char':
+					case 'varchar': $expected['char_collation'] = 'utf8_general_ci'; break;
+					case 'enum':
+					case 'set': $expected['char_collation'] = 'utf8_general_ci'; break;
+					case 'text':
+					case 'tinytext':
+					case 'mediumtext':
+					case 'longtext': $expected['char_collation'] = 'utf8_general_ci'; break;
+					default: $expected['char_collation'] = null;
+				}
+				//throw new Exception("Undefined char_collation for field '{$fldName}'");
+			}
+			if (!array_key_exists('extra', $expected)) {
+				$expected['extra'] = null;//throw new Exception("Undefined extra for field '{$fldName}'");
+			}
+			if (!array_key_exists('values', $expected) || !is_array($expected['values']) || empty($expected['values'])) {
+				switch ($expected['type']) {
+					case 'enum':
+					case 'set': throw new Exception("Undefined values for field '{$fldName}' with type '{$expected['type']}'");
+					default: $expected['values'] = null;
+				}
+			}
+
+			$fixedStruct[$fldName] = $expected;
+		}
+		return $fixedStruct;
+	}
+
+	public function showCreateStructMysql($tblName, $struct, $params = array()) {
+		$expectedStruct = $this->_fixExpectedStruct($struct);
+		$linesSql = array();
+		$dbgSql = array();
+		foreach ($expectedStruct as $fldName => $fld) {
+			$nullSql = ($fld['is_nullable'])? '' : 'NOT NULL';
+			$defaultSql = (is_null($fld['default_value']))? 'DEFAULT NULL' : "DEFAULT '{$fld['default_value']}'";
+			if (is_null($fld['default_value']) && !$fld['is_nullable']) $defaultSql = '';
+			switch ($fld['type']) {
+				case 'char':
+				case 'varchar': $linesSql[] = "`{$fldName}` {$fld['type']}({$fld['max_length']}) {$nullSql} {$defaultSql}"; break;
+				case 'text':
+				case 'tinytext':
+				case 'longtext':
+				case 'mediumtext': $linesSql[] = "`{$fldName}` {$fld['type']} {$nullSql}"; break;
+				case 'time':
+				case 'timestamp':
+				case 'year':
+				case 'date':
+				case 'datetime': $linesSql[] = "`{$fldName}` {$fld['type']} {$nullSql} {$defaultSql}"; break;
+// 		-- Unimplemented type 'int': {"type":"int","is_nullable":false,"default_is_null":false,"default_value":null,"max_length":null,"num_precision":10,"num_scale":0,"char_encoding":null,"char_collation":null,"extra":null}
+//		-- Unimplemented type 'int': {"type":"int","num_precision":11,"default_value":null,"is_nullable":true,"default_is_null":false,"max_length":null,"num_scale":0,"char_encoding":null,"char_collation":null,"extra":null}
+				case 'int':
+				case 'tinyint':
+				case 'smallint':
+				case 'mediumint':
+				case 'bigint':
+					if ($fld['num_precision'] > 0) {
+						$typeParamsSql = "(" . ($fld['num_precision'] + 1) . ")";
+					}
+					//if ($fld['num_scale']) $typeParamsSql = ",{$fld['num_scale']}";
+					$linesSql[] = "`{$fldName}` {$fld['type']}{$typeParamsSql} {$nullSql} {$defaultSql}";
+					break;
+				case 'decimal':
+					$typeParamsSql = "{$fld['num_precision']},{$fld['num_scale']}";
+					$linesSql[] = "`{$fldName}` {$fld['type']}({$typeParamsSql}) {$nullSql} {$defaultSql}";
+					break;
+				case 'float':
+				case 'double':
+				case 'real':
+					$linesSql[] = "`{$fldName}` {$fld['type']} {$nullSql} {$defaultSql}";
+					break;
+				case 'enum':
+				case 'set':
+					$typeParamsSql = "'" . implode("','", $fld['values']) . "'";
+					$linesSql[] = "`{$fldName}` {$fld['type']}({$typeParamsSql}) {$nullSql} {$defaultSql}";
+					break;
+				case 'bit':
+				case 'binary':
+				case 'varbinary':
+					$linesSql[] = "`{$fldName}` {$fld['type']}({$fld['max_length']}) {$nullSql} {$defaultSql}";
+					break;
+				case 'boolean':
+				case 'serial':
+				case 'blob':
+				case 'tinyblob':
+				case 'mediumblob':
+				case 'longblob':
+					$linesSql[] = "`{$fldName}` {$fld['type']} {$nullSql} {$defaultSql}";
+					break;
+				case 'geometry':
+				case 'point':
+				case 'linestring':
+				case 'polygon':
+				case 'multipoint':
+				case 'multilinestring':
+				case 'multipolygon':
+				case 'geometrycollection':
+					$linesSql[] = "`{$fldName}` {$fld['type']} {$nullSql} {$defaultSql}";
+					break;
+				case 'UNIQUE KEY':
+					$keyFieldsSql = "`" . implode("`,`", $fld['key_fields']) . "`";
+					$linesSql[] = "UNIQUE KEY `{$fldName}` ({$keyFieldsSql})";
+					break;
+				case 'KEY':
+					$keyFieldsSql = "`" . implode("`,`", $fld['key_fields']) . "`";
+					$linesSql[] = "KEY `{$fldName}` ({$keyFieldsSql})";
+					break;
+//			$expectedStruct['uniq_key_1'] = array('type'=>'UNIQUE KEY', 'key_fields'=>array('who','when','what'));// UNIQUE KEY `uniq_key_1` (`who`,`when`,`what`)
+//			$expectedStruct['key_who'] = array('type'=>'KEY', 'key_fields'=>array('who'));// KEY `key_who` (`who`)
+				default: $dbgSql[] = "-- Unimplemented type '{$fld['type']}': " . json_encode($fld);
+			}
+		}
+		$linesSql = implode("\n\t\t, ", $linesSql);
+		$dbgSql = implode("\n\t\t", $dbgSql);
+		$tblCharEncoding = V::get('char_encoding', 'utf8', $params);
+		$structSql = <<<EOF_STRUCT_MYSQL
+	CREATE TABLE IF NOT EXISTS `{$tblName}` (
+		{$linesSql}
+		{$dbgSql}
+	) ENGINE=MyISAM DEFAULT CHARSET={$tblCharEncoding}
+EOF_STRUCT_MYSQL;
+		return $structSql;
+	}
+
 	public function fetchAll($sql) {
 	public function fetchAll($sql) {
 		$sth = $this->prepare($sql);
 		$sth = $this->prepare($sql);
 		$sth->execute();
 		$sth->execute();

+ 406 - 0
SE/se-lib/Route/Notify.php

@@ -0,0 +1,406 @@
+<?php
+
+Lib::loadClass('RouteBase');
+
+/*
+# Nofitication system:
+
+Sends mail by settings `CRM_NOTIFY`:
+- who - user login
+- when - shedule (once a day, once on houd)
+- what - action type
+- last_exec_time
+
+*/
+class Route_Notify extends RouteBase {
+
+	public function handleAuth() {
+		if (!User::logged()) {
+			throw new HttpException('Unauthorized', 401);
+		}
+	}
+
+	public function defaultAction() {
+		SE_Layout::gora();
+		?>
+<div class="container">
+	<h1>Nofitication system</h1>
+	...
+</div>
+		<?php
+		SE_Layout::dol();
+	}
+
+	public function reinstallAction() {
+		try {
+			$this->reinstall();
+		} catch (Exception $e) {
+			echo "Error #" . $e->getCode() .  "|" . $e->getLine() .  ": " . $e->getMessage();
+		}
+		die('OK');
+	}
+
+	public function reinstallFunctionsAction() {
+		$this->reinstallFunctions();
+		die('OK');
+	}
+
+	public function runAction() {
+		$msgId = V::get('_msgId', 0, $_REQUEST, 'int');
+		if ($msgId > 0) {
+			$this->runByMessageId($msgId);
+		}
+		$jsonData = new stdClass();
+		$jsonData->type = 'success';
+		$jsonData->msg = 'Gotowe';
+		echo json_encode($jsonData);
+		exit;
+	}
+
+	public function reinstallFunctions() {
+		$sqlList = array();
+		$db = DB::getDB();
+		if ($db->has_errors()) {
+			throw new Exception("DB Errors: " . implode("\n<br>", $db->get_errors()));
+		}
+		foreach ($sqlList as $sqlName => $sql) {
+			$res = $db->query($sql);
+			if ($db->has_errors()) {
+				throw new Exception("DB Errors at sql '{$sqlName}': " . implode("\n<br>", $db->get_errors()));
+			}
+		}
+	}
+
+	public function reinstall() {
+		$sqlList = array();
+		$pdo = DB::getPDO();
+		/*
+			`CRM_NOTIFY`:
+				- who - user login
+				- when - shedule (once a day, once on houd)
+				- what - action type
+				- last_exec_time
+		*/
+		if(1){// TEST
+			$expectedStruct = array();
+			$expectedStruct['who'] = array('type'=>'varchar', 'max_length'=>20, 'is_nullable'=>false);// `who` varchar(20) -- TODO:  NOT NULL
+			$expectedStruct['when'] = array('type'=>'varchar', 'max_length'=>255, 'is_nullable'=>false);// `when` varchar(255) NOT NULL
+			$expectedStruct['what'] = array('type'=>'varchar', 'max_length'=>255, 'is_nullable'=>false);// `what` varchar(255) NOT NULL
+			$expectedStruct['last_exec_time'] = array('type'=>'datetime', 'is_nullable'=>true);// `last_exec_time` datetime
+			$expectedStruct['_created'] = array('type'=>'datetime', 'is_nullable'=>false);// `_created` datetime
+			$expectedStruct['uniq_key_1'] = array('type'=>'UNIQUE KEY', 'key_fields'=>array('who','when','what'));// UNIQUE KEY `uniq_key_1` (`who`,`when`,`what`)
+			$expectedStruct['key_who'] = array('type'=>'KEY', 'key_fields'=>array('who'));// KEY `key_who` (`who`)
+//			$expectedStruct['t1'] = array('type'=>'varchar', 'max_length'=>300, 'default_value'=>null);
+//			$expectedStruct['t2'] = array('type'=>'int');
+//			$expectedStruct['t3'] = array('type'=>'int', 'num_precision'=>11, 'default_value'=>null);
+//			$expectedStruct['t_dec_11_x'] = array('type'=>'decimal', 'num_precision'=>11);
+//			$expectedStruct['t_dec_11_2'] = array('type'=>'decimal', 'num_precision'=>11, 'num_scale'=>2);
+			$expectedStruct['t_int'] = array('type'=>'int');
+			$expectedStruct['t_tinyint'] = array('type'=>'tinyint');
+			$expectedStruct['t_smallint'] = array('type'=>'smallint');
+			$expectedStruct['t_mediumint'] = array('type'=>'mediumint');
+			$expectedStruct['t_bigint'] = array('type'=>'bigint');
+			$expectedStruct['t_decimal'] = array('type'=>'decimal');
+			$expectedStruct['t_float'] = array('type'=>'float');
+			$expectedStruct['t_double'] = array('type'=>'double');
+			$expectedStruct['t_real'] = array('type'=>'real');
+			$expectedStruct['t_date'] = array('type'=>'date');
+			$expectedStruct['t_datetime'] = array('type'=>'datetime');
+			$expectedStruct['t_timestamp'] = array('type'=>'timestamp');
+			$expectedStruct['t_time'] = array('type'=>'time');
+			$expectedStruct['t_year'] = array('type'=>'year');
+			$expectedStruct['t_char'] = array('type'=>'char');
+			$expectedStruct['t_varchar'] = array('type'=>'varchar');
+			$expectedStruct['t_text'] = array('type'=>'text');
+			$expectedStruct['t_tinytext'] = array('type'=>'tinytext');
+			$expectedStruct['t_mediumtext'] = array('type'=>'mediumtext');
+			$expectedStruct['t_longtext'] = array('type'=>'longtext');
+			$expectedStruct['t_enum'] = array('type'=>'enum', 'values'=>array('v1'));
+			$expectedStruct['t_set'] = array('type'=>'set', 'values'=>array('v1'));
+			$expectedStruct['t_bit'] = array('type'=>'bit', 'max_length'=>2);
+			$expectedStruct['t_boolean'] = array('type'=>'boolean');
+			$expectedStruct['t_serial'] = array('type'=>'serial');
+			$expectedStruct['t_binary'] = array('type'=>'binary');
+			$expectedStruct['t_varbinary'] = array('type'=>'varbinary');
+			$expectedStruct['t_blob'] = array('type'=>'blob');
+			$expectedStruct['t_tinyblob'] = array('type'=>'tinyblob');
+			$expectedStruct['t_mediumblob'] = array('type'=>'mediumblob');
+			$expectedStruct['t_longblob'] = array('type'=>'longblob');
+			$expectedStruct['t_geometry'] = array('type'=>'geometry');
+			$expectedStruct['t_point'] = array('type'=>'point');
+			$expectedStruct['t_linestring'] = array('type'=>'linestring');
+			$expectedStruct['t_polygon'] = array('type'=>'polygon');
+			$expectedStruct['t_multipoint'] = array('type'=>'multipoint');
+			$expectedStruct['t_multilinestring'] = array('type'=>'multilinestring');
+			$expectedStruct['t_multipolygon'] = array('type'=>'multipolygon');
+			$expectedStruct['t_geometrycollection'] = array('type'=>'geometrycollection');
+
+			$sqlCreate = $pdo->showCreateStructMysql('CRM_NOTIFY', $expectedStruct, array('char_encoding'=>'latin2'));
+			DBG::_(true, true, "fixedEpectedStruct", $sqlCreate, __CLASS__, __FUNCTION__, __LINE__);
+			$pdo->assertTableStruct('CRM_NOTIFY', $expectedStruct, array('char_encoding'=>'latin2'));
+			{// force - drop/create
+				$pdo->exec("DROP TABLE IF EXISTS CRM_NOTIFY");
+				$pdo->exec($sqlCreate);
+			}
+			return;
+		}
+
+		$sth = $pdo->prepare("
+				CREATE TABLE IF NOT EXISTS CRM_NOTIFY (
+					`who` varchar(20) -- TODO:  NOT NULL
+					, `when` varchar(255) NOT NULL
+					, `what` varchar(255) NOT NULL
+					, `last_exec_time` datetime -- TODO: DEFAULT NULL
+					, `_created` datetime
+					, UNIQUE KEY `uniq` (`who`,`when`,`what`)
+					, KEY `who` (`who`)
+				) ENGINE=MyISAM DEFAULT CHARSET=latin2
+		");
+		$sth->execute();
+		if(1){// TEST nulls
+			$sth = $pdo->prepare("DROP TABLE IF EXISTS CRM_NOTIFY");
+			$sth->execute();
+			$sth = $pdo->prepare("
+					CREATE TABLE IF NOT EXISTS CRM_NOTIFY (
+						`who` varchar(20)
+						, `when` varchar(255) NOT NULL
+						, `what` varchar(255) DEFAULT ''
+						, `t1` varchar(255) NOT NULL -- same as with DEFAULT ''
+						, `t2` varchar(255) NOT NULL DEFAULT ''
+						, `t2z` varchar(255) NOT NULL DEFAULT '0'
+					--	, `t3` varchar(255) NOT NULL DEFAULT NULL -- SQLSTATE[42000]: Syntax error or access violation: 1067 Invalid default value
+						, `t4` varchar(255) NULL -- same as with DEFAULT NULL
+						, `t5` varchar(255) NULL DEFAULT ''
+						, `t6` varchar(255) NULL DEFAULT NULL
+						, `i1` int(11) NOT NULL -- same as with DEFAULT 0
+						, `i2` int(11) NOT NULL DEFAULT 0
+					--	, `i3` int(11) NOT NULL DEFAULT NULL -- SQLSTATE[42000]: Syntax error or access violation: 1067 Invalid default value
+						, `i4` int(11) NULL -- same as with DEFAULT NULL
+						, `i5` int(11) NULL DEFAULT 0
+						, `i6` int(11) NULL DEFAULT NULL
+						, `last_exec_time` datetime DEFAULT NULL
+						, KEY `who` (`who`)
+					) ENGINE=MyISAM DEFAULT CHARSET=latin2
+			");
+			$sth->execute();
+			$sth = $pdo->prepare("insert into CRM_NOTIFY (`last_exec_time`) values (NULL)");
+			$sth->execute();
+			// INSERT INTO CRM_NOTIFY (t1, t2, t2z, t4,   t5, t6,   i1, i2, i4,   i5, i6,   last_exec_time)
+			//                 VALUES ('', '', '0', NULL, '', NULL, 0,  0,  NULL, 0,  NULL, NULL);
+			$struct = $pdo->getTableStruct('CRM_NOTIFY');
+			DBG::_(true, true, "struct", $struct, __CLASS__, __FUNCTION__, __LINE__);
+			DBG::_(true, true, "`t2`: is_null(struct[4]['Default'])=(".is_null($struct[4]['Default']).")", null, __CLASS__, __FUNCTION__, __LINE__);
+			DBG::_(true, true, "`t4`: is_null(struct[6]['Default'])=(".is_null($struct[6]['Default']).")", null, __CLASS__, __FUNCTION__, __LINE__);
+			DBG::_(true, true, "`t5`: is_null(struct[7]['Default'])=(".is_null($struct[7]['Default']).")", null, __CLASS__, __FUNCTION__, __LINE__);
+			DBG::_(true, true, "`t6`: is_null(struct[8]['Default'])=(".is_null($struct[8]['Default']).")", null, __CLASS__, __FUNCTION__, __LINE__);
+			$expectedStruct = array();
+			$expectedStruct['t1'] = array('type'=>'varchar', 'max_length'=>300, 'default_value'=>null);
+			//$expectedStruct['t2'] = array('type'=>'int', 'num_precision'=>11, 'default_value'=>null);
+			$pdo->assertTableStruct('CRM_NOTIFY', $expectedStruct);
+			return;
+		}
+
+		$struct = $pdo->getTableStruct('CRM_NOTIFY');
+		DBG::_(true, true, "struct", $struct, __CLASS__, __FUNCTION__, __LINE__);
+
+		$expectedStruct = array();
+		$expectedStruct['who'] = array('type'=>'varchar', 'max_length'=>20);
+		$expectedStruct['when'] = array('type'=>'varchar', 'max_length'=>255);
+		$expectedStruct['what'] = array('type'=>'varchar', 'max_length'=>255);
+		$expectedStruct['last_exec_time'] = array('type'=>'datetime');
+		$expectedStruct['_created'] = array('type'=>'datetime');
+		$expectedStruct['tX'] = array('type'=>'varchar', 'max_length'=>255, 'default_value'=>null, 'is_nullable'=>true);
+
+		DBG::_(true, true, "is_null('')=(".is_null('').") is_null()=(".is_null().") is_null(null)=(".is_null(null).")", null, __CLASS__, __FUNCTION__, __LINE__);
+
+		if(0){// keys
+			$sth = $pdo->prepare("show keys from CRM_NOTIFY");
+			$sth->execute();
+			$keys = $sth->fetchAll();
+			DBG::_(true, true, "keys", $keys, __CLASS__, __FUNCTION__, __LINE__);
+		}
+
+		//$this->reinstallFunctions();
+	}
+
+	public function getActiveMessagesForTable($tblName) {
+		if (empty($tblName)) return;
+		$db = DB::getDB();
+		$tblName = $db->_($tblName);
+
+		$usrLogin = User::getLogin();
+		$msgs = null;
+		$sql = "select m.*
+			from `CRM_UI_MSGS` m
+			where m.`uiTargetType`='default_db_table'
+				and m.`A_STATUS`='WAITING'
+				and m.`uiTargetName`='{$tblName}'
+				and (m.`userTargetType` in('everyone')
+					or (m.`userTargetType`='user' and m.`userTargetName`='{$usrLogin}')
+					-- TODO: use 'admin', 'group'
+				)
+		";
+		$db = DB::getDB();
+		$res = $db->query($sql);
+		while ($r = $db->fetch($res)) {
+			if ($msg = $this->parseMessage($r)) {
+				$msg['link'] = 'index.php?_route=Msgs&_task=run&_msgId=' . $r->ID;
+				$msg['linkType'] = 'ajax';
+				$msgs[$r->ID] = $msg;
+			}
+		}
+		return $msgs;
+	}
+
+	public function getActiveMessagesForTableRecord($tblName, $id) {
+		if (empty($tblName)) return;
+		$db = DB::getDB();
+		$tblName = $db->_($tblName);
+
+		$usrLogin = User::getLogin();
+		$msgs = null;
+		$sql = "select m.*
+			from `CRM_UI_MSGS` m
+			where m.`uiTargetType`='default_db_table_record'
+				and m.`uiTargetName`='{$tblName}.{$id}'
+				and (m.`userTargetType` in('everyone')
+					or (m.`userTargetType`='user' and m.`userTargetName`='{$usrLogin}')
+					-- TODO: use group id
+				)
+				and m.`A_STATUS`='WAITING'
+			order by m.`ID` DESC
+		";
+		$db = DB::getDB();
+		$res = $db->query($sql);
+		while ($r = $db->fetch($res)) {
+			if ($msg = $this->parseMessage($r)) {
+				$msg['link'] = 'index.php?_route=Msgs&_task=run&_msgId=' . $r->ID;
+				$msg['linkType'] = 'ajax';
+				$msgs[$r->ID] = $msg;
+			}
+		}
+		return $msgs;
+	}
+
+	public function parseMessage($r) {
+		$msg = null;// ['type'=>'info', 'message'=>'...']
+		// $r->app_className - for automatic msgs to search for msg text
+		// $r->msg - for automatic msgs to search for msg text
+		// $r->msgType - 'info','danger','warning','success'
+		if (!empty($r->app_className)) {
+			$route = Router::getRoute($r->app_className);
+			$msg = array();
+			$msg['message'] = $route->parseMessageFromMsgsSystem($r->msg);
+			$msg['type'] = $r->msgType;
+			$msg['_raw'] = $r;
+		} else {
+			$msg = array();
+			$msg['message'] = $this->parseMessageFromMsgsSystem($r->msg);
+			$msg['type'] = $r->msgType;
+			$msg['_raw'] = $r;
+		}
+		return $msg;
+	}
+
+	public function parseMessageFromMsgsSystem($msg) {
+		return $msg;
+	}
+
+	public function runByMessageId($id) {
+		$msgRow = $this->getActiveMessage($id);
+		$execNotes = '';
+		if (!empty($msgRow->app_className)) {
+			$route = Router::getRoute($msgRow->app_className);
+			$route->runByMessageFromMsgsSystem($msgRow->msg, $execNotes);
+		}
+		$this->forceFinishMessage($id, $execNotes);
+	}
+
+	public function getMessage($id) {
+		if (empty($id)) return;
+		$id = intval($id);
+		if ($id <= 0) return;
+
+		$msg = null;
+		$sql = "select * from `CRM_UI_MSGS` where `ID`='{$id}' ";
+		$db = DB::getDB();
+		$res = $db->query($sql);
+		if ($r = $db->fetch($res)) {
+			$msg = $r;
+		}
+		return $msg;
+	}
+
+	public function getActiveMessage($id) {
+		if (empty($id)) return;
+		$id = intval($id);
+		if ($id <= 0) return;
+
+		$msg = null;
+		$sql = "select m.*
+			from `CRM_UI_MSGS` m
+			where m.`ID`='{$id}'
+				and m.`A_STATUS`='WAITING'
+		";
+		$db = DB::getDB();
+		$res = $db->query($sql);
+		if ($r = $db->fetch($res)) {
+			$msg = $r;
+		}
+		if (!$msg) {
+			throw new HttpException("Message not found", 404);
+		}
+		return $msg;
+	}
+
+	public function forceFinishMessage($id, $execNotes) {
+		if (empty($id)) return;
+		$id = intval($id);
+		if ($id <= 0) return;
+
+		$usrLogin = User::getLogin();
+		$db = DB::getDB();
+		$execNotes = $db->_($execNotes);
+		$sql = "update `CRM_UI_MSGS`
+			set `A_STATUS`='OFF_HARD'
+				, `actionExecutedTime`=NOW()
+				, `actionNotes`='{$execNotes}'
+				, `A_RECORD_UPDATE_DATE`=NOW()
+				, `A_RECORD_UPDATE_AUTHOR`='{$usrLogin}'
+			where `ID`='{$id}'
+		";
+		$db->query($sql);
+		return;
+	}
+
+	public function removeMessage($id) {
+		if (empty($id)) return;
+		$id = intval($id);
+		if ($id <= 0) return;
+
+		$sql = "update `CRM_UI_MSGS` set `A_STATUS`='DELETED' where `ID`='{$id}' ";
+		$db = DB::getDB();
+		$db->query($sql);
+	}
+
+	public function removeTableRecordMsg($idMsg) {
+		// IDEA: do kosza - add trigger to insert into `CRM_UI_MSGS__TRASH` after DELETE on `CRM_UI_MSGS`
+		$idMsg = intval($idMsg);
+		if ($idMsg <= 0) throw new Exception("Brak wiadomości!");
+		$usrLogin = User::getLogin();
+		$db = DB::getDB();
+		if (!$db) throw new Exception("Brak dazy danych!");
+		if ($db->has_errors()) throw new Exception("DB Errors: " . implode("\n<br>", $db->get_errors()));
+		$sqlTODO = "delete `CRM_UI_MSGS` where `ID`='{$idMsg}' ";
+		$sql = "update `CRM_UI_MSGS`
+			set `A_STATUS`=IF('{$usrLogin}'=`A_RECORD_CREATE_AUTHOR`, 'DELETED', 'OFF_SOFT')
+				, `A_RECORD_DELETE_AUTHOR`='{$usrLogin}'
+				, `A_RECORD_DELETE_DATE`=NOW()
+			where `ID`='{$idMsg}'
+		";
+		DBG::_('DBG_MSGS', '>1', "sql", $sql, __CLASS__, __FUNCTION__, __LINE__);
+		$res = $db->query($sql);
+		if (!$res || $db->has_errors()) throw new Exception("Wystąpiły błędy podczas próby zapisu wiadomości: " . implode("\n<br>", $db->get_errors()));
+	}
+
+}