diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php index ac0184b..ff38a22 100644 --- a/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php @@ -383,6 +383,22 @@ public function rollbackSavepoint($savepoint_name = 'mimic_implicit_commit') { $this->rollback($savepoint_name); } } + + /** + * {@inheritdoc} + */ + public function upsert($table, array $options = array()) { + // Use the (faster) native Upsert implementation for PostgreSQL >= 9.5. + if (version_compare($this->version(), '9.5', '>=')) { + $class = $this->getDriverClass('NativeUpsert'); + } + else { + $class = $this->getDriverClass('Upsert'); + } + + return new $class($this, $table, $options); + } + } /** diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/NativeUpsert.php b/core/lib/Drupal/Core/Database/Driver/pgsql/NativeUpsert.php new file mode 100644 index 0000000..d1c6d11 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/NativeUpsert.php @@ -0,0 +1,116 @@ +preExecute()) { + return NULL; + } + + $stmt = $this->connection->prepareQuery((string) $this); + + // Fetch the list of blobs and sequences used on that table. + $table_information = $this->connection->schema()->queryTableInformation($this->table); + + $max_placeholder = 0; + $blobs = []; + $blob_count = 0; + foreach ($this->insertValues as $insert_values) { + foreach ($this->insertFields as $idx => $field) { + if (isset($table_information->blob_fields[$field])) { + $blobs[$blob_count] = fopen('php://memory', 'a'); + fwrite($blobs[$blob_count], $insert_values[$idx]); + rewind($blobs[$blob_count]); + + $stmt->bindParam(':db_insert_placeholder_' . $max_placeholder++, $blobs[$blob_count], \PDO::PARAM_LOB); + + // Pre-increment is faster in PHP than increment. + ++$blob_count; + } + else { + $stmt->bindParam(':db_insert_placeholder_' . $max_placeholder++, $insert_values[$idx]); + } + } + // Check if values for a serial field has been passed. + if (!empty($table_information->serial_fields)) { + foreach ($table_information->serial_fields as $index => $serial_field) { + $serial_key = array_search($serial_field, $this->insertFields); + if ($serial_key !== FALSE) { + $serial_value = $insert_values[$serial_key]; + + // Sequences must be greater than or equal to 1. + if ($serial_value === NULL || !$serial_value) { + $serial_value = 1; + } + // Set the sequence to the bigger value of either the passed + // value or the max value of the column. It can happen that another + // thread calls nextval() which could lead to a serial number being + // used twice. However, trying to insert a value into a serial + // column should only be done in very rare cases and is not thread + // safe by definition. + $this->connection->query("SELECT setval('" . $table_information->sequences[$index] . "', GREATEST(MAX(" . $serial_field . "), :serial_value)) FROM {" . $this->table . "}", array(':serial_value' => (int)$serial_value)); + } + } + } + } + + $options = $this->queryOptions; + if (!empty($table_information->sequences)) { + $options['sequence_name'] = $table_information->sequences[0]; + } + + $this->connection->query($stmt, [], $options); + + // Re-initialize the values array so that we can re-use this query. + $this->insertValues = []; + + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function __toString() { + // Create a sanitized comment string to prepend to the query. + $comments = $this->connection->makeComment($this->comments); + + // Default fields are always placed first for consistency. + $insert_fields = array_merge($this->defaultFields, $this->insertFields); + $insert_fields = array_map(function($f) { return $this->connection->escapeField($f); }, $insert_fields); + + $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES '; + + $values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields); + $query .= implode(', ', $values); + + // Updating the unique / primary key is not necessary. + unset($insert_fields[$this->key]); + + $update = []; + foreach ($insert_fields as $field) { + $update[] = "$field = EXCLUDED.$field"; + } + + $query .= ' ON CONFLICT (' . $this->connection->escapeField($this->key) . ') DO UPDATE SET ' . implode(', ', $update); + + return $query; + } + +}