<?php

/**
 * This file is part of the pd-admin pd-widget package.
 *
 * @package     pd-widget
 *
 * @license     LICENSE
 * @author      Kerem APAYDIN <kerem@apaydin.me>
 *
 * @link        https://github.com/appaydin/pd-widget
 */

namespace Pd\WidgetBundle\Render;

use Pd\WidgetBundle\Builder\ItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

/**
 * Widget Twig Render.
 *
 * @author Kerem APAYDIN <kerem@apaydin.me>
 */
class TwigRender implements RenderInterface
{
    /**
     * @var \Twig_Environment
     */
    private $engine;

    /**
     * @var string
     */
    private $baseTemplate;

    /**
     * @var CacheItemPoolInterface
     */
    private $cache;

    /**
     * @var TokenStorageInterface
     */
    private $tokenStorage;

    /**
     * WidgetRender constructor.
     *
     * @param \Twig_Environment $engine
     * @param string            $baseTemplate
     */
    public function __construct(\Twig_Environment $engine, CacheItemPoolInterface $cache, TokenStorageInterface $tokenStorage, string $baseTemplate)
    {
        $this->engine = $engine;
        $this->cache = $cache;
        $this->tokenStorage = $tokenStorage;
        $this->baseTemplate = $baseTemplate;
    }

    /**
     * Render Widgets.
     *
     * @param $widgets ItemInterface[]
     * @param bool $base
     *
     * @throws \Twig_Error_Loader
     * @throws \Twig_Error_Runtime
     * @throws \Twig_Error_Syntax
     *
     * @return string
     */
    public function render($widgets, bool $base = true)
    {
        if (!$widgets) {
            return false;
        }

        // Output Storage
        $output = '';

        // Get User ID
        $userId = $this->tokenStorage->getToken()->getUser()->getId();

        foreach ($widgets as $widget) {
            if ($widget->isActive()) {
                $output .= $this->getOutput($widget, $userId);
            }
        }

        // Render Base
        if ($base) {
            $output = $this->engine->render($this->baseTemplate, ['widgets' => $output]);
        }

        return $output;
    }

    /**
     * Get Widget Output for Cache.
     *
     * @param ItemInterface $item
     * @param $userId
     *
     * @throws \Psr\Cache\InvalidArgumentException
     * @throws \Twig_Error_Loader
     * @throws \Twig_Error_Runtime
     * @throws \Twig_Error_Syntax
     *
     * @return mixed|string
     */
    public function getOutput(ItemInterface $item, $userId)
    {
        if ($item->getCacheTime()) {
            // Get Cache Item
            $cache = $this->cache->getItem($item->getId().$userId);

            // Set Cache Expires
            $cache->expiresAfter($item->getCacheTime());

            // Save
            if (false === $cache->isHit()) {
                $cache->set($item->getTemplate() ? $this->engine->render($item->getTemplate(), ['widget' => $item]) : $item->getContent());
                $this->cache->save($cache);
            }

            return $cache->get();
        }

        return $item->getTemplate() ? $this->engine->render($item->getTemplate(), ['widget' => $item]) : $item->getContent();
    }
}


<?php

/**
 * This file is part of the pd-admin pd-widget package.
 *
 * @package     pd-widget
 *
 * @license     LICENSE
 * @author      Kerem APAYDIN <kerem@apaydin.me>
 *
 * @link        https://github.com/appaydin/pd-widget
 */

namespace Pd\WidgetBundle\Widget;

use Pd\WidgetBundle\Builder\ItemInterface;
use Pd\WidgetBundle\Event\WidgetEvent;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

/**
 * Widget Main.
 *
 * @author Kerem APAYDIN <kerem@apaydin.me>
 */
class Widget implements WidgetInterface
{
    /**
     * Widget Storage.
     *
     * @var array|ItemInterface[]
     */
    private $widgets = [];

    /**
     * @var AuthorizationCheckerInterface
     */
    private $security;

    /**
     * @var EventDispatcherInterface
     */
    private $eventDispatcher;

    /**
     * @var bool
     */
    private $checkRole;

    /**
     * @var CacheItemPoolInterface
     */
    private $cache;

    /**
     * @var TokenStorageInterface
     */
    private $token;

    /**
     * @param AuthorizationCheckerInterface $security
     * @param EventDispatcherInterface      $eventDispatcher
     */
    public function __construct(AuthorizationCheckerInterface $security, EventDispatcherInterface $eventDispatcher, CacheItemPoolInterface $cache, TokenStorageInterface $token)
    {
        $this->security = $security;
        $this->eventDispatcher = $eventDispatcher;
        $this->cache = $cache;
        $this->token = $token;
    }

    /**
     * Get Widgets.
     *
     * @return array|ItemInterface[]
     */
    public function getWidgets($checkRole = true)
    {
        // Check Role
        $this->checkRole = $checkRole;

        // Dispatch Event
        if (!$this->widgets) {
            $this->eventDispatcher->dispatch(WidgetEvent::WIDGET_START, new WidgetEvent($this));
        }

        return $this->widgets;
    }

    /**
     * Add Widget.
     *
     * @param ItemInterface $item
     *
     * @return $this
     */
    public function addWidget(ItemInterface $item)
    {
        // Check Security
        if ($this->checkRole) {
            if ($item->getRole() && !$this->security->isGranted($item->getRole())) {
                return $this;
            }
        }

        // Add
        $this->widgets[$item->getId()] = $item;

        return $this;
    }

    /**
     * Remove Widget.
     *
     * @param string $widgetId
     *
     * @return $this
     */
    public function removeWidget(string $widgetId)
    {
        if (isset($this->widgets[$widgetId])) {
            unset($this->widgets[$widgetId]);
        }

        return $this;
    }

    /**
     * Clear current user widget cache.
     *
     * @throws \Psr\Cache\InvalidArgumentException
     */
    public function clearWidgetCache()
    {
        // Get Widgets
        $widgets = $this->getWidgets(false);
        $userId = $this->token->getToken()->getUser()->getId();

        // Clear Cache
        foreach ($widgets as $widget) {
            $this->cache->deleteItem($widget->getId().$userId);
        }
    }
}


<?php

/**
 * This file is part of the pd-admin pd-widget package.
 *
 * @package     pd-widget
 *
 * @license     LICENSE
 * @author      Kerem APAYDIN <kerem@apaydin.me>
 *
 * @link        https://github.com/appaydin/pd-widget
 */

namespace Pd\WidgetBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Widget Private User Data.
 *
 * @ORM\Entity(repositoryClass="Pd\WidgetBundle\Repository\WidgetUserRepository")
 * @ORM\Table(name="widget_user")
 *
 * @author Kerem APAYDIN <kerem@apaydin.me>
 */
class WidgetUser
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="array")
     */
    private $config;

    /**
     * @ORM\OneToOne(targetEntity="UserInterface")
     * @ORM\JoinColumn(name="owner_id", referencedColumnName="id", unique=true, onDelete="CASCADE")
     */
    private $owner;

    /**
     * Get id.
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set config.
     *
     * @param array $config
     *
     * @return WidgetUser
     */
    public function setConfig($config)
    {
        $this->config = $config;

        return $this;
    }

    /**
     * Get config.
     *
     * @return array
     */
    public function getConfig()
    {
        return $this->config;
    }

    /**
     * Add Widget Config.
     *
     * @param string $widgetId
     * @param array  $config
     *
     * @return $this
     */
    public function addWidgetConfig(string $widgetId, array $config = [])
    {
        $this->config[$widgetId] = array_merge($this->config[$widgetId] ?? [], $config);

        return $this;
    }

    /**
     * Remove Widget Config.
     *
     * @param string $widgetId
     * @param array  $ids
     *
     * @return $this
     */
    public function removeWidgetConfig(string $widgetId, array $config = [])
    {
        foreach ($config as $id => $content) {
            if (isset($this->config[$widgetId][$id])) {
                unset($this->config[$widgetId][$id]);
            }
        }

        return $this;
    }

    /**
     * Set owner.
     *
     * @param UserInterface|null $owner
     *
     * @return WidgetUser
     */
    public function setOwner($owner = null)
    {
        $this->owner = $owner;

        return $this;
    }

    /**
     * Get owner.
     *
     * @return UserInterface|null
     */
    public function getOwner()
    {
        return $this->owner;
    }
}


<?php
/**
 * Yii2 Pages Module
 *
 * @link      https://github.com/hiqdev/yii2-module-pages
 * @package   yii2-module-pages
 * @license   BSD-3-Clause
 * @copyright Copyright (c) 2016-2017, HiQDev (http://hiqdev.com/)
 */

namespace hiqdev\yii2\modules\pages\controllers;

use hiqdev\yii2\modules\pages\models\AbstractPage;
use Yii;
use yii\helpers\Html;
use yii\web\NotFoundHttpException;

class RenderController extends \yii\web\Controller
{
    public function getViewPath()
    {
        return dirname(__DIR__) . '/views/render';
    }

    /**
     * Index action.
     * @param string|null $page
     * @return string rendered page
     * @throws NotFoundHttpException
     * @throws \yii\base\InvalidConfigException
     */
    public function actionIndex(string $page = null): string
    {
        if (!$page) {
            $page = $this->getPageName();
        }

        $page = $this->module->find($page);

        if ($page === null) {
            throw new NotFoundHttpException(Yii::t('yii', 'Page not found.'));
        }

        return $this->renderPage($page);
    }

    /**
     * @return string
     * @throws \yii\base\InvalidConfigException
     */
    private function getPageName(): string
    {
        preg_match('/^.+pages\/(?<pageName>.+)$/', Yii::$app->request->getUrl(), $matches);

        return trim($matches['pageName'], '/');
    }

    /**
     * @param AbstractPage $page
     * @param array $params
     * @return string
     */
    private function renderPage(AbstractPage $page, array $params = []): string
    {
        if ($page->layout) {
            $this->layout = $page->layout;
        }
        if ($page->title) {
            $this->view->title = Html::encode($page->title);
        }
        $this->view->params = $page->getData();
        $params['controller'] = $this;

        return $this->renderContent($page->render($params));
    }

    /**
     * @param string|null $listName
     * @return string
     */
    public function actionList(string $listName = null): string
    {
        $list = $this->module->findList($listName);

        return $this->render('index', ['dataProvider' => $list->getDataProvider()]);
    }
}


<?php
/**
 * Yii2 Pages Module
 *
 * @link      https://github.com/hiqdev/yii2-module-pages
 * @package   yii2-module-pages
 * @license   BSD-3-Clause
 * @copyright Copyright (c) 2016-2017, HiQDev (http://hiqdev.com/)
 */

namespace hiqdev\yii2\modules\pages;

use Yii;

/**
 * Get module trait.
 * Provides get/set module methods.
 *
 * @property Module $module The module to be used, can be found by default.
 */
trait GetModuleTrait
{
    protected $_module = 'pages';

    /**
     * Setter for module.
     * @var string|Module module name or the module object
     */
    public function setModule($module)
    {
        $this->_module = $module;
    }

    /**
     * Getter for module.
     * @return Module
     */
    public function getModule()
    {
        if (!is_object($this->_module)) {
            $this->_module = Yii::$app->getModule($this->_module);
        }

        return $this->_module;
    }
}


<?php

declare(strict_types=1);

namespace Keboola\Db\Import\Snowflake;

use Keboola\Db\Import\Exception;

class Connection
{
    /**
     * @var resource odbc handle
     */
    private $connection;

    /**
     * The connection constructor accepts the following options:
     * - host (string, required) - hostname
     * - port (int, optional) - port - default 443
     * - user (string, required) - username
     * - password (string, required) - password
     * - warehouse (string) - default warehouse to use
     * - database (string) - default database to use
     * - tracing (int) - the level of detail to be logged in the driver trace files
     * - loginTimeout (int) - Specifies how long to wait for a response when connecting to the Snowflake service before returning a login failure error.
     * - networkTimeout (int) - Specifies how long to wait for a response when interacting with the Snowflake service before returning an error. Zero (0) indicates no network timeout is set.
     * - queryTimeout (int) - Specifies how long to wait for a query to complete before returning an error. Zero (0) indicates to wait indefinitely.
     *
     * @param array $options
     */
    public function __construct(array $options)
    {
        $requiredOptions = [
            'host',
            'user',
            'password',
        ];

        $missingOptions = array_diff($requiredOptions, array_keys($options));
        if (!empty($missingOptions)) {
            throw new Exception('Missing options: ' . implode(', ', $missingOptions));
        }

        $port = isset($options['port']) ? (int) $options['port'] : 443;
        $tracing = isset($options['tracing']) ? (int) $options['tracing'] : 0;
        $maxBackoffAttempts = isset($options['maxBackoffAttempts']) ? (int) $options['maxBackoffAttempts'] : 5;

        $dsn = "Driver=SnowflakeDSIIDriver;Server=" . $options['host'];
        $dsn .= ";Port=" . $port;
        $dsn .= ";Tracing=" . $tracing;

        if (isset($options['loginTimeout'])) {
            $dsn .= ";Login_timeout=" . (int) $options['loginTimeout'];
        }

        if (isset($options['networkTimeout'])) {
            $dsn .= ";Network_timeout=" . (int) $options['networkTimeout'];
        }

        if (isset($options['queryTimeout'])) {
            $dsn .= ";Query_timeout=" . (int) $options['queryTimeout'];
        }

        if (isset($options['database'])) {
            $dsn .= ";Database=" . $this->quoteIdentifier($options['database']);
        }

        if (isset($options['warehouse'])) {
            $dsn .= ";Warehouse=" . $this->quoteIdentifier($options['warehouse']);
        }

        $attemptNumber = 0;
        do {
            if ($attemptNumber > 0) {
                sleep(pow(2, $attemptNumber));
            }
            try {
                $this->connection = odbc_connect($dsn, $options['user'], $options['password']);
            } catch (\Throwable $e) {
                // try again if it is a failed rest request
                if (stristr($e->getMessage(), "S1000") !== false) {
                    $attemptNumber++;
                    if ($attemptNumber > $maxBackoffAttempts) {
                        throw new Exception("Initializing Snowflake connection failed: " . $e->getMessage(), 0, $e);
                    }
                } else {
                    throw new Exception("Initializing Snowflake connection failed: " . $e->getMessage(), 0, $e);
                }
            }
        } while ($this->connection === null);
    }

    public function __destruct()
    {
        odbc_close($this->connection);
    }

    public function quoteIdentifier(string $value): string
    {
        $q = '"';
        return ($q . str_replace("$q", "$q$q", $value) . $q);
    }

    /**
     * Returns information about table:
     *  - name
     *  - bytes
     *  - rows
     * @param string $schemaName
     * @param string $tableName
     * @return array
     * @throws Exception
     */
    public function describeTable(string $schemaName, string $tableName): array
    {
        $tables = $this->fetchAll(sprintf(
            "SHOW TABLES LIKE %s IN SCHEMA %s",
            "'" . addslashes($tableName) . "'",
            $this->quoteIdentifier($schemaName)
        ));

        foreach ($tables as $table) {
            if ($table['name'] === $tableName) {
                return $table;
            }
        }

        throw new Exception("Table $tableName not found in schema $schemaName");
    }

    public function describeTableColumns(string $schemaName, string $tableName): array
    {
        return $this->fetchAll(sprintf(
            'SHOW COLUMNS IN %s.%s',
            $this->quoteIdentifier($schemaName),
            $this->quoteIdentifier($tableName)
        ));
    }

    public function getTableColumns(string $schemaName, string $tableName): array
    {
        return array_map(function ($column) {
            return $column['column_name'];
        }, $this->describeTableColumns($schemaName, $tableName));
    }

    public function getTablePrimaryKey(string $schemaName, string $tableName): array
    {
        $cols = $this->fetchAll(sprintf(
            "DESC TABLE %s.%s",
            $this->quoteIdentifier($schemaName),
            $this->quoteIdentifier($tableName)
        ));
        $pkCols = [];
        foreach ($cols as $col) {
            if ($col['primary key'] !== 'Y') {
                continue;
            }
            $pkCols[] = $col['name'];
        }

        return $pkCols;
    }

    public function query(string $sql, array $bind = []): void
    {
        try {
            $stmt = odbc_prepare($this->connection, $sql);
            odbc_execute($stmt, $this->repairBinding($bind));
            odbc_free_result($stmt);
        } catch (\Throwable $e) {
            throw (new ExceptionHandler())->createException($e);
        }
    }

    public function fetchAll(string $sql, array $bind = []): array
    {
        try {
            $stmt = odbc_prepare($this->connection, $sql);
            odbc_execute($stmt, $this->repairBinding($bind));
            $rows = [];
            while ($row = odbc_fetch_array($stmt)) {
                $rows[] = $row;
            }
            odbc_free_result($stmt);
        } catch (\Throwable $e) {
            throw (new ExceptionHandler())->createException($e);
        }
        return $rows;
    }

    public function fetch(string $sql, array $bind, callable $callback): void
    {
        try {
            $stmt = odbc_prepare($this->connection, $sql);
            odbc_execute($stmt, $this->repairBinding($bind));
            while ($row = odbc_fetch_array($stmt)) {
                $callback($row);
            }
            odbc_free_result($stmt);
        } catch (\Throwable $e) {
            throw (new ExceptionHandler())->createException($e);
        }
    }

    /**
     * Avoid odbc file open http://php.net/manual/en/function.odbc-execute.php
     * @param array $bind
     * @return array
     */
    private function repairBinding(array $bind): array
    {
        return array_map(function ($value) {
            if (preg_match("/^'.*'$/", $value)) {
                return " {$value} ";
            } else {
                return $value;
            }
        }, $bind);
    }
}


<?php

declare(strict_types=1);

namespace Keboola\Db\Import\Snowflake;

use Keboola\Db\Import\ImportInterface;
use Keboola\Db\Import\Exception;
use Tracy\Debugger;
use Keboola\Db\Import\Result;

abstract class ImportBase implements ImportInterface
{

    /**
     * @var Connection
     */
    protected $connection;

    /** @var string */
    protected $schemaName;

    /** @var array  */
    protected $warnings = [];

    /** @var int  */
    protected $importedRowsCount = 0;

    /** @var array  */
    private $timers = [];

    /** @var array  */
    private $importedColumns = [];

    /** @var int  */
    private $ignoreLines = 0;

    /** @var bool  */
    private $incremental = false;

    public const TIMESTAMP_COLUMN_NAME = '_timestamp';

    public function __construct(Connection $connection, string $schemaName)
    {
        $this->connection = $connection;
        $this->schemaName = $schemaName;
    }

    public function import(string $tableName, array $columns, array $sourceData, array $options = []): Result
    {
        $this->validateColumns($tableName, $columns);
        $stagingTableName = $this->createStagingTable($columns);

        try {
            $this->importDataToStagingTable($stagingTableName, $columns, $sourceData);

            if ($this->getIncremental()) {
                $this->insertOrUpdateTargetTable(
                    $stagingTableName,
                    $tableName,
                    $columns,
                    isset($options['useTimestamp']) ? $options['useTimestamp'] : true,
                    isset($options["convertEmptyValuesToNull"]) ? $options["convertEmptyValuesToNull"] : []
                );
            } else {
                Debugger::timer('dedup');
                $this->dedupe(
                    $stagingTableName,
                    $columns,
                    $this->connection->getTablePrimaryKey($this->schemaName, $tableName)
                );
                $this->addTimer('dedup', Debugger::timer('dedup'));
                $this->insertAllIntoTargetTable(
                    $stagingTableName,
                    $tableName,
                    $columns,
                    isset($options['useTimestamp']) ? $options['useTimestamp'] : true,
                    isset($options["convertEmptyValuesToNull"]) ? $options["convertEmptyValuesToNull"] : []
                );
            }
            $this->dropTable($stagingTableName);
            $this->importedColumns = $columns;

            return new Result([
                'warnings' => $this->warnings,
                'timers' => $this->timers,
                'importedRowsCount' => $this->importedRowsCount,
                'importedColumns' => $this->importedColumns,
            ]);
        } catch (\Throwable $e) {
            $this->dropTable($stagingTableName);
            throw $e;
        }
    }

    abstract protected function importDataToStagingTable(string $stagingTableName, array $columns, array $sourceData): void;

    private function validateColumns(string $tableName, array $columnsToImport): void
    {
        if (count($columnsToImport) == 0) {
            throw new Exception(
                'No columns found in CSV file.',
                Exception::NO_COLUMNS
            );
        }

        $tableColumns = $this->connection->getTableColumns($this->schemaName, $tableName);

        $moreColumns = array_diff($columnsToImport, $tableColumns);
        if (!empty($moreColumns)) {
            throw new Exception(
                'Columns doest not match. Non existing columns: ' . implode(', ', $moreColumns),
                Exception::COLUMNS_COUNT_NOT_MATCH
            );
        }
    }

    private function insertAllIntoTargetTable(string $stagingTableName, string $targetTableName, array $columns, bool $useTimestamp = true, array $convertEmptyValuesToNull = []): void
    {
        $this->connection->query('BEGIN TRANSACTION');

        $targetTableNameWithSchema = $this->nameWithSchemaEscaped($targetTableName);
        $stagingTableNameWithSchema = $this->nameWithSchemaEscaped($stagingTableName);

        $this->connection->query('TRUNCATE TABLE ' . $targetTableNameWithSchema);

        $columnsSql = implode(', ', array_map(function ($column) {
            return $this->quoteIdentifier($column);
        }, $columns));

        $columnsSetSqlSelect = implode(', ', array_map(function ($column) use ($convertEmptyValuesToNull) {
            if (in_array($column, $convertEmptyValuesToNull)) {
                return sprintf(
                    'IFF(%s = \'\', NULL, %s)',
                    $this->quoteIdentifier($column),
                    $this->quoteIdentifier($column)
                );
            }

            return sprintf(
                "COALESCE(%s, '') AS %s",
                $this->quoteIdentifier($column),
                $this->quoteIdentifier($column)
            );
        }, $columns));

        $now = $this->getNowFormatted();
        if (in_array(self::TIMESTAMP_COLUMN_NAME, $columns) || $useTimestamp === false) {
            $sql = "INSERT INTO {$targetTableNameWithSchema} ($columnsSql) (SELECT $columnsSetSqlSelect FROM $stagingTableNameWithSchema)";
        } else {
            $sql = "INSERT INTO {$targetTableNameWithSchema} ($columnsSql, \"" . self::TIMESTAMP_COLUMN_NAME . "\") (SELECT $columnsSetSqlSelect, '{$now}' FROM $stagingTableNameWithSchema)";
        }

        Debugger::timer('copyFromStagingToTarget');
        $this->connection->query($sql);
        $this->addTimer('copyFromStagingToTarget', Debugger::timer('copyFromStagingToTarget'));

        $this->connection->query('COMMIT');
    }

    /**
     * Performs merge operation according to http://docs.aws.amazon.com/redshift/latest/dg/merge-specify-a-column-list.html
     */
    private function insertOrUpdateTargetTable(string $stagingTableName, string $targetTableName, array $columns, bool $useTimestamp = true, array $convertEmptyValuesToNull = []): void
    {
        $this->connection->query('BEGIN TRANSACTION');
        $nowFormatted = $this->getNowFormatted();

        $targetTableNameWithSchema = $this->nameWithSchemaEscaped($targetTableName);
        $stagingTableNameWithSchema = $this->nameWithSchemaEscaped($stagingTableName);

        $primaryKey = $this->connection->getTablePrimaryKey($this->schemaName, $targetTableName);

        if (!empty($primaryKey)) {
            // Update target table
            $sql = "UPDATE " . $targetTableNameWithSchema . " AS \"dest\" SET ";

            $columnsSet = [];
            foreach ($columns as $columnName) {
                if (in_array($columnName, $convertEmptyValuesToNull)) {
                    $columnsSet[] = sprintf(
                        '%s = IFF("src".%s = \'\', NULL, "src".%s)',
                        $this->quoteIdentifier($columnName),
                        $this->quoteIdentifier($columnName),
                        $this->quoteIdentifier($columnName)
                    );
                } else {
                    $columnsSet[] = sprintf(
                        '%s = COALESCE("src".%s, \'\')',
                        $this->quoteIdentifier($columnName),
                        $this->quoteIdentifier($columnName)
                    );
                }
            }

            $sql .= implode(', ', $columnsSet);
            if ($useTimestamp) {
                $sql .= ", " . $this->quoteIdentifier(self::TIMESTAMP_COLUMN_NAME) . " = '{$nowFormatted}' ";
            }
            $sql .= " FROM " . $stagingTableNameWithSchema . ' AS "src" ';
            $sql .= " WHERE ";

            $pkWhereSql = [];
            foreach ($primaryKey as $pkColumn) {
                $pkWhereSql[] = sprintf(
                    '"dest".%s = COALESCE("src".%s, \'\')',
                    $this->quoteIdentifier($pkColumn),
                    $this->quoteIdentifier($pkColumn)
                );
            }

            $sql .= implode(' AND ', $pkWhereSql) . " ";

            // update only changed rows - mysql TIMESTAMP ON UPDATE behaviour simulation
            $columnsComparsionSql = array_map(function ($columnName) {
                return sprintf(
                    'COALESCE(TO_VARCHAR("dest".%s), \'\') != COALESCE("src".%s, \'\')',
                    $this->quoteIdentifier($columnName),
                    $this->quoteIdentifier($columnName)
                );
            }, $columns);
            $sql .= " AND (" . implode(' OR ', $columnsComparsionSql) . ") ";

            Debugger::timer('updateTargetTable');
            $this->connection->query($sql);
            $this->addTimer('updateTargetTable', Debugger::timer('updateTargetTable'));

            // Delete updated rows from staging table
            $sql = "DELETE FROM " . $stagingTableNameWithSchema . ' "src" ';
            $sql .= "USING " . $targetTableNameWithSchema . ' AS "dest" ';
            $sql .= "WHERE " . implode(' AND ', $pkWhereSql);

            Debugger::timer('deleteUpdatedRowsFromStaging');
            $this->connection->query($sql);
            $this->addTimer('deleteUpdatedRowsFromStaging', Debugger::timer('deleteUpdatedRowsFromStaging'));

            // Dedup staging table
            Debugger::timer('dedupStaging');
            $this->dedupe($stagingTableName, $columns, $primaryKey);
            $this->addTimer('dedupStaging', Debugger::timer('dedupStaging'));
        }

        // Insert from staging to target table
        $insColumns = ($useTimestamp) ? array_merge($columns, [self::TIMESTAMP_COLUMN_NAME]) : $columns;
        $sql = "INSERT INTO " . $targetTableNameWithSchema . ' (' . implode(', ', array_map(function ($column) {
            return $this->quoteIdentifier($column);
        }, $insColumns)) . ")";

        $columnsSetSql = [];

        foreach ($columns as $columnName) {
            if (in_array($columnName, $convertEmptyValuesToNull)) {
                $columnsSetSql[] = sprintf(
                    'IFF("src".%s = \'\', NULL, %s)',
                    $this->quoteIdentifier($columnName),
                    $this->quoteIdentifier($columnName)
                );
            } else {
                $columnsSetSql[] = sprintf(
                    'COALESCE("src".%s, \'\')',
                    $this->quoteIdentifier($columnName)
                );
            }
        }

        $sql .= " SELECT " . implode(',', $columnsSetSql);
        if ($useTimestamp) {
            $sql .= ", '{$nowFormatted}' ";
        }
        $sql .= " FROM " . $stagingTableNameWithSchema . ' AS "src"';
        Debugger::timer('insertIntoTargetFromStaging');

        $this->connection->query($sql);
        $this->addTimer('insertIntoTargetFromStaging', Debugger::timer('insertIntoTargetFromStaging'));

        $this->connection->query('COMMIT');
    }

    private function replaceTables(string $sourceTableName, string $targetTableName): void
    {
        $this->dropTable($targetTableName);
        $this->connection->query(
            "ALTER TABLE {$this->nameWithSchemaEscaped($sourceTableName)} RENAME TO {$this->nameWithSchemaEscaped($targetTableName)}"
        );
    }

    private function dropTable(string $tableName): void
    {
        $this->connection->query("DROP TABLE " . $this->nameWithSchemaEscaped($tableName));
    }

    protected function nameWithSchemaEscaped(string $tableName, ?string $schemaName = null): string
    {
        if ($schemaName === null) {
            $schemaName = $this->schemaName;
        }
        return sprintf(
            '%s.%s',
            $this->connection->quoteIdentifier($schemaName),
            $this->connection->quoteIdentifier($tableName)
        );
    }

    private function uniqueValue(): string
    {
        return str_replace('.', '_', uniqid('csvimport', true));
    }

    private function dedupe(string $tableName, array $columns, array $primaryKey): void
    {
        if (empty($primaryKey)) {
            return;
        }

        $pkSql = implode(',', array_map(function ($column) {
            return $this->quoteIdentifier($column);
        }, $primaryKey));

        $sql = "SELECT ";

        $sql .= implode(",", array_map(function ($column) {
            return "a." . $this->quoteIdentifier($column);
        }, $columns));

        $sql .= sprintf(
            " FROM (SELECT %s, ROW_NUMBER() OVER (PARTITION BY %s ORDER BY %s) AS \"_row_number_\" FROM %s)",
            implode(",", array_map(function ($column) {
                return $this->quoteIdentifier($column);
            }, $columns)),
            $pkSql,
            $pkSql,
            $this->nameWithSchemaEscaped($tableName)
        );

        $sql .= " AS a WHERE a.\"_row_number_\" = 1";
        $columnsSql = implode(', ', array_map(function ($column) {
            return $this->quoteIdentifier($column);
        }, $columns));

        $tempTable = $this->createStagingTable($columns);

        $this->connection->query("INSERT INTO {$this->nameWithSchemaEscaped($tempTable)} ($columnsSql) " . $sql);
        $this->replaceTables($tempTable, $tableName);
    }

    private function createStagingTable(array $columns): string
    {

        $tempName = '__temp_' . $this->uniqueValue();

        $columnsSql = array_map(function ($column) {
            return sprintf('%s varchar', $this->quoteIdentifier($column));
        }, $columns);

        $this->connection->query(sprintf(
            'CREATE TEMPORARY TABLE %s (%s)',
            $this->nameWithSchemaEscaped($tempName),
            implode(', ', $columnsSql)
        ));

        return $tempName;
    }

    private function getNowFormatted(): string
    {
        $currentDate = new \DateTime('now', new \DateTimeZone('UTC'));
        return $currentDate->format('Y-m-d H:i:s');
    }

    public function getIncremental(): bool
    {
        return $this->incremental;
    }

    public function setIncremental(bool $incremental): ImportInterface
    {
        $this->incremental = $incremental;
        return $this;
    }

    public function getIgnoreLines(): int
    {
        return $this->ignoreLines;
    }

    public function setIgnoreLines(int $linesCount): ImportInterface
    {
        $this->ignoreLines = $linesCount;
        return $this;
    }

    protected function addTimer(string $name, float $value): void
    {
        $this->timers[] = [
            'name' => $name,
            'durationSeconds' => $value,
        ];
    }

    protected function quoteIdentifier(string $value): string
    {
        return $this->connection->quoteIdentifier($value);
    }
}


<?php

declare(strict_types=1);

namespace Keboola\Db\Import;

use Tracy\Debugger;

abstract class RedshiftBase implements ImportInterface
{

    /** @var \PDO  */
    protected $connection;

    /** @var array  */
    protected $warnings = [];

    /** @var int  */
    private $importedRowsCount = 0;

    /** @var array  */
    private $timers = [];

    /** @var array  */
    private $importedColumns = [];

    /** @var int  */
    private $ignoreLines = 0;

    /** @var bool  */
    private $incremental = false;

    /** @var string */
    private $schemaName;

    /** @var bool  */
    private $legacyFullImport = false;

    public function __construct(\PDO $connection, string $schemaName, bool $legacyFullImport = false)
    {
        $this->connection = $connection;
        $this->schemaName = $schemaName;
        $this->legacyFullImport = $legacyFullImport;
    }

    /**
     * @param string $tableName
     * @param array $columns
     * @param array $sourceData
     * @param array $options
     *  - useTimestamp - update and use timestamp column. default true
     *  - copyOptions - additional copy options for import command
     *  - convertEmptyValuesToNull - convert empty values to NULL
     * @return Result
     */
    public function import(string $tableName, array $columns, array $sourceData, array $options = []): Result
    {
        $this->validateColumns($tableName, $columns);
        $primaryKey = $this->getTablePrimaryKey($tableName);
        $stagingTableName = $this->createTemporaryTableFromDestinationTable($tableName, $primaryKey, $this->schemaName);

        $this->importDataToStagingTable($stagingTableName, $columns, $sourceData, $options);

        if ($this->getIncremental()) {
            $this->insertOrUpdateTargetTable(
                $stagingTableName,
                $tableName,
                $primaryKey,
                $columns,
                isset($options['useTimestamp']) ? $options['useTimestamp'] : true,
                isset($options["convertEmptyValuesToNull"]) ? $options["convertEmptyValuesToNull"] : []
            );
        } else {
            Debugger::timer('dedup');
            $this->dedup($stagingTableName, $columns, $primaryKey);
            $this->addTimer('dedup', Debugger::timer('dedup'));
            if ($this->legacyFullImport) {
                $this->insertAllIntoTargetTableLegacy(
                    $stagingTableName,
                    $tableName,
                    $columns,
                    isset($options['useTimestamp']) ? $options['useTimestamp'] : true,
                    isset($options["convertEmptyValuesToNull"]) ? $options["convertEmptyValuesToNull"] : []
                );
            } else {
                $this->insertAllIntoTargetTable(
                    $stagingTableName,
                    $tableName,
                    $primaryKey,
                    $columns,
                    isset($options['useTimestamp']) ? $options['useTimestamp'] : true,
                    isset($options["convertEmptyValuesToNull"]) ? $options["convertEmptyValuesToNull"] : []
                );
            }
        }
        $this->dropTempTable($stagingTableName);
        $this->importedColumns = $columns;
        return new Result([
            'warnings' => $this->warnings,
            'timers' => $this->timers,
            'importedRowsCount' => $this->importedRowsCount,
            'importedColumns' => $this->importedColumns,
            'legacyFullImport' => $this->legacyFullImport,
        ]);
    }

    abstract protected function importDataToStagingTable(string $stagingTempTableName, array $columns, array $sourceData, array $options = []): void;

    private function validateColumns(string $tableName, array $columnsToImport): void
    {
        if (count($columnsToImport) == 0) {
            throw new Exception(
                'No columns found in CSV file.',
                Exception::NO_COLUMNS
            );
        }

        $tableColumns = $this->getTableColumns($tableName);
        $columnsToImport = array_map('strtolower', $columnsToImport);
        $moreColumns = array_diff($columnsToImport, $tableColumns);
        if (!empty($moreColumns)) {
            throw new Exception('Columns doest not match', Exception::COLUMNS_COUNT_NOT_MATCH);
        }
    }

    private function insertAllIntoTargetTable(
        string $stagingTempTableName,
        string $targetTableName,
        array $primaryKey,
        array $columns,
        bool $useTimestamp = true,
        array $convertEmptyValuesToNull = []
    ): void {
        // create table same as target table
        $newTargetTableName = $this->createTableFromSourceTable($targetTableName, $primaryKey, $this->schemaName);

        // insert data to new table from staging table
        $columnsSql = implode(', ', array_map(function ($column) {
            return $this->quoteIdentifier($column);
        }, $columns));

        $columnsSelectSql = implode(', ', array_map(function ($column) use ($convertEmptyValuesToNull) {
            if (in_array($column, $convertEmptyValuesToNull)) {
                return "CASE {$this->quoteIdentifier($column)}::VARCHAR WHEN '' THEN NULL ELSE {$this->quoteIdentifier($column)} END";
            }
            return $this->quoteIdentifier($column);
        }, $columns));

        $now = $this->getNowFormatted();
        $newTargetTableNameWithSchema = $this->nameWithSchemaEscaped($newTargetTableName);
        $stagingTableNameEscaped = $this->tableNameEscaped($stagingTempTableName);
        if (in_array('_timestamp', $columns) || $useTimestamp === false) {
            $sql = "INSERT INTO {$newTargetTableNameWithSchema} ($columnsSql) (SELECT $columnsSelectSql FROM $stagingTableNameEscaped)";
        } else {
            $sql = "INSERT INTO {$newTargetTableNameWithSchema} ($columnsSql, _timestamp) (SELECT $columnsSelectSql, '{$now}' FROM $stagingTableNameEscaped)";
        }

        Debugger::timer('copyFromStagingToTarget');
        $this->query($sql);
        $this->addTimer('copyFromStagingToTarget', Debugger::timer('copyFromStagingToTarget'));

        // swap tables in transaction
        $this->connection->beginTransaction();
        try {
            $targetTableNameWithSchema = $this->nameWithSchemaEscaped($targetTableName);
            $this->query(sprintf('DROP TABLE %s', $targetTableNameWithSchema));
            $this->query(sprintf(
                "ALTER TABLE %s RENAME TO %s",
                $newTargetTableNameWithSchema,
                $this->tableNameEscaped($targetTableName)
            ));
            $this->connection->commit();
        } catch (\Throwable $e) {
            $this->connection->rollBack();
            throw $e;
        }
    }

    private function insertAllIntoTargetTableLegacy(
        string $stagingTempTableName,
        string $targetTableName,
        array $columns,
        bool $useTimestamp = true,
        array $convertEmptyValuesToNull = []
    ): void {
        $this->connection->beginTransaction();

        $targetTableNameWithSchema = $this->nameWithSchemaEscaped($targetTableName);
        $stagingTableNameEscaped = $this->tableNameEscaped($stagingTempTableName);

        $this->query('TRUNCATE TABLE ' . $targetTableNameWithSchema);

        $columnsSql = implode(', ', array_map(function ($column) {
            return $this->quoteIdentifier($column);
        }, $columns));

        $columnsSelectSql = implode(', ', array_map(function ($column) use ($convertEmptyValuesToNull) {
            if (in_array($column, $convertEmptyValuesToNull)) {
                return "CASE {$this->quoteIdentifier($column)}::VARCHAR WHEN '' THEN NULL ELSE {$this->quoteIdentifier($column)} END";
            }
            return $this->quoteIdentifier($column);
        }, $columns));

        $now = $this->getNowFormatted();
        if (in_array('_timestamp', $columns) || $useTimestamp === false) {
            $sql = "INSERT INTO {$targetTableNameWithSchema} ($columnsSql) (SELECT $columnsSelectSql FROM $stagingTableNameEscaped)";
        } else {
            $sql = "INSERT INTO {$targetTableNameWithSchema} ($columnsSql, _timestamp) (SELECT $columnsSelectSql, '{$now}' FROM $stagingTableNameEscaped)";
        }

        Debugger::timer('copyFromStagingToTarget');
        $this->query($sql);
        $this->addTimer('copyFromStagingToTarget', Debugger::timer('copyFromStagingToTarget'));

        $this->connection->commit();
    }

    /**
     * Performs merge operation according to http://docs.aws.amazon.com/redshift/latest/dg/merge-specify-a-column-list.html
     */
    private function insertOrUpdateTargetTable(
        string $stagingTempTableName,
        string $targetTableName,
        array $primaryKey,
        array $columns,
        bool $useTimestamp = true,
        array $convertEmptyValuesToNull = []
    ): void {
        $this->connection->beginTransaction();
        $nowFormatted = $this->getNowFormatted();

        $targetTableNameWithSchema = $this->nameWithSchemaEscaped($targetTableName);
        $stagingTableNameEscaped = $this->tableNameEscaped($stagingTempTableName);

        if (!empty($primaryKey)) {
            // Update target table
            $sql = "UPDATE " . $targetTableNameWithSchema . " SET ";

            $columnsSet = [];
            foreach ($columns as $columnName) {
                $columnsSet[] = sprintf(
                    "%s = %s.%s",
                    $this->quoteIdentifier($columnName),
                    $stagingTableNameEscaped,
                    $this->quoteIdentifier($columnName)
                );
            }

            $sql .= implode(', ', $columnsSet);
            if ($useTimestamp) {
                $sql .= ", _timestamp = '{$nowFormatted}' ";
            }
            $sql .= " FROM " . $stagingTableNameEscaped . " ";
            $sql .= " WHERE ";

            $pkWhereSql = [];
            foreach ($primaryKey as $pkColumn) {
                $pkWhereSql[] = sprintf(
                    "%s.%s = %s.%s",
                    $targetTableNameWithSchema,
                    $this->quoteIdentifier($pkColumn),
                    $stagingTableNameEscaped,
                    $this->quoteIdentifier($pkColumn)
                );
            }

            $sql .= implode(' AND ', $pkWhereSql) . " ";

            // update only changed rows - mysql TIMESTAMP ON UPDATE behaviour simulation
            $columnsComparsionSql = array_map(function ($columnName) use (
                $targetTableNameWithSchema,
                $stagingTableNameEscaped
            ) {
                return sprintf(
                    "COALESCE(CAST(%s.%s as varchar), '') != COALESCE(CAST(%s.%s as varchar), '')",
                    $targetTableNameWithSchema,
                    $this->quoteIdentifier($columnName),
                    $stagingTableNameEscaped,
                    $this->quoteIdentifier($columnName)
                );
            }, $columns);
            $sql .= " AND (" . implode(' OR ', $columnsComparsionSql) . ") ";

            Debugger::timer('updateTargetTable');
            $this->query($sql);
            $this->addTimer('updateTargetTable', Debugger::timer('updateTargetTable'));

            // Delete updated rows from staging table
            $sql = "DELETE FROM " . $stagingTableNameEscaped . " ";
            $sql .= "USING " . $targetTableNameWithSchema . " ";
            $sql .= "WHERE " . implode(' AND ', $pkWhereSql);

            Debugger::timer('deleteUpdatedRowsFromStaging');
            $this->query($sql);
            $this->addTimer('deleteUpdatedRowsFromStaging', Debugger::timer('deleteUpdatedRowsFromStaging'));

            // Dedup staging table
            Debugger::timer('dedupStaging');
            $this->dedup($stagingTempTableName, $columns, $primaryKey);
            $this->addTimer('dedupStaging', Debugger::timer('dedupStaging'));
        }

        // Insert from staging to target table
        $sql = "INSERT INTO " . $targetTableNameWithSchema . " (" . implode(', ', array_map(function ($column) {
            return $this->quoteIdentifier($column);
        }, $columns));

        $sql .= ($useTimestamp) ? ", _timestamp) " : ")";

        $columnsSetSql = [];

        foreach ($columns as $columnName) {
            if (in_array($columnName, $convertEmptyValuesToNull)) {
                $columnsSetSql[] = sprintf(
                    "CASE %s.%s::VARCHAR WHEN '' THEN NULL ELSE %s.%s END",
                    $stagingTableNameEscaped,
                    $this->quoteIdentifier($columnName),
                    $stagingTableNameEscaped,
                    $this->quoteIdentifier($columnName)
                );
            } else {
                $columnsSetSql[] = sprintf(
                    "%s.%s",
                    $stagingTableNameEscaped,
                    $this->quoteIdentifier($columnName)
                );
            }
        }
        $sql .= "SELECT " . implode(', ', $columnsSetSql);
        $sql .= ($useTimestamp) ? ", '{$nowFormatted}'" : "";
        $sql .= " FROM " . $stagingTableNameEscaped;
        Debugger::timer('insertIntoTargetFromStaging');
        $this->query($sql);
        $this->addTimer('insertIntoTargetFromStaging', Debugger::timer('insertIntoTargetFromStaging'));

        $this->connection->commit();
    }

    private function replaceTempTables(string $sourceTableName, string $targetTableName): void
    {
        $this->dropTempTable($targetTableName);
        $this->query("ALTER TABLE {$this->tableNameEscaped($sourceTableName)} RENAME TO {$this->tableNameEscaped($targetTableName)}");
    }

    private function dropTempTable(string $tableName): void
    {
        $this->query("DROP TABLE " . $this->tableNameEscaped($tableName));
    }

    protected function nameWithSchemaEscaped(string $tableName, ?string $schemaName = null): string
    {
        if ($schemaName === null) {
            $schemaName = $this->schemaName;
        }
        return "\"{$schemaName}\"." . $this->tableNameEscaped($tableName);
    }

    protected function tableNameEscaped(string $tableName): string
    {
        $tableNameFiltered = preg_replace('/[^a-zA-Z0-9_\-\.]+/', "", $tableName);
        return "\"{$tableNameFiltered}\"";
    }

    private function uniqueValue(): string
    {
        return str_replace('.', '_', uniqid('csvimport', true));
    }

    private function dedup(string $inputTempTableName, array $columns, array $primaryKey): void
    {
        if (empty($primaryKey)) {
            return;
        }

        $pkSql = implode(', ', array_map(function ($column) {
            return $this->quoteIdentifier($column);
        }, $primaryKey));

        $sql = "SELECT ";

        $sql .= implode(", ", array_map(function ($column) {
            return "a." . $this->quoteIdentifier($column);
        }, $columns));

        $sql .= sprintf(
            " FROM (SELECT %s, ROW_NUMBER() OVER (PARTITION BY %s ORDER BY %s) AS \"_row_number_\" FROM %s)",
            implode(", ", array_map(function ($column) {
                return $this->quoteIdentifier($column);
            }, $columns)),
            $pkSql,
            $pkSql,
            $this->tableNameEscaped($inputTempTableName)
        );

        $sql .= " AS a WHERE a.\"_row_number_\" = 1";
        $columnsSql = implode(', ', array_map(function ($column) {
            return $this->quoteIdentifier($column);
        }, $columns));

        $tempTable = $this->createTemporaryTableFromDestinationTable($inputTempTableName, $primaryKey);

        $this->query("INSERT INTO {$this->tableNameEscaped($tempTable)} ($columnsSql) " . $sql);
        $this->replaceTempTables($tempTable, $inputTempTableName);
    }

    private function createTemporaryTableFromDestinationTable(
        string $targetTableName,
        array $primaryKey,
        ?string $schemaName = null
    ): string {
        $tempName = '__temp_' . $this->uniqueValue();
        $this->query(sprintf(
            'CREATE TEMPORARY TABLE %s (LIKE %s)',
            $this->tableNameEscaped($tempName),
            $schemaName ? $this->nameWithSchemaEscaped($targetTableName, $schemaName) : $this->tableNameEscaped($targetTableName)
        ));

        // PK is not copied - add it to the table
        $tableIdentifier = $this->tableNameEscaped($tempName);
        if (!empty($primaryKey)) {
            $this->query(sprintf(
                "
                    ALTER TABLE %s
                    ADD PRIMARY KEY (%s)
                ",
                $tableIdentifier,
                implode(', ', array_map(function ($columnName) {
                    return $this->quoteIdentifier($columnName);
                }, $primaryKey))
            ));
        }

        return $tempName;
    }

    private function createTableFromSourceTable(string $sourceTableName, array $primaryKey, string $schemaName): string
    {
        $tempName = '__temp_' . $this->uniqueValue();
        $this->query(sprintf(
            'CREATE TABLE %s (LIKE %s)',
            $this->nameWithSchemaEscaped($tempName, $schemaName),
            $this->nameWithSchemaEscaped($sourceTableName, $schemaName)
        ));

        // PK is not copied - add it to the table
        $tableIdentifier = $this->nameWithSchemaEscaped($tempName, $schemaName);
        if (!empty($primaryKey)) {
            $this->query(sprintf(
                "
                    ALTER TABLE %s
                    ADD PRIMARY KEY (%s)
                ",
                $tableIdentifier,
                implode(', ', array_map(function ($columnName) {
                    return $this->quoteIdentifier($columnName);
                }, $primaryKey))
            ));
        }

        return $tempName;
    }

    private function getTablePrimaryKey(string $tableName): array
    {
        $sql = sprintf("
			SELECT pa.attname FROM pg_catalog.pg_index i
				JOIN pg_catalog.pg_class ci on ci.oid = i.indexrelid
				JOIN pg_catalog.pg_class ct on ct.oid = i.indrelid
				JOIN pg_catalog.pg_attribute pa on pa.attrelid = ci.oid
				JOIN pg_catalog.pg_namespace pn on pn.oid = ct.relnamespace
				WHERE ct.relname = %s AND pn.nspname = %s
				AND i.indisprimary;
		", $this->connection->quote(strtolower($tableName)), $this->connection->quote(strtolower($this->schemaName)));

        return array_map(function ($row) {
            return $row['attname'];
        }, $this->queryFetchAll($sql));
    }

    private function getTableColumns(string $tableName): array
    {
        return array_map(
            'strtolower',
            array_keys($this->describeTable(strtolower($tableName), strtolower($this->schemaName)))
        );
    }

    protected function query(string $sql, array $bind = []): void
    {
        try {
            $this->connection->prepare($sql)->execute($bind);
        } catch (\PDOException $e) {
            throw $this->handleQueryException($e);
        }
    }

    private function queryFetchAll(string $sql): array
    {
        try {
            return $this->connection->query($sql)->fetchAll();
        } catch (\PDOException $e) {
            throw $this->handleQueryException($e);
        }
    }

    private function handleQueryException(\PDOException $e): \Throwable
    {
        if (strpos($e->getMessage(), 'Mandatory url is not present in manifest file') !== false) {
            return new Exception('Mandatory url is not present in manifest file', Exception::MANDATORY_FILE_NOT_FOUND);
        }

        if (strpos($e->getMessage(), 'SQLSTATE[57014]') !== false) {
            return new Exception('Statement timeout. Maximum query execution time exeeded.', Exception::QUERY_TIMEOUT);
        }

        return $e;
    }

    private function getNowFormatted(): string
    {
        $currentDate = new \DateTime('now', new \DateTimeZone('UTC'));
        return $currentDate->format('Ymd H:i:s');
    }

    public function getIncremental(): bool
    {
        return $this->incremental;
    }

    public function setIncremental(bool $incremental): ImportInterface
    {
        $this->incremental = $incremental;
        return $this;
    }

    public function getIgnoreLines(): int
    {
        return $this->ignoreLines;
    }

    public function setIgnoreLines(int $linesCount): ImportInterface
    {
        $this->ignoreLines = $linesCount;
        return $this;
    }

    protected function addTimer(string $name, float $value): void
    {
        $this->timers[] = [
            'name' => $name,
            'durationSeconds' => $value,
        ];
    }

    protected function describeTable(string $tableName, ?string $schemaName = null): array
    {
        $sql = "SELECT
                a.attnum,
                n.nspname,
                c.relname,
                a.attname AS colname,
                t.typname AS type,
                a.atttypmod,
                FORMAT_TYPE(a.atttypid, a.atttypmod) AS complete_type,
                d.adsrc AS default_value,
                a.attnotnull AS notnull,
                a.attlen AS length,
                co.contype,
                ARRAY_TO_STRING(co.conkey, ',') AS conkey
            FROM pg_attribute AS a
                JOIN pg_class AS c ON a.attrelid = c.oid
                JOIN pg_namespace AS n ON c.relnamespace = n.oid
                JOIN pg_type AS t ON a.atttypid = t.oid
                LEFT OUTER JOIN pg_constraint AS co ON (co.conrelid = c.oid
                    AND a.attnum = ANY(co.conkey) AND co.contype = 'p')
                LEFT OUTER JOIN pg_attrdef AS d ON d.adrelid = c.oid AND d.adnum = a.attnum
            WHERE a.attnum > 0 AND c.relname = " . $this->connection->quote($tableName);
        if ($schemaName) {
            $sql .= " AND n.nspname = " . $this->connection->quote($schemaName);
        }
        $sql .= ' ORDER BY a.attnum';

        $result = $this->queryFetchAll($sql);

        $attnum = 0;
        $nspname = 1;
        $relname = 2;
        $colname = 3;
        $type = 4;
        $atttypemod = 5;
        $complete_type = 6;
        $default_value = 7;
        $notnull = 8;
        $length = 9;
        $contype = 10;
        $conkey = 11;

        $desc = [];
        foreach ($result as $key => $row) {
            $defaultValue = (string) $row[$default_value];
            if ($row[$type] == 'varchar' || $row[$type] == 'bpchar') {
                if (preg_match('/character(?: varying)?(?:\((\d+)\))?/', $row[$complete_type], $matches)) {
                    if (isset($matches[1])) {
                        $row[$length] = $matches[1];
                    } else {
                        $row[$length] = null; // unlimited
                    }
                }
                if (preg_match("/^'(.*?)'::(?:character varying|bpchar)$/", $defaultValue, $matches)) {
                    $defaultValue = $matches[1];
                }
            }
            list($primary, $primaryPosition, $identity) = [false, null, false];
            if ($row[$contype] == 'p') {
                $primary = true;
                $primaryPosition = array_search($row[$attnum], explode(',', $row[$conkey])) + 1;
                $identity = (bool) (preg_match('/^nextval/', (string) $row[$default_value]));
            }
            $desc[$row[$colname]] = [
                'SCHEMA_NAME' => $row[$nspname],
                'TABLE_NAME' => $row[$relname],
                'COLUMN_NAME' => $row[$colname],
                'COLUMN_POSITION' => $row[$attnum],
                'DATA_TYPE' => $row[$type],
                'DEFAULT' => $defaultValue,
                'NULLABLE' => ($row[$notnull] !== 't'),
                'LENGTH' => $row[$length],
                'SCALE' => null, // @todo
                'PRECISION' => null, // @todo
                'UNSIGNED' => null, // @todo
                'PRIMARY' => $primary,
                'PRIMARY_POSITION' => $primaryPosition,
                'IDENTITY' => $identity,
            ];
        }
        return $desc;
    }

    protected function quoteIdentifier(string $value): string
    {
        $q = '"';
        return ($q . str_replace("$q", "$q$q", $value) . $q);
    }
}


<?php

namespace SimpleBus\Asynchronous\MessageBus;

use Psr\Log\LoggerInterface;
use SimpleBus\Asynchronous\Publisher\Publisher;
use SimpleBus\Message\Bus\Middleware\MessageBusMiddleware;
use SimpleBus\Message\CallableResolver\Exception\UndefinedCallable;

class PublishesUnhandledMessages implements MessageBusMiddleware
{
    /**
     * @var Publisher
     */
    private $publisher;

    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * @var string
     */
    private $logLevel;

    public function __construct(Publisher $publisher, LoggerInterface $logger, $logLevel)
    {
        $this->publisher = $publisher;
        $this->logger = $logger;
        $this->logLevel = $logLevel;
    }

    /**
     * Handle the message by letting the next middleware handle it. If no handler is defined for this message, then
     * it is published to be processed asynchronously
     *
     * @param object $message
     * @param callable $next
     */
    public function handle($message, callable $next)
    {
        try {
            $next($message);
        } catch (UndefinedCallable $exception) {
            $this->logger->log(
                $this->logLevel,
                'No message handler found, trying to handle it asynchronously',
                [
                    'type' => get_class($message)
                ]
            );

            $this->publisher->publish($message);
        }
    }
}


<?php

namespace SimpleBus\Asynchronous\Properties;

class DelegatingAdditionalPropertiesResolver implements AdditionalPropertiesResolver
{
    /**
     * @var AdditionalPropertiesResolver[]
     */
    private $resolvers;

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

    /**
     * Combine properties
     *
     * {@inheritdoc}
     */
    public function resolveAdditionalPropertiesFor($message)
    {
        $properties = [];

        foreach ($this->resolvers as $resolver) {
            $properties = array_merge($properties, $resolver->resolveAdditionalPropertiesFor($message));
        }

        return $properties;
    }
}


<?php

namespace SimpleBus\Asynchronous\MessageBus;

use SimpleBus\Asynchronous\Publisher\Publisher;
use SimpleBus\Message\Bus\Middleware\MessageBusMiddleware;
use SimpleBus\Message\Name\MessageNameResolver;

class PublishesPredefinedMessages implements MessageBusMiddleware
{
    /**
     * @var \SimpleBus\Asynchronous\Publisher\Publisher
     */
    private $publisher;

    /**
     * @var MessageNameResolver
     */
    private $messageNameResolver;

    /**
     * @var array names
     */
    private $names;

    /**
     * @param Publisher $publisher
     * @param MessageNameResolver $messageNameResolver
     * @param array $names an array with names on messages to be published.
     */
    public function __construct(Publisher $publisher, MessageNameResolver $messageNameResolver, array $names)
    {
        $this->publisher = $publisher;
        $this->messageNameResolver = $messageNameResolver;
        $this->names = $names;
    }

    /**
     * Handle a message by publishing it to a queue (always), then calling the next middleware
     *
     * {@inheritdoc}
     */
    public function handle($message, callable $next)
    {
        $name = $this->messageNameResolver->resolve($message);
        if (in_array($name, $this->names)) {
            $this->publisher->publish($message);
        }

        $next($message);
    }
}


<?php

/** Single row representation
*/
class NotORM_Row extends NotORM_Abstract implements IteratorAggregate, ArrayAccess, Countable, JsonSerializable {
	private $modified = array();
	protected $row, $result, $primary;

	/** @access protected must be public because it is called from Result */
	function __construct(array $row, NotORM_Result $result) {
		$this->row = $row;
		$this->result = $result;
		if (array_key_exists($result->primary, $row)) {
			$this->primary = $row[$result->primary];
		}
	}

	/** Get primary key value
	* @return string
	*/
	function __toString() {
		return (string) $this[$this->result->primary]; // (string) - PostgreSQL returns int
	}

	/** Get referenced row
	* @param string
	* @return NotORM_Row or null if the row does not exist
	*/
	function __get($name) {
		$column = $this->result->notORM->structure->getReferencedColumn($name, $this->result->table);
		$referenced = &$this->result->referenced[$name];
		if (!isset($referenced)) {
			$keys = array();
			foreach ($this->result->rows as $row) {
				if ($row[$column] !== null) {
					$keys[$row[$column]] = null;
				}
			}
			if ($keys) {
				$table = $this->result->notORM->structure->getReferencedTable($name, $this->result->table);
				$referenced = new NotORM_Result($table, $this->result->notORM);
				$referenced->where("$table." . $this->result->notORM->structure->getPrimary($table), array_keys($keys));
			} else {
				$referenced = array();
			}
		}
		if (!isset($referenced[$this[$column]])) { // referenced row may not exist
			return null;
		}
		return $referenced[$this[$column]];
	}

	/** Test if referenced row exists
	* @param string
	* @return bool
	*/
	function __isset($name) {
		return ($this->__get($name) !== null);
	}

	/** Store referenced value
	* @param string
	* @param NotORM_Row or null
	* @return null
	*/
	function __set($name, NotORM_Row $value = null) {
		$column = $this->result->notORM->structure->getReferencedColumn($name, $this->result->table);
		$this[$column] = $value;
	}

	/** Remove referenced column from data
	* @param string
	* @return null
	*/
	function __unset($name) {
		$column = $this->result->notORM->structure->getReferencedColumn($name, $this->result->table);
		unset($this[$column]);
	}

	/** Get referencing rows
	* @param string table name
	* @param array (["condition"[, array("value")]])
	* @return NotORM_MultiResult
	*/
	function __call($name, array $args) {
		$table = $this->result->notORM->structure->getReferencingTable($name, $this->result->table);
		$column = $this->result->notORM->structure->getReferencingColumn($table, $this->result->table);
		$return = new NotORM_MultiResult($table, $this->result, $column, $this[$this->result->primary]);
		$return->where("$table.$column", array_keys((array) $this->result->rows)); // (array) - is null after insert
		if ($args) {
			call_user_func_array(array($return, 'where'), $args);
		}
		return $return;
	}

	/** Update row
	* @param array or null for all modified values
	* @return int number of affected rows or false in case of an error
	*/
	function update($data = null) {
		// update is an SQL keyword
		if (!isset($data)) {
			$data = $this->modified;
		}
		$result = new NotORM_Result($this->result->table, $this->result->notORM);
		$return = $result->where($this->result->primary, $this->primary)->update($data);
		$this->primary = $this[$this->result->primary];
		return $return;
	}

	/** Delete row
	* @return int number of affected rows or false in case of an error
	*/
	function delete() {
		// delete is an SQL keyword
		$result = new NotORM_Result($this->result->table, $this->result->notORM);
		$return = $result->where($this->result->primary, $this->primary)->delete();
		$this->primary = $this[$this->result->primary];
		return $return;
	}

	protected function access($key, $delete = false) {
		if ($this->result->notORM->cache && !isset($this->modified[$key]) && $this->result->access($key, $delete)) {
			$id = (isset($this->primary) ? $this->primary : $this->row);
			$this->row = $this->result[$id]->row;
		}
	}

	// IteratorAggregate implementation

	function getIterator() {
		$this->access(null);
		return new ArrayIterator($this->row);
	}

	// Countable implementation

	function count() {
		return count($this->row);
	}

	// ArrayAccess implementation

	/** Test if column exists
	* @param string column name
	* @return bool
	*/
	function offsetExists($key) {
		$this->access($key);
		$return = array_key_exists($key, $this->row);
		if (!$return) {
			$this->access($key, true);
		}
		return $return;
	}

	/** Get value of column
	* @param string column name
	* @return string
	*/
	function offsetGet($key) {
		$this->access($key);
		if (!array_key_exists($key, $this->row)) {
			$this->access($key, true);
		}
		return $this->row[$key];
	}

	/** Store value in column
	* @param string column name
	* @return null
	*/
	function offsetSet($key, $value) {
		$this->row[$key] = $value;
		$this->modified[$key] = $value;
	}

	/** Remove column from data
	* @param string column name
	* @return null
	*/
	function offsetUnset($key) {
		unset($this->row[$key]);
		unset($this->modified[$key]);
	}

	// JsonSerializable implementation

	function jsonSerialize() {
		return $this->row;
	}

    // @dogstar 2014-10-24
    function toArray()
    {
        return $this->row();
    }

}