keboola/gooddata-php-client

View on GitHub
src/ProjectModel.php

Summary

Maintainability
D
2 days
Test Coverage
<?php
/**
 * @package gooddata-php-client
 * @copyright Keboola
 * @author Jakub Matejka <jakub@keboola.com>
 */
namespace Keboola\GoodData;

class ProjectModel
{
    /** @var  Client */
    protected $client;

    public function __construct(Client $client)
    {
        $this->client = $client;
    }

    public function view($pid)
    {
        $uri = "/gdc/projects/$pid/model/view?includeDeprecated=true&includeGrain=true";
        $result = $this->client->get($uri);

        if (isset($result['asyncTask']['link']['poll'])) {
            $try = 1;
            do {
                sleep(10 * $try);
                $taskResponse = $this->client->get($result['asyncTask']['link']['poll']);

                if (!isset($taskResponse['asyncTask']['link']['poll'])) {
                    if (isset($taskResponse['projectModelView']['model'])) {
                        return $taskResponse['projectModelView']['model'];
                    } else {
                        throw Exception::unexpectedResponseError(
                            'Polling of Model view could not be finished',
                            'GET',
                            $result['asyncTask']['link']['poll'],
                            $taskResponse
                        );
                    }
                }

                $try++;
            } while (true);
        } else {
            throw Exception::unexpectedResponseError('Model view failed', 'GET', $uri, $result);
        }

        return false;
    }

    public function diff($pid, $model)
    {
        $uri = "/gdc/projects/$pid/model/diff?includeDeprecated=true&includeGrain=true";
        $result = $this->client->post($uri, ['diffRequest' => ['targetModel' => $model]]);

        if (isset($result['asyncTask']['link']['poll'])) {
            $try = 1;
            do {
                sleep(10 * $try);
                $taskResponse = $this->client->get($result['asyncTask']['link']['poll']);

                if (!isset($taskResponse['asyncTask']['link']['poll'])) {
                    if (isset($taskResponse['projectModelDiff']['updateScripts'])) {
                        $lessDestructive = [];
                        $moreDestructive = [];
                        // Preserve data if possible
                        foreach ($taskResponse['projectModelDiff']['updateScripts'] as $updateScript) {
                            if ($updateScript['updateScript']['preserveData']
                                && !$updateScript['updateScript']['cascadeDrops']) {
                                $lessDestructive = $updateScript['updateScript']['maqlDdlChunks'];
                            }
                            if (!count($lessDestructive) && !$updateScript['updateScript']['preserveData']
                                && !$updateScript['updateScript']['cascadeDrops']) {
                                $lessDestructive = $updateScript['updateScript']['maqlDdlChunks'];
                            }
                            if (!$updateScript['updateScript']['preserveData']
                                && $updateScript['updateScript']['cascadeDrops']) {
                                $moreDestructive = $updateScript['updateScript']['maqlDdlChunks'];
                            }
                            if (!count($moreDestructive) && $updateScript['updateScript']['preserveData']
                                && $updateScript['updateScript']['cascadeDrops']) {
                                $moreDestructive = $updateScript['updateScript']['maqlDdlChunks'];
                            }
                        }

                        $description = [];
                        foreach ($taskResponse['projectModelDiff']['updateOperations'] as $o) {
                            $description[] = vsprintf(
                                $o['updateOperation']['description'],
                                $o['updateOperation']['parameters']
                            );
                        }

                        if (!count($lessDestructive) && count($moreDestructive)) {
                            $lessDestructive = $moreDestructive;
                            $moreDestructive = [];
                        }

                        return [
                            'moreDestructiveMaql' => $moreDestructive,
                            'lessDestructiveMaql' => $lessDestructive,
                            'description' => $description
                        ];
                    } else {
                        throw Exception::unexpectedResponseError(
                            'Polling of Model diff could not be finished',
                            'GET',
                            $result['asyncTask']['link']['poll'],
                            $taskResponse
                        );
                    }
                }

                $try++;
            } while (true);
        } else {
            throw Exception::unexpectedResponseError(
                'Polling of Model diff could not be started',
                'POST',
                $uri,
                $result
            );
        }

        return false;
    }

    public function normalizeModel($model)
    {
        foreach ($model['projectModel']['datasets'] as &$d) {
            if (isset($d['dataset']['anchor']['attribute']['labels'])) {
                foreach ($d['dataset']['anchor']['attribute']['labels'] as &$l) {
                    if (isset($l['label']['dataType']) && $l['label']['dataType'] == 'DECIMAL(16,2)') {
                        $l['label']['dataType'] = 'DECIMAL(15,2)';
                    }
                }
            }
            if (isset($d['dataset']['attributes'])) {
                foreach ($d['dataset']['attributes'] as &$a) {
                    if (isset($a['attribute']['labels'])) {
                        foreach ($a['attribute']['labels'] as &$l) {
                            if (isset($l['label']['dataType']) && $l['label']['dataType'] == 'DECIMAL(16,2)') {
                                $l['label']['dataType'] = 'DECIMAL(15,2)';
                            }
                        }
                    }
                }
            }
            if (isset($d['dataset']['facts'])) {
                foreach ($d['dataset']['facts'] as &$f) {
                    if (isset($f['fact']['dataType']) && $f['fact']['dataType'] == 'DECIMAL(16,2)') {
                        $f['fact']['dataType'] = 'DECIMAL(15,2)';
                    }
                }
            }
        }
        return $model;
    }

    public function update($pid, $model, $dryRun = false, $multivalueAttributes = [])
    {
        $model = $this->normalizeModel($model);

        $update = $this->diff($pid, $model);
        if ($dryRun) {
            return $update;
        } else {
            if (count($update['lessDestructiveMaql'])) {
                foreach ($update['lessDestructiveMaql'] as $i => $m) {
                    try {
                        $m = $this->updateMaqlWithMultivalueAttributes($m, $multivalueAttributes);
                        $this->client->getDatasets()->executeMaql($pid, $m);
                    } catch (Exception $e) {
                        if (!empty($update['moreDestructiveMaql'][$i])) {
                            $m = $this->updateMaqlWithMultivalueAttributes($update['moreDestructiveMaql'][$i], $multivalueAttributes);
                            $this->client->getDatasets()->executeMaql($pid, $m);
                        } else {
                            throw $e;
                        }
                    }
                    return [
                        'description' => $update['description'],
                        'maql' => $m,
                    ];
                }
            }
            return false;
        }
    }

    private function updateMaqlWithMultivalueAttributes(string $maql, array $multivalueAttributes): string
    {
        $lines = explode("\n", $maql);

        foreach ($multivalueAttributes as $fkName => $attribute) {
            foreach ($lines as &$line) {
                if (strpos($line, sprintf('ALTER ATTRIBUTE {%s} ADD KEYS {%s}', $attribute, $fkName)) === 0) {
                    $line = str_replace(';', ' MULTIVALUE;', $line);
                    break;
                }
            }
        }

        return implode("\n", $lines);
    }

    public function updateDataSet($pid, $datasetModel, $dryRun = false)
    {
        $projectModel = $this->view($pid);

        if (!isset($projectModel['projectModel']['datasets'])) {
            $projectModel['projectModel']['datasets'] = [];
        }
        $dataSetFound = false;
        foreach ($projectModel['projectModel']['datasets'] as &$dataSet) {
            if ($dataSet['dataset']['identifier'] == $datasetModel['identifier']) {
                $dataSetFound = true;
                $dataSet['dataset'] = $datasetModel;
                break;
            }
        }

        if (!$dataSetFound) {
            $projectModel['projectModel']['datasets'][] = ['dataset' => $datasetModel];
        }

        return $this->update($pid, $projectModel, $dryRun);
    }


    public function dropDataSet($pid, $dataSetName)
    {
        $dataSetId = Identifiers::getIdentifier($dataSetName);
        $model = $this->view($pid);
        if (isset($model['projectModel']['datasets'])) {
            foreach ($model['projectModel']['datasets'] as $i => $dataSet) {
                if ($dataSet['dataset']['title'] == $dataSetName) {
                    unset($model['projectModel']['datasets'][$i]);
                    break;
                }
            }
            $model['projectModel']['datasets'] = array_values($model['projectModel']['datasets']);
        }

        $update = $this->diff($pid, $model);

        if (!count($update['moreDestructiveMaql'])) {
            $update['moreDestructiveMaql'] = $update['lessDestructiveMaql'];
        }
        if (count($update['moreDestructiveMaql'])) {
            foreach ($update['moreDestructiveMaql'] as $m) {
                $this->client->getDatasets()->executeMaql($pid, $m);
            }
            $this->client->getDatasets()->executeMaql($pid, sprintf('DROP IF EXISTS {dim.%s};', $dataSetId));
            $this->client->getDatasets()->executeMaql($pid, sprintf('DROP IF EXISTS {ffld.%s};', $dataSetId));

            return $update['description'];
        }
        return false;
    }

    public function updateProject($pid, $model, $dryRun = false)
    {
        // Get current GD model
        $gdModel = $this->view($pid);
        if (!isset($gdModel['projectModel']['datasets'])) {
            $gdModel['projectModel']['datasets'] = [];
        }

        // Get writer model
        $writerDatasets = [];
        foreach ($model['projectModel']['datasets'] as $d) {
            $writerDatasets[$d['dataset']['identifier']] = $d;
        }

        // Replace existing datasets
        foreach ($gdModel['projectModel']['datasets'] as &$d) {
            if (in_array($d['dataset']['identifier'], array_keys($writerDatasets))) {
                $d['dataset'] = $writerDatasets[$d['dataset']['identifier']]['dataset'];
                unset($writerDatasets[$d['dataset']['identifier']]);
            }
        }
        unset($d);

        // Add new datasets
        foreach ($writerDatasets as $d) {
            $gdModel['projectModel']['datasets'][] = $d;
        }

        // Update
        return $this->update($pid, $gdModel, $dryRun, $model['multivalueAttributes'] ?? []);
    }
}