diff --git a/bin/duckdb/duckdb b/bin/duckdb/duckdb new file mode 100755 index 0000000000..1aadb62e36 Binary files /dev/null and b/bin/duckdb/duckdb differ diff --git a/bin/duckdb/mysql_scanner.duckdb_extension b/bin/duckdb/mysql_scanner.duckdb_extension new file mode 100755 index 0000000000..87f1fec507 Binary files /dev/null and b/bin/duckdb/mysql_scanner.duckdb_extension differ diff --git a/config/zentaopms.php b/config/zentaopms.php index 28bd0de9bb..0fff380e40 100644 --- a/config/zentaopms.php +++ b/config/zentaopms.php @@ -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(); diff --git a/db/update20.0.alpha1.sql b/db/update20.0.alpha1.sql index 81e1799619..c25fa39187 100644 --- a/db/update20.0.alpha1.sql +++ b/db/update20.0.alpha1.sql @@ -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, diff --git a/db/update20.0.sql b/db/update20.0.sql index 33a599c697..028439a95e 100644 --- a/db/update20.0.sql +++ b/db/update20.0.sql @@ -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'); diff --git a/db/update20.1.sql b/db/update20.1.sql new file mode 100644 index 0000000000..0813a7fcf5 --- /dev/null +++ b/db/update20.1.sql @@ -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`; diff --git a/db/zentao.sql b/db/zentao.sql index 4511ba4a62..763a35bdf3 100755 --- a/db/zentao.sql +++ b/db/zentao.sql @@ -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, @@ -16654,5 +16656,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()); diff --git a/framework/router.class.php b/framework/router.class.php index a5f27dfa62..81c5c68bf0 100755 --- a/framework/router.class.php +++ b/framework/router.class.php @@ -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. * diff --git a/lib/duckdb/duckdb.class.php b/lib/duckdb/duckdb.class.php new file mode 100644 index 0000000000..66e67da7ed --- /dev/null +++ b/lib/duckdb/duckdb.class.php @@ -0,0 +1,363 @@ +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(); + } +} diff --git a/lib/sqlparser/sqlparser.class.php b/lib/sqlparser/sqlparser.class.php index 21c42bc1f8..354fb28d3a 100644 --- a/lib/sqlparser/sqlparser.class.php +++ b/lib/sqlparser/sqlparser.class.php @@ -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; } } diff --git a/lib/zin/wg/modulemenu/v1.php b/lib/zin/wg/modulemenu/v1.php index 1d5b1e8d45..bb9a97a0ef 100644 --- a/lib/zin/wg/modulemenu/v1.php +++ b/lib/zin/wg/modulemenu/v1.php @@ -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 diff --git a/module/bi/config.php b/module/bi/config.php index eecec6a389..8390ef1323 100644 --- a/module/bi/config.php +++ b/module/bi/config.php @@ -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 = <<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'] = <<bi->duckdb->tables['account'] = <<bi->duckdb->tables['attend'] = <<bi->duckdb->tables['bug'] = <<bi->duckdb->tables['build'] = <<bi->duckdb->tables['burn'] = <<bi->duckdb->tables['case'] = <<bi->duckdb->tables['casestep'] = <<bi->duckdb->tables['config'] = <<bi->duckdb->tables['demand'] = <<bi->duckdb->tables['dept'] = <<bi->duckdb->tables['doc'] = <<bi->duckdb->tables['effort'] = <<bi->duckdb->tables['feedback'] = <<bi->duckdb->tables['file'] = <<bi->duckdb->tables['group'] = <<bi->duckdb->tables['history'] = <<bi->duckdb->tables['issue'] = <<bi->duckdb->tables['meeting'] = <<bi->duckdb->tables['metric'] = <<bi->duckdb->tables['metriclib'] = <<bi->duckdb->tables['module'] = <<bi->duckdb->tables['opportunity'] = <<bi->duckdb->tables['pivot'] = <<bi->duckdb->tables['product'] = <<bi->duckdb->tables['productplan'] = <<bi->duckdb->tables['planstory'] = <<bi->duckdb->tables['project'] = <<bi->duckdb->tables['projectcase'] = <<bi->duckdb->tables['projectproduct'] = <<bi->duckdb->tables['projectstory'] = <<bi->duckdb->tables['release'] = <<bi->duckdb->tables['risk'] = <<bi->duckdb->tables['story'] = <<bi->duckdb->tables['task'] = <<bi->duckdb->tables['team'] = <<bi->duckdb->tables['testreport'] = <<bi->duckdb->tables['testresult'] = <<bi->duckdb->tables['testtask'] = <<bi->duckdb->tables['ticket'] = <<bi->duckdb->tables['todo'] = <<bi->duckdb->tables['user'] = <<bi->duckdb->ztvtables['dayactions'] = <<bi->duckdb->ztvtables['dayuserlogin'] = <<bi->duckdb->ztvtables['dayeffort'] = <<bi->duckdb->ztvtables['daystoryopen'] = <<bi->duckdb->ztvtables['daystoryclose'] = <<bi->duckdb->ztvtables['daytaskopen'] = <<bi->duckdb->ztvtables['daytaskfinish'] = <<bi->duckdb->ztvtables['daybugopen'] = <<bi->duckdb->ztvtables['daybugresolve'] = <<bi->duckdb->ztvtables['productstories'] = <<bi->duckdb->ztvtables['productbugs'] = <<bi->duckdb->ztvtables['projectsummary'] = <<bi->duckdb->ztvtables['executionsummary'] = <<bi->duckdb->ztvtables['projectstories'] = <<bi->duckdb->ztvtables['projectbugs'] = <<bi->duckdb->ztvtables['projectteams'] = <<bi->builtin->charts[] = array 'id' => 1001, 'name' => '年度总结-登录次数', 'code' => 'annualSummary_countLogin', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'card', 'group' => '0', 'sql' => << array ( @@ -32,15 +43,27 @@ $config->bi->builtin->charts[] = array 'id' => 1002, 'name' => '年度总结-操作次数', 'code' => 'annualSummary_countAction', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'card', 'group' => '0', 'sql' => << array ( @@ -58,15 +81,28 @@ $config->bi->builtin->charts[] = array 'id' => 1003, 'name' => '年度总结-消耗工时', 'code' => 'annualSummary_consumed', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'card', 'group' => '0', 'sql' => << array ( @@ -84,15 +120,33 @@ $config->bi->builtin->charts[] = array 'id' => 1004, 'name' => '年度总结-待办数', 'code' => 'annualSummary_countTodo', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'card', 'group' => '0', 'sql' => << array ( @@ -110,31 +164,55 @@ $config->bi->builtin->charts[] = array 'id' => 1005, 'name' => '年度总结-贡献数', 'code' => 'annualSummary_countContributions', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'card', 'group' => '0', 'sql' => << array ( @@ -152,112 +230,115 @@ $config->bi->builtin->charts[] = array 'id' => 1006, 'name' => '年度总结-贡献数据', 'code' => 'annualSummary_contributions', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'bar', 'group' => '0', 'sql' => << array ( @@ -281,51 +362,50 @@ $config->bi->builtin->charts[] = array 'id' => 1007, 'name' => '年度总结-能力雷达图', 'code' => 'annualSummary_capabilityRadar', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'radar', 'group' => '0', 'sql' => << array ( @@ -352,79 +432,54 @@ $config->bi->builtin->charts[] = array 'id' => 1008, 'name' => '年度总结-迭代数据', 'code' => 'annualSummary_executions', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'table', 'group' => '0', 'sql' => << array ( @@ -448,49 +503,41 @@ $config->bi->builtin->charts[] = array 'id' => 1009, 'name' => '年度总结-产品数据', 'code' => 'annualSummary_products', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'table', 'group' => '0', 'sql' => << array ( @@ -515,25 +562,21 @@ $config->bi->builtin->charts[] = array 'id' => 1010, 'name' => '年度总结-任务状态分布', 'code' => 'annualSummary_taskStatus', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'pie', 'group' => '0', 'sql' => << array ( @@ -556,30 +599,37 @@ $config->bi->builtin->charts[] = array 'id' => 1011, 'name' => '年度总结-每月任务操作情况', 'code' => 'annualSummary_monthlyTaskAction', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'bar', 'group' => '0', 'sql' => << array ( @@ -608,25 +658,21 @@ $config->bi->builtin->charts[] = array 'id' => 1012, 'name' => '年度总结-需求状态分布', 'code' => 'annualSummary_storyStatus', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'pie', 'group' => '0', 'sql' => << array @@ -650,27 +696,32 @@ $config->bi->builtin->charts[] = array 'id' => 1013, 'name' => '年度总结-每月需求操作情况', 'code' => 'annualSummary_monthlyStoryAction', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'bar', 'group' => '0', 'sql' => << array ( @@ -696,24 +747,21 @@ $config->bi->builtin->charts[] = array 'id' => 1014, 'name' => '年度总结-Bug状态分布', 'code' => 'annualSummary_bugStatus', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'pie', 'group' => '0', 'sql' => << array @@ -737,28 +785,32 @@ $config->bi->builtin->charts[] = array 'id' => 1015, 'name' => '年度总结-每月Bug操作情况', 'code' => 'annualSummary_monthlyBugAction', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'bar', 'group' => '0', 'sql' => << array ( @@ -785,21 +837,29 @@ $config->bi->builtin->charts[] = array 'id' => 1016, 'name' => '年度总结-用例结果分布', 'code' => 'annualSummary_caseResult', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'pie', 'group' => '0', 'sql' => << array ( @@ -829,11 +889,11 @@ $config->bi->builtin->charts[] = array SELECT SUM(createdCases) AS createdCases, SUM(toBugCases) AS toBugCases, SUM(runCases) AS runCases, YEAR(CONCAT(t2.actionDate, '-01')) AS `year`, t1.account, realname, dept FROM zt_user t1 LEFT JOIN ( - SELECT t21.actor, LEFT(t21.`date`, 7) as actionDate, - SUM(IF((t22.id IS NOT NULL AND t23.id IS NULL), 1, 0)) AS createdCases, - SUM(IF((t22.id IS NOT NULL AND t23.id IS NOT NULL), 1, 0)) AS toBugCases, + SELECT t21.actor, LEFT(t21.`date`, 7) as actionDate, + SUM(IF((t22.id IS NOT NULL AND t23.id IS NULL), 1, 0)) AS createdCases, + SUM(IF((t22.id IS NOT NULL AND t23.id IS NOT NULL), 1, 0)) AS toBugCases, SUM(IF((t24.lastRunner = t21.actor AND t21.action = 'run' AND t21.`date` = t24.`date`), 1, 0)) AS runCases - FROM zt_action t21 + FROM zt_action t21 LEFT JOIN zt_case t22 on t21.objectID = t22.id LEFT JOIN zt_bug t23 on t22.id = t23.case LEFT JOIN zt_testresult t24 on t22.id = t24.`case` AND t24.lastRunner = t21.actor AND t21.action = 'run' AND t21.`date` = t24.`date` @@ -1140,7 +1200,7 @@ $config->bi->builtin->charts[] = array 'group' => '58', 'sql' => << 0, concat(t2.`year`, '年', t2.`day`, '天'), concat(t2.`day`, '天')) as period from ( -SELECT TIMESTAMPDIFF(YEAR,t1.firstDay,t1.today) AS `year`,DATEDIFF(DATE_SUB(t1.today,INTERVAL TIMESTAMPDIFF(YEAR,t1.firstDay,t1.today) YEAR), t1.firstDay) AS `day` +SELECT TIMESTAMPDIFF(YEAR,t1.firstDay,t1.today) AS `year`,DATEDIFF(DATE_SUB(t1.today,INTERVAL TIMESTAMPDIFF(YEAR,t1.firstDay,t1.today) YEAR), t1.firstDay) AS `day` FROM (SELECT `value` AS firstDay, now() AS today FROM zt_config WHERE `owner` = 'system' AND `key` = 'installedDate') AS t1 ) t2 EOT, @@ -1160,11 +1220,12 @@ $config->bi->builtin->charts[] = array 'id' => 1031, 'name' => '宏观数据-需求完成率', 'code' => 'macro_storyFinishedRate', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'waterpolo', 'group' => '36', 'sql' => << array ( @@ -1199,11 +1260,12 @@ $config->bi->builtin->charts[] = array 'id' => 1032, 'name' => '宏观数据-Bug修复率', 'code' => 'macro_bugFixedRate', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'waterpolo', 'group' => '44', 'sql' => << array ( @@ -1471,28 +1533,29 @@ $config->bi->builtin->charts[] = array 'id' => 1042, 'name' => '宏观数据-项目集需求完成率与Bug修复率', 'code' => 'macro_programStoryFinishedRateAndBugFixedRate', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '45', 'sql' => << array ( @@ -1540,11 +1603,12 @@ $config->bi->builtin->charts[] = array 'id' => 1043, 'name' => '宏观数据-公司项目集状态分布', 'code' => 'macro_programStatus', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'pie', 'group' => '45', 'sql' => << array ( @@ -1581,11 +1645,12 @@ $config->bi->builtin->charts[] = array 'id' => 1044, 'name' => '宏观数据-公司项目状态分布', 'code' => 'macro_projectStatus', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'pie', 'group' => '38', 'sql' => << array ( @@ -1626,22 +1691,22 @@ $config->bi->builtin->charts[] = array 'type' => 'table', 'group' => '63', 'sql' => <<bi->builtin->charts[] = array 'id' => 1046, 'name' => '宏观数据-产品需求完成率', 'code' => 'macro_productStoryFinishedRate', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '36', 'sql' => << array ( @@ -1732,24 +1798,25 @@ $config->bi->builtin->charts[] = array 'id' => 1047, 'name' => '宏观数据-产品Bug修复率', 'code' => 'macro_productBugFixedRate', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '44', 'sql' => << array ( @@ -1794,18 +1861,19 @@ $config->bi->builtin->charts[] = array 'id' => 1049, 'name' => '宏观数据-部门人员分布图', 'code' => 'macro_deptAccountStatus', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '56', 'sql' => << array ( @@ -1844,36 +1912,37 @@ $config->bi->builtin->charts[] = array 'id' => 1050, 'name' => '宏观数据-公司角色分布图', 'code' => 'macro_roleStatus', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'pie', 'group' => '56', 'sql' => << array @@ -1911,21 +1980,22 @@ $config->bi->builtin->charts[] = array 'id' => 1051, 'name' => '宏观数据-人员工龄分布图', 'code' => 'macro_workingStatus', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '56', 'sql' => << DATE_SUB(NOW(), INTERVAL 1 YEAR) +select count(1) as count, '0-1年' as joindate from zt_user where deleted = '0' and "join" > (current_date() - interval '1 year') union -SELECT count(1) as count, "1-3年" as joinDate FROM zt_user WHERE deleted = '0' AND `join` > DATE_SUB(NOW(), INTERVAL 3 YEAR) AND `join` <= DATE_SUB(NOW(), INTERVAL 1 YEAR) +select count(1) as count, '1-3年' as joindate from zt_user where deleted = '0' and "join" > (current_date() - interval '3 year') and "join" <= (current_date() - interval '1 year') union -SELECT count(1) as count, "3-5年" as joinDate FROM zt_user WHERE deleted = '0' AND `join` > DATE_SUB(NOW(), INTERVAL 5 YEAR) AND `join` <= DATE_SUB(NOW(), INTERVAL 3 YEAR) +select count(1) as count, '3-5年' as joindate from zt_user where deleted = '0' and "join" > (current_date() - interval '5 year') and "join" <= (current_date() - interval '3 year') union -SELECT count(1) as count, "5-10年" as joinDate FROM zt_user WHERE deleted = '0' AND `join` > DATE_SUB(NOW(), INTERVAL 10 YEAR) AND `join` <= DATE_SUB(NOW(), INTERVAL 5 YEAR) +select count(1) as count, '5-10年' as joindate from zt_user where deleted = '0' and "join" > (current_date() - interval '10 year') and "join" <= (current_date() - interval '5 year') union -SELECT count(1) as count, "10年以上" as joinDate FROM zt_user WHERE deleted = '0' AND `join` < DATE_SUB(NOW(), INTERVAL 10 YEAR) AND LEFT(`join`, 4) != '0000' -union -SELECT count(1) as count, "未知" as joinDate FROM zt_user WHERE deleted = '0' AND LEFT(`join`, 4) = '0000' +select count(1) as count, '10年以上' as joindate from zt_user where deleted = '0' and "join" < (current_date() - interval '10 year') and date_part('year', "join") != '0000' +union +select count(1) as count, '未知' as joindate from zt_user where deleted = '0' and date_part('year', "join") = '0000' EOT, 'settings' => array ( @@ -1975,14 +2045,14 @@ FROM LEFT JOIN ( SELECT id, name, - YEAR ( openedDate ) AS `year` + YEAR ( openedDate ) AS `year` FROM - zt_project + zt_project WHERE - `type` = 'program' - AND deleted = '0' - AND grade = '1' - ) t2 ON t1.`year` = t2.`year` + `type` = 'program' + AND deleted = '0' + AND grade = '1' + ) t2 ON t1.`year` = t2.`year` WHERE t2.id IS NOT NULL EOT, 'settings' => array @@ -2013,13 +2083,13 @@ FROM LEFT JOIN ( SELECT id, name, - YEAR ( createdDate ) AS `year` + YEAR ( createdDate ) AS `year` FROM - zt_product + zt_product WHERE - deleted = '0' - AND shadow = '0' - ) t2 ON t1.`year` = t2.`year` + deleted = '0' + AND shadow = '0' + ) t2 ON t1.`year` = t2.`year` WHERE t2.id IS NOT NULL EOT, 'settings' => array @@ -2047,7 +2117,7 @@ SELECT t2.title FROM ( SELECT DISTINCT YEAR ( `date` ) AS "year" FROM zt_action ) AS t1 - LEFT JOIN ( SELECT id, title, YEAR ( openedDate ) AS `year` FROM zt_story WHERE deleted = '0' ) AS t2 ON t1.`year` = t2.`year` + LEFT JOIN ( SELECT id, title, YEAR ( openedDate ) AS `year` FROM zt_story WHERE deleted = '0' ) AS t2 ON t1.`year` = t2.`year` WHERE t2.id IS NOT NULL EOT, 'settings' => array @@ -2075,7 +2145,7 @@ SELECT t2.title FROM ( SELECT DISTINCT YEAR ( `date` ) AS "year" FROM zt_action ) AS t1 - LEFT JOIN ( SELECT id, title, YEAR ( openedDate ) AS `year` FROM zt_bug WHERE deleted = '0' ) AS t2 ON t1.`year` = t2.`year` + LEFT JOIN ( SELECT id, title, YEAR ( openedDate ) AS `year` FROM zt_bug WHERE deleted = '0' ) AS t2 ON t1.`year` = t2.`year` WHERE t2.id IS NOT NULL EOT, 'settings' => array @@ -2103,7 +2173,7 @@ SELECT t2.title FROM ( SELECT DISTINCT YEAR ( `date` ) AS "year" FROM zt_action ) AS t1 - LEFT JOIN ( SELECT id, title, YEAR ( createdDate ) AS `year` FROM zt_productplan WHERE deleted = '0') AS t2 ON t1.`year` = t2.`year` + LEFT JOIN ( SELECT id, title, YEAR ( createdDate ) AS `year` FROM zt_productplan WHERE deleted = '0') AS t2 ON t1.`year` = t2.`year` WHERE t2.id IS NOT NULL EOT, 'settings' => array @@ -2134,13 +2204,13 @@ FROM LEFT JOIN ( SELECT id, name, - YEAR ( openedDate ) AS `year` + YEAR ( openedDate ) AS `year` FROM - zt_project + zt_project WHERE - `type` = 'project' - AND deleted = '0' - ) t2 ON t1.`year` = t2.`year` + `type` = 'project' + AND deleted = '0' + ) t2 ON t1.`year` = t2.`year` WHERE t2.id IS NOT NULL EOT, 'settings' => array @@ -2164,7 +2234,7 @@ $config->bi->builtin->charts[] = array 'sql' => << array @@ -2192,7 +2262,7 @@ SELECT t2.name FROM ( SELECT DISTINCT YEAR ( `date` ) AS "year" FROM zt_action ) AS t1 - LEFT JOIN ( SELECT id, name, YEAR ( openedDate ) AS `year` FROM zt_task WHERE deleted = '0') AS t2 ON t1.`year` = t2.`year` + LEFT JOIN ( SELECT id, name, YEAR ( openedDate ) AS `year` FROM zt_task WHERE deleted = '0') AS t2 ON t1.`year` = t2.`year` WHERE t2.id IS NOT NULL EOT, 'settings' => array @@ -2220,7 +2290,7 @@ SELECT t2.title FROM ( SELECT DISTINCT YEAR ( `date` ) AS "year" FROM zt_action ) AS t1 - LEFT JOIN ( SELECT id, title, YEAR ( addedDate ) AS `year` FROM zt_doc WHERE deleted = '0') AS t2 ON t1.`year` = t2.`year` + LEFT JOIN ( SELECT id, title, YEAR ( addedDate ) AS `year` FROM zt_doc WHERE deleted = '0') AS t2 ON t1.`year` = t2.`year` WHERE t2.id IS NOT NULL EOT, 'settings' => array @@ -2279,15 +2349,15 @@ FROM LEFT JOIN ( SELECT account, realname, - YEAR ( t112.`date` ) AS 'year' + YEAR ( t112.`date` ) AS 'year' FROM zt_user AS t111 - LEFT JOIN zt_action t112 ON t111.id = t112.objectID - AND t112.objectType = 'user' + LEFT JOIN zt_action t112 ON t111.id = t112.objectID + AND t112.objectType = 'user' WHERE - t111.deleted = '0' - AND t112.action = 'created' - ) AS t2 ON t1.`year` = t2.`year` + t111.deleted = '0' + AND t112.action = 'created' + ) AS t2 ON t1.`year` = t2.`year` WHERE t2.account IS NOT NULL EOT, 'settings' => array @@ -2318,12 +2388,12 @@ FROM LEFT JOIN ( SELECT id, name, -YEAR ( closedDate ) AS `year` +YEAR ( closedDate ) AS `year` FROM -zt_project +zt_project WHERE -`type` = 'project' -AND deleted = '0' +`type` = 'project' +AND deleted = '0' ) t2 ON t1.`year` = t2.`year` WHERE t2.id IS NOT NULL EOT, @@ -2348,7 +2418,7 @@ $config->bi->builtin->charts[] = array 'sql' => << array @@ -2376,7 +2446,7 @@ SELECT t2.name FROM ( SELECT DISTINCT YEAR ( `date` ) AS "year" FROM zt_action ) AS t1 - LEFT JOIN ( SELECT id, name, YEAR ( `date` ) AS `year` FROM zt_release WHERE deleted = '0') AS t2 ON t1.`year` = t2.`year` + LEFT JOIN ( SELECT id, name, YEAR ( `date` ) AS `year` FROM zt_release WHERE deleted = '0') AS t2 ON t1.`year` = t2.`year` WHERE t2.id IS NOT NULL EOT, 'settings' => array @@ -2407,14 +2477,14 @@ FROM LEFT JOIN ( SELECT id, title, - YEAR ( closedDate ) AS `year` + YEAR ( closedDate ) AS `year` FROM - zt_story + zt_story WHERE - deleted = '0' - AND closedReason = 'done' - AND STATUS = 'closed' - ) AS t2 ON t1.`year` = t2.`year` + deleted = '0' + AND closedReason = 'done' + AND STATUS = 'closed' + ) AS t2 ON t1.`year` = t2.`year` WHERE t2.id IS NOT NULL EOT, 'settings' => array @@ -2445,14 +2515,14 @@ FROM LEFT JOIN ( SELECT id, title, - YEAR ( closedDate ) AS `year` + YEAR ( closedDate ) AS `year` FROM - zt_bug + zt_bug WHERE - deleted = '0' - AND resolution = 'fixed' - AND STATUS = 'closed' - ) AS t2 ON t1.`year` = t2.`year` + deleted = '0' + AND resolution = 'fixed' + AND STATUS = 'closed' + ) AS t2 ON t1.`year` = t2.`year` WHERE t2.id IS NOT NULL EOT, 'settings' => array @@ -2483,14 +2553,14 @@ FROM LEFT JOIN ( SELECT id, name, - YEAR ( finishedDate ) AS `year` + YEAR ( finishedDate ) AS `year` FROM - zt_task + zt_task WHERE - deleted = '0' - AND STATUS = 'closed' - AND closedReason = 'done' - ) AS t2 ON t1.`year` = t2.`year` + deleted = '0' + AND STATUS = 'closed' + AND closedReason = 'done' + ) AS t2 ON t1.`year` = t2.`year` WHERE t2.id IS NOT NULL EOT, 'settings' => array @@ -2514,10 +2584,10 @@ $config->bi->builtin->charts[] = array 'sql' => << array ( @@ -2639,7 +2709,7 @@ $config->bi->builtin->charts[] = array 'type' => 'table', 'group' => '64', 'sql' => <<bi->builtin->charts[] = array 'id' => 1077, 'name' => '年度新增-需求年度新增和完成趋势图', 'code' => 'annualCreated_storyTendency', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'line', 'group' => '36', 'sql' => << array ( @@ -2822,7 +2893,7 @@ EOT, 'type' => 'line', 'xaxis' => array ( - array('field' => 'YEARMONTH', 'name' => 'YEARMONTH', 'group' => '') + array('field' => 'yearmonth', 'name' => 'yearmonth', 'group' => '') ), 'yaxis' => array ( @@ -2838,7 +2909,7 @@ EOT, ), 'fields' => array ( - 'YEARMONTH' => array('name' => 'YEARMONTH', 'object' => 'story', 'field' => 'YEARMONTH', 'type' => 'string'), + 'yearmonth' => array('name' => 'yearmonth', 'object' => 'story', 'field' => 'yearmonth', 'type' => 'string'), 'year' => array('name' => 'year', 'object' => 'story', 'field' => 'year', 'type' => 'number'), 'month' => array('name' => 'month', 'object' => 'story', 'field' => 'month', 'type' => 'number'), 'newStory' => array('name' => '继续添加研发需求', 'object' => 'story', 'field' => 'newStory', 'type' => 'string'), @@ -2846,7 +2917,7 @@ EOT, ), 'langs' => array ( - 'YEARMONTH' => array('zh-cn' => 'YEARMONTH', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), + 'yearmonth' => array('zh-cn' => 'yearmonth', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), 'year' => array('zh-cn' => '年度', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), 'month' => array('zh-cn' => '月份', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), 'newStory' => array('zh-cn' => '新增需求数', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), @@ -2861,15 +2932,18 @@ $config->bi->builtin->charts[] = array 'id' => 1078, 'name' => '年度新增-Bug年度新增和解决趋势图', 'code' => 'annualCreated_bugTendency', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'line', 'group' => '44', 'sql' => << array ( @@ -2878,7 +2952,7 @@ EOT, 'type' => 'line', 'xaxis' => array ( - array('field' => 'YEARMONTH', 'name' => 'YEARMONTH', 'group' => '') + array('field' => 'yearmonth', 'name' => 'yearmonth', 'group' => '') ), 'yaxis' => array ( @@ -2893,7 +2967,7 @@ EOT, ), 'fields' => array ( - 'YEARMONTH' => array('name' => 'YEARMONTH', 'object' => 'bug', 'field' => 'YEARMONTH', 'type' => 'string'), + 'yearmonth' => array('name' => 'yearmonth', 'object' => 'bug', 'field' => 'yearmonth', 'type' => 'string'), 'year' => array('name' => 'year', 'object' => 'bug', 'field' => 'year', 'type' => 'number'), 'month' => array('name' => 'month', 'object' => 'bug', 'field' => 'month', 'type' => 'number'), 'newBug' => array('name' => 'newBug', 'object' => 'bug', 'field' => 'newBug', 'type' => 'string'), @@ -2901,7 +2975,7 @@ EOT, ), 'langs' => array ( - 'YEARMONTH' => array('zh-cn' => '', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), + 'yearmonth' => array('zh-cn' => '', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), 'year' => array('zh-cn' => '年度', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), 'month' => array('zh-cn' => '月份', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), 'newBug' => array('zh-cn' => '新增Bug数', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), @@ -2916,15 +2990,16 @@ $config->bi->builtin->charts[] = array 'id' => 1079, 'name' => '年度新增-任务年度新增和完成趋势图', 'code' => 'annualCreated_taskTendency', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'line', 'group' => '39', 'sql' => << array ( @@ -2933,7 +3008,7 @@ EOT, 'type' => 'line', 'xaxis' => array ( - array('field' => 'YEARMONTH', 'name' => 'YEARMONTH', 'group' => '') + array('field' => 'yearmonth', 'name' => 'yearmonth', 'group' => '') ), 'yaxis' => array ( @@ -2948,7 +3023,7 @@ EOT, ), 'fields' => array ( - 'YEARMONTH' => array('name' => 'YEARMONTH', 'object' => 'task', 'field' => 'YEARMONTH', 'type' => 'string'), + 'yearmonth' => array('name' => 'yearmonth', 'object' => 'task', 'field' => 'yearmonth', 'type' => 'string'), 'year' => array('name' => 'year', 'object' => 'task', 'field' => 'year', 'type' => 'number'), 'month' => array('name' => 'month', 'object' => 'task', 'field' => 'month', 'type' => 'string'), 'newTask' => array('name' => 'newTask', 'object' => 'task', 'field' => 'newTask', 'type' => 'string'), @@ -2956,7 +3031,7 @@ EOT, ), 'langs' => array ( - 'YEARMONTH' => array('zh-cn' => 'YEARMONTH', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), + 'yearmonth' => array('zh-cn' => 'yearmonth', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), 'year' => array('zh-cn' => '年度', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), 'month' => array('zh-cn' => '月份', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), 'newTask' => array('zh-cn' => '新增任务数', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), @@ -2971,15 +3046,16 @@ $config->bi->builtin->charts[] = array 'id' => 1080, 'name' => '年度新增-项目年度新增和完成趋势图', 'code' => 'annualCreated_projectTendency', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'line', 'group' => '38', 'sql' => << array ( @@ -2988,7 +3064,7 @@ EOT, 'type' => 'line', 'xaxis' => array ( - array('field' => 'YEARMONTH', 'name' => 'YEARMONTH', 'group' => '') + array('field' => 'yearmonth', 'name' => 'yearmonth', 'group' => '') ), 'yaxis' => array ( @@ -3003,7 +3079,7 @@ EOT, ), 'fields' => array ( - 'YEARMONTH' => array('name' => 'YEARMONTH', 'object' => 'project', 'field' => 'YEARMONTH', 'type' => 'string'), + 'yearmonth' => array('name' => 'yearmonth', 'object' => 'project', 'field' => 'yearmonth', 'type' => 'string'), 'year' => array('name' => 'year', 'object' => 'project', 'field' => 'year', 'type' => 'number'), 'month' => array('name' => 'month', 'object' => 'project', 'field' => 'month', 'type' => 'string'), 'newProject' => array('name' => 'newProject', 'object' => 'project', 'field' => 'newProject', 'type' => 'string'), @@ -3011,7 +3087,7 @@ EOT, ), 'langs' => array ( - 'YEARMONTH' => array('zh-cn' => 'YEARMONTH', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), + 'yearmonth' => array('zh-cn' => 'yearmonth', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), 'year' => array('zh-cn' => '年度', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), 'month' => array('zh-cn' => '月份', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), 'newProject' => array('zh-cn' => '新增项目数', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), @@ -3026,15 +3102,16 @@ $config->bi->builtin->charts[] = array 'id' => 1081, 'name' => '年度新增-执行年度新增和完成趋势图', 'code' => 'annualCreated_executionTendency', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'line', 'group' => '40', 'sql' => << array ( @@ -3043,7 +3120,7 @@ EOT, 'type' => 'line', 'xaxis' => array ( - array('field' => 'YEARMONTH', 'name' => 'YEARMONTH', 'group' => '') + array('field' => 'yearmonth', 'name' => 'yearmonth', 'group' => '') ), 'yaxis' => array ( @@ -3058,7 +3135,7 @@ EOT, ), 'fields' => array ( - 'YEARMONTH' => array('name' => 'YEARMONTH', 'object' => 'project', 'field' => 'YEARMONTH', 'type' => 'string'), + 'yearmonth' => array('name' => 'yearmonth', 'object' => 'project', 'field' => 'yearmonth', 'type' => 'string'), 'year' => array('name' => 'year', 'object' => 'project', 'field' => 'year', 'type' => 'number'), 'month' => array('name' => 'month', 'object' => 'project', 'field' => 'month', 'type' => 'string'), 'newExecution' => array('name' => 'newExecution', 'object' => 'project', 'field' => 'newExecution', 'type' => 'string'), @@ -3066,7 +3143,7 @@ EOT, ), 'langs' => array ( - 'YEARMONTH' => array('zh-cn' => 'YEARMONTH', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), + 'yearmonth' => array('zh-cn' => 'yearmonth', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), 'year' => array('zh-cn' => '年度', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), 'month' => array('zh-cn' => '月份', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), 'newExecution' => array('zh-cn' => '新增执行数', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), @@ -3081,14 +3158,15 @@ $config->bi->builtin->charts[] = array 'id' => 1082, 'name' => '年度新增-产品发布次数年度趋势图', 'code' => 'annualCreated_releaseTendency', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'line', 'group' => '37', 'sql' => << array ( @@ -3097,7 +3175,7 @@ EOT, 'type' => 'line', 'xaxis' => array ( - array('field' => 'YEARMONTH', 'name' => 'YEARMONTH', 'group' => '') + array('field' => 'yearmonth', 'name' => 'yearmonth', 'group' => '') ), 'yaxis' => array ( @@ -3111,14 +3189,14 @@ EOT, ), 'fields' => array ( - 'YEARMONTH' => array('name' => 'YEARMONTH', 'object' => 'release', 'field' => 'YEARMONTH', 'type' => 'string'), + 'yearmonth' => array('name' => 'yearmonth', 'object' => 'release', 'field' => 'yearmonth', 'type' => 'string'), 'year' => array('name' => 'year', 'object' => 'release', 'field' => 'year', 'type' => 'number'), 'month' => array('name' => 'month', 'object' => 'release', 'field' => 'month', 'type' => 'string'), 'release' => array('name' => 'release', 'object' => 'release', 'field' => 'release', 'type' => 'string') ), 'langs' => array ( - 'YEARMONTH' => array('zh-cn' => 'YEARMONTH', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), + 'yearmonth' => array('zh-cn' => 'yearmonth', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), 'year' => array('zh-cn' => '年度', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), 'month' => array('zh-cn' => '月份', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), 'release' => array('zh-cn' => '发布次数', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => '') @@ -3132,14 +3210,17 @@ $config->bi->builtin->charts[] = array 'id' => 1083, 'name' => '年度新增-年度投入产出比', 'code' => 'annualCreated_IORatio', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'line', 'group' => '45', 'sql' => << array ( @@ -3163,36 +3244,37 @@ $config->bi->builtin->charts[] = array 'id' => 1085, 'name' => '年度排行-项目集-预算投入榜', 'code' => 'annualRank_programBudget', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '41', 'sql' => << array ( @@ -3236,23 +3318,24 @@ $config->bi->builtin->charts[] = array 'id' => 1086, 'name' => '年度排行-项目集-人员投入榜', 'code' => 'annualRank_programPersonnel', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '41', 'sql' => << array ( @@ -3294,44 +3377,45 @@ $config->bi->builtin->charts[] = array 'id' => 1087, 'name' => '年度排行-项目集-工时消耗榜', 'code' => 'annualRank_programConsumed', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '41', 'sql' => << array ( @@ -3375,33 +3459,21 @@ $config->bi->builtin->charts[] = array 'id' => 1088, 'name' => '年度排行-项目集-新增需求条目榜', 'code' => 'annualRank_programStoryCount_created', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '36', 'sql' => << array ( @@ -3445,36 +3517,21 @@ $config->bi->builtin->charts[] = array 'id' => 1089, 'name' => '年度排行-项目集-新增需求规模榜', 'code' => 'annualRank_programStoryEstimate_created', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '36', 'sql' => << array ( @@ -3518,33 +3575,18 @@ $config->bi->builtin->charts[] = array 'id' => 1090, 'name' => '年度排行-项目集-新增Bug条目榜', 'code' => 'annualRank_programBug_created', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '44', 'sql' => << array ( @@ -3588,34 +3630,21 @@ $config->bi->builtin->charts[] = array 'id' => 1091, 'name' => '年度排行-项目集-完成需求条目榜', 'code' => 'annualRank_programStoryCount_finished', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '43', 'sql' => << array ( @@ -3659,37 +3688,21 @@ $config->bi->builtin->charts[] = array 'id' => 1092, 'name' => '年度排行-项目集-完成需求规模榜', 'code' => 'annualRank_programStoryEstimate_finished', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '36', 'sql' => << array ( @@ -3733,35 +3746,21 @@ $config->bi->builtin->charts[] = array 'id' => 1093, 'name' => '年度排行-项目集-修复Bug条目榜', 'code' => 'annualRank_programBug_fixed', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '44', 'sql' => << array ( @@ -3805,13 +3804,22 @@ $config->bi->builtin->charts[] = array 'id' => 1094, 'name' => '年度排行-项目-工期榜', 'code' => 'annualRank_projectDuration', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '42', 'sql' => <<= `year`) OR (status = 'closed' AND YEAR(realEnd) >= `year`)) HAVING 1=1 ORDER BY `year`, duration desc +select year, id, name, status, realBegan, realEnd, + if(status = 'closed', datediff('day', realBegan, realEnd), datediff('day', realBegan, current_date())) as duration +from (select distinct year(date) as year from zt_action) as t1 +left join zt_project as t2 on 1 = 1 +where deleted = '0' +and type = 'project' +and year(realBegan) <= year +and year(realBegan) is not null +and (status ='doing' or (status = 'suspended' and year(suspendedDate) >= year) +or (status = 'closed' and year(realEnd) >= year)) +order by year, duration desc EOT, 'settings' => array ( @@ -3861,19 +3869,22 @@ $config->bi->builtin->charts[] = array 'id' => 1096, 'name' => '年度排行-项目-工期偏差榜', 'code' => 'annualRank_projectDurationDeviation', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '42', 'sql' => <<= `year` OR LEFT(realEnd, 4) = '0000') AND YEAR(`end`) != '2059' -HAVING 1=1 -ORDER BY duration ASC +select `year`, id, name, status, begin, "end", realBegan, realEnd, + round((if(year(realEnd) is not null, + datediff('day', realBegan, realEnd), + datediff('day', realBegan, current_date())) - datediff('day', begin, "end")) / datediff('day', begin, "end") * 100) as duration +from (select distinct year(date) as year from zt_action) as t1 +left join zt_project as t2 on 1=1 +where deleted = '0' and t2.type = 'project' +and (year(realBegan) <= year and year(realBegan) is not null) +and (year(realEnd) >= year and year(realEnd) is not null) +and year("end") != '2059' +order by duration asc EOT, 'settings' => array ( @@ -3932,9 +3943,9 @@ $config->bi->builtin->charts[] = array 'group' => '41', 'sql' => <<bi->builtin->charts[] = array 'id' => 1098, 'name' => '年度排行-项目-工时消耗榜', 'code' => 'annualRank_projectConsumed', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '41', 'sql' => << array ( @@ -4060,43 +4052,22 @@ $config->bi->builtin->charts[] = array 'id' => 1099, 'name' => '年度排行-项目-完成需求条目榜', 'code' => 'annualRank_projectStoryCount_finished', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '36', 'sql' => << array ( @@ -4140,47 +4111,22 @@ $config->bi->builtin->charts[] = array 'id' => 1100, 'name' => '年度排行-项目-完成需求规模榜', 'code' => 'annualRank_projectStoryEstimate_finished', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '43', 'sql' => << array ( @@ -4224,16 +4170,17 @@ $config->bi->builtin->charts[] = array 'id' => 1101, 'name' => '年度排行-产品-新增需求条目榜', 'code' => 'annualRank_productStoryCount_created', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '36', 'sql' => << array ( @@ -4277,16 +4224,17 @@ $config->bi->builtin->charts[] = array 'id' => 1102, 'name' => '年度排行-产品-完成需求规模榜', 'code' => 'annualRank_productStoryEstimate_finished', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '36', 'sql' => << array ( @@ -4330,16 +4278,17 @@ $config->bi->builtin->charts[] = array 'id' => 1103, 'name' => '年度排行-产品-新增Bug条目榜', 'code' => 'annualRank_productBug_created', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '44', 'sql' => << array ( @@ -4383,16 +4332,20 @@ $config->bi->builtin->charts[] = array 'id' => 1104, 'name' => '年度排行-产品-修复Bug条目榜', 'code' => 'annualRank_productBug_fixed', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '44', 'sql' => << array ( @@ -4436,15 +4389,18 @@ $config->bi->builtin->charts[] = array 'id' => 1105, 'name' => '年度排行-个人-创建需求条目榜', 'code' => 'annualRank_personalStoryCount_created', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '56', 'sql' => << array ( @@ -4486,15 +4442,20 @@ $config->bi->builtin->charts[] = array 'id' => 1106, 'name' => '年度排行-个人-创建用例条目榜', 'code' => 'annualRank_personalCaseCount_created', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '56', 'sql' => << array ( @@ -4536,15 +4497,20 @@ $config->bi->builtin->charts[] = array 'id' => 1107, 'name' => '年度排行-个人-创建Bug条目榜', 'code' => 'annualRank_personalBug_created', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '56', 'sql' => << array ( @@ -4586,15 +4552,18 @@ $config->bi->builtin->charts[] = array 'id' => 1108, 'name' => '年度排行-个人-修复Bug条目榜', 'code' => 'annualRank_personalBug_Fixed', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '56', 'sql' => << array ( @@ -4636,15 +4605,19 @@ $config->bi->builtin->charts[] = array 'id' => 1109, 'name' => '年度排行-个人-工时消耗榜', 'code' => 'annualRank_personalConsumed', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '56', 'sql' => << array ( @@ -4686,11 +4659,19 @@ $config->bi->builtin->charts[] = array 'id' => 1110, 'name' => '年度排行-个人-禅道操作次数榜', 'code' => 'annualRank_personalAction', + 'driver' => 'duckdb', 'dimension' => '1', 'type' => 'cluBarY', 'group' => '56', 'sql' => << array ( @@ -4798,11 +4779,16 @@ $config->bi->builtin->charts[] = array 'id' => 10003, 'name' => '年度完成项目-完成需求条目数', 'code' => 'annualFinishedProject_storyCount_finished', + 'driver' => 'duckdb', 'dimension' => '2', 'type' => 'card', 'group' => '75', 'sql' => << array ( @@ -5284,7 +5270,7 @@ select tt.*, tt.`故事点` / tt.`工时` as "单位时间交付需求规模数" from ( select -t1.name as project, +t1.name as project, ( select round(sum(t3.estimate), 1) from zt_projectstory t2 left join zt_story t3 on t3.id= t2.story and t3.status='closed' and t3.closedReason = 'done' @@ -5627,9 +5613,9 @@ LEFT JOIN ( GROUP BY t21.id, t21.percent, t22.project ) t GROUP BY t.project -) AS t2 ON t1.id = t2.project +) AS t2 ON t1.id = t2.project WHERE t1.deleted = '0' -AND t1.status = 'doing' +AND t1.status = 'doing' AND t1.type = 'project' AND ((IFNULL(prograss, 0) >= (DATEDIFF(NOW(), t1.`begin`) / DATEDIFF(t1.`end`, t1.`begin`) * 100) AND LEFT(t1.`end`, 4) != '2059' AND DATEDIFF(`end`, NOW()) >= 0) OR LEFT(t1.`end`, 4) = '2059' ) EOT, @@ -5709,9 +5695,9 @@ LEFT JOIN ( GROUP BY t21.id, t21.percent, t22.project ) t GROUP BY t.project -) AS t2 ON t1.id = t2.project +) AS t2 ON t1.id = t2.project WHERE t1.deleted = '0' -AND t1.status = 'doing' +AND t1.status = 'doing' AND t1.type = 'project' AND LEFT(t1.`end`, 4) != '2059' AND IFNULL(prograss, 0) < (DATEDIFF(NOW(), t1.`begin`) / DATEDIFF(t1.`end`, t1.`begin`) * 100) AND DATEDIFF(`end`, NOW()) >= 0 @@ -5902,9 +5888,9 @@ LEFT JOIN ( WHERE t21.deleted = '0' AND t21.type IN ('sprint', 'stage', 'kanban') AND t22.deleted = '0' AND t22.parent < 1 GROUP BY t22.project -) AS t2 ON t1.id = t2.project +) AS t2 ON t1.id = t2.project WHERE t1.deleted = '0' -AND t1.status = 'doing' +AND t1.status = 'doing' AND t1.type = 'project' EOT, 'settings' => array @@ -5952,12 +5938,12 @@ $config->bi->builtin->charts[] = array 'type' => 'pie', 'group' => '69', 'sql' => <<= (DATEDIFF(NOW(), t1.`begin`) / DATEDIFF(t1.`end`, t1.`begin`) * 100) AND LEFT(t1.`end`, 4) != '2059') + (IFNULL(prograss, 0) >= (DATEDIFF(NOW(), t1.`begin`) / DATEDIFF(t1.`end`, t1.`begin`) * 100) AND LEFT(t1.`end`, 4) != '2059') OR LEFT(t1.`end`, 4) = '2059' , "顺利", "滞后" @@ -5985,9 +5971,9 @@ LEFT JOIN ( GROUP BY t21.id, t21.percent, t22.project ) t GROUP BY t.project -) AS t2 ON t1.id = t2.project +) AS t2 ON t1.id = t2.project WHERE t1.deleted = '0' -AND t1.status = 'doing' +AND t1.status = 'doing' AND t1.type = 'project' EOT, 'settings' => array @@ -6107,10 +6093,10 @@ $config->bi->builtin->charts[] = array SELECT t1.id, t1.name, IFNULL(t3.name, '/') AS program,t1.`begin`, IF(YEAR(t1.`end`) = '2059', "长期", t1.`end`) AS `end`, IF(YEAR(t1.`end`) = '2059', "长期", DATEDIFF(t1.`end`, t1.`begin`) + 1) AS planDuration, IF(LEFT(t1.realBegan, 4) = '0000', '/', t1.realBegan) AS realBegan, IF(YEAR(t1.`end`) = '2059', "长期", IF(DATEDIFF(t1.`end`, NOW()) >= 0, DATEDIFF(t1.`end`, NOW()) + 1, 0)) AS realDuration, IF( - DATEDIFF(t1.`end`, NOW()) < 0, - "延期", + DATEDIFF(t1.`end`, NOW()) < 0, + "延期", (IF( - (IFNULL(prograss, 0) >= (DATEDIFF(NOW(), t1.`begin`) / DATEDIFF(t1.`end`, t1.`begin`) * 100) AND LEFT(t1.`end`, 4) != '2059') + (IFNULL(prograss, 0) >= (DATEDIFF(NOW(), t1.`begin`) / DATEDIFF(t1.`end`, t1.`begin`) * 100) AND LEFT(t1.`end`, 4) != '2059') OR LEFT(t1.`end`, 4) = '2059' , "顺利", "滞后" @@ -6138,10 +6124,10 @@ LEFT JOIN ( GROUP BY t21.id, t21.percent, t22.project ) t GROUP BY t.project -) AS t2 ON t1.id = t2.project +) AS t2 ON t1.id = t2.project LEFT JOIN zt_project AS t3 ON SUBSTR(t1.path, 2, POSITION(',' IN SUBSTR(t1.path, 2)) -1) = t3.id AND t3.type = 'program' AND t3.deleted = '0' WHERE t1.deleted = '0' -AND t1.status = 'doing' +AND t1.status = 'doing' AND t1.type = 'project' EOT, 'settings' => array @@ -6266,7 +6252,7 @@ SELECT IFNULL(t2.name, '/') AS project, IFNULL(t3.story, 0) AS story, IFNULL(t3.estimate, 0) AS estimate, - IFNULL(t4.task, 0) AS task, + IFNULL(t4.task, 0) AS task, IFNULL(t4.workhour, 0) AS workhour FROM zt_project AS t1 LEFT JOIN zt_project AS t2 ON t1.project = t2.id AND t2.type = 'project' @@ -6571,7 +6557,7 @@ $config->bi->builtin->charts[] = array 'type' => 'waterpolo', 'group' => '91', 'sql' => <<bi->builtin->charts[] = array 'type' => 'cluBarX', 'group' => '91', 'sql' => << (select DATE_sub(MAX(NOW()), INTERVAL '30' DAY)) +where left(openedDate,10) > (select DATE_sub(MAX(NOW()), INTERVAL '30' DAY)) and left(openedDate,10) < NOW() and deleted='0' EOT, @@ -6666,7 +6652,7 @@ count(a.id) as totalBugCount, sum(a.effectivebug) as effectiveBugCount, sum(a.effectivebug)/count(a.id) effectiveBugRate from( -select +select left(openedDate,4) year, id, (case when resolution in ('fixed','postponed') or status='active' then 1 else 0 end) effectivebug, @@ -6725,17 +6711,17 @@ $config->bi->builtin->charts[] = array 'sql' => <<bi->builtin->charts[] = array 'type' => 'cluBarY', 'group' => '91', 'sql' => <<bi->builtin->charts[] = array 'type' => 'cluBarY', 'group' => '91', 'sql' => <<bi->builtin->charts[] = array 'type' => 'pie', 'group' => '91', 'sql' => <<bi->builtin->charts[] = array 'type' => 'card', 'group' => '47', 'sql' => <<bi->builtin->pivots[] = array 'id' => 1000, 'name' => array('zh-cn' => '完成项目工期透视表', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), 'code' => 'finishedProjectDuration', + 'driver' => 'duckdb', 'dimension' => '2', 'group' => '86', 'sql' => << array ( @@ -56,23 +56,23 @@ EOT, array('field' => 'duration_deviation', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), array('field' => 'rate', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow') ), - 'group3' => 'program1', - 'group6' => 'name', + 'group1' => 'program1', + 'group2' => 'name', 'lastStep' => '4' ), 'filters' => array(), 'fields' => array ( - 'name' => array('name' => '项目名称', 'object' => 'project', 'field' => 'name', 'type' => 'string'), - 'program1' => array('name' => 'program1', 'object' => 'project', 'field' => 'program1', 'type' => 'string'), - 'begin' => array('name' => '计划开始日期', 'object' => 'project', 'field' => 'begin', 'type' => 'date'), - 'end' => array('name' => '计划完成日期', 'object' => 'project', 'field' => 'end', 'type' => 'date'), - 'realBegan' => array('name' => '实际开始日期', 'object' => 'project', 'field' => 'realBegan', 'type' => 'date'), - 'realEnd' => array('name' => '实际完成日期', 'object' => 'project', 'field' => 'realEnd', 'type' => 'date'), - 'closedDate' => array('name' => '关闭日期', 'object' => 'project', 'field' => 'closedDate', 'type' => 'date'), - 'realduration' => array('name' => 'realduration', 'object' => 'project', 'field' => 'realduration', 'type' => 'number'), - 'duration_deviation' => array('name' => 'duration_deviation', 'object' => 'project', 'field' => 'duration_deviation', 'type' => 'number'), - 'rate' => array('name' => 'rate', 'object' => 'project', 'field' => 'rate', 'type' => 'number') + 'name' => array('object' => 'project', 'field' => 'name', 'type' => 'string'), + 'program1' => array('object' => 'project', 'field' => 'program1', 'type' => 'string'), + 'begin' => array('object' => 'project', 'field' => 'begin', 'type' => 'date'), + 'end' => array('object' => 'project', 'field' => 'end', 'type' => 'date'), + 'realBegan' => array('object' => 'project', 'field' => 'realBegan', 'type' => 'date'), + 'realEnd' => array('object' => 'project', 'field' => 'realEnd', 'type' => 'date'), + 'closedDate' => array('object' => 'project', 'field' => 'closedDate', 'type' => 'date'), + 'realduration' => array('object' => 'project', 'field' => 'realduration', 'type' => 'number'), + 'duration_deviation' => array('object' => 'project', 'field' => 'duration_deviation', 'type' => 'number'), + 'rate' => array('object' => 'project', 'field' => 'rate', 'type' => 'number') ), 'langs' => array ( @@ -96,88 +96,94 @@ $config->bi->builtin->pivots[] = array 'id' => 1001, 'name' => array('zh-cn' => '完成项目工时透视表', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), 'code' => 'finishedProjectHour', + 'driver' => 'duckdb', 'dimension' => '2', 'group' => '85', 'sql' => << array ( 'columns' => array ( - array('field' => '预计工时', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), - array('field' => '消耗工时', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), - array('field' => '工时偏差', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), - array('field' => '工时偏差率', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), - array('field' => '完成需求数', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), - array('field' => '完成需求规模数', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), - array('field' => '单位时间交付需求规模数', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow') + array('field' => 'estimate', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), + array('field' => 'consumed', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), + array('field' => 'deviation', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), + array('field' => 'deviationrate', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), + array('field' => 'finishedstorys', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), + array('field' => 'finishedstorysmate', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), + array('field' => 'demandsizesperunittime', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow') ), 'columnTotal' => 'noShow', - 'group1' => '一级项目集', - 'group2' => '项目名称', + 'group1' => 'topprogram', + 'group2' => 'projectname', 'lastStep' => '4' ), 'filters' => array ( array ( - 'field' => 'closedDate', + 'field' => 'closeddate', 'type' => 'date', 'name' => '关闭日期', 'default' => array('begin' => '', 'end' => '') @@ -185,16 +191,29 @@ EOT, ), 'fields' => array ( - '项目名称' => array('name' => '项目名称', 'object' => 'project', 'field' => '项目名称', 'type' => 'string'), - '一级项目集' => array('name' => '一级项目集', 'object' => 'project', 'field' => '一级项目集', 'type' => 'string'), - '预计工时' => array('name' => '预计工时', 'object' => 'project', 'field' => '预计工时', 'type' => 'number'), - '消耗工时' => array('name' => '消耗工时', 'object' => 'project', 'field' => '消耗工时', 'type' => 'number'), - '工时偏差' => array('name' => '工时偏差', 'object' => 'project', 'field' => '工时偏差', 'type' => 'number'), - '工时偏差率' => array('name' => '工时偏差率', 'object' => 'project', 'field' => '工时偏差率', 'type' => 'number'), - '完成需求数' => array('name' => '完成需求数', 'object' => 'project', 'field' => '完成需求数', 'type' => 'string'), - '完成需求规模数' => array('name' => '完成需求规模数', 'object' => 'project', 'field' => '完成需求规模数', 'type' => 'number'), - '单位时间交付需求规模数' => array('name' => '单位时间交付需求规模数', 'object' => 'project', 'field' => '单位时间交付需求规模数', 'type' => 'number'), - 'closedDate' => array('name' => '关闭日期', 'object' => 'project', 'field' => 'closedDate', 'type' => 'date') + 'projectname' => array('object' => 'project', 'field' => 'projectname', 'type' => 'string'), + 'topprogram' => array('object' => 'project', 'field' => 'topprogram', 'type' => 'string'), + 'estimate' => array('object' => 'project', 'field' => 'estimate', 'type' => 'number'), + 'consumed' => array('object' => 'project', 'field' => 'consumed', 'type' => 'number'), + 'deviation' => array('object' => 'project', 'field' => 'deviation', 'type' => 'number'), + 'deviationrate' => array('object' => 'project', 'field' => 'deviationrate', 'type' => 'number'), + 'finishedstorys' => array('object' => 'project', 'field' => 'deviationrate', 'type' => 'string'), + 'finishedstorysmate' => array('object' => 'project', 'field' => 'finishedstorysmate', 'type' => 'number'), + 'demandsizesperunittime' => array('object' => 'project', 'field' => 'demandsizesperunittime', 'type' => 'number'), + 'closeddate' => array('object' => 'project', 'field' => 'closeddate', 'type' => 'date') + ), + 'langs' => array + ( + 'projectname' => array('zh-cn' => '项目名称', 'zh-tw' => '项目名称', 'en' => 'projectname'), + 'topprogram' => array('zh-cn' => '一级项目集', 'zh-tw' => '一级项目集', 'en' => 'topprogram'), + 'estimate' => array('zh-cn' => '预计工时', 'zh-tw' => '预计工时', 'en' => 'estimate'), + 'consumed' => array('zh-cn' => '消耗工时', 'zh-tw' => '消耗工时', 'en' => 'consumed'), + 'deviation' => array('zh-cn' => '工时偏差', 'zh-tw' => '工时偏差', 'en' => 'deviation'), + 'deviationrate' => array('zh-cn' => '工时偏差率', 'zh-tw' => '工时偏差率', 'en' => 'deviationrate'), + 'finishedstorys' => array('zh-cn' => '完成需求数', 'zh-tw' => '完成需求数', 'en' => 'finishedstorys'), + 'finishedstorysmate' => array('zh-cn' => '完成需求规模数', 'zh-tw' => '完成需求规模数', 'en' => 'finishedstorysmate'), + 'demandsizesperunittime' => array('zh-cn' => '单位时间交付需求规模数', 'zh-tw' => '单位时间交付需求规模数', 'en' => 'demandsizesperunittime'), + 'closeddate' => array('zh-cn' => '关闭日期', 'zh-tw' => '关闭日期', 'en' => 'closeddate') ), 'stage' => 'published', 'builtin' => '1' @@ -205,98 +224,133 @@ $config->bi->builtin->pivots[] = array 'id' => 1002, 'name' => array('zh-cn' => '产品缺陷数据汇总表', 'zh-tw' => '', 'en' => '', 'de' => '', 'fr' => ''), 'code' => 'productBugSummary', + 'driver' => 'duckdb', 'dimension' => '3', 'group' => '100', 'sql' => << array ( 'columns' => array ( - array('field' => '研发完成需求数', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), - array('field' => '研发完成需求规模数', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), - array('field' => '需求用例数', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), - array('field' => '用例密度', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), - array('field' => '用例覆盖率', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), - array('field' => 'Bug数', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), - array('field' => '有效Bug数', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), - array('field' => '优先级为1,2的Bug数', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), - array('field' => 'Bug密度', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), - array('field' => '修复Bug数', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), - array('field' => 'Bug修复率', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow') + array('field' => 'exfixedstorys', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), + array('field' => 'exfixedstorysmate', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), + array('field' => 'storycases', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), + array('field' => 'casedensity', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), + array('field' => 'casecoveragerate', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), + array('field' => 'bugs', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), + array('field' => 'effectivebugs', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), + array('field' => 'pri12bugs', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), + array('field' => 'bugdensity', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), + array('field' => 'fixedbugs', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow'), + array('field' => 'fixedbugsrate', 'stat' => 'sum', 'slice' => 'noSlice', 'showMode' => 'default', 'monopolize' => '0', 'showTotal' => 'noShow') ), 'columnTotal' => 'noShow', - 'group1' => '一级项目集', - 'group3' => '产品线', - 'group2' => '产品', + 'group1' => 'topprogram', + 'group2' => 'productline', + 'group3' => 'product', 'lastStep' => '4' ), 'filters' => array(), 'fields' => array ( - '产品' => array('name' => '产品', 'object' => 'story', 'field' => '产品', 'type' => 'string'), - '一级项目集' => array('name' => '一级项目集', 'object' => 'story', 'field' => '一级项目集', 'type' => 'string'), - '产品线' => array('name' => '产品线', 'object' => 'story', 'field' => '产品线', 'type' => 'string'), - '研发完成需求数' => array('name' => '研发完成需求数', 'object' => 'story', 'field' => '研发完成需求数', 'type' => 'string'), - '研发完成需求规模数' => array('name' => '研发完成需求规模数', 'object' => 'story', 'field' => '研发完成需求规模数', 'type' => 'number'), - '需求用例数' => array('name' => '需求用例数', 'object' => 'story', 'field' => '需求用例数', 'type' => 'string'), - '用例密度' => array('name' => '用例密度', 'object' => 'story', 'field' => '用例密度', 'type' => 'number'), - '用例覆盖率' => array('name' => '用例覆盖率', 'object' => 'story', 'field' => '用例覆盖率', 'type' => 'number'), - 'Bug数' => array('name' => 'Bug数', 'object' => 'story', 'field' => 'Bug数', 'type' => 'string'), - '有效Bug数' => array('name' => '有效Bug数', 'object' => 'story', 'field' => '有效Bug数', 'type' => 'number'), - '优先级为1,2的Bug数' => array('name' => '优先级为1,2的Bug数', 'object' => 'story', 'field' => '优先级为1,2的Bug数', 'type' => 'number'), - 'Bug密度' => array('name' => 'Bug密度', 'object' => 'story', 'field' => 'Bug密度', 'type' => 'number'), - '修复Bug数' => array('name' => '修复Bug数', 'object' => 'story', 'field' => '修复Bug数', 'type' => 'number'), - 'Bug修复率' => array('name' => 'Bug修复率', 'object' => 'story', 'field' => 'Bug修复率', 'type' => 'number') + 'product' => array('object' => 'story', 'field' => 'product', 'type' => 'string'), + 'topprogram' => array('object' => 'story', 'field' => 'topprogram', 'type' => 'string'), + 'productline' => array('object' => 'story', 'field' => 'productline', 'type' => 'string'), + 'exfixedstorys' => array('object' => 'story', 'field' => 'exfixedstorys', 'type' => 'string'), + 'exfixedstorysmate' => array('object' => 'story', 'field' => 'exfixedstorysmate', 'type' => 'number'), + 'storycases' => array('object' => 'story', 'field' => 'storycases', 'type' => 'string'), + 'casedensity' => array('object' => 'story', 'field' => 'casedensity', 'type' => 'number'), + 'casecoveragerate' => array('object' => 'story', 'field' => 'casecoveragerate', 'type' => 'number'), + 'bugs' => array('object' => 'story', 'field' => 'bugs', 'type' => 'string'), + 'effectivebugs' => array('object' => 'story', 'field' => 'effectviebugs', 'type' => 'number'), + 'pri12bugs' => array('object' => 'story', 'field' => 'pri12bugs', 'type' => 'number'), + 'bugdensity' => array('object' => 'story', 'field' => 'bugdensity', 'type' => 'number'), + 'fixedbugs' => array('object' => 'story', 'field' => 'fixedbugs', 'type' => 'number'), + 'fixedbugsrate' => array('object' => 'story', 'field' => 'fixedbugsrate', 'type' => 'number') + ), + 'langs' => array + ( + 'product' => array('zh-cn' => '产品', 'zh-tw' => '产品', 'en' => 'product'), + 'topprogram' => array('zh-cn' => '一级项目集', 'zh-tw' => '一级项目集', 'en' => 'topprogram'), + 'productline' => array('zh-cn' => '产品线', 'zh-tw' => '产品线', 'en' => 'productline'), + 'exfixedstorys' => array('zh-cn' => '研发完成需求数', 'zh-tw' => '研发完成需求数', 'en' => 'exfixedstorys'), + 'exfixedstorysmate' => array('zh-cn' => '研发完成需求规模数', 'zh-tw' => '研发完成需求规模数', 'en' => 'exfixedstorysmate'), + 'storycases' => array('zh-cn' => '需求用例数', 'zh-tw' => '需求用例数', 'en' => 'storycases'), + 'casedensity' => array('zh-cn' => '用例密度', 'zh-tw' => '用例密度', 'en' => 'casedensity'), + 'casecoveragerate' => array('zh-cn' => '用例覆盖率', 'zh-tw' => '用例覆盖率', 'en' => 'casecoveragerate'), + 'bugs' => array('zh-cn' => 'Bug数', 'zh-tw' => 'Bug数', 'en' => 'bugs'), + 'effectivebugs' => array('zh-cn' => '有效Bug数', 'zh-tw' => '有效Bug数', 'en' => 'effectviebugs'), + 'pri12bugs' => array('zh-cn' => '优先级为1,2的Bug数', 'zh-tw' => '优先级为1,2的Bug数', 'en' => 'pri12bugs'), + 'bugdensity' => array('zh-cn' => 'Bug密度', 'zh-tw' => 'Bug密度', 'en' => 'bugdensity'), + 'fixedbugs' => array('zh-cn' => '修复Bug数', 'zh-tw' => '修复Bug数', 'en' => 'fixedbugs'), + 'fixedbugsrate' => array('zh-cn' => 'Bug修复率', 'zh-tw' => 'Bug修复率', 'en' => 'fixedbugsrate') ), 'stage' => 'published', 'builtin' => '1' @@ -307,14 +361,20 @@ $config->bi->builtin->pivots[] = array 'id' => 1003, 'name' => array('zh-cn' => '产品完成度统计表', 'zh-tw' => '產品完成度統計表', 'en' => 'Product Progress', 'de' => 'Product Progress', 'fr' => 'Product Progress'), 'code' => 'productProgress', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '按照产品列出需求总数,完成的总数(状态是关闭,或者研发阶段是发布),完成的百分比。', 'zh-tw' => '按照產品列出需求總數,完成的總數(狀態是關閉,或者研發階段是發布),完成的百分比。', 'en' => 'Number of total stories,done stories(state is closed, or stage is released), percent of completion.', 'de' => 'Number of total stories,done stories(state is closed, or stage is released), percent of completion.', 'fr' => 'Number of total stories,done stories(state is closed, or stage is released), percent of completion.'), 'dimension' => '1', 'group' => '59', 'sql' => << array @@ -401,14 +461,19 @@ $config->bi->builtin->pivots[] = array 'id' => 1004, 'name' => array('zh-cn' => '产品需求状态分布表', 'zh-tw' => '產品需求狀態分布表', 'en' => 'Story Status', 'de' => 'Story Status', 'fr' => 'Story Status'), 'code' => 'productStoryStatus', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '按照产品列出需求总数,状态的分布情况。', 'zh-tw' => '按照產品列出需求總數,狀態的分布情況。', 'en' => 'Total number and status distribution of stories.', 'de' => 'Total number and status distribution of stories.', 'fr' => 'Total number and status distribution of stories.'), 'dimension' => '1', 'group' => '59', 'sql' => << array @@ -487,14 +552,19 @@ $config->bi->builtin->pivots[] = array 'id' => 1005, 'name' => array('zh-cn' => '产品需求阶段分布表', 'zh-tw' => '產品需求階段分布表', 'en' => 'Story Stage', 'de' => 'Story Stage', 'fr' => 'Story Stage'), 'code' => 'productStoryStage', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '按照产品列出需求总数,研发阶段的分布情况。', 'zh-tw' => '按照產品列出需求總數,研發階段的分布情況。', 'en' => 'Total number and stage distribution of stories ', 'de' => 'Total number and stage distribution of stories ', 'fr' => 'Total number and stage distribution of stories '), 'dimension' => '1', 'group' => '59', 'sql' => << array @@ -573,14 +643,19 @@ $config->bi->builtin->pivots[] = array 'id' => 1006, 'name' => array('zh-cn' => '产品发布数量统计表', 'zh-tw' => '產品發布數量統計表', 'en' => 'Product Release', 'de' => 'Product Release', 'fr' => 'Product Release'), 'code' => 'productRelease', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '按照产品列出发布的数量。', 'zh-tw' => '按照產品列出發布的數量。', 'en' => 'Product Release.', 'de' => 'Product Release.', 'fr' => 'Product Release.'), 'dimension' => '1', 'group' => '59', 'sql' => << array @@ -612,14 +687,35 @@ $config->bi->builtin->pivots[] = array 'id' => 1007, 'name' => array('zh-cn' => '任务状态统计表', 'zh-tw' => '任務狀態統計表', 'en' => 'Task Status Report', 'de' => 'Task Status Report', 'fr' => 'Task Status Report', 'vi' => 'Task Status Report', 'ja' => 'Task Status Report'), 'code' => 'taskStatus', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '按照执行统计任务的状态分布情况。', 'zh-tw' => '按照執行統計任務的狀態分布情況。', 'en' => '', 'de' => '', 'fr' => '', 'vi' => '', 'ja' => ''), 'dimension' => '1', 'group' => '60', 'sql' => <<=\$beginDate) and if(\$endDate='',1,t1.end<=\$endDate) +select + t1.id, + t3.name as project, + t1.name, + t2.status, + (case when t3.multiple='1' then t1.name else '' end) as execution, + t2.id as taskID, + t1.status as projectstatus, + (case when t2.deadline < current_date() + and t2.deadline is not null + and t2.status != 'closed' + and t2.status != 'done' + and t2.status != 'cancel' then 1 else 0 end + ) as timeout +from zt_project as t1 +left join zt_task as t2 on t1.id=t2.execution +left join zt_project as t3 on t3.id=t1.project +where t1.deleted='0' +and t1.type in ('sprint','stage') +and t2.deleted='0' +and (case when \$project='' then 1 else t3.id=\$project end) +and (case when \$status='' then 1 else t1.status=\$status end) +and (case when \$beginDate='' then 1 else t1.begin>=\$beginDate end) +and (case when \$endDate='' then 1 else t1.end<=\$endDate end) EOT, 'settings' => array ( @@ -671,14 +767,28 @@ $config->bi->builtin->pivots[] = array 'id' => 1008, 'name' => array('zh-cn' => '任务类型统计表', 'zh-tw' => '任務類型統計表', 'en' => 'Task Type Report', 'de' => 'Task Type Report', 'fr' => 'Task Type Report', 'vi' => 'Task Type Report', 'ja' => 'Task Type Report'), 'code' => 'taskType', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '按照项目统计任务的类型分布情况。', 'zh-tw' => '按照項目統計任務的類型分布情況。', 'en' => '', 'de' => '', 'fr' => '', 'vi' => '', 'ja' => ''), 'dimension' => '1', 'group' => '60', 'sql' => <<=\$beginDate) and if(\$endDate='',1,t1.end<=\$endDate) +where t1.deleted='0' +and t1.type in ('sprint','stage') +and t2.deleted='0' +and (case when \$project='' then 1 else t3.id=\$project end) +and (case when \$status='' then 1 else t1.status=\$status end) +and (case when \$beginDate='' then 1 else t1.begin>=\$beginDate end) +and (case when \$endDate='' then 1 else t1.end<=\$endDate end) EOT, 'settings' => array ( @@ -732,15 +842,29 @@ $config->bi->builtin->pivots[] = array 'id' => 1009, 'name' => array('zh-cn' => '项目任务指派统计表', 'zh-tw' => '項目任務指派統計表', 'en' => 'Task Assign Report', 'de' => 'Task Assign Report', 'fr' => 'Task Assign Report', 'vi' => 'Task Assign Report', 'ja' => 'Task Assign Report'), 'code' => 'projectTaskAssign', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '按照项目统计任务的指派给分布情况。', 'zh-tw' => '按照項目統計任務的指派給分布情況。', 'en' => '', 'de' => '', 'fr' => '', 'vi' => '', 'ja' => ''), 'dimension' => '1', 'group' => '60', 'sql' => <<=\$beginDate) and if(\$endDate='',1,t1.end<=\$endDate) +where t1.deleted='0' +and t1.type in ('sprint','stage') +and t2.deleted='0' +and (case when \$project='' then 1 else t4.id=\$project end) +and (case when \$status='' then 1 else t1.status=\$status end) +and (case when \$beginDate='' then 1 else t1.begin>=\$beginDate end) +and (case when \$endDate='' then 1 else t1.end<=\$endDate end) EOT, 'settings' => array ( @@ -790,14 +914,29 @@ $config->bi->builtin->pivots[] = array 'id' => 1010, 'name' => array('zh-cn' => '项目任务完成者统计表', 'zh-tw' => '項目任務完成者統計表', 'en' => 'Task Finish Report', 'de' => 'Task Finish Report', 'fr' => 'Task Finish Report', 'vi' => 'Task Finish Report', 'ja' => 'Task Finish Report'), 'code' => 'projectTaskFinished', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '按照项目统计任务的完成者分布情况。', 'zh-tw' => '按照項目統計任務的完成者分布情況。', 'en' => '', 'de' => '', 'fr' => '', 'vi' => '', 'ja' => ''), 'dimension' => '1', 'group' => '60', 'sql' => <<=\$beginDate) and if(\$endDate='',1,t1.end<=\$endDate) +where t1.deleted='0' +and t1.type in ('sprint','stage') +and t2.deleted='0' +and t2.finishedBy!='' +and (case when \$project='' then 1 else t3.id=\$project end) +and (case when \$status='' then 1 else t1.status=\$status end) +and (case when \$beginDate='' then 1 else t1.begin>=\$beginDate end) +and (case when \$endDate='' then 1 else t1.end<=\$endDate end) EOT, 'settings' => array ( @@ -847,17 +986,32 @@ $config->bi->builtin->pivots[] = array 'id' => 1011, 'name' => array('zh-cn' => '项目投入统计表', 'zh-tw' => '項目投入統計表', 'en' => 'Project Invest Report', 'de' => 'Project Invest Report', 'fr' => 'Project Invest Report', 'vi' => 'Project Invest Report', 'ja' => 'Project Invest Report'), 'code' => 'projectInvested', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '按照项目列出:任务数,需求数,人数,总消耗工时。', 'zh-tw' => '按照項目列出:任務數,需求數,人數,總消耗工時。', 'en' => '', 'de' => '', 'fr' => '', 'vi' => '', 'ja' => ''), 'dimension' => '1', 'group' => '60', 'sql' => <<=\$beginDate) and if(\$endDate='',1,t1.end<=\$endDate) +where t1.deleted='0' +and t1.type in ('sprint','stage') +and (case when \$project='' then 1 else t5.id=\$project end) +and (case when \$status='' then 1 else t1.status=\$status end) +and (case when \$beginDate='' then 1 else t1.begin>=\$beginDate end) +and (case when \$endDate='' then 1 else t1.end<=\$endDate end) EOT, 'settings' => array ( @@ -918,15 +1072,25 @@ $config->bi->builtin->pivots[] = array 'id' => 1012, 'name' => array('zh-cn' => '项目需求状态分布表', 'zh-tw' => '項目需求狀態分布表', 'en' => 'Project Story Status', 'de' => 'Project Story Status', 'fr' => 'Project Story Status', 'vi' => 'Project Story Status', 'ja' => 'Project Story Status'), 'code' => 'projectStoryStatus', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '按照项目统计需求的状态分布情况。', 'zh-tw' => '按照項目統計需求的狀態分布情況。', 'en' => '', 'de' => '', 'fr' => '', 'vi' => '', 'ja' => ''), 'dimension' => '1', 'group' => '60', 'sql' => << array ( @@ -973,15 +1137,25 @@ $config->bi->builtin->pivots[] = array 'id' => 1013, 'name' => array('zh-cn' => '项目需求阶段分布表', 'zh-tw' => '項目需求階段分布表', 'en' => 'Project Stage Report', 'de' => 'Project Stage Report', 'fr' => 'Project Stage Report', 'vi' => 'Project Stage Report', 'ja' => 'Project Stage Report'), 'code' => 'projectStoryStage', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '按照项目统计需求阶段分布情况。', 'zh-tw' => '按照項目統計需求階段分布情況。', 'en' => '', 'de' => '', 'fr' => '', 'vi' => '', 'ja' => ''), 'dimension' => '1', 'group' => '60', 'sql' => << array ( @@ -1030,14 +1204,26 @@ $config->bi->builtin->pivots[] = array 'id' => 1014, 'name' => array('zh-cn' => '项目Bug解决方案分布表', 'zh-tw' => '項目Bug解決方案分布表', 'en' => 'Project Bug Resolution', 'de' => 'Project Bug Resolution', 'fr' => 'Project Bug Resolution', 'vi' => 'Project Bug Resolution', 'ja' => 'Project Bug Resolution'), 'code' => 'projectBugResolution', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '按照项目统计Bug的解决方案分布情况。', 'zh-tw' => '按照項目統計Bug的解決方案分布情況。', 'en' => '', 'de' => '', 'fr' => '', 'vi' => '', 'ja' => ''), 'dimension' => '1', 'group' => '60,61', 'sql' => << array ( @@ -1085,14 +1271,25 @@ $config->bi->builtin->pivots[] = array 'id' => 1015, 'name' => array('zh-cn' => '项目Bug状态分布表', 'zh-tw' => '項目Bug狀態分布表', 'en' => 'Project Bug Status', 'de' => 'Project Bug Status', 'fr' => 'Project Bug Status', 'vi' => 'Project Bug Status', 'ja' => 'Project Bug Status'), 'code' => 'projectBugStatus', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '按照项目统计Bug的状态分布情况。', 'zh-tw' => '按照項目統計Bug的狀態分布情況。', 'en' => '', 'de' => '', 'fr' => '', 'vi' => '', 'ja' => ''), 'dimension' => '1', 'group' => '60,61', 'sql' => << array ( @@ -1140,14 +1337,25 @@ $config->bi->builtin->pivots[] = array 'id' => 1016, 'name' => array('zh-cn' => '项目Bug创建者分布表', 'zh-tw' => '項目Bug創建者分布表', 'en' => 'Project Bug Opened', 'de' => 'Project Bug Opened', 'fr' => 'Project Bug Opened', 'vi' => 'Project Bug Opened', 'ja' => 'Project Bug Opened'), 'code' => 'projectBugOpenedBy', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '按照项目统计Bug的创建者分布情况。', 'zh-tw' => '按照項目統計Bug的創建者分布情況。', 'en' => '', 'de' => '', 'fr' => '', 'vi' => '', 'ja' => ''), 'dimension' => '1', 'group' => '60,61', 'sql' => << array ( @@ -1195,14 +1403,27 @@ $config->bi->builtin->pivots[] = array 'id' => 1017, 'name' => array('zh-cn' => '项目Bug解决者分布表', 'zh-tw' => '項目Bug解決者分布表', 'en' => 'Project Bug Resolve', 'de' => 'Project Bug Resolve', 'fr' => 'Project Bug Resolve', 'vi' => 'Project Bug Resolve', 'ja' => 'Project Bug Resolve'), 'code' => 'projectBugResolvedBy', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '按照项目统计Bug的解决者分布情况。', 'zh-tw' => '按照項目統計Bug的解決者分布情況。', 'en' => '', 'de' => '', 'fr' => '', 'vi' => '', 'ja' => ''), 'dimension' => '1', 'group' => '60,61', 'sql' => << array ( @@ -1250,14 +1471,25 @@ $config->bi->builtin->pivots[] = array 'id' => 1018, 'name' => array('zh-cn' => '项目Bug指派给分布表', 'zh-tw' => '項目Bug指派給分布表', 'en' => 'Project Bug Assign', 'de' => 'Project Bug Assign', 'fr' => 'Project Bug Assign', 'vi' => 'Project Bug Assign', 'ja' => 'Project Bug Assign'), 'code' => 'projectBugAssignedBy', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '按照项目统计Bug的指派给分布情况。', 'zh-tw' => '按照項目統計Bug的指派給分布情況。', 'en' => '', 'de' => '', 'fr' => '', 'vi' => '', 'ja' => ''), 'dimension' => '1', 'group' => '60,61', 'sql' => << array ( @@ -1305,16 +1537,34 @@ $config->bi->builtin->pivots[] = array 'id' => 1019, 'name' => array('zh-cn' => '项目质量表', 'zh-tw' => '項目質量表', 'en' => 'Project Quality Report', 'de' => 'Project Quality Report', 'fr' => 'Project Quality Report', 'vi' => 'Project Quality Report', 'ja' => 'Project Quality Report'), 'code' => 'projectQuality', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '列出项目的需求总数,完成需求数,任务总数,完成的任务数,Bug数,解决的Bug数,Bug/需求,Bug/任务,重要Bug数量(严重程度不大于3)。', 'zh-tw' => '列出項目的需求總數,完成需求數,任務總數,完成的任務數,Bug數,解決的Bug數,Bug/需求,Bug/任務,重要Bug數量(嚴重程度不大於3)。', 'en' => '', 'de' => '', 'fr' => '', 'vi' => '', 'ja' => ''), 'dimension' => '1', 'group' => '60', 'sql' => << array ( @@ -1387,14 +1637,21 @@ $config->bi->builtin->pivots[] = array 'id' => 1020, 'name' => array('zh-cn' => '产品Bug类型统计表', 'zh-tw' => '產品Bug類型統計表', 'en' => 'Bug Type of Product', 'de' => 'Bug Type of Product', 'fr' => 'Bug Type of Product'), 'code' => 'productBugType', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '按照产品统计Bug的类型分布情况。', 'zh-tw' => '按照產品統計Bug的類型分布情況。', 'en' => 'Type distribution of Bugs.', 'de' => 'Type distribution of Bugs.', 'fr' => 'Type distribution of Bugs.'), 'dimension' => '1', 'group' => '59,61', 'sql' => << array @@ -1432,15 +1689,25 @@ $config->bi->builtin->pivots[] = array 'id' => 1021, 'name' => array('zh-cn' => '产品质量表', 'zh-tw' => '產品質量表', 'en' => 'Product Quality', 'de' => 'Product Quality', 'fr' => 'Product Quality'), 'code' => 'productQuality', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '列出产品的需求数,完成的需求总数,Bug数,解决的Bug总数,Bug/需求,重要Bug数量(严重程度小于3)。', 'zh-tw' => '列出產品的需求數,完成的需求總數,Bug數,解決的Bug總數,Bug/需求,重要Bug數量(嚴重程度小於3)。', 'en' => 'Serious Bug (severity is less than 3).', 'de' => 'Serious Bug (severity is less than 3).', 'fr' => 'Serious Bug (severity is less than 3).'), 'dimension' => '1', 'group' => '59', 'sql' => << array @@ -1488,11 +1755,19 @@ $config->bi->builtin->pivots[] = array 'id' => 1022, 'name' => array('zh-cn' => '员工登录次数统计表', 'zh-tw' => '員工登錄次數統計表', 'en' => 'Login Times', 'de' => 'Login Times', 'fr' => 'Login Times'), 'code' => 'loginTimes', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '实现员工登录次数统计报表,按照天统计每天每个人的登录次数,以及总数。', 'zh-tw' => '實現員工登錄次數統計報表,按照天統計每天每個人的登錄次數,以及總數。', 'en' => 'The summary of user login times.', 'de' => 'The summary of user login times.', 'fr' => 'The summary of user login times.'), 'dimension' => '1', 'group' => '62', 'sql' => <<=\$startDate) and if(\$endDate='',1,`date`<=\$endDate) order by `date` asc, actor asc +select + actor, + cast(date as date) as day +from zt_action +where "action"='login' +and (case when \$startDate='' then 1 else date>=cast(\$startDate as date) end) +and (case when \$endDate='' then 1 else date<=cast(\$endDate as date) end) +order by date asc, actor asc EOT, 'settings' => array ( @@ -1538,11 +1813,26 @@ $config->bi->builtin->pivots[] = array 'id' => 1023, 'name' => array('zh-cn' => '日志汇总表', 'zh-tw' => '日誌匯總表', 'en' => 'Effort Summary', 'de' => 'Effort Summary', 'fr' => 'Effort Summary'), 'code' => 'effortSummary', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '查看某个时间段内的日志情况,可以按照部门选择。', 'zh-tw' => '查看某個時間段內的日誌情況,可以按照部門選擇。', 'en' => 'Effort summary of users.', 'de' => 'Effort summary of users', 'fr' => 'Effort summary of users'), 'dimension' => '1', 'group' => '62', 'sql' => <<= \$startDate) and if(\$endDate='', 1, t1.`date` <= \$endDate) and (t3.path like concat((select path from zt_dept where id=\$dept), '%') or \$dept=0) order by t1.`date` asc +select + t1.account, + t1.consumed, + t1.`date`, + t2.dept as dept +from zt_effort as t1 +left join zt_user as t2 on t1.account = t2.account +left join zt_dept as t3 on t2.dept = t3.id +where t1.`deleted` = '0' +and (case when \$startDate='' then 1 else t1.`date` >= cast(\$startDate as date) end) +and (case when \$endDate='' then 1 else t1.`date` <= cast(\$endDate as date) end) +and (case when \$dept='' then 1 +else t3.path like concat((select path from zt_dept where id=coalesce(cast(nullif(\$dept, '') as integer), 0)), '%') +end) +order by t1.`date` asc EOT, 'settings' => array ( @@ -1592,107 +1882,48 @@ $config->bi->builtin->pivots[] = array 'id' => 1024, 'name' => array('zh-cn' => '公司动态汇总表', 'zh-tw' => '公司動態匯總表', 'en' => 'Company Dynamics', 'de' => 'Company Dynamics', 'fr' => 'Company Dynamics'), 'code' => 'companyDynamics', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '可以指定一个时期,列出相应的数据:1. 每天的登录次数。2. 每天的日志工时量。3. 每天新增的需求数。4. 每天关闭的需求数。5. 每天新增的任务数。6. 每天完成的任务数。7. 每天新增的Bug数。8. 每天解决的Bug数。9. 每天的动态数。', 'zh-tw' => '可以指定一個時期,列出相應的數據:1. 每天的登錄次數。2. 每天的日誌工時量。3. 每天新增的需求數。4. 每天關閉的需求數。5. 每天新增的任務數。6. 每天完成的任務數。7. 每天新增的Bug數。8. 每天解決的Bug數。9. 每天的動態數。', 'en' => 'The summary of company dynamics', 'de' => 'The summary of company dynamics', 'fr' => 'The summary of company dynamics'), 'dimension' => '1', 'group' => '62', - 'sql' => <<<'EOT' + 'sql' => <<=$startDate) and if($endDate='',1,t1.date<=$endDate) - group by cast(t1.date as date) - union all - select - cast(t1.date as date) as day, - 0 as userlogin, - 0 as consumed, - sum(case when t1.action = 'opened' then 1 else 0 end) as storyopen, - count(distinct case when t1.action = 'closed' and t2.status = 'closed' then t2.id else 0 end) - 1 as storyclose, - 0 as taskopen, - 0 as taskfinish, - 0 as bugopen, - 0 as bugresolve, - 0 as actions - from zt_action t1 - left join zt_story t2 on t1.objectID = t2.id - where t2.deleted = '0' and t1.objectType = 'story' and t1.action in ('opened', 'closed') and if($startDate='',1,t1.date>=$startDate) and if($endDate='',1,t1.date<=$endDate) - group by cast(t1.date as date) - union all - select - cast(t1.date as date) as day, - 0 as userlogin, - 0 as consumed, - 0 as storyopen, - 0 as storyclose, - sum(case when t1.action = 'opened' then 1 else 0 end) as taskopen, - count(distinct case when t1.action = 'finished' and t2.status in ('done', 'closed') then t2.id else 0 end) - 1 as taskfinish, - 0 as bugopen, - 0 as bugresolve, - 0 as actions - from zt_action t1 - left join zt_task t2 on t1.objectID = t2.id - where t2.deleted = '0' and t1.objectType = 'task' and t1.action in ('opened', 'finished') and if($startDate='',1,t1.date>=$startDate) and if($endDate='',1,t1.date<=$endDate) - group by cast(t1.date as date) - union all - select - cast(t1.date as date) as day, - 0 as userlogin, - 0 as consumed, - 0 as storyopen, - 0 as storyclose, - 0 as taskopen, - 0 as taskfinish, - sum(case when t1.action = 'opened' then 1 else 0 end) as bugopen, - count(distinct case when t1.action = 'resolved' and t2.status in ('resolved', 'closed') then t2.id else 0 end) - 1 as bugresolve, - 0 as actions - from zt_action t1 - left join zt_bug t2 on t1.objectID = t2.id - where t2.deleted = '0' and t1.objectType = 'bug' and t1.action in ('opened', 'resolved') and if($startDate='',1,t1.date>=$startDate) and if($endDate='',1,t1.date<=$endDate) - group by cast(t1.date as date) - union all - select - t2.date as day, - 0 as userlogin, - round(sum(t2.`consumed`), 1) as consumed, - 0 as storyopen, - 0 as storyclose, - 0 as taskopen, - 0 as taskfinish, - 0 as bugopen, - 0 as bugresolve, - 0 as actions - from zt_effort t2 - where if($startDate='',1,t2.date>=$startDate) and if($endDate='',1,t2.date<=$endDate) - group by t2.date -) as t1 -group by t1.day -order by t1.day desc limit 99999999 + day, + ifnull(max(actions),0) actions, + ifnull(max(userlogin),0) userlogin, + ifnull(max(consumed),0) consumed, + ifnull(max(storyopen),0) storyopen, + ifnull(max(storyclose),0) storyclose, + ifnull(max(taskopen),0) taskopen, + ifnull(max(taskfinish),0) taskfinish, + ifnull(max(bugopen),0) bugopen, + ifnull(max(bugresolve),0) bugresolve, + from +(select day,actions from ztv_dayactions +union all by name +select day,userlogin from ztv_dayuserlogin +union all by name +select date as day,consumed from ztv_dayeffort +union all by name +select day,storyopen from ztv_daystoryopen +union all by name +select day,storyclose from ztv_daystoryclose +union all by name +select day,taskopen from ztv_daytaskopen +union all by name +select day,taskfinish from ztv_daytaskfinish +union all by name +select day,bugopen from ztv_daybugopen +union all by name +select day,bugresolve from ztv_daybugresolve +) as uniontable +where (case when \$startDate='' then 1 else day>=\$startDate end) +and (case when \$endDate='' then 1 else day<=\$endDate end) +group by day EOT, 'settings' => array ( 'group1' => 'day', - 'group2' => '', 'columnTotal' => 'sum', 'columns' => array ( @@ -1755,11 +1986,22 @@ $config->bi->builtin->pivots[] = array 'id' => 1025, 'name' => array('zh-cn' => 'Bug解决表', 'zh-tw' => 'Bug解決表', 'en' => 'Solved Bugs', 'de' => 'Solved Bugs', 'fr' => 'Solved Bugs'), 'code' => 'slovedBugs', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '列出解决的Bug总数,解决方案的分布,占的比例(该用户解决的Bug的数量占所有的解决的Bug的数量)。', 'zh-tw' => '列出解決的Bug總數,解決方案的分布,占的比例(該用戶解決的Bug的數量占所有的解決的Bug的數量)。', 'en' => 'percentage:self resolved / all resolved', 'de' => 'percentage:self resolved / all resolved', 'fr' => 'percentage:self resolved / all resolved'), 'dimension' => '1', 'group' => '61', 'sql' => <<=\$startDate) and if(\$endDate='',1,t1.resolvedDate<=\$endDate) having customproduct=\$product +select + t1.*, + (case when \$product='' then 0 else t1.product end) as customproduct +from zt_bug as t1 +left join zt_product as t2 on t1.product = t2.id +where t1.deleted='0' +and t2.deleted='0' +and t1.resolution!='' +and (case when \$startDate='' then 1 else t1.resolvedDate>=cast(\$startDate as date) end) +and (case when \$endDate='' then 1 else t1.resolvedDate<=cast(\$endDate as date) end) +and (case when \$product = '' then 1 else customproduct=\$product end) EOT, 'settings' => array ( @@ -1866,15 +2108,31 @@ $config->bi->builtin->pivots[] = array 'id' => 1026, 'name' => array('zh-cn' => '项目进展表', 'zh-tw' => '項目進展表', 'en' => 'Project Progress Report', 'de' => 'Project Progress Report', 'fr' => 'Project Progress Report', 'vi' => 'Project Progress Report', 'ja' => 'Project Progress Report'), 'code' => 'projectProgress', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '项目的需求数,任务数,已消耗工时,剩余工时,剩余需求数,剩余任务数,进度。', 'zh-tw' => '項目的需求數,任務數,已消耗工時,剩餘工時,剩餘需求數,剩餘任務數,進度。', 'en' => '', 'de' => '', 'fr' => '', 'vi' => '', 'ja' => ''), 'dimension' => '1', 'group' => '60', 'sql' => << array ( @@ -1940,14 +2198,23 @@ $config->bi->builtin->pivots[] = array 'id' => 1027, 'name' => array('zh-cn' => '项目Bug类型统计表', 'zh-tw' => '項目Bug類型統計表', 'en' => 'Project Bug Type', 'de' => 'Project Bug Type', 'fr' => 'Project Bug Type', 'vi' => 'Project Bug Type', 'ja' => 'Project Bug Type'), 'code' => 'projectBugType', + 'driver' => 'duckdb', 'desc' => array('zh-cn' => '按照项目统计Bug的类型分布情况。', 'zh-tw' => '按照項目統計Bug的類型分布情況。', 'en' => '', 'de' => '', 'fr' => '', 'vi' => '', 'ja' => ''), 'dimension' => '1', 'group' => '60,61', 'sql' => << array ( @@ -1999,7 +2266,6 @@ EOT, 'stage' => 'published', 'builtin' => '0' ); - $config->bi->builtin->pivots[] = array ( 'id' => 1028, diff --git a/module/bi/control.php b/module/bi/control.php new file mode 100644 index 0000000000..0f78527cd9 --- /dev/null +++ b/module/bi/control.php @@ -0,0 +1,55 @@ + + * @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); + } +} diff --git a/module/bi/model.php b/module/bi/model.php index d5a10db647..72768ef9d8 100644 --- a/module/bi/model.php +++ b/module/bi/model.php @@ -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. diff --git a/module/bi/test/lib/bi.unittest.class.php b/module/bi/test/lib/bi.unittest.class.php new file mode 100644 index 0000000000..86e2e4f189 --- /dev/null +++ b/module/bi/test/lib/bi.unittest.class.php @@ -0,0 +1,34 @@ +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; + } +} diff --git a/module/bi/test/model/parsesql.php b/module/bi/test/model/parsesql.php new file mode 100755 index 0000000000..b18626f5d4 --- /dev/null +++ b/module/bi/test/model/parsesql.php @@ -0,0 +1,97 @@ +#!/usr/bin/env php +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] = << t1.estimate +) +EOT; + +$sqls[3] = << t1.estimate +) +EOT; + +$sqls[4] = << '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 \ No newline at end of file diff --git a/module/chart/control.php b/module/chart/control.php index 672399e284..10714ea947 100644 --- a/module/chart/control.php +++ b/module/chart/control.php @@ -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); diff --git a/module/chart/model.php b/module/chart/model.php index 7443ff97be..20f000a92d 100644 --- a/module/chart/model.php +++ b/module/chart/model.php @@ -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. diff --git a/module/chart/tao.php b/module/chart/tao.php index e13fe25fdc..2a66ce6054 100644 --- a/module/chart/tao.php +++ b/module/chart/tao.php @@ -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); - } } diff --git a/module/dataview/config.php b/module/dataview/config.php index 702c837065..7d2fa973a2 100644 --- a/module/dataview/config.php +++ b/module/dataview/config.php @@ -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'; diff --git a/module/dataview/config/table.php b/module/dataview/config/table.php new file mode 100644 index 0000000000..5ee9f627ad --- /dev/null +++ b/module/dataview/config/table.php @@ -0,0 +1,23 @@ +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'; diff --git a/module/dataview/js/querybase.js b/module/dataview/js/querybase.js index 98d835eb37..afc3445c48 100644 --- a/module/dataview/js/querybase.js +++ b/module/dataview/js/querybase.js @@ -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'); diff --git a/module/dataview/lang/de.php b/module/dataview/lang/de.php index ec3d170c60..254eea6deb 100644 --- a/module/dataview/lang/de.php +++ b/module/dataview/lang/de.php @@ -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'; diff --git a/module/dataview/lang/en.php b/module/dataview/lang/en.php index ec3d170c60..254eea6deb 100644 --- a/module/dataview/lang/en.php +++ b/module/dataview/lang/en.php @@ -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'; diff --git a/module/dataview/lang/fr.php b/module/dataview/lang/fr.php index ec3d170c60..254eea6deb 100644 --- a/module/dataview/lang/fr.php +++ b/module/dataview/lang/fr.php @@ -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'; diff --git a/module/dataview/lang/zh-cn.php b/module/dataview/lang/zh-cn.php index 29847fd14a..d72914ba8b 100644 --- a/module/dataview/lang/zh-cn.php +++ b/module/dataview/lang/zh-cn.php @@ -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 = '详情'; diff --git a/module/dataview/model.php b/module/dataview/model.php index 421666baa2..6558700b0e 100644 --- a/module/dataview/model.php +++ b/module/dataview/model.php @@ -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; } diff --git a/module/pivot/control.php b/module/pivot/control.php index cc141d791d..94f9b1435f 100644 --- a/module/pivot/control.php +++ b/module/pivot/control.php @@ -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); diff --git a/module/pivot/model.php b/module/pivot/model.php index b1e5ae7a9e..d05bce223f 100644 --- a/module/pivot/model.php +++ b/module/pivot/model.php @@ -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; diff --git a/module/pivot/zen.php b/module/pivot/zen.php index d06fbdca41..890ffcdf06 100644 --- a/module/pivot/zen.php +++ b/module/pivot/zen.php @@ -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; diff --git a/module/screen/model.php b/module/screen/model.php index 3bb9350af1..37c22de89b 100644 --- a/module/screen/model.php +++ b/module/screen/model.php @@ -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); } /** diff --git a/module/tree/model.php b/module/tree/model.php index ee29e764ee..2d42d172fe 100644 --- a/module/tree/model.php +++ b/module/tree/model.php @@ -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) { diff --git a/module/upgrade/control.php b/module/upgrade/control.php index 9c5c4bab98..50fbcb0acb 100644 --- a/module/upgrade/control.php +++ b/module/upgrade/control.php @@ -28,6 +28,11 @@ class upgrade extends control $this->locate(inlink('backup')); } + public function importBIData() + { + $this->loadModel('install')->importBIData(); + } + /** * 授权协议页面。 * Check agree license. diff --git a/module/upgrade/model.php b/module/upgrade/model.php index 0ad39902cd..1784ce6a82 100644 --- a/module/upgrade/model.php +++ b/module/upgrade/model.php @@ -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;