1<?php
2/*
3 * Copyright 2015-2017 MongoDB, Inc.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *   http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18namespace MongoDB\Operation;
19
20use MongoDB\Driver\Command;
21use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
22use MongoDB\Driver\Server;
23use MongoDB\Driver\Session;
24use MongoDB\Driver\WriteConcern;
25use MongoDB\Exception\InvalidArgumentException;
26use MongoDB\Exception\UnsupportedException;
27use MongoDB\Model\IndexInput;
28use function array_map;
29use function is_array;
30use function is_integer;
31use function MongoDB\server_supports_feature;
32use function sprintf;
33
34/**
35 * Operation for the createIndexes command.
36 *
37 * @api
38 * @see \MongoDB\Collection::createIndex()
39 * @see \MongoDB\Collection::createIndexes()
40 * @see http://docs.mongodb.org/manual/reference/command/createIndexes/
41 */
42class CreateIndexes implements Executable
43{
44    /** @var integer */
45    private static $wireVersionForCollation = 5;
46
47    /** @var integer */
48    private static $wireVersionForWriteConcern = 5;
49
50    /** @var string */
51    private $databaseName;
52
53    /** @var string */
54    private $collectionName;
55
56    /** @var array */
57    private $indexes = [];
58
59    /** @var boolean */
60    private $isCollationUsed = false;
61
62    /** @var array */
63    private $options = [];
64
65    /**
66     * Constructs a createIndexes command.
67     *
68     * Supported options:
69     *
70     *  * maxTimeMS (integer): The maximum amount of time to allow the query to
71     *    run.
72     *
73     *  * session (MongoDB\Driver\Session): Client session.
74     *
75     *    Sessions are not supported for server versions < 3.6.
76     *
77     *  * writeConcern (MongoDB\Driver\WriteConcern): Write concern.
78     *
79     *    This is not supported for server versions < 3.4 and will result in an
80     *    exception at execution time if used.
81     *
82     * @param string  $databaseName   Database name
83     * @param string  $collectionName Collection name
84     * @param array[] $indexes        List of index specifications
85     * @param array   $options        Command options
86     * @throws InvalidArgumentException for parameter/option parsing errors
87     */
88    public function __construct($databaseName, $collectionName, array $indexes, array $options = [])
89    {
90        if (empty($indexes)) {
91            throw new InvalidArgumentException('$indexes is empty');
92        }
93
94        $expectedIndex = 0;
95
96        foreach ($indexes as $i => $index) {
97            if ($i !== $expectedIndex) {
98                throw new InvalidArgumentException(sprintf('$indexes is not a list (unexpected index: "%s")', $i));
99            }
100
101            if (! is_array($index)) {
102                throw InvalidArgumentException::invalidType(sprintf('$index[%d]', $i), $index, 'array');
103            }
104
105            if (! isset($index['ns'])) {
106                $index['ns'] = $databaseName . '.' . $collectionName;
107            }
108
109            if (isset($index['collation'])) {
110                $this->isCollationUsed = true;
111            }
112
113            $this->indexes[] = new IndexInput($index);
114
115            $expectedIndex += 1;
116        }
117
118        if (isset($options['maxTimeMS']) && ! is_integer($options['maxTimeMS'])) {
119            throw InvalidArgumentException::invalidType('"maxTimeMS" option', $options['maxTimeMS'], 'integer');
120        }
121
122        if (isset($options['session']) && ! $options['session'] instanceof Session) {
123            throw InvalidArgumentException::invalidType('"session" option', $options['session'], Session::class);
124        }
125
126        if (isset($options['writeConcern']) && ! $options['writeConcern'] instanceof WriteConcern) {
127            throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], WriteConcern::class);
128        }
129
130        if (isset($options['writeConcern']) && $options['writeConcern']->isDefault()) {
131            unset($options['writeConcern']);
132        }
133
134        $this->databaseName = (string) $databaseName;
135        $this->collectionName = (string) $collectionName;
136        $this->options = $options;
137    }
138
139    /**
140     * Execute the operation.
141     *
142     * @see Executable::execute()
143     * @param Server $server
144     * @return string[] The names of the created indexes
145     * @throws UnsupportedException if collation or write concern is used and unsupported
146     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
147     */
148    public function execute(Server $server)
149    {
150        if ($this->isCollationUsed && ! server_supports_feature($server, self::$wireVersionForCollation)) {
151            throw UnsupportedException::collationNotSupported();
152        }
153
154        if (isset($this->options['writeConcern']) && ! server_supports_feature($server, self::$wireVersionForWriteConcern)) {
155            throw UnsupportedException::writeConcernNotSupported();
156        }
157
158        $inTransaction = isset($this->options['session']) && $this->options['session']->isInTransaction();
159        if ($inTransaction && isset($this->options['writeConcern'])) {
160            throw UnsupportedException::writeConcernNotSupportedInTransaction();
161        }
162
163        $this->executeCommand($server);
164
165        return array_map(function (IndexInput $index) {
166            return (string) $index;
167        }, $this->indexes);
168    }
169
170    /**
171     * Create options for executing the command.
172     *
173     * @see http://php.net/manual/en/mongodb-driver-server.executewritecommand.php
174     * @return array
175     */
176    private function createOptions()
177    {
178        $options = [];
179
180        if (isset($this->options['session'])) {
181            $options['session'] = $this->options['session'];
182        }
183
184        if (isset($this->options['writeConcern'])) {
185            $options['writeConcern'] = $this->options['writeConcern'];
186        }
187
188        return $options;
189    }
190
191    /**
192     * Create one or more indexes for the collection using the createIndexes
193     * command.
194     *
195     * @param Server $server
196     * @throws DriverRuntimeException for other driver errors (e.g. connection errors)
197     */
198    private function executeCommand(Server $server)
199    {
200        $cmd = [
201            'createIndexes' => $this->collectionName,
202            'indexes' => $this->indexes,
203        ];
204
205        if (isset($this->options['maxTimeMS'])) {
206            $cmd['maxTimeMS'] = $this->options['maxTimeMS'];
207        }
208
209        $server->executeWriteCommand($this->databaseName, new Command($cmd), $this->createOptions());
210    }
211}
212