/**
  * Save a workflow definition to the database.
  *
  * @param  ezcWorkflow $workflow
  * @throws ezcWorkflowDefinitionStorageException
  */
 public function save(\ezcWorkflow $workflow)
 {
     // Verify the workflow.
     $workflow->verify();
     if (strlen($workflow->name) == 0) {
         throw new \ezcWorkflowDefinitionStorageException();
     }
     $platform = $this->conn->getDatabasePlatform();
     // what mode of saving should it be? Update or Re-Generate?
     //
     // Conditions that an update sufficies:
     // 1. No node has been deleted
     // 2. No node has changed its meaning (action class or type)
     // 3. For simplicitly only zero or one new nodes will be created.
     $hasExistingNodeIds = array();
     $newNodes = 0;
     foreach ($workflow->nodes as $node) {
         $oid = spl_object_hash($node);
         if (!isset($this->nodeMap[$oid])) {
             $newNodes++;
         } else {
             $hasExistingNodeIds[] = $this->nodeMap[$oid];
         }
     }
     $canBeUpdate = false;
     if ($newNodes < 2 && count(array_diff($hasExistingNodeIds, $this->workflowNodeIds[$workflow->id])) == 0 && $workflow->id) {
         $canBeUpdate = true;
     }
     $this->workflowNodeIds[$workflow->id] = array();
     try {
         $this->conn->beginTransaction();
         $workflowVersion = $this->getCurrentVersion($workflow->name) + 1;
         $this->conn->update($this->options->workflowTable(), array('workflow_outdated' => 1), array('workflow_name' => $workflow->name));
         $date = new \DateTime("now");
         if ($canBeUpdate) {
             $this->conn->update($this->options->workflowTable(), array('workflow_version' => $workflowVersion, 'workflow_created' => $date->format($platform->getDateTimeFormatString())), array('workflow_id' => $workflow->id));
         } else {
             $data = array('workflow_name' => $workflow->name, 'workflow_version' => $workflowVersion, 'workflow_created' => $date->format($platform->getDateTimeFormatString()), 'workflow_outdated' => 0);
             // For sequences: get id before insert
             if ($platform->prefersSequences()) {
                 $id = (int) $this->conn->fetchColumn($platform->getSequenceNextValSQL($this->options->workflowSequence()));
                 $data['workflow_id'] = $id;
                 $workflow->id = $id;
             }
             $this->conn->insert($this->options->workflowTable(), $data);
             if ($platform->prefersIdentityColumns()) {
                 $workflow->id = (int) $this->conn->lastInsertId();
             }
             $workflow->definitionStorage = $this;
         }
         // Write node table rows.
         $nodeMap = array();
         foreach ($workflow->nodes as $node) {
             /* @var $node \ezcWorkflowNode */
             $oid = spl_object_hash($node);
             if ($canBeUpdate && isset($this->nodeMap[$oid])) {
                 $nodeId = (int) $this->nodeMap[$oid];
                 $this->conn->update($this->options->nodeTable(), array('node_configuration' => $this->options->getSerializer()->serialize($node->getConfiguration())), array('node_id' => $nodeId));
             } else {
                 $data = array('workflow_id' => (int) $workflow->id, 'node_class' => get_class($node), 'node_configuration' => $this->options->getSerializer()->serialize($node->getConfiguration()));
                 if ($platform->prefersSequences()) {
                     $nodeId = (int) $this->conn->fetchColumn($platform->getSequenceNextValSQL($this->options->nodeSequence()));
                     $data['node_id'] = $nodeId;
                 }
                 $this->conn->insert($this->options->nodeTable(), $data);
                 if ($platform->prefersIdentityColumns()) {
                     $nodeId = (int) $this->conn->lastInsertId();
                 }
             }
             $nodeMap[$nodeId] = $node;
             $this->workflowNodeIds[$workflow->id][] = $nodeId;
             $this->nodeMap[$oid] = $nodeId;
         }
         if ($canBeUpdate) {
             // Delete all the node connections, NodeMap Keys are casted to (int) so usage here is safe.
             $query = "DELETE FROM " . $this->options->nodeConnectionTable() . " " . "WHERE incoming_node_id IN (" . implode(",", array_keys($nodeMap)) . ") OR " . "outgoing_node_id IN (" . implode(",", array_keys($nodeMap)) . ")";
             $this->conn->executeUpdate($query);
         }
         foreach ($workflow->nodes as $node) {
             foreach ($node->getOutNodes() as $outNode) {
                 $incomingNodeId = null;
                 $outgoingNodeId = null;
                 foreach ($nodeMap as $_id => $_node) {
                     if ($_node === $node) {
                         $incomingNodeId = $_id;
                     } else {
                         if ($_node === $outNode) {
                             $outgoingNodeId = $_id;
                         }
                     }
                     if ($incomingNodeId !== NULL && $outgoingNodeId !== NULL) {
                         break;
                     }
                 }
                 $data = array('incoming_node_id' => $incomingNodeId, 'outgoing_node_id' => $outgoingNodeId);
                 if ($platform->prefersSequences()) {
                     $id = (int) $this->conn->fetchColumn($platform->getSequenceNextValSQL($this->options->nodeConnectionSequence()));
                     $data['id'] = $id;
                 }
                 $this->conn->insert($this->options->nodeConnectionTable(), $data);
             }
         }
         unset($nodeMap);
         if ($canBeUpdate) {
             $this->conn->delete($this->options->variableHandlerTable(), array('workflow_id' => (int) $workflow->id));
         }
         foreach ($workflow->getVariableHandlers() as $variable => $class) {
             $this->conn->insert($this->options->variableHandlerTable(), array('workflow_id' => (int) $workflow->id, 'variable' => $variable, 'class' => $class));
         }
         $this->conn->commit();
     } catch (\Exception $e) {
         $this->conn->rollBack();
         throw new \ezcWorkflowDefinitionStorageException("Error while persisting workflow: " . $e->getMessage());
     }
 }
 public function getWorkflowSchema(WorkflowOptions $options)
 {
     $schema = new \Doctrine\DBAL\Schema\Schema();
     $workflowTable = $schema->createTable($options->workflowTable());
     $columnOptions = $this->_handlePrimaryKey($schema, $options->workflowTable(), $options->workflowSequence());
     $workflowTable->addColumn('workflow_id', 'integer', $columnOptions);
     $workflowTable->addColumn('workflow_name', 'string');
     $workflowTable->addColumn('workflow_version', 'integer');
     $workflowTable->addColumn('workflow_outdated', 'integer');
     $workflowTable->addColumn('workflow_created', 'datetime');
     $workflowTable->setPrimaryKey(array('workflow_id'));
     $workflowTable->addUniqueIndex(array('workflow_name', 'workflow_version'));
     $nodeTable = $schema->createTable($options->nodeTable());
     $columnOptions = $this->_handlePrimaryKey($schema, $options->nodeTable(), $options->nodeSequence());
     $nodeTable->addColumn('node_id', 'integer', $columnOptions);
     $nodeTable->addColumn('workflow_id', 'integer');
     $nodeTable->addColumn('node_class', 'string');
     $nodeTable->addColumn('node_configuration', 'text', array('notnull' => false, "length" => null));
     $nodeTable->setPrimaryKey(array('node_id'));
     $nodeTable->addIndex(array('workflow_id'));
     $nodeTable->addForeignKeyConstraint($options->workflowTable(), array('workflow_id'), array('workflow_id'), array('onDelete' => 'CASCADE'));
     $connectionTable = $schema->createTable($options->nodeConnectionTable());
     $columnOptions = $this->_handlePrimaryKey($schema, $options->nodeConnectionTable(), $options->nodeConnectionSequence());
     $connectionTable->addColumn('id', 'integer', $columnOptions);
     $connectionTable->addColumn('incoming_node_id', 'integer');
     $connectionTable->addColumn('outgoing_node_id', 'integer');
     $connectionTable->setPrimaryKey(array('id'));
     $connectionTable->addForeignKeyConstraint($options->nodeTable(), array('incoming_node_id'), array('node_id'), array('onDelete' => 'CASCADE'));
     $connectionTable->addForeignKeyConstraint($options->nodeTable(), array('outgoing_node_id'), array('node_id'), array('onDelete' => 'CASCADE'));
     $variableHandlerTable = $schema->createTable($options->variableHandlerTable());
     $variableHandlerTable->addColumn('workflow_id', 'integer');
     $variableHandlerTable->addColumn('variable', 'string');
     $variableHandlerTable->addColumn('class', 'string');
     $variableHandlerTable->setPrimaryKey(array('workflow_id', 'variable'));
     $variableHandlerTable->addForeignKeyconstraint($options->workflowTable(), array('workflow_id'), array('workflow_id'));
     $executionTable = $schema->createTable($options->executionTable());
     $columnOptions = $this->_handlePrimaryKey($schema, $options->executionTable(), $options->executionSequence());
     $executionTable->addColumn('execution_id', 'integer', $columnOptions);
     $executionTable->addColumn('workflow_id', 'integer');
     $executionTable->addColumn('execution_parent', 'integer', array('notnull' => false));
     $executionTable->addColumn('execution_started', 'datetime');
     $executionTable->addColumn('execution_suspended', 'datetime', array('notnull' => false));
     $executionTable->addColumn('execution_variables', 'text', array('notnull' => false, "length" => null));
     $executionTable->addColumn('execution_waiting_for', 'text', array('notnull' => false, "length" => null));
     $executionTable->addColumn('execution_threads', 'text', array('notnull' => false, "length" => null));
     $executionTable->addColumn('execution_next_thread_id', 'integer');
     $executionTable->addColumn('execution_next_poll_date', 'datetime', array('notnull' => false));
     $executionTable->addIndex(array('execution_next_poll_date'));
     $executionTable->setPrimaryKey(array('execution_id'));
     $executionTable->addIndex(array('execution_parent'));
     $executionTable->addForeignKeyConstraint($options->workflowTable(), array('workflow_id'), array('workflow_id'));
     $executionTable->addForeignKeyConstraint($options->executionTable(), array('execution_parent'), array('execution_id'));
     $executionStateTable = $schema->createTable($options->executionStateTable());
     $executionStateTable->addColumn('execution_id', 'integer');
     $executionStateTable->addColumn('node_id', 'integer');
     $executionStateTable->addColumn('node_state', 'text', array('notnull' => false, "length" => null));
     $executionStateTable->addColumn('node_activated_from', 'text', array('notnull' => false, "length" => null));
     $executionStateTable->addColumn('node_thread_id', 'integer');
     $executionStateTable->setPrimaryKey(array('execution_id', 'node_id'));
     $executionStateTable->addForeignKeyConstraint($options->executionTable(), array('execution_id'), array('execution_id'));
     $executionStateTable->addForeignKeyConstraint($options->nodeTable(), array('node_id'), array('node_id'));
     return $schema;
 }