Merge branch 'ztflow-qixinzhi-main#qixinzhi' of zentao/zentaopms (#586)

This commit is contained in:
songchenxuan
2024-06-25 10:45:10 +08:00
committed by Gitfox
36 changed files with 3595 additions and 1725 deletions
BIN
View File
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -756,7 +756,7 @@ $config->db->sqliteBlacklist = array('sqlite_queue', 'cron');
$config->hasDropmenuApps = array('program', 'project', 'product', 'execution', 'qa', 'admin', 'bi', 'feedback', 'demandpool');
$config->hasBranchMenuModules = array('product', 'story', 'release', 'bug', 'testcase', 'testtask', 'branch', 'tree');
$config->excludeBranchMenu = array('product-dashboard', 'product-view');
$config->excludeDropmenuList = array('program-browse', 'program-productview', 'program-kanban', 'product-all', 'product-index', 'product-kanban', 'project-kanban', 'execution-all', 'execution-executionkanban', 'project-browse', 'project-batchedit', 'product-batchedit', 'admin-index', 'product-create', 'project-create', 'execution-create', 'program-create', 'metric-preview', 'metric-browse', 'metric-view', 'metriclib-browse', 'qa-index', 'caselib-create', 'feedback-batchedit', 'feedback-batchclose', 'dimension-browse');
$config->excludeDropmenuList = array('program-browse', 'program-productview', 'program-kanban', 'product-all', 'product-index', 'product-kanban', 'project-kanban', 'execution-all', 'execution-executionkanban', 'project-browse', 'project-batchedit', 'product-batchedit', 'admin-index', 'product-create', 'project-create', 'execution-create', 'program-create', 'execution-batchedit', 'metric-preview', 'metric-browse', 'metric-view', 'metriclib-browse', 'qa-index', 'caselib-create', 'feedback-batchedit', 'feedback-batchclose', 'dimension-browse', 'dataview-browse');
$config->hasSwitcherModules = array('design');
$config->hasSwitcherMethods = array('project-bug', 'project-testcase', 'execution-bug', 'execution-testcase', 'testtask-cases', 'testtask-view', 'testtask-report', 'testtask-groupcase', 'testtask-linkcase');
$config->excludeSwitcherList = array();
+1 -1
View File
@@ -53,7 +53,7 @@ UPDATE `zt_config` SET `module` = 'product', `section` = 'browse' WHERE `module`
UPDATE `zt_config` SET `module` = 'project', `section` = 'bug' WHERE `module` = 'datatable' AND `section` = 'projectBug' AND `key` = 'showModule';
UPDATE `zt_config` SET `module` = 'testcase', `section` = 'browse' WHERE `module` = 'datatable' AND `section` = 'testcaseBrowse' AND `key` = 'showModule';
INSERT IGNORE INTO `zt_config` (`owner`, `module`, `key`, `value`) VALUES ('system', 'execution', 'defaultWorkhours', '7');
REPLACE INTO `zt_config` (`owner`, `module`, `key`, `value`) VALUES ('system', 'execution', 'defaultWorkhours', '7');
CREATE TABLE IF NOT EXISTS `zt_session` (
`id` varchar(32) NOT NULL,
+9
View File
@@ -41,3 +41,12 @@ UPDATE `zt_kanbancard` SET `color` = '#ff9f46' WHERE `color` = '#2a5f29';
INSERT INTO `zt_cron`(`m`, `h`, `dom`, `mon`, `dow`, `command`, `remark`, `type`, `buildin`, `status`, `lastTime`) VALUES ('0', '*/1', '*', '*', '*', 'moduleName=metric&methodName=updateDashboardMetricLib', '计算仪表盘数据', 'zentao', 1, 'normal', NUll);
UPDATE `zt_todo` SET `type` = 'custom' WHERE `type` = 'cycle' AND `cycle` = 0;
INSERT INTO `zt_metric` (`purpose`, `scope`, `object`, `stage`, `type`, `name`, `code`, `alias`, `unit`, `desc`, `definition`, `when`, `createdBy`, `createdDate`, `builtin`, `deleted`, `dateType`) VALUES
('scale', 'product', 'feedback', 'released', 'php', '按产品统计的每周新增反馈数', 'count_of_weekly_created_feedback_in_product', '新增反馈数', 'count', '按产品统计的每周新增反馈数是指在一个周内收集到的用户反馈的数量。这个度量项可以帮助团队了解用户对产品的发展趋势和需求变化,并进行产品策略的调整和优化', '产品中创建时间为某个周的反馈的个数求和\r\n过滤已删除的反馈\r\n过滤已删除的产品', 'realtime', 'system', '2024-05-07 08:00:00', '1', '0', 'week'),
('scale', 'product', 'feedback', 'released', 'php', '按产品统计的处理中的反馈数', 'count_of_doing_feedback_in_product', '处理中反馈数', 'count', '按产品统计的处理中的反馈数表示产品中状态为处理中的反馈数量之和。该数值越大,说明团队并行处理的反馈越多,可以帮助团队了解当前的工作负载情况', '产品中所有反馈个数求和\r\n状态为处理中\r\n过滤已删除的反馈\r\n过滤已删除的产品', 'realtime', 'system', '2024-05-07 08:00:00', '1', '0', 'nodate'),
('scale', 'product', 'feedback', 'released', 'php', '按产品统计的已处理的反馈数', 'count_of_done_feedback_in_product', '已处理反馈数', 'count', '按产品统计的已处理的反馈数表示产品中状态为已处理的反馈数量之和。该数值越大,说明团队成员处理的反馈越多,有利于提高用户满意度', '产品中所有反馈个数求和\r\n状态为已处理\r\n过滤已删除的反馈\r\n过滤已删除的产品', 'realtime', 'system', '2024-05-07 08:00:00', '1', '0', 'nodate'),
('scale', 'product', 'feedback', 'released', 'php', '按产品统计的待完善的反馈数', 'count_of_clarify_feedback_in_product', '待完善反馈数', 'count', '按产品统计的待完善的反馈数表示产品中状态为待完善的反馈数量之和。该数值越大,说明有较多的反馈信息不清晰或比较复杂。需要反馈者更多的澄清和解释', '产品中所有反馈个数求和\r\n状态为待完善\r\n过滤已删除的反馈\r\n过滤已删除的产品', 'realtime', 'system', '2024-05-07 08:00:00', '1', '0', 'nodate'),
('scale', 'product', 'feedback', 'released', 'php', '按产品统计的待处理的反馈数', 'count_of_wait_feedback_in_product', '待处理反馈数', 'count', '按产品统计的待处理的反馈数表示产品中状态为待处理的反馈数量之和。该度量项可能暗示产品团队的反馈处理效率,待处理反馈数越多,可能会导致客户满意度降低', '产品中所有反馈个数求和\r\n状态为待处理\r\n过滤已删除的反馈\r\n过滤已删除的产品', 'realtime', 'system', '2024-05-07 08:00:00', '1', '0', 'nodate'),
('scale', 'product', 'feedback', 'released', 'php', '按产品统计的追问中的反馈数', 'count_of_asked_feedback_in_product', '追问中反馈数', 'count', '按产品统计的追问中的反馈数表示产品中状态为追问中的反馈数量之和。该度量项可能暗示着反馈的复杂性或对处理方案的疑惑,追问中的反馈数量越多,可能意味着团队需要更多时间和资源来回复并解决这些问题', '产品中所有反馈个数求和\r\n状态为追问中\r\n过滤已删除的反馈\r\n过滤已删除的产品', 'realtime', 'system', '2024-05-07 08:00:00', '1', '0', 'nodate'),
('scale', 'product', 'feedback', 'released', 'php', '按产品统计的未关闭的反馈数', 'count_of_unclosed_feedback_in_product', '未关闭反馈数', 'count', '按产品统计的未关闭的反馈数表示产品中状态为未关闭的反馈数量之和。这个度量项可以一定程度反映产品团队响应用户反馈的效率和及时处理用户问题的能力', '产品中所有反馈个数求和\r\n过滤状态为已关闭的反馈\r\n过滤已删除的反馈\r\n过滤已删除的产品', 'realtime', 'system', '2024-05-07 08:00:00', '1', '0', 'nodate');
+2
View File
@@ -0,0 +1,2 @@
ALTER TABLE `zt_pivot` ADD `driver` enum('mysql', 'duckdb') not NULL default 'mysql' AFTER `code`;
ALTER TABLE `zt_chart` ADD `driver` enum('mysql', 'duckdb') not NULL default 'mysql' AFTER `code`;
+13 -2
View File
@@ -491,6 +491,7 @@ CREATE TABLE IF NOT EXISTS `zt_chart` (
`id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL DEFAULT '',
`code` varchar(255) NOT NULL DEFAULT '',
`driver` enum('mysql', 'duckdb') not NULL default 'duckdb';
`dimension` mediumint(8) unsigned NOT NULL DEFAULT '0',
`type` varchar(30) NOT NULL DEFAULT '',
`group` varchar(255) NOT NULL DEFAULT '',
@@ -15545,6 +15546,7 @@ CREATE TABLE IF NOT EXISTS `zt_pivot` (
`dimension` mediumint(8) unsigned NOT NULL DEFAULT 0,
`group` varchar(255) NOT NULL DEFAULT '',
`code` varchar(255) NOT NULL DEFAULT '',
`driver` enum('mysql', 'duckdb') not NULL default 'duckdb';
`name` text NULL,
`desc` text NULL,
`sql` mediumtext NULL,
@@ -16655,5 +16657,14 @@ CREATE INDEX `date` ON zt_metriclib (date);
INSERT INTO `zt_cron`(`m`, `h`, `dom`, `mon`, `dow`, `command`, `remark`, `type`, `buildin`, `status`, `lastTime`) VALUES ('1', '1', '*', '*', '*', 'moduleName=metric&methodName=updateMetricLib', '计算度量数据', 'zentao', 1, 'normal', NUll);
INSERT INTO `zt_cron`(`m`, `h`, `dom`, `mon`, `dow`, `command`, `remark`, `type`, `buildin`, `status`, `lastTime`) VALUES ('0', '*/1', '*', '*', '*', 'moduleName=metric&methodName=updateDashboardMetricLib', '计算仪表盘数据', 'zentao', 1, 'normal', NUll);
INSERT INTO `zt_config` (`vision`, `owner`, `module`, `section`, `key`, `value`) VALUES ('', 'system', 'common', '', 'closedFeatures', 'otherOA');
INSERT INTO `zt_config` (`vision`, `owner`, `module`, `section`, `key`, `value`) VALUES ('', 'system', 'common', 'global', 'installedDate', CURDATE());
INSERT INTO `zt_cron` (`m`, `h`, `dom`, `mon`, `dow`, `command`, `remark`, `type`, `buildin`, `status`) VALUES ('*/10', '*', '*', '*', '*', 'moduleName=bi&methodName=syncParquetFile', '生成parquet文件', 'zentao', 1, 'normal');
UPDATE `zt_chart` SET `createdBy` = 'system' where `createdBy` = 'admin';
UPDATE `zt_pivot` SET `createdBy` = 'system' where `createdBy` = 'admin';
UPDATE `zt_pivot` SET `createdBy` = 'system' where `createdBy` = 'admin';
UPDATE `zt_chart` SET `editedBy` = 'system' where `editedBy` = 'admin';
UPDATE `zt_pivot` SET `editedBy` = 'system' where `editedBy` = 'admin';
UPDATE `zt_pivot` SET `editedBy` = 'system' where `editedBy` = 'admin';
INSERT INTO `zt_config` ( `vision`, `owner`, `module`, `section`, `key`, `value` ) VALUES ('', 'system', 'common', '', 'closedFeatures', 'otherOA');
INSERT INTO `zt_config`(`vision`, `owner`, `module`, `section`, `key`, `value`) VALUES ('', 'system', 'common', 'global', 'installedDate', CURDATE());
+22
View File
@@ -465,6 +465,28 @@ class router extends baseRouter
return $this->loadModuleConfig($moduleName);
}
/**
* Get sql driver instance.
*
* @param string $driverName mysql|duckdb
* @access public
* @return object
*/
public function loadDriver($driverName = 'mysql')
{
if($driverName == 'mysql')
{
return $this->dbh;
}
elseif($driverName == 'duckdb')
{
$this->loadClass('duckdb');
$duckdb = new duckdb();
return $duckdb;
}
}
/**
* Export config.
*
+363
View File
@@ -0,0 +1,363 @@
<?php
/**
* ZenTaoPHP的DuckDB类。
* The dao and sql class file of ZenTaoPHP framework.
*
* The author disclaims copyright to this source code. In place of
* a legal notice, here is a blessing:
*
* May you do good and not evil.
* May you find forgiveness for yourself and forgive others.
* May you share freely, never taking more than you give.
*/
/**
* DuckDB类。
* DuckDB, data access object.
*
* @package framework
*/
class duckdb
{
/**
* 全局变量$baseRoot
* The global basePath variable.
*
* @var object
* @access public
*/
public $baseRoot;
/**
* 全局变量$tmpRoot
* The global tmpRoot variable.
*
* @var object
* @access public
*/
public $tmpRoot;
/**
* 全局变量$prefix
* The global prefix variable.
*
* @var object
* @access public
*/
public $prefix;
/**
* 全局变量$sql
* The global sql variable.
*
* @var object
* @access public
*/
public $sql;
/**
* 全局变量$binPath
* The global binPath variable.
*
* @var object
* @access public
*/
public $binPath;
/**
* 全局变量$tmpPath
* The global tmpPath variable.
*
* @var object
* @access public
*/
public $tmpPath;
/**
* 全局变量$fields
* The global fields variable.
*
* @var object
* @access public
*/
public $fields;
/**
* 全局变量$tables
* The global fields variable.
*
* @var object
* @access public
*/
public $tables;
/**
* 构造方法。
* The construct method.
*
* @access public
* @return void
*/
public function __construct()
{
global $app, $config;
$this->prefix = $config->db->prefix;
$this->baseRoot = $app->getBasePath();
$this->tmpRoot = $app->getTmpRoot();
$this->setBinPath();
$this->setTmpPath();
}
/**
* 设置 duckdb 可执行文件路径。
* Set duckdb bin path.
*
* @access public
* @return void.
*/
public function setBinPath()
{
$this->binPath = $this->baseRoot . 'bin' . DS . 'duckdb' . DS . 'duckdb';
}
/**
* 设置 duckdb tmp parquet 文件路径。
* Set duckdb tmp parquet file path.
*
* @access public
* @return void.
*/
public function setTmpPath($module = 'bi')
{
$this->tmpPath = $this->tmpRoot . 'duckdb' . DS . $module . DS;
}
/**
* Query 方法。
* Query function.
*
* @param string sql
* @access public
* @return this.
*/
public function query($sql = '')
{
$sqlparser = dirname(dirname(__FILE__)) . '/sqlparser/sqlparser.class.php';
include_once $sqlparser;
$parser = new sqlparser($sql);
$statement = $parser->statements[0];
if(isset($statement->bodyParser->statements[0])) $statement = $statement->bodyParser->statements[0];
$this->fields = $this->getFields($statement);
$this->tables = $this->getTables($statement);
$sql = $this->replaceBackQuote($sql);
$sql = $this->replaceTable2Parquet($sql);
$sql = $this->standLimit($sql);
$this->sql = $sql;
return $this;
}
/**
* 将mysql的``替换为duckdb可执行的""。
* Replace ` to ".
*
* @param string sql
* @access public
* @return sql.
*/
private function replaceBackQuote($sql)
{
$sql = trim($sql);
$sql = trim($sql, ';');
$sql = str_replace(array('`', '"'), array('', '\"'), $sql);
return $sql;
}
/**
* 替换sql语句中的表为parquet文件路径。
* Replace sql table to parquet file path.
*
* @param string sql
* @access public
* @return sql.
*/
private function replaceTable2Parquet($sql)
{
if(empty($this->tables)) return $sql;
foreach($this->tables as $table)
{
$pattern = "/\b{$table}\b/";
$replace = "'{$this->tmpPath}{$table}.parquet'";
$sql = preg_replace($pattern, $replace, $sql);
}
return $sql;
}
/**
* 获取sql中的字段。
* Get fields form sqlparser statment.
*
* @param object $statment
* @access public
* @return array
*/
public function getFields(object $statement)
{
$fields = array();
if($statement->expr)
{
foreach($statement->expr as $fieldInfo)
{
$column = $fieldInfo->column;
$alias = $fieldInfo->alias;
$fields[$column] = array('column' => $column, 'alias' => $alias);
}
}
return $fields;
}
/**
* 获取sql中的表名。
* Get tables form sqlparser statment.
*
* @param object $statment
* @access public
* @return array
*/
private function getTables(object $statement)
{
$tables = array();
if($statement->from)
{
foreach($statement->from as $fromInfo)
{
if($fromInfo->table)
{
$tables[] = $fromInfo->table;
}
elseif($fromInfo->subquery)
{
$parser = new sqlparser($fromInfo->expr);
$subTables = $this->getTables($parser->statements[0]);
$tables = array_merge($tables, $subTables);
}
}
}
if($statement->join)
{
foreach($statement->join as $joinInfo)
{
if($joinInfo->expr->table)
{
$tables[] = $joinInfo->expr->table;
}
elseif($joinInfo->expr->subquery)
{
$parser = new sqlparser($joinInfo->expr->expr);
$subTables = $this->getTables($parser->statements[0]);
$tables = array_merge($tables, $subTables);
}
}
}
return array_filter(array_unique($tables));
}
/**
* 将LIMIT语句替换为duckdb可执行的格式。
* Standard LIMIT syntax.
*
* @param string sql
* @access public
* @return sql.
*/
private function standLimit($sql)
{
// 匹配 "LIMIT x, y" 并替换为 "LIMIT y OFFSET x"
$limitpattern = '/LIMIT\s+(\d+)\s*,\s*(\d+)/i';
$replacement = 'LIMIT $2 OFFSET $1';
$sql = preg_replace($limitpattern, $replacement, $sql);
return $sql;
}
/**
* 尝试执行duckdb sql并获取返回结果。
* Try to exec duckdb sql.
*
* @access public
* @return this.
*/
public function getResult()
{
$this->checkFieldNamesMatch();
$exec = "$this->binPath :memory: \"$this->sql\" -json 2>&1";
$output = shell_exec($exec);
if(empty($output)) $output = '';
$rows = json_decode($output);
/* 有内容但是 json 解析失败,说明是报错。*/
if($output and !$rows)
{
return throw new Exception($output);
}
else
{
return $rows ? $rows : array();
}
}
/**
* 检查sql返回的字段不能与表名同名。
* Check sql fields can not same as table name.
*
* @access public
* @return this.
*/
public function checkFieldNamesMatch()
{
$flipTable = array_flip($this->tables);
foreach($this->fields as $field)
{
if(isset($flipTable[$field['column']])) return throw new Exception("sql fields can not named {$field['column']}");
if(isset($flipTable[$field['alias']])) return throw new Exception("sql fields can not named {$field['alias']}");
}
}
//-------------------- Fetch相关方法(Fetch related methods) -------------------//
/**
* 获取一个记录。
* Fetch one record.
*
* @access public
* @return object|mixed
*/
public function fetch()
{
$rows = $this->getResult();
return $rows ? current($rows) : '';
}
/**
* 获取所有记录。
* Fetch all records.
*
* @access public
* @return array
*/
public function fetchAll()
{
return $this->getResult();
}
}
+326 -2
View File
@@ -3,9 +3,333 @@ require __DIR__ . '/vendor/autoload.php';
class sqlparser
{
/**
* Parser object of SqlParser.
*
* @var object
* @access public
*/
public $parser;
/**
* Statements of parser.
*
* @var array
* @access public
*/
public $statements;
/**
* First statement.
*
* @var object
* @access public
*/
public $statement;
/**
* Count statements.
*
* @var int
* @access public
*/
public $statementsCount = 0;
/**
* Is first statemnt select type.
*
* @var bool
* @access public
*/
public $isSelect = false;
/**
* DAO
*
* @var object
* @access public
*/
public $dao = null;
/**
* Origin tables.
*
* @var array
* @access public
*/
public $originTables = array();
/**
* Columns.
*
* @var array
* @access public
*/
public $columns = array();
/**
* Tables.
*
* @var array
* @access public
*/
public $tables = array();
public function __construct($query)
{
$this->parser = new PhpMyAdmin\SqlParser\Parser($query);
$this->statements = $this->parser->statements;
$query = $this->skipLineBreak($query);
$this->parser = new PhpMyAdmin\SqlParser\Parser($query);
$this->statements = $this->parser->statements;
$this->statementsCount = count($this->statements);
$this->statement = $this->statementsCount > 0 ? current($this->statements) : null;
$this->isSelect = $this->statement instanceof PhpMyAdmin\SqlParser\Statements\SelectStatement === true;
}
/**
* Parse statement.
*
* @access public
* @return void
*/
public function parseStatement()
{
$this->columns = $this->parseColumns();
$this->tables = $this->parseTables();
}
/**
* Match columns with table.
*
* @access public
* @return array
*/
public function matchColumnsWithTable()
{
if(count($this->tables) == 1) return $this->combineSingleTable();
return $this->combineMultipleTable();
}
/**
* Combine single table to columns.
*
* @access public
* @return array
*/
public function combineSingleTable()
{
$combineColumns = array();
$fromTable = current($this->tables);
foreach($this->columns as $columnName => $column)
{
$column['table'] = array_merge($fromTable, array('column' => $column['origin']));
$combineColumns[$columnName] = $column;
}
return $combineColumns;
}
/**
* Combine multiple table to columns.
*
* @access public
* @return array
*/
public function combineMultipleTable()
{
foreach($this->columns as $columnName => $column)
{
$column['table'] = $this->searchTables($column['table'], $this->tables, $column['origin']);
$combineColumns[$columnName] = $column;
}
return $combineColumns;
}
/**
* Search table from origin tables.
*
* @param string $tableName
* @param string $tables
* @param string $column
* @access public
* @return string|false
*/
public function searchTables($tableName, $tables, $column)
{
/* 如果能使用别名匹配上,那么直接返回。*/
/* If it can be matched using an alias, then it returns. */
foreach($tables as $table) if($tableName == $table['alias']) return array_merge($table, array('column' => $column));
/* 如果匹配不上,则字段没有使用别名进行限制,那么需要通过字段去遍历所有表。*/
/* If it doesn't match, then the field is not aliased, and you need to iterate over all tables using the field. */
foreach($tables as $table)
{
$isTable = $table['isTable'];
$originTable = $table['originTable'];
/* 如果是原始表,并且列在原始表中存在,那么返回这个表。*/
/* If it is the original table and the column exists in the original table, then the table is returned. */
if($isTable && $this->columnExistInOriginTable($originTable, $column)) return array_merge($table, array('column' => $column));
/* 如果不是原始表,并且列在子句中存在,那么返回子句中这个字段对应的表。*/
/* If it is not the original table and the column exists in the clause, then the table corresponding to the field in the clause is returned. */
if(!$isTable && isset($originTable[$column])) return array_merge($originTable[$column]['table'], array('column' => $originTable[$column]['origin']));
}
return false;
}
/**
* Parse columns.
*
* @access public
* @return void
*/
public function parseColumns()
{
$fields = array();
foreach($this->statement->expr as $expr)
{
/* 获取查询数据后真正展示出来的列名 */
$columnName = empty($expr->alias) ? $expr->column : $expr->alias;
$fields[$columnName] = array('origin' => $expr->column, 'table' => $expr->table);
}
return $fields;
}
/**
* Parse tables.
*
* @access public
* @return void
*/
public function parseTables()
{
$from = current($this->statement->from);
$joins = $this->statement->join;
$tables = array();
$tables[] = $this->parseTable($from, 'from');
foreach($joins as $join) $tables[] = $this->parseTable($join->expr, 'join');
return $tables;
}
/**
* Parse table.
*
* @param object $expr
* @param string $type
* @access public
* @return void
*/
public function parseTable($expr, $type)
{
$isTable = empty($expr->subquery);
$table = array('alias' => $expr->alias, 'isTable' => $isTable, 'type' => $type);
$table['originTable'] = $this->getOriginTable($expr->expr, $isTable);
return $table;
}
/**
* Get origin table from table name or expr.
*
* @param string $table
* @param bool $isTable
* @access public
* @return string|array
*/
public function getOriginTable($table, $isTable)
{
if(!$isTable)
{
$parser = new sqlparser($table);
$parser->setDAO($this->dao);
$parser->parseStatement();
return $parser->matchColumnsWithTable();
}
$this->storeOriginTable($table);
return $table;
}
/**
* Judge column exist in origin table or not.
*
* @param string $table
* @param string $column
* @access public
* @return bool
*/
public function columnExistInOriginTable($table, $column)
{
$originTable = $this->getOriginTableColumns($table);
if(!$originTable) return false;
return isset($originTable[$column]);
}
/**
* Get origin table columns.
*
* @param string $table
* @access public
* @return array|null
*/
public function getOriginTableColumns($table)
{
$originTables = $this->originTables;
return isset($originTables[$table]) ? $originTables[$table] : null;
}
/**
* Store origin table.
*
* @param string $table
* @access public
* @return void
*/
public function storeOriginTable($table)
{
if(!isset($this->originTables[$table])) $this->originTables[$table] = $this->dao->descTable($table);
}
/**
* Set dao.
*
* @param object $dao
* @access public
* @return void
*/
public function setDAO($dao)
{
$this->dao = $dao;
}
/**
* Skip line break in sql.
*
* @param string $sql
* @access public
* @return string
*/
public function skipLineBreak($sql)
{
$sql = str_replace("\n\t", " ", $sql);
$sql = str_replace("\t\n", " ", $sql);
$sql = str_replace("\n\r", " ", $sql);
$sql = str_replace("\r\n", " ", $sql);
$sql = str_replace("\r", " ", $sql);
$sql = str_replace("\n", " ", $sql);
return $sql;
}
}
+2 -2
View File
@@ -31,8 +31,8 @@ class moduleMenu extends wg
protected static array $defineBlocks = array
(
'header' => array(),
'footer' => array()
'header' => array(),
'footer' => array()
);
public static function getPageCSS(): ?string
+255
View File
@@ -86,3 +86,258 @@ $pivots['100'] = array("root" => 3, "name" => "Bug", "grade" => 2);
$config->bi->builtin->modules->charts = $charts;
$config->bi->builtin->modules->pivots = $pivots;
$config->bi->duckSQLTemp = <<<EOT
LOAD '{EXTENSIONPATH}';
ATTACH 'host={HOST} user={USER} password={PASSWORD} port={PORT} database={DATABASE}' as mysqldb(TYPE MYSQL);
USE mysqldb;
{COPYSQL}
EOT;
$config->bi->drivers = array('mysql', 'duckdb');
$config->bi->columnTypes = new stdclass();
$config->bi->columnTypes->mysql['TINY'] = 'number';
$config->bi->columnTypes->mysql['SHORT'] = 'number';
$config->bi->columnTypes->mysql['LONG'] = 'number';
$config->bi->columnTypes->mysql['FLOAT'] = 'number';
$config->bi->columnTypes->mysql['DOUBLE'] = 'number';
$config->bi->columnTypes->mysql['TIMESTAMP'] = 'string';
$config->bi->columnTypes->mysql['LONGLONG'] = 'string';
$config->bi->columnTypes->mysql['INT24'] = 'number';
$config->bi->columnTypes->mysql['DATE'] = 'date';
$config->bi->columnTypes->mysql['TIME'] = 'string';
$config->bi->columnTypes->mysql['DATETIME'] = 'date';
$config->bi->columnTypes->mysql['YEAR'] = 'date';
$config->bi->columnTypes->mysql['ENUM'] = 'string';
$config->bi->columnTypes->mysql['SET'] = 'string';
$config->bi->columnTypes->mysql['TINYBLOB'] = 'string';
$config->bi->columnTypes->mysql['MEDIUMBLOB'] = 'string';
$config->bi->columnTypes->mysql['LONG_BLOB'] = 'string';
$config->bi->columnTypes->mysql['BLOB'] = 'string';
$config->bi->columnTypes->mysql['VAR_STRING'] = 'string';
$config->bi->columnTypes->mysql['STRING'] = 'string';
$config->bi->columnTypes->mysql['NULL'] = 'null';
$config->bi->columnTypes->mysql['NEWDATE'] = 'date';
$config->bi->columnTypes->mysql['INTERVAL'] = 'string';
$config->bi->columnTypes->mysql['GEOMETRY'] = 'string';
$config->bi->columnTypes->mysql['NEWDECIMAL'] = 'number';
/* Dameng native_type. */
$config->bi->columnTypes->mysql['int'] = 'number';
$config->bi->columnTypes->mysql['varchar'] = 'string';
$config->bi->columnTypes->mysql['text'] = 'string';
$config->bi->columnTypes->mysql['timestamp'] = 'string';
$config->bi->columnTypes->mysql['date'] = 'date';
$config->bi->columnTypes->mysql['time'] = 'string';
$config->bi->columnTypes->mysql['double'] = 'number';
$config->bi->columnTypes->mysql['number'] = 'number';
$config->bi->columnTypes->mysql['bigint'] = 'number';
/* DuckDB native_type. */
$config->bi->columnTypes->duckdb['BIGINT'] = 'number';
$config->bi->columnTypes->duckdb['UBIGINT'] = 'number';
$config->bi->columnTypes->duckdb['HUGEINT'] = 'number';
$config->bi->columnTypes->duckdb['UHUGEINT'] = 'number';
$config->bi->columnTypes->duckdb['INTEGER'] = 'number';
$config->bi->columnTypes->duckdb['UINTEGER'] = 'number';
$config->bi->columnTypes->duckdb['SMALLINT'] = 'number';
$config->bi->columnTypes->duckdb['USMALLINT'] = 'number';
$config->bi->columnTypes->duckdb['TINYINT'] = 'number';
$config->bi->columnTypes->duckdb['UTINYINT'] = 'number';
$config->bi->columnTypes->duckdb['REAL'] = 'number';
$config->bi->columnTypes->duckdb['DOUBLE'] = 'number';
$config->bi->columnTypes->duckdb['BOOLEAN'] = 'number';
$config->bi->columnTypes->duckdb['BLOB'] = 'string';
$config->bi->columnTypes->duckdb['VARCHAR'] = 'string';
$config->bi->columnTypes->duckdb['DATE'] = 'date';
$config->bi->columnTypes->duckdb['TIMESTAMP'] = 'date';
$config->bi->columnTypes->duckdb['TIME'] = 'date';
$config->bi->columnTypes->INTEGER = 'number';
$config->bi->columnTypes->UINTEGER = 'number';
$config->bi->columnTypes->UTINYINT = 'number';
$config->bi->columnTypes->SMALLINT = 'number';
$config->bi->columnTypes->FLOAT = 'number';
$config->bi->columnTypes->BOOLEAN = 'number';
$config->bi->columnTypes->VARCHAR = 'string';
$config->bi->columnTypes->TIMESTAMP = 'date';
$config->bi->columnTypes->DATE = 'date';
$config->bi->duckdb = new stdclass();
$config->bi->duckdb->tables = array();
$config->bi->duckdb->tables['action'] = <<<EOT
SELECT id,objectType,objectID,product,project,execution,actor,action,date,read,vision,efforted FROM zt_action
EOT;
$config->bi->duckdb->tables['account'] = <<<EOT
SELECT * FROM zt_account
EOT;
$config->bi->duckdb->tables['attend'] = <<<EOT
SELECT * FROM zt_attend
EOT;
$config->bi->duckdb->tables['bug'] = <<<EOT
SELECT * FROM zt_bug
EOT;
$config->bi->duckdb->tables['build'] = <<<EOT
SELECT * FROM zt_build
EOT;
$config->bi->duckdb->tables['burn'] = <<<EOT
SELECT * FROM zt_burn
EOT;
$config->bi->duckdb->tables['case'] = <<<EOT
SELECT * FROM zt_case
EOT;
$config->bi->duckdb->tables['casestep'] = <<<EOT
SELECT * FROM zt_casestep
EOT;
$config->bi->duckdb->tables['config'] = <<<EOT
SELECT * FROM zt_config
EOT;
$config->bi->duckdb->tables['demand'] = <<<EOT
SELECT * FROM zt_demand
EOT;
$config->bi->duckdb->tables['dept'] = <<<EOT
SELECT * FROM zt_dept
EOT;
$config->bi->duckdb->tables['doc'] = <<<EOT
SELECT * FROM zt_doc
EOT;
$config->bi->duckdb->tables['effort'] = <<<EOT
SELECT * FROM zt_effort
EOT;
$config->bi->duckdb->tables['feedback'] = <<<EOT
SELECT * FROM zt_feedback
EOT;
$config->bi->duckdb->tables['file'] = <<<EOT
SELECT * FROM zt_file
EOT;
$config->bi->duckdb->tables['group'] = <<<EOT
SELECT * FROM zt_group
EOT;
$config->bi->duckdb->tables['history'] = <<<EOT
SELECT id,action,field FROM zt_history
EOT;
$config->bi->duckdb->tables['issue'] = <<<EOT
SELECT * FROM zt_issue
EOT;
$config->bi->duckdb->tables['meeting'] = <<<EOT
SELECT * FROM zt_meeting
EOT;
$config->bi->duckdb->tables['metric'] = <<<EOT
SELECT * FROM zt_metric
EOT;
$config->bi->duckdb->tables['metriclib'] = <<<EOT
SELECT * FROM zt_metriclib
EOT;
$config->bi->duckdb->tables['module'] = <<<EOT
SELECT * FROM zt_module
EOT;
$config->bi->duckdb->tables['opportunity'] = <<<EOT
SELECT * FROM zt_opportunity
EOT;
$config->bi->duckdb->tables['pivot'] = <<<EOT
SELECT * FROM zt_pivot
EOT;
$config->bi->duckdb->tables['product'] = <<<EOT
SELECT * FROM zt_product
EOT;
$config->bi->duckdb->tables['productplan'] = <<<EOT
SELECT * FROM zt_productplan
EOT;
$config->bi->duckdb->tables['planstory'] = <<<EOT
SELECT * FROM zt_planstory
EOT;
$config->bi->duckdb->tables['project'] = <<<EOT
SELECT * FROM zt_project
EOT;
$config->bi->duckdb->tables['projectcase'] = <<<EOT
SELECT * FROM zt_projectcase
EOT;
$config->bi->duckdb->tables['projectproduct'] = <<<EOT
SELECT * FROM zt_projectproduct
EOT;
$config->bi->duckdb->tables['projectstory'] = <<<EOT
SELECT * FROM zt_projectstory
EOT;
$config->bi->duckdb->tables['release'] = <<<EOT
SELECT * FROM zt_release
EOT;
$config->bi->duckdb->tables['risk'] = <<<EOT
SELECT * FROM zt_risk
EOT;
$config->bi->duckdb->tables['story'] = <<<EOT
SELECT * FROM zt_story
EOT;
$config->bi->duckdb->tables['task'] = <<<EOT
SELECT * FROM zt_task
EOT;
$config->bi->duckdb->tables['team'] = <<<EOT
SELECT * FROM zt_team
EOT;
$config->bi->duckdb->tables['testreport'] = <<<EOT
SELECT * FROM zt_testreport
EOT;
$config->bi->duckdb->tables['testresult'] = <<<EOT
SELECT * FROM zt_testresult
EOT;
$config->bi->duckdb->tables['testtask'] = <<<EOT
SELECT * FROM zt_testtask
EOT;
$config->bi->duckdb->tables['ticket'] = <<<EOT
SELECT * FROM zt_ticket
EOT;
$config->bi->duckdb->tables['todo'] = <<<EOT
SELECT * FROM zt_todo
EOT;
$config->bi->duckdb->tables['user'] = <<<EOT
SELECT * FROM zt_user
EOT;
$config->bi->duckdb->ztvtables['dayactions'] = <<<EOT
SELECT * FROM ztv_dayactions
EOT;
$config->bi->duckdb->ztvtables['dayuserlogin'] = <<<EOT
SELECT * FROM ztv_dayuserlogin
EOT;
$config->bi->duckdb->ztvtables['dayeffort'] = <<<EOT
SELECT * FROM ztv_dayeffort
EOT;
$config->bi->duckdb->ztvtables['daystoryopen'] = <<<EOT
SELECT * FROM ztv_daystoryopen
EOT;
$config->bi->duckdb->ztvtables['daystoryclose'] = <<<EOT
SELECT * FROM ztv_daystoryclose
EOT;
$config->bi->duckdb->ztvtables['daytaskopen'] = <<<EOT
SELECT * FROM ztv_daytaskopen
EOT;
$config->bi->duckdb->ztvtables['daytaskfinish'] = <<<EOT
SELECT * FROM ztv_daytaskfinish
EOT;
$config->bi->duckdb->ztvtables['daybugopen'] = <<<EOT
SELECT * FROM ztv_daybugopen
EOT;
$config->bi->duckdb->ztvtables['daybugresolve'] = <<<EOT
SELECT * FROM ztv_daybugresolve
EOT;
$config->bi->duckdb->ztvtables['productstories'] = <<<EOT
SELECT * FROM ztv_productstories
EOT;
$config->bi->duckdb->ztvtables['productbugs'] = <<<EOT
SELECT * FROM ztv_productbugs
EOT;
$config->bi->duckdb->ztvtables['projectsummary'] = <<<EOT
SELECT * FROM ztv_projectsummary
EOT;
$config->bi->duckdb->ztvtables['executionsummary'] = <<<EOT
SELECT * FROM ztv_executionsummary
EOT;
$config->bi->duckdb->ztvtables['projectstories'] = <<<EOT
SELECT * FROM ztv_projectstories
EOT;
$config->bi->duckdb->ztvtables['projectbugs'] = <<<EOT
SELECT * FROM ztv_projectbugs
EOT;
$config->bi->duckdb->ztvtables['projectteams'] = <<<EOT
SELECT * FROM ztv_projectteams
EOT;
+1118 -1132
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+55
View File
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/**
* The control file of branch of ZenTaoPMS.
*
* @copyright Copyright 2009-2024 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
* @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
* @author Chenxuan Song <songchenxuan@cnezsoft.com>
* @package bi
* @version $Id$
* @link https://www.zentao.net
*/
class bi extends control
{
/**
* 使用 duckDB 生成最新的 parquet 文件。
* Use duckDB gen lastest parquet.
*
* @access public
* @return void
*/
public function syncParquetFile()
{
$startTime = microtime(true);
$duckdb = $this->bi->getDuckDBPath();
if(!$duckdb)
{
echo("DuckDB bin path not exists.");
return;
}
$duckdbTmpPath = $this->bi->getDuckDBTmpDir();
if(!$duckdbTmpPath)
{
echo("Create DuckDB tmp dir permission denied.");
return;
}
$copySQL = $this->bi->prepareCopySQL($duckdbTmpPath);
$command = $this->bi->prepareSyncCommand($duckdb->bin, $duckdb->extension, $copySQL);
$output = shell_exec($command);
$endTime = microtime(true);
$runTime = $endTime - $startTime;
echo "$runTime \n";
if(empty($output))
{
echo('success');
return;
}
echo($output);
}
}
+566 -5
View File
@@ -90,6 +90,154 @@ class biModel extends model
return array_filter(array_unique($tables));
}
/**
* Try to explain sql.
*
* @param string $sql
* @param string $driver mysql|duckdb
* @access public
* @return array
*/
public function explainSQL($sql, $driver = 'mysql')
{
$dbh = $this->app->loadDriver($driver);
$prefixSQL = $driver == 'mysql' ? 'EXPLAIN' : 'PRAGMA enable_profiling=json; EXPLAIN ANALYZE';
try
{
$rows = $dbh->query("$prefixSQL $sql")->fetchAll();
}
catch(Exception $e)
{
$message = preg_replace("/\r|\n|\t/", "", $e->getMessage());
$message = strip_tags($message);
return array('result' => 'fail', 'message' => $message);
}
return array('result' => 'success');
}
/**
* Query a sql with driver.
*
* @param string $driver mysql|duckdb
* @param string $sql
* @param bool $fetchAll
* @access public
* @return array|string
*/
public function queryWithDriver($driver, $sql, $fetchAll = true)
{
$dbh = $this->app->loadDriver($driver);
if($fetchAll) $results = $dbh->query($sql)->fetchAll();
else $results = $dbh->query($sql)->fetch();
return $results;
}
/**
* Try to explain sql.
*
* @param string $sql
* @param string $limitSql
* @param string $driver mysql|duckdb
* @access public
* @return array
*/
public function querySQL($sql, $limitSql, $driver = 'mysql')
{
$dbh = $this->app->loadDriver($driver);
try
{
if($driver == 'mysql')
{
$rows = $dbh->query($limitSql)->fetchAll();
$count = $dbh->query("SELECT FOUND_ROWS() as count")->fetch();
$rowsCount = $count->count;
}
elseif($driver == 'duckdb')
{
$rows = $dbh->query($limitSql)->fetchAll();
$allRows = $dbh->query("SELECT COUNT(1) as count FROM ( $sql )")->fetch();
$rowsCount = $allRows->count;
}
}
catch(Exception $e)
{
$message = preg_replace("/\r|\n|\t/", "", $e->getMessage());
$message = strip_tags($message);
return array('result' => 'fail', 'message' => $message);
}
return array('result' => 'success', 'rows' => $rows, 'rowsCount' => $rowsCount);
}
/**
* Get sql result columns.
*
* @param string $sql
* @param string $driver mysql|duckdb
* @access public
* @return array|false
*/
public function getColumns(string $sql, $driver = 'mysql'): array|false
{
if(!in_array($driver, $this->config->bi->drivers)) return false;
if($driver == 'mysql')
{
$columns = $this->dao->getColumns($sql);
}
else
{
$dbh = $this->app->loadDriver('duckdb');
$columns = $dbh->query("DESCRIBE $sql")->fetchAll();
}
$result = array();
foreach($columns as $column)
{
$column = (array)$column;
$name = $driver == 'mysql' ? $column['name'] : $column['column_name'];
$nativeType = $driver == 'mysql' ? $column['native_type'] : $column['column_type'];
$result[$name] = array('name' => $name, 'native_type' => $nativeType);
}
return $result;
}
/**
* 获取表的字段类型。
* Get table data.
*
* @param string $sql
* @param string $driverName mysql|duckdb
* @param array $columns
* @access public
* @return object
*/
public function getColumnsType(string $sql, string $driverName = 'mysql', array $columns = array()): object
{
if(empty($columns)) $columns = $this->getColumns($sql, $driverName);
$columnTypes = new stdclass();
foreach($columns as $column)
{
$field = $column['name'];
$nativeType = $column['native_type'];
$type = $this->config->bi->columnTypes->$driverName[$nativeType];
if(isset($columnTypes->$field)) $field = $column['table'] . $field;
$columnTypes->$field = $type;
}
return $columnTypes;
}
/**
* Get object options.
*
@@ -192,13 +340,15 @@ class biModel extends model
* @param string $sql
* @param string $keyField
* @param string $valueField
* @param string $driver
* @access public
* @return array
*/
public function getOptionsFromSql(string $sql, string $keyField, string $valueField): array
public function getOptionsFromSql(string $sql, $driver, string $keyField, string $valueField): array
{
$options = array();
$cols = $this->dbh->query($sql)->fetchAll();
$dbh = $this->app->loadDriver($driver);
$cols = $dbh->query($sql)->fetchAll();
$sample = current($cols);
if(!isset($sample->$keyField) or !isset($sample->$valueField)) return $options;
@@ -276,7 +426,7 @@ class biModel extends model
* @access public
* @return void
*/
public function getMultiData($settings, $defaultSql, $filters, $sort = false)
public function getMultiData($settings, $defaultSql, $filters, $driver, $sort = false)
{
$this->loadModel('chart');
@@ -328,7 +478,7 @@ class biModel extends model
$sql .= " where $whereStr";
}
$sql .= " group by $groupBySql";
$rows = $this->dao->query($sql)->fetchAll();
$rows = $this->queryWithDriver($driver, $sql);
$stat = $this->processRows($rows, $date, $group, $metric);
$maxCount = 50;
@@ -385,7 +535,7 @@ class biModel extends model
return $stat;
}
/**
/*
* 准备内置的图表sql语句。
* Prepare builtin chart sql.
*
@@ -578,6 +728,417 @@ class biModel extends model
return $screenSQLs;
}
/*
* 获取DuckDB的可执行文件路径。
* Get DcukDB path.
*
* @access public
* @return object|false
*/
public function getDuckDBPath()
{
$binPath = $this->app->getBasePath() . 'bin' . DS . 'duckdb' . DS;
$file = $binPath . 'duckdb';
$extension = $binPath . 'mysql_scanner.duckdb_extension';
if(!file_exists($file) && !file_exists($extension) && !is_executable($file)) return false;
return (object)array('bin' => $file, 'extension' => $extension);
}
/**
* 获取DuckDB临时目录。
* Get DuckDB temp directory.
*
* @access public
* @return string|false
*/
public function getDuckDBTmpDir()
{
$duckdbTmpPath = $this->app->getTmpRoot() . 'duckdb' . DS . 'bi' . DS;
if(!is_dir($duckdbTmpPath) && !mkdir($duckdbTmpPath, 0755, true)) return false;
return $duckdbTmpPath;
}
/**
* 准备同步数据库所需的复制SQL。
* Prepare copy SQL for sync.
*
* @param string $duckdbTmpPath
* @access public
* @return string
*/
public function prepareCopySQL($duckdbTmpPath)
{
$tables = $this->config->bi->duckdb->tables;
$ztvtables = $this->config->bi->duckdb->ztvtables;
if(empty($tables)) return '';
$tablePrefix = $this->config->db->prefix;
$ztvPrefix = 'ztv_';
$copySQL = '';
foreach($tables as $table => $sql)
{
$table = $tablePrefix . $table;
$sql = str_replace('zt_', $tablePrefix, $sql);
$tablePath = $duckdbTmpPath . $table;
$copySQL .= "COPY ($sql) TO '$tablePath.parquet';\n";
}
foreach($ztvtables as $table => $sql)
{
$table = $ztvPrefix . $table;
$tablePath = $duckdbTmpPath . $table;
$copySQL .= "COPY ($sql) TO '$tablePath.parquet';\n";
}
return $copySQL;
}
/**
* 准备同步命令。
* Prepare sync command.
*
* @param string $binPath
* @param string $extensionPath
* @param string $copySQL
* @access public
* @return string
*/
public function prepareSyncCommand($binPath, $extensionPath, $copySQL)
{
$sqlContent = $this->config->bi->duckSQLTemp;
$dbConfig = $this->config->db;
$variables = array(
'{EXTENSIONPATH}' => $extensionPath,
'{DATABASE}' => $dbConfig->name,
'{USER}' => $dbConfig->user,
'{PASSWORD}' => $dbConfig->password,
'{HOST}' => $dbConfig->host,
'{PORT}' => $dbConfig->port,
'{COPYSQL}' => $copySQL
);
foreach($variables as $key => $value)
{
$sqlContent = str_replace($key, $value, $sqlContent);
}
return "$binPath :memory: \"$sqlContent\" 2>&1";
}
/**
* Process filter variables in sql.
*
* @param string $sql
* @param array $filters
* @access public
* @return string
*/
public function processVars($sql, $filters = array())
{
foreach($filters as $index => $filter)
{
if(empty($filter['default'])) continue;
if(!isset($filter['from']) || $filter['from'] != 'query') continue;
$filters[$index]['default'] = $this->loadModel('pivot')->processDateVar($filter['default']);
}
$sql = $this->loadModel('chart')->parseSqlVars($sql, $filters);
$sql = trim($sql, ';');
return $sql;
}
/**
* Build statement object from sql.
*
* @param string $sql
* @access public
* @return object
*/
public function sql2Statement($sql)
{
$this->app->loadClass('sqlparser', true);
$parser = new sqlparser($sql);
if($parser->statementsCount == 0) return $this->lang->dataview->empty;
if($parser->statementsCount > 1) return $this->lang->dataview->onlyOne;
if(!$parser->isSelect) return $this->lang->dataview->allowSelect;
return $parser->statement;
}
/**
* Parse sql.
*
* @param string $sql
* @access public
* @return array
*/
public function parseSql(string $sql): array
{
$this->app->loadClass('sqlparser', true);
$parser = new sqlparser($sql);
$parser->setDAO($this->dao);
$parser->parseStatement();
return $parser->matchColumnsWithTable();
}
/**
* Validate sql.
*
* @param string $sql
* @access public
* @return string|true
*/
public function validateSql($sql)
{
$this->loadModel('dataview');
$driver = 'mysql';
if(empty($sql)) return $this->lang->dataview->empty;
try
{
$rows = $this->dbh->query("EXPLAIN $sql")->fetchAll();
}
catch(Exception $e)
{
return $e->getMessage();
}
$sqlColumns = $this->getColumns($sql, $driver);
list($isUnique, $repeatColumn) = $this->dataview->checkUniColumn($sql, $driver, true, $sqlColumns);
if(!$isUnique) return sprintf($this->lang->dataview->duplicateField, implode(',', $repeatColumn));
return true;
}
/**
* Prepare pager from sql.
*
* @param object $statement
* @param int $recPerPage
* @param int $pageID
* @access public
* @return string
*/
public function prepareSqlPager($statement, $recPerPage, $pageID)
{
if(!$statement->limit)
{
$statement->limit = new stdclass();
}
$statement->limit->offset = $recPerPage * ($pageID - 1);
$statement->limit->rowCount = $recPerPage;
$statement->options->options[] = 'SQL_CALC_FOUND_ROWS';
$limitSql = $statement->build();
return $limitSql;
}
/**
* Prepare columns setting from sql.
*
* @param string $sql
* @param object $statement
* @access public
* @return array
*/
public function prepareColumns($sql, $statement)
{
$this->loadModel('chart');
$this->loadModel('dataview');
// TODO 需要替换成实际图表或者透视表的driver。
$driver = 'mysql';
$sqlColumns = $this->getColumns($sql, $driver);
$columnTypes = $this->getColumnsType($sql, $driver, $sqlColumns);
$columnFields = array();
foreach($columnTypes as $column => $type) $columnFields[$column] = $column;
$tableAndFields = $this->getTableAndFields($sql);
$tables = $tableAndFields['tables'];
$fields = $tableAndFields['fields'];
$moduleNames = array();
$aliasNames = array();
if($tables)
{
$moduleNames = $this->dataview->getModuleNames($tables);
$aliasNames = $this->dataview->getAliasNames($statement, $moduleNames);
}
list($fields, $objects) = $this->dataview->mergeFields($columnFields, $fields, $moduleNames, $aliasNames);
$columns = array();
foreach($fields as $field => $name)
{
$columns[$field] = array('name' => $name, 'field' => $field, 'type' => $columnTypes->$field, 'object' => $objects[$field]);
}
return $columns;
}
/**
* Query sql.
*
* @param string $sql
* @param int $recPerPage
* @param int $pageID
* @access public
* @return object
*/
public function query($sql, $recPerPage = 100, $pageID = 1)
{
$queryResult = new stdclass();
$queryResult->error = false;
$queryResult->errorMsg = '';
$queryResult->cols = array();
$queryResult->rows = array();
$queryResult->sql = '';
$queryResult->fieldSettings = array();
$statement = $this->sql2Statement($sql);
if(is_string($statement))
{
$queryResult->error = true;
$queryResult->errorMsg = $statement;
return $queryResult;
}
$checked = $this->validateSql($sql);
if($checked !== true)
{
$queryResult->error = true;
$queryResult->errorMsg = $checked;
return $queryResult;
}
$limitSql = $this->prepareSqlPager($statement, $recPerPage, $pageID);
try
{
$queryResult->rows = $this->dbh->query($limitSql)->fetchAll();
$queryResult->rowsCount = $this->dbh->query("SELECT FOUND_ROWS() as count")->fetch()->count;
}
catch(Exception $e)
{
$queryResult->error = true;
$queryResult->errorMsg = $e;
return $queryResult;
}
$columns = $this->prepareColumns($limitSql, $statement);
$clientLang = $this->app->getClientLang();
$fieldSettings = array();
foreach($columns as $field => $settings)
{
$title = $settings['name'];
if(!isset($settings[$clientLang]) || empty($settings[$clientLang])) $settings[$clientLang] = $title;
$fieldSettings[$field] = $settings;
}
$queryResult->cols = $this->buildQueryResultTableColumns($fieldSettings);
$queryResult->fieldSettings = $fieldSettings;
$queryResult->sql = $limitSql;
return $queryResult;
}
/**
* Build table columns from query result.
*
* @param array $fieldSettings
* @access public
* @return array
*/
public function buildQueryResultTableColumns($fieldSettings)
{
$cols = array();
$clientLang = $this->app->getClientLang();
foreach($fieldSettings as $field => $settings)
{
$settings = (array)$settings;
$title = $settings[$clientLang];
$type = $settings['type'];
$cols[] = array('name' => $field, 'title' => $title, 'sortType' => false);
}
return $cols;
}
/**
* Prepare field objects.
*
* @access public
* @return array
*/
public function prepareFieldObjects()
{
$this->loadModel('dataview');
$options = array();
foreach($this->lang->dataview->objects as $table => $name)
{
$fields = $this->dataview->getTypeOptions($table);
$options[] = array('text' => $name, 'value' => $table, 'fields' => $fields);
}
return $options;
}
/**
* Prepare field setting form data.
*
* @param object $settings
* @access public
* @return array
*/
public function prepareFieldSettingFormData($settings)
{
$formData = array();
foreach((array)$settings as $key => $setting)
{
$setting = (array)$setting;
$setting['key'] = $key;
$formData[] = $setting;
}
return $formData;
}
/**
* Convert json string to array.
*
* @param string|object|array $json
* @access public
* @return array
*/
public function json2Array(string|object|array $json): array
{
if(empty($json)) return array();
if(is_string($json)) return json_decode($json, true);
if(is_object($json)) return json_decode(json_encode($json), true);
return $json;
}
/**
* 根据类型和模块ID获取正确的模块ID。
* Get correct group id with type.
+34
View File
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
class biTest
{
public function __construct()
{
global $tester;
$this->objectModel = $tester->loadModel('bi');
}
/**
* Parse sql test
*
* @param string $sql
* @access public
* @return array
*/
public function parseSqlTest($sql)
{
$columns = $this->objectModel->parseSql($sql);
$result = array();
foreach($columns as $field => $column)
{
if(empty($column['table'])) continue;
$result[$field] = "{$column['table']['originTable']}=>{$column['table']['column']}";
}
arsort($result);
return $result;
}
}
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env php
<?php
include dirname(__FILE__, 5) . '/test/lib/init.php';
include dirname(__FILE__, 2) . '/lib/bi.unittest.class.php';
/**
title=biModel->parseSql();
timeout=0
cid=1
- 测试第1条sql
- 属性id @zt_product=>id
- 属性name @zt_product=>name
- 属性bugID @zt_bug=>id
- 属性type @zt_bug=>type
- 属性fixedBugs @zt_product=>fixedBugs
- 属性caseTitle @zt_case=>title
- 测试第2条sql
- 属性id @zt_task=>id
- 属性name @zt_task=>name
- 属性consumed @zt_task=>consumed
- 测试第3条sql
- 属性id @zt_story=>id
- 属性estimate @zt_story=>estimate
- 测试第4条sql
- 属性id @zt_story=>id
- 属性hour @zt_story=>estimate
- 测试第5条sql
- 属性name @zt_project=>name
- 属性PM @zt_project=>PM
- 属性begin @zt_project=>begin
- 属性realBegan @zt_project=>realBegan
- 属性realname @zt_user=>realname
*/
$sqls = array();
$sqls[0] = <<<EOT
select t1.id,t1.name,t2.id as bugID,t2.type, fixedBugs, caseTitle
from zt_product as t1
left join zt_bug as t2 on t1.id=t2.product and t2.deleted = '0'
left join zt_project as t3 on t1.program=t3.id
left join (select id, title as caseTitle from zt_case where deleted = '0') as t4 on t2.case = t4.id
where t1.deleted='0' and t2.deleted='0'
order by t3.`order` asc, t1.line desc, t1.`order` asc
EOT;
$sqls[1] = <<<EOT
select id,name,consumed from zt_task
EOT;
$sqls[2] = <<<EOT
SELECT DISTINCT id,estimate
FROM zt_story t1
WHERE 1 = (
SELECT COUNT(DISTINCT id,estimate)
FROM zt_story t2
WHERE t2.estimate> t1.estimate
)
EOT;
$sqls[3] = <<<EOT
SELECT DISTINCT id,estimate hour
FROM zt_story t1
WHERE 1 = (
SELECT COUNT(DISTINCT id,estimate)
FROM zt_story t2
WHERE t2.estimate> t1.estimate
)
EOT;
$sqls[4] = <<<EOT
SELECT
t1.name,
t1.PM,
t1.begin,
t1.realBegan,
t3.realname
FROM
zt_project AS t1
LEFT JOIN
zt_team AS t2 ON t1.id = t2.root AND t2.type = 'execution'
LEFT JOIN
zt_user AS t3 ON t2.account = t3.account
WHERE t1.name LIKE '%zentaopms%'
AND t1.realBegan > '2022-01-01'
order by t1.id
EOT;
$bi=new biTest();
r($bi->parseSqlTest($sqls[0])) && p('id,name,bugID,type,fixedBugs,caseTitle') && e('zt_product=>id,zt_product=>name,zt_bug=>id,zt_bug=>type,zt_product=>fixedBugs,zt_case=>title'); // 测试第1条sql
r($bi->parseSqlTest($sqls[1])) && p('id,name,consumed') && e('zt_task=>id,zt_task=>name,zt_task=>consumed'); // 测试第2条sql
r($bi->parseSqlTest($sqls[2])) && p('id,estimate') && e('zt_story=>id,zt_story=>estimate'); // 测试第3条sql
r($bi->parseSqlTest($sqls[3])) && p('id,hour') && e('zt_story=>id,zt_story=>estimate'); // 测试第4条sql
r($bi->parseSqlTest($sqls[4])) && p('name,PM,begin,realBegan,realname') && e('zt_project=>name,zt_project=>PM,zt_project=>begin,zt_project=>realBegan,zt_user=>realname'); // 测试第5条sql
+6
View File
@@ -11,6 +11,12 @@
*/
class chart extends control
{
/**
* __construct method.
*
* @access public
* @return void
*/
public function __construct(string $moduleName = '', string $methodName = '', string $appName = '')
{
parent::__construct($moduleName, $methodName, $appName);
+22 -62
View File
@@ -200,16 +200,17 @@ class chartModel extends model
{
$settings = current($chart->settings);
$type = $settings['type'];
$driver = $chart->driver;
$options = array();
$filterFormat = $this->getFilterFormat($chart->filters);
if($type == 'pie') $options = $this->genPie($chart->fieldSettings, $settings, $chart->sql, $filterFormat);
if($type == 'radar') $options = $this->genRadar($chart->fieldSettings, $settings, $chart->sql, $filterFormat, $chart->langs);
if($type == 'line') $options = $this->genLineChart($chart->fieldSettings, $settings, $chart->sql, $filterFormat, $chart->langs);
if($type == 'cluBarX' || $type == 'cluBarY') $options = $this->genCluBar($chart->fieldSettings, $settings, $chart->sql, $filterFormat, '', $chart->langs);
if($type == 'stackedBar' || $type == 'stackedBarY') $options = $this->genCluBar($chart->fieldSettings, $settings, $chart->sql, $filterFormat, 'total', $chart->langs);
if($type == 'waterpolo') $options = $this->bi->genWaterpolo($chart->fieldSettings, $settings, $chart->sql, $filterFormat);
if($type == 'pie') $options = $this->genPie($chart->fieldSettings, $settings, $chart->sql, $filterFormat, $driver);
if($type == 'radar') $options = $this->genRadar($chart->fieldSettings, $settings, $chart->sql, $filterFormat, $chart->langs, $driver);
if($type == 'line') $options = $this->genLineChart($chart->fieldSettings, $settings, $chart->sql, $filterFormat, $chart->langs, $driver);
if($type == 'cluBarX' || $type == 'cluBarY') $options = $this->genCluBar($chart->fieldSettings, $settings, $chart->sql, $filterFormat, '', $chart->langs, $driver);
if($type == 'stackedBar' || $type == 'stackedBarY') $options = $this->genCluBar($chart->fieldSettings, $settings, $chart->sql, $filterFormat, 'total', $chart->langs, $driver);
if($type == 'waterpolo') $options = $this->bi->genWaterpolo($chart->fieldSettings, $settings, $chart->sql, $filterFormat, $driver);
if(empty($options)) return array();
@@ -280,9 +281,9 @@ class chartModel extends model
* @access public
* @return array
*/
public function genRadar(array $fields, array $settings, string $defaultSql, array $filters, array $langs = array()): array
public function genRadar(array $fields, array $settings, string $defaultSql, array $filters, array $langs = array(), $driver = 'mysql'): array
{
list($group, $metrics, $aggs, $xLabels, $yStats) = $this->getMultiData($settings, $defaultSql, $filters);
list($group, $metrics, $aggs, $xLabels, $yStats) = $this->getMultiData($settings, $defaultSql, $filters, $driver);
$yDatas = array();
$max = 0;
@@ -332,14 +333,14 @@ class chartModel extends model
* @access public
* @return array
*/
public function genPie(array $fields, array $settings, string $sql, array $filters): array
public function genPie(array $fields, array $settings, string $sql, array $filters, string $driver = 'mysql'): array
{
$group = isset($settings['group'][0]['field']) ? $settings['group'][0]['field'] : '';
$date = isset($settings['group'][0]['group']) ? zget($this->config->chart->dateConvert, $settings['group'][0]['group']) : '';
$metric = isset($settings['metric'][0]['field']) ? $settings['metric'][0]['field'] : '';
$agg = isset($settings['metric'][0]['valOrAgg']) ? $settings['metric'][0]['valOrAgg'] : '';
$rows = $this->chartTao->getRows(str_replace(';', '', $sql), $filters, $date, $group, $metric, $agg);
$rows = $this->chartTao->getRows(str_replace(';', '', $sql), $filters, $date, $group, $metric, $agg, $driver);
$stat = $this->chartTao->processRows($rows, $date, $group, $metric);
if(empty($date)) arsort($stat);
@@ -390,9 +391,9 @@ class chartModel extends model
* @access public
* @return array
*/
public function genLineChart(array $fields, array $settings, string $defaultSql, array $filters, array $langs = array()): array
public function genLineChart(array $fields, array $settings, string $defaultSql, array $filters, array $langs = array(), string $driver = 'mysql'): array
{
list($group, $metrics, $aggs, $xLabels, $yStats) = $this->getMultiData($settings, $defaultSql, $filters);
list($group, $metrics, $aggs, $xLabels, $yStats) = $this->getMultiData($settings, $defaultSql, $filters, $driver);
$fieldType = $fields[$settings['xaxis'][0]['field']]['type'];
if($fieldType == 'date') sort($xLabels);
@@ -438,9 +439,9 @@ class chartModel extends model
* @access public
* @return array
*/
public function genCluBar(array $fields, array $settings, string $defaultSql, array $filters, string $stack = '', array $langs = array()): array
public function genCluBar(array $fields, array $settings, string $defaultSql, array $filters, string $stack = '', array $langs = array(), $driver = 'mysql'): array
{
list($group, $metrics, $aggs, $xLabels, $yStats) = $this->getMultiData($settings, $defaultSql, $filters);
list($group, $metrics, $aggs, $xLabels, $yStats) = $this->getMultiData($settings, $defaultSql, $filters, $driver);
$yDatas = array();
foreach($yStats as $yStat)
@@ -508,7 +509,7 @@ class chartModel extends model
* @access public
* @return array
*/
public function getMultiData(array $settings, string $defaultSql, array $filters, bool $sort = false): array
public function getMultiData(array $settings, string $defaultSql, array $filters, string $driver, bool $sort = false): array
{
$group = isset($settings['xaxis'][0]['field']) ? $settings['xaxis'][0]['field'] : '';
$date = isset($settings['xaxis'][0]['group']) ? zget($this->config->chart->dateConvert, $settings['xaxis'][0]['group']) : '';
@@ -552,7 +553,7 @@ class chartModel extends model
* @access public
* @return array
*/
public function genWaterpolo(array $settings, string $sql, array $filters)
public function genWaterpolo(array $settings, string $sql, array $filters, string $driver = 'mysql')
{
$operate = "{$settings['calc']}({$settings['goal']})";
$sql = "select $operate count from ($sql) tt ";
@@ -583,8 +584,8 @@ class chartModel extends model
if($moleculeWheres) $moleculeSQL .= 'where ' . implode(' and ', $moleculeWheres);
if($denominatorWheres) $denominatorSQL .= 'where ' . implode(' and ', $denominatorWheres);
$molecule = $this->dao->query($moleculeSQL)->fetch();
$denominator = $this->dao->query($denominatorSQL)->fetch();
$molecule = $this->bi->queryWithDriver($driver, $moleculeSQL, false);
$denominator = $this->bi->queryWithDriver($driver, $denominatorSQL, false);
$percent = $denominator->count ? round($molecule->count / $denominator->count, 4) : 0;
@@ -607,7 +608,7 @@ class chartModel extends model
* @access public
* @return array
*/
public function getSysOptions(string $type, string $object = '', string $field = '', string $sql = '', string $saveAs = ''): array
public function getSysOptions(string $type, string $object = '', string $field = '', string $sql = '', string $saveAs = '', $driver = 'mysql'): array
{
if(in_array($type, array('user', 'product', 'project', 'execution', 'dept'))) return $this->bi->getScopeOptions($type);
if(!$field) return array();
@@ -627,14 +628,14 @@ class chartModel extends model
{
$keyField = $field;
$valueField = $saveAs ? $saveAs : $field;
$options = $this->bi->getOptionsFromSql($sql, $keyField, $valueField);
$options = $this->bi->getOptionsFromSql($sql, $driver, $keyField, $valueField);
}
break;
}
if($sql and $saveAs and in_array($type, array('user', 'product', 'project', 'execution', 'dept', 'option', 'object')))
{
$options = $this->bi->getOptionsFromSql($sql, $field, $saveAs);
$options = $this->bi->getOptionsFromSql($sql, $driver, $field, $saveAs);
}
return array_filter($options);
@@ -738,47 +739,6 @@ class chartModel extends model
return $filterFormat;
}
/**
* 获取sql中的表、字段。
* Get tables and fields form sql.
*
* @param string $sql
* @access public
* @return array|false
*/
public function getTables(string $sql): array|false
{
$processedSql = trim($sql, ';');
$processedSql = str_replace(array("\r\n", "\n"), ' ', $processedSql);
$processedSql = str_replace('`', '', $processedSql);
preg_match_all('/^select (.+) from (.+)$/i', $processedSql, $selectAndFrom);
if(empty($selectAndFrom[2][0])) return false;
$tableSql = $fromSql = $selectAndFrom[2][0];
if(stripos($fromSql, 'where') !== false) $tableSql = trim(substr($fromSql, 0, stripos($fromSql, 'where')));
if(stripos($fromSql, 'limit') !== false) $tableSql = trim(substr($fromSql, 0, stripos($fromSql, 'limit')));
if(stripos($fromSql, 'having') !== false) $tableSql = trim(substr($fromSql, 0, stripos($fromSql, 'having')));
if(stripos($fromSql, 'group by') !== false) $tableSql = trim(substr($fromSql, 0, stripos($fromSql, 'group by')));
/* Remove such as "left join|right join|join", "on (t1.id=t2.id)", result like t1, t2 as t3. */
$tableSql .= ' ';
if(stripos($tableSql, 'join') !== false) $tableSql = preg_replace(array('/join\s+([A-Z]+_\w+ .*)on/Ui', '/,\s*on\s+[^,]+/i'), array(',$1,on', ''), $tableSql);
$tableSql = str_replace(array('left', 'right'), '', $tableSql);
/* Match t2 as t3 */
preg_match_all('/(\w+) +as +(\w+)/i', $tableSql, $matchOut);
$tableSql = preg_replace('/as +\w+/i', ' ', $tableSql);
$tableSql = trim(str_replace(array('(', ')', ','), ' ', $tableSql));
$tableSql = preg_replace('/ +/', ' ', $tableSql);
$tables = explode(' ', $tableSql);
list($fields, $sql) = $this->chartTao->getFieldsBySql($selectAndFrom[1][0], $sql, $matchOut);
return array('sql' => $sql, 'tables' => $tables, 'fields' => $fields);
}
/**
* 在sql中将变量解析为空字符串。
* Parse variables to null string in sql.
+3 -54
View File
@@ -15,7 +15,7 @@ class chartTao extends chartModel
* @access protected
* @return array
*/
protected function getRows(string $defaultSql, array $filters, string $date, string $group, string $metric, string $agg): array
protected function getRows(string $defaultSql, array $filters, string $date, string $group, string $metric, string $agg, string $driver): array
{
$groupSql = $groupBySql = "tt.`$group`";
if(!empty($date))
@@ -47,7 +47,8 @@ class chartTao extends chartModel
}
$sql .= " group by $groupBySql";
return $this->dao->query($sql)->fetchAll();
$dbh = $this->app->loadDriver($driver);
return $dbh->query($sql)->fetchAll();
}
/**
@@ -121,56 +122,4 @@ class chartTao extends chartModel
return $fieldName;
}
/**
* 在sql中提取字段。
* Switch field name.
*
* @param string $selectSql
* @param string $sql
* @param array $out
* @access protected
* @return array
*/
protected function getFieldsBySql(string $selectSql, string $sql, array $out): array
{
$fields = explode(',', $selectSql);
foreach($fields as $i => $field)
{
if($field) $asField = '';
if(strrpos($field, ' as ') !== false) list($field, $asField) = explode(' as ', $field);
if(strrpos($field, ' AS ') !== false) list($field, $asField) = explode(' AS ', $field);
$field = trim($field);
$asField = trim($asField);
$fieldName = $field;
if(strrpos($field, '.') !== false)
{
$table = substr($field, 0, strrpos($field, '.'));
$fieldName = substr($field, strrpos($field, '.') + 1);
if(!empty($out[0]) and in_array($table, $out[2]))
{
$realTable = $out[1][array_search($table, $out[2])];
$tableFieldName = str_replace($table . '.', $realTable . '.', $field);
if(isset($fields[$fieldName]) && !$asField)
{
$fieldName = str_replace('.', '', $field);
$sql = preg_replace(array("/$field/", "/`$field`/"), array("$field AS $fieldName", "`$field` AS $fieldName"), $sql, 1);
$field = $tableFieldName;
}
}
if($fieldName == '*') $fieldName = $field;
}
$fieldName = $asField ? $asField : $fieldName;
$fields[$fieldName] = $field;
unset($fields[$i]);
}
return array($fields, $sql);
}
}
-38
View File
@@ -12,41 +12,3 @@ $config->dataview->groupChild['company'] = array('user' => 'user,usercontact,use
$config->dataview->multipleMappingFields = array();
$config->dataview->multipleMappingFields[] = 'testcase-stage';
$config->dataview->columnTypes = new stdclass();
$config->dataview->columnTypes->TINY = 'number';
$config->dataview->columnTypes->SHORT = 'number';
$config->dataview->columnTypes->LONG = 'number';
$config->dataview->columnTypes->FLOAT = 'number';
$config->dataview->columnTypes->DOUBLE = 'number';
$config->dataview->columnTypes->TIMESTAMP = 'string';
$config->dataview->columnTypes->LONGLONG = 'string';
$config->dataview->columnTypes->INT24 = 'number';
$config->dataview->columnTypes->DATE = 'date';
$config->dataview->columnTypes->TIME = 'string';
$config->dataview->columnTypes->DATETIME = 'date';
$config->dataview->columnTypes->YEAR = 'date';
$config->dataview->columnTypes->ENUM = 'string';
$config->dataview->columnTypes->SET = 'string';
$config->dataview->columnTypes->TINYBLOB = 'string';
$config->dataview->columnTypes->MEDIUMBLOB = 'string';
$config->dataview->columnTypes->LONG_BLOB = 'string';
$config->dataview->columnTypes->BLOB = 'string';
$config->dataview->columnTypes->VAR_STRING = 'string';
$config->dataview->columnTypes->STRING = 'string';
$config->dataview->columnTypes->NULL = 'null';
$config->dataview->columnTypes->NEWDATE = 'date';
$config->dataview->columnTypes->INTERVAL = 'string';
$config->dataview->columnTypes->GEOMETRY = 'string';
$config->dataview->columnTypes->NEWDECIMAL = 'number';
/* Dameng native_type. */
$config->dataview->columnTypes->int = 'number';
$config->dataview->columnTypes->varchar = 'string';
$config->dataview->columnTypes->text = 'string';
$config->dataview->columnTypes->timestamp = 'string';
$config->dataview->columnTypes->date = 'date';
$config->dataview->columnTypes->time = 'string';
$config->dataview->columnTypes->double = 'number';
$config->dataview->columnTypes->number = 'number';
$config->dataview->columnTypes->bigint = 'number';
+23
View File
@@ -0,0 +1,23 @@
<?php
global $lang;
$config->dataview->schema = new stdclass();
$config->dataview->schema->dtable = new stdclass();
$config->dataview->schema->dtable->fieldList = array();
$config->dataview->schema->dtable->fieldList['id']['name'] = 'id';
$config->dataview->schema->dtable->fieldList['id']['type'] = 'id';
$config->dataview->schema->dtable->fieldList['name']['name'] = 'name';
$config->dataview->schema->dtable->fieldList['name']['type'] = 'title';
$config->dataview->schema->dtable->fieldList['desc']['name'] = 'desc';
$config->dataview->schema->dtable->fieldList['desc']['type'] = 'desc';
$config->dataview->schema->dtable->fieldList['type']['name'] = 'type';
$config->dataview->schema->dtable->fieldList['type']['type'] = 'category';
$config->dataview->schema->dtable->fieldList['length']['name'] = 'length';
$config->dataview->schema->dtable->fieldList['length']['title'] = $lang->dataview->length;
$config->dataview->schema->dtable->fieldList['length']['type'] = 'number';
$config->dataview->schema->dtable->fieldList['null']['name'] = 'null';
$config->dataview->schema->dtable->fieldList['null']['type'] = 'text';
+1 -1
View File
@@ -33,7 +33,7 @@ function query(callback) {
$('.query').addClass('disabled');
$('#querying').removeClass('hidden');
$.post(createLink('dataview', 'ajaxQuery'), {sql: $('#sql').val(), filters: filters, recPerPage: DataStorage.recPerPage, pageID: DataStorage.pageID}, function(resp)
$.post(createLink('dataview', 'ajaxQuery'), {sql: $('#sql').val(), driver: DataStorage.driver, filters: filters, recPerPage: DataStorage.recPerPage, pageID: DataStorage.pageID}, function(resp)
{
resp = JSON.parse(resp);
$('.query').removeClass('disabled');
+1
View File
@@ -11,6 +11,7 @@ $lang->dataview->code = 'Code';
$lang->dataview->group = 'Group';
$lang->dataview->view = 'View Name';
$lang->dataview->desc = 'Description';
$lang->dataview->length = 'length';
$lang->dataview->data = 'Data';
$lang->dataview->schema = 'Schema';
$lang->dataview->details = 'Details';
+1
View File
@@ -11,6 +11,7 @@ $lang->dataview->code = 'Code';
$lang->dataview->group = 'Group';
$lang->dataview->view = 'View Name';
$lang->dataview->desc = 'Description';
$lang->dataview->length = 'length';
$lang->dataview->data = 'Data';
$lang->dataview->schema = 'Schema';
$lang->dataview->details = 'Details';
+1
View File
@@ -11,6 +11,7 @@ $lang->dataview->code = 'Code';
$lang->dataview->group = 'Group';
$lang->dataview->view = 'View Name';
$lang->dataview->desc = 'Description';
$lang->dataview->length = 'length';
$lang->dataview->data = 'Data';
$lang->dataview->schema = 'Schema';
$lang->dataview->details = 'Details';
+1
View File
@@ -11,6 +11,7 @@ $lang->dataview->code = '代号';
$lang->dataview->group = '所属分组';
$lang->dataview->view = '视图名称';
$lang->dataview->desc = '描述';
$lang->dataview->length = '长度';
$lang->dataview->data = '数据';
$lang->dataview->schema = '字段';
$lang->dataview->details = '详情';
+11 -38
View File
@@ -22,7 +22,7 @@ class dataviewModel extends model
public function __construct()
{
parent::__construct();
$this->loadBIDAO();
$this->loadModel('bi');
}
@@ -229,61 +229,34 @@ class dataviewModel extends model
return array($mergeFields, $relatedObject);
}
/**
* 获取表的字段类型。
* Get table data.
*
* @param string $sql
* @access public
* @return object
*/
public function getColumns(string $sql): object
{
$columns = $this->dao->getColumns($sql);
$columnTypes = new stdclass();
foreach($columns as $column)
{
$field = $column['name'];
$nativeType = $column['native_type'];
$type = $this->config->dataview->columnTypes->$nativeType;
if(isset($columnTypes->$field)) $field = $column['table'] . $field;
$columnTypes->$field = $type;
}
return $columnTypes;
}
/**
* 检查查询结果的唯一性。
* Check that the column of an sql query is unique.
*
* @param string $sql
* @param string $driverName mysql|duckdb
* @param bool $repeat
* @param array $columns
* @access public
* @return bool|array
*/
public function checkUniColumn(string $sql, bool $repeat = false): bool|array
public function checkUniColumn(string $sql, string $driverName = 'mysql', bool $repeat = false, $columns = array()): bool|array
{
$columns = $this->dao->getColumns($sql);
if(empty($columns)) $columns = $this->bi->getColumns($sql, $driverName);
$isUnique = true;
$columnList = array();
$repeatColumn = array();
foreach($columns as $column)
$repeatFields = array();
$fieldCounts = array_count_values(array_column($columns, 'name'));
foreach($fieldCounts as $field => $value)
{
$field = $column['name'];
if(isset($columnTypes[$field]))
if($value > 1)
{
$isUnique = false;
$repeatColumn[$field] = $field;
$repeatFields[$field] = $field;
}
$columnTypes[$field] = $field;
}
if($repeat) return array($isUnique, $repeatColumn);
if($repeat) return array($isUnique, $repeatFields);
return $isUnique;
}
+3 -5
View File
@@ -112,19 +112,17 @@ class pivot extends control
$settings['filterType'] = $filterType;
$sql = str_replace(';', '', "$post->sql");
$driver = $pivot->driver;
$fields = $post->fieldSettings;
$langs = isset($post->langs) ? (is_array($post->langs) ? $post->langs : json_decode($post->langs, true)) : array();
$processSqlData = $this->loadModel('chart')->getTables($sql);
$sql = $processSqlData['sql'];
if(isset($settings['summary']) and $settings['summary'] == 'notuse')
{
list($data, $configs) = $this->pivot->genOriginSheet($fields, $settings, $sql, $filterFormat, $langs);
list($data, $configs) = $this->pivot->genOriginSheet($fields, $settings, $sql, $filterFormat, $langs, $driver);
}
else
{
list($data, $configs) = $this->pivot->genSheet($fields, $settings, $sql, $filterFormat, $langs);
list($data, $configs) = $this->pivot->genSheet($fields, $settings, $sql, $filterFormat, $langs, $driver);
}
$this->pivot->buildPivotTable($data, $configs, $page);
+24 -19
View File
@@ -188,6 +188,7 @@ class pivotModel extends model
*/
public function processFieldSettings(object $pivot): void
{
$this->loadModel('dataview');
$fieldSettings = $pivot->fieldSettings ?? $this->getFieldsFromPivot($pivot, 'fields', array(), true);
if(empty($fieldSettings)) return;
@@ -202,11 +203,11 @@ class pivotModel extends model
$stmt = $this->dbh->query($querySQL);
if(!$stmt) return;
$columns = $this->loadModel('dataview')->getColumns($querySQL);
$columns = $this->bi->getColumnsType($querySQL);
$columnFields = array();
foreach(array_keys(get_object_vars($columns)) as $type) $columnFields[$type] = $type;
extract($this->chart->getTables($querySQL));
extract($this->bi->getTableAndFields($querySQL));
/* 获取field的键值对以及相关联的对象。 */
/* Get field key value pairs and related objects. */
@@ -800,7 +801,7 @@ class pivotModel extends model
$connectSQL .= " where $whereStr";
}
$sql = "select * from ($sql) tt" . $connectSQL;
$sql = "select * from ( $sql ) tt" . $connectSQL;
return $sql;
}
@@ -814,11 +815,11 @@ class pivotModel extends model
* @access public
* @return array
*/
public function mapRecordValueWithFieldOptions(array $records, array $fields, string $sql): array
public function mapRecordValueWithFieldOptions(array $records, array $fields, string $sql, string $driver): array
{
$this->app->loadConfig('dataview');
$records = json_decode(json_encode($records), true);
$fieldOptions = $this->getFieldsOptions($fields, $sql);
$fieldOptions = $this->getFieldsOptions($fields, $sql, $driver);
foreach($records as $index => $record)
{
foreach($record as $field => $value)
@@ -1323,7 +1324,7 @@ class pivotModel extends model
* @access public
* @return array
*/
public function genSheet(array $fields, array $settings, string $sql, array $filters, array $langs = array()): array
public function genSheet(array $fields, array $settings, string $sql, array $filters, array $langs = array(), string $driver = 'mysql'): array
{
$groups = $this->getGroupsFromSettings($settings);
$cols = $this->generateTableCols($fields, $groups, $langs);
@@ -1333,8 +1334,9 @@ class pivotModel extends model
$sql = $this->trimSemicolon($sql);
$sql = $this->appendWhereFilterToSql($sql, $filters);
$records = $this->dao->query($sql)->fetchAll();
$records = $this->mapRecordValueWithFieldOptions($records, $fields, $sql);
$dbh = $this->app->loadDriver($driver);
$records = $dbh->query($sql)->fetchAll();
$records = $this->mapRecordValueWithFieldOptions($records, $fields, $sql, $driver);
$showColTotal = zget($settings, 'columnTotal', 'noShow');
@@ -1350,7 +1352,7 @@ class pivotModel extends model
$columnField = $columnSetting['field'];
$columnSlice = zget($columnSetting, 'slice', 'noSlice');
$cols = $this->getTableHeader($records, $columnSetting, $fields, $cols, $sql, $langs, $columnShowOrigin);
$cols = $this->getTableHeader($records, $columnSetting, $fields, $cols, $sql, $langs, $columnShowOrigin, $driver);
if($columnShowOrigin)
{
@@ -1470,7 +1472,7 @@ class pivotModel extends model
* @access public
* @return string
*/
public function genOriginSheet($fields, $settings, $sql, $filters, $langs = array())
public function genOriginSheet($fields, $settings, $sql, $filters, $langs = array(), $driver = 'mysql')
{
$sql = $this->initVarFilter($filters, $sql);
@@ -1500,7 +1502,9 @@ class pivotModel extends model
$sql = $statement->build();
$columnSQL = "select * from ($sql) tt" . $connectSQL;
$rows = $this->dao->query($columnSQL)->fetchAll();
$dbh = $this->app->loadDriver($driver);
$rows = $dbh->query($columnSQL)->fetchAll();
$rows = json_decode(json_encode($rows), true);
$cols = array();
@@ -1847,7 +1851,7 @@ class pivotModel extends model
* @access public
* @return array
*/
public function getTableHeader($columnRows, $column, $fields, $cols, $sql, $langs = array(), $showOrigin = false)
public function getTableHeader($columnRows, $column, $fields, $cols, $sql, $langs = array(), $showOrigin = false, $driver = 'mysql')
{
$stat = zget($column, 'stat', '');
$showMode = zget($column, 'showMode', 'default');
@@ -1890,7 +1894,7 @@ class pivotModel extends model
$sliceList = array();
foreach($columnRows as $rows) $sliceList[$rows->{$slice}] = $rows->{$slice};
$optionList = $this->getSysOptions($fields[$slice]['type'], $fields[$slice]['object'], $fields[$slice]['field'], $sql);
$optionList = $this->getSysOptions($fields[$slice]['type'], $fields[$slice]['object'], $fields[$slice]['field'], $sql, '', $driver);
foreach($sliceList as $field)
{
$childCol = new stdclass();
@@ -2200,7 +2204,7 @@ class pivotModel extends model
* @access public
* @return array
*/
public function getSysOptions($type, $object = '', $field = '', $source = '', $saveAs = '')
public function getSysOptions($type, $object = '', $field = '', $source = '', $saveAs = '', $driver = 'mysql')
{
if(in_array($type, array('user', 'product', 'project', 'execution', 'dept', 'project.status'))) return $this->bi->getScopeOptions($type);
if(!$field) return array();
@@ -2235,7 +2239,7 @@ class pivotModel extends model
{
$keyField = $field;
$valueField = $saveAs ? $saveAs : $field;
$options = $this->bi->getOptionsFromSql($source, $keyField, $valueField);
$options = $this->bi->getOptionsFromSql($source, $driver, $keyField, $valueField);
}
}
break;
@@ -2243,7 +2247,7 @@ class pivotModel extends model
if(is_string($source) and $source and $saveAs and in_array($type, array('user', 'product', 'project', 'execution', 'dept', 'option', 'object')))
{
$options = $this->bi->getOptionsFromSql($source, $field, $saveAs);
$options = $this->bi->getOptionsFromSql($source, $driver, $field, $saveAs);
}
return array_filter($options);
@@ -2317,11 +2321,12 @@ class pivotModel extends model
* @return array
*
*/
public function getFieldsOptions(array $fieldSettings, string $sql): array
public function getFieldsOptions(array $fieldSettings, string $sql, string $driver = 'mysql'): array
{
$options = array();
$sqlRecords = $this->dbh->query($sql)->fetchAll();
$dbh = $this->app->loadDriver($driver);
$sqlRecords = $dbh->query($sql)->fetchAll();
foreach($fieldSettings as $key => $fieldSetting)
{
@@ -2332,7 +2337,7 @@ class pivotModel extends model
$source = $sql;
if(in_array($type, array('string', 'number', 'date'))) $source = $sqlRecords;
$options[$key] = $this->getSysOptions($type, $object, $field, $source);
$options[$key] = $this->getSysOptions($type, $object, $field, $source, '', $driver);
}
return $options;
+4 -5
View File
@@ -152,7 +152,8 @@ class pivotZen extends pivot
*/
public function show(int $groupID, int $pivotID): void
{
$pivot = $this->pivot->getByID($pivotID);
$pivot = $this->pivot->getByID($pivotID);
$driver = $pivot->driver;
if(isset($_POST['filterValues']) and $_POST['filterValues'])
{
foreach($this->post->filterValues as $key => $value) $pivot->filters[$key]['default'] = $value;
@@ -161,18 +162,16 @@ class pivotZen extends pivot
list($sql, $filterFormat) = $this->pivot->getFilterFormat($pivot->sql, $pivot->filters);
$tables = $this->loadModel('chart')->getTables($sql);
$sql = $tables['sql'];
$fields = json_decode(json_encode($pivot->fieldSettings), true);
$langs = json_decode($pivot->langs, true) ?? array();
if(isset($pivot->settings['summary']) and $pivot->settings['summary'] == 'notuse')
{
list($data, $configs) = $this->pivot->genOriginSheet($fields, $pivot->settings, $sql, $filterFormat, $langs);
list($data, $configs) = $this->pivot->genOriginSheet($fields, $pivot->settings, $sql, $filterFormat, $langs, $driver);
}
else
{
list($data, $configs) = $this->pivot->genSheet($fields, $pivot->settings, $sql, $filterFormat, $langs);
list($data, $configs) = $this->pivot->genSheet($fields, $pivot->settings, $sql, $filterFormat, $langs, $driver);
}
$this->view->pivotName = $pivot->name;
+16 -16
View File
@@ -757,7 +757,7 @@ class screenModel extends model
$settings = $settings[0];
$isSort = in_array($chart->id, $this->config->screen->annualRankingChart) ? true : false;
list($group, $metrics, $aggs, $xLabels, $yStats) = $this->bi->getMultiData($settings, $chart->sql, $filters, $isSort);
list($group, $metrics, $aggs, $xLabels, $yStats) = $this->bi->getMultiData($settings, $chart->sql, $filters, $chart->driver, $isSort);
$fields = json_decode($chart->fields, true);
$dimensions = array($settings['xaxis'][0]['field']);
@@ -809,7 +809,7 @@ class screenModel extends model
$langs = json_decode($chart->langs, true);
$settings = $settings[0];
list($group, $metrics, $aggs, $xLabels, $yStats) = $this->bi->getMultiData($settings, $chart->sql, $filters);
list($group, $metrics, $aggs, $xLabels, $yStats) = $this->bi->getMultiData($settings, $chart->sql, $filters, $chart->driver);
$fields = json_decode($chart->fields, true);
$dimensions = array($settings['xaxis'][0]['field']);
@@ -868,7 +868,7 @@ class screenModel extends model
$settings = json_decode($chart->settings, true);
$settings = $settings[0];
$options = $this->loadModel('chart')->genPie(json_decode($chart->fields, true), $settings, $chart->sql, $filters);
$options = $this->loadModel('chart')->genPie(json_decode($chart->fields, true), $settings, $chart->sql, $filters, $chart->driver);
if($settings['group'][0]['field'] == $settings['metric'][0]['field']) $settings['group'][0]['field'] = $settings['group'][0]['field'] . '1';
$dimensions = array($settings['group'][0]['field'], $settings['metric'][0]['field']);
@@ -915,7 +915,7 @@ class screenModel extends model
$langs = json_decode($chart->langs, true);
$settings = $settings[0];
list($group, $metrics, $aggs, $xLabels, $yStats) = $this->bi->getMultiData($settings, $chart->sql, $filters);
list($group, $metrics, $aggs, $xLabels, $yStats) = $this->bi->getMultiData($settings, $chart->sql, $filters, $chart->driver);
$fields = json_decode($chart->fields, true);
$radarIndicator = array();
@@ -984,11 +984,11 @@ class screenModel extends model
if(isset($settings['summary']) and $settings['summary'] == 'notuse')
{
list($options, $config) = $this->loadModel('pivot')->genOriginSheet($fields, $settings, $chart->sql, $filters, $langs);
list($options, $config) = $this->loadModel('pivot')->genOriginSheet($fields, $settings, $chart->sql, $filters, $langs, $chart->driver);
}
else
{
list($options, $config) = $this->loadModel('pivot')->genSheet($fields, $settings, $chart->sql, $filters, $langs);
list($options, $config) = $this->loadModel('pivot')->genSheet($fields, $settings, $chart->sql, $filters, $langs, $chart->driver);
}
$colspan = array();
@@ -1617,7 +1617,7 @@ class screenModel extends model
{
$field = $settings->value->field;
$sql = $this->setFilterSQL($chart);
$results = $this->buildDataset($chart->id, $sql);
$results = $this->buildDataset($chart->id, $chart->driver, $sql);
if($settings->value->type === 'text')
{
@@ -1685,7 +1685,7 @@ class screenModel extends model
$sourceData = array();
$sql = $this->setFilterSQL($chart);
$results = $this->dao->query($sql)->fetchAll();
$results = $this->bi->queryWithDriver($chart->driver, $sql);
foreach($results as $result)
{
$key = $settings->xaxis[0]->name;
@@ -1744,7 +1744,7 @@ class screenModel extends model
}
$sql = $this->setFilterSQL($chart);
$results = $this->buildDataset($chart->id, $sql);
$results = $this->buildDataset($chart->id, $chart->driver, $sql);
foreach($results as $result)
{
@@ -1798,7 +1798,7 @@ class screenModel extends model
$sourceData = array();
$sql = $this->setFilterSQL($chart);
$results = $this->dao->query($sql)->fetchAll();
$results = $this->bi->queryWithDriver($chart->driver, $sql);
foreach($results as $result)
{
@@ -1869,7 +1869,7 @@ class screenModel extends model
$sourceData = array();
$sql = $this->setFilterSQL($chart);
$results = $this->dao->query($sql)->fetchAll();
$results = $this->bi->queryWithDriver($driver, $sql);
$group = $settings->group[0]->field;
$groupCount = array();
@@ -1944,7 +1944,7 @@ class screenModel extends model
$sourceData = array();
$sql = $this->setFilterSQL($chart);
$results = $this->dao->query($sql)->fetchAll();
$results = $this->bi->queryWithDriver($chart->driver, $sql);
$group = $settings->group[0]->field;
$groupCount = array();
@@ -2035,7 +2035,7 @@ class screenModel extends model
else
{
$setting = json_decode($chart->settings, true)[0];
$options = $this->bi->genWaterPolo(json_decode($chart->fields, true), $setting, $chart->sql, $filters);
$options = $this->bi->genWaterPolo(json_decode($chart->fields, true), $setting, $chart->sql, $filters, $chart->driver);
$component->option->dataset = $options['series'][0]['data'][0];
return $this->setComponentDefaults($component);
@@ -2284,7 +2284,7 @@ class screenModel extends model
if($settings and isset($settings->metric))
{
$sql = $this->setFilterSQL($chart);
$results = $this->dao->query($sql)->fetchAll();
$results = $this->bi->queryWithDriver($driver, $sql);
$group = $settings->group[0]->field;
$metrics = array();
@@ -2603,10 +2603,10 @@ class screenModel extends model
* @access public
* @return array
*/
public function buildDataset($chartID, $sql = '')
public function buildDataset($chartID, $driver, $sql = '')
{
if(in_array($chartID, $this->config->screen->phpChart)) return $this->getDatasetForUsageReport($chartID);
return $this->dao->query($sql)->fetchAll();
return $this->bi->queryWithDriver($driver, $sql);
}
/**
+1 -1
View File
@@ -1914,7 +1914,7 @@ class treeModel extends model
if($self)
{
if($type == 'host') $module->root = 0;
if($type == 'host' || !isset($module->root)) $module->root = 0;
if($self->root && !isset($module->root)) $module->root = $self->root;
if($self->parent != $module->parent || $self->root != $module->root)
{
+5
View File
@@ -28,6 +28,11 @@ class upgrade extends control
$this->locate(inlink('backup'));
}
public function importBIData()
{
$this->loadModel('install')->importBIData();
}
/**
* 授权协议页面。
* Check agree license.
+2 -1
View File
@@ -7848,10 +7848,11 @@ class upgradeModel extends model
{
if(!$sql) return array();
$this->loadModel('bi');
$this->loadModel('dataview');
$this->loadModel('chart');
$columns = $this->dataview->getColumns($sql);
$columns = $this->bi->getColumnsType($sql);
$columnFields = array();
foreach($columns as $column => $type) $columnFields[$column] = $column;