File: view.rb

package info (click to toggle)
ruby-mongo 2.21.3-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 14,764 kB
  • sloc: ruby: 108,806; makefile: 5; sh: 2
file content (232 lines) | stat: -rw-r--r-- 8,675 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# frozen_string_literal: true

module Mongo
  module SearchIndex
    # A class representing a view of search indexes.
    class View
      include Enumerable
      include Retryable
      include Collection::Helpers

      # @return [ Mongo::Collection ] the collection this view belongs to
      attr_reader :collection

      # @return [ nil | String ] the index id to query
      attr_reader :requested_index_id

      # @return [ nil | String ] the index name to query
      attr_reader :requested_index_name

      # @return [ Hash ] the options hash to use for the aggregate command
      #   when querying the available indexes.
      attr_reader :aggregate_options

      # Create the new search index view.
      #
      # @param [ Collection ] collection The collection.
      # @param [ Hash ] options The options that configure the behavior of the view.
      #
      # @option options [ String ] :id The specific index id to query (optional)
      # @option options [ String ] :name The name of the specific index to query (optional)
      # @option options [ Hash ] :aggregate The options hash to send to the
      #   aggregate command when querying the available indexes.
      def initialize(collection, options = {})
        @collection = collection
        @requested_index_id = options[:id]
        @requested_index_name = options[:name]
        @aggregate_options = options[:aggregate] || {}

        return if @aggregate_options.is_a?(Hash)

        raise ArgumentError, "The :aggregate option must be a Hash (got a #{@aggregate_options.class})"
      end

      # Create a single search index with the given definition. If the name is
      # provided, the new index will be given that name.
      #
      # @param [ Hash ] definition The definition of the search index.
      # @param [ nil | String ] name The name to give the new search index.
      #
      # @return [ String ] the name of the new search index.
      def create_one(definition, name: nil, type: 'search')
        create_many([ { name: name, definition: definition, type: type } ]).first
      end

      # Create multiple search indexes with a single command.
      #
      # @param [ Array<Hash> ] indexes The description of the indexes to
      #   create. Each element of the list must be a hash with a definition
      #   key, and an optional name key.
      #
      # @return [ Array<String> ] the names of the new search indexes.
      def create_many(indexes)
        spec = spec_with(indexes: indexes.map { |v| validate_search_index!(v) })
        result = Operation::CreateSearchIndexes.new(spec).execute(next_primary, context: execution_context)
        result.first['indexesCreated'].map { |idx| idx['name'] }
      end

      # Drop the search index with the given id, or name. One or the other must
      # be specified, but not both.
      #
      # @param [ String ] id the id of the index to drop
      # @param [ String ] name the name of the index to drop
      #
      # @return [ Mongo::Operation::Result | false ] the result of the
      #    operation, or false if the given index does not exist.
      def drop_one(id: nil, name: nil)
        validate_id_or_name!(id, name)

        spec = spec_with(index_id: id, index_name: name)
        op = Operation::DropSearchIndex.new(spec)

        # per the spec:
        # Drivers MUST suppress NamespaceNotFound errors for the
        # ``dropSearchIndex`` helper.  Drop operations should be idempotent.
        do_drop(op, nil, execution_context)
      end

      # Iterate over the search indexes.
      #
      # @param [ Proc ] block if given, each search index will be yieleded to
      #    the block.
      #
      # @return [ self | Enumerator ] if a block is given, self is returned.
      #    Otherwise, an enumerator will be returned.
      def each(&block)
        @result ||= begin
          spec = {}.tap do |s|
            s[:id] = requested_index_id if requested_index_id
            s[:name] = requested_index_name if requested_index_name
          end

          collection.with(read_concern: {}).aggregate(
            [ { '$listSearchIndexes' => spec } ],
            aggregate_options
          )
        end

        return @result.to_enum unless block

        @result.each(&block)
        self
      end

      # Update the search index with the given id or name. One or the other
      # must be provided, but not both.
      #
      # @param [ Hash ] definition the definition to replace the given search
      #    index with.
      # @param [ nil | String ] id the id of the search index to update
      # @param [ nil | String ] name the name of the search index to update
      #
      # @return [ Mongo::Operation::Result ] the result of the operation
      def update_one(definition, id: nil, name: nil)
        validate_id_or_name!(id, name)

        spec = spec_with(index_id: id, index_name: name, index: definition)
        Operation::UpdateSearchIndex.new(spec).execute(next_primary, context: execution_context)
      end

      # The following methods are to make the view act more like an array,
      # without having to explicitly make it an array...

      # Queries whether the search index enumerable is empty.
      #
      # @return [ true | false ] whether the enumerable is empty or not.
      def empty?
        count.zero?
      end

      private

      # A helper method for building the specification document with certain
      # values pre-populated.
      #
      # @param [ Hash ] extras the values to put into the specification
      #
      # @return [ Hash ] the specification document
      def spec_with(extras)
        {
          coll_name: collection.name,
          db_name: collection.database.name,
        }.merge(extras)
      end

      # A helper method for retrieving the primary server from the cluster.
      #
      # @return [ Mongo::Server ] the server to use
      def next_primary(ping = nil, session = nil)
        collection.cluster.next_primary(ping, session)
      end

      # A helper method for constructing a new operation context for executing
      # an operation.
      #
      # @return [ Mongo::Operation::Context ] the operation context
      def execution_context
        Operation::Context.new(client: collection.client)
      end

      # Validates the given id and name, ensuring that exactly one of them
      # is non-nil.
      #
      # @param [ nil | String ] id the id to validate
      # @param [ nil | String ] name the name to validate
      #
      # @raise [ ArgumentError ] if neither or both arguments are nil
      def validate_id_or_name!(id, name)
        return unless (id.nil? && name.nil?) || (!id.nil? && !name.nil?)

        raise ArgumentError, 'exactly one of id or name must be specified'
      end

      # Validates the given search index document, ensuring that it has no
      # extra keys, and that the name and definition are valid.
      #
      # @param [ Hash ] doc the document to validate
      #
      # @raise [ ArgumentError ] if the document is invalid.
      def validate_search_index!(doc)
        validate_search_index_keys!(doc.keys)
        validate_search_index_name!(doc[:name] || doc['name'])
        validate_search_index_definition!(doc[:definition] || doc['definition'])
        doc
      end

      # Validates the keys of a search index document, ensuring that
      # they are all valid.
      #
      # @param [ Array<String | Hash> ] keys the keys of a search index document
      #
      # @raise [ ArgumentError ] if the list contains any invalid keys
      def validate_search_index_keys!(keys)
        extras = keys - [ 'name', 'definition', 'type', :name, :definition, :type ]

        raise ArgumentError, "invalid keys in search index creation: #{extras.inspect}" if extras.any?
      end

      # Validates the name of a search index, ensuring that it is either a
      # String or nil.
      #
      # @param [ nil | String ] name the name of a search index
      #
      # @raise [ ArgumentError ] if the name is not valid
      def validate_search_index_name!(name)
        return if name.nil? || name.is_a?(String)

        raise ArgumentError, "search index name must be nil or a string (got #{name.inspect})"
      end

      # Validates the definition of a search index.
      #
      # @param [ Hash ] definition the definition of a search index
      #
      # @raise [ ArgumentError ] if the definition is not valid
      def validate_search_index_definition!(definition)
        return if definition.is_a?(Hash)

        raise ArgumentError, "search index definition must be a Hash (got #{definition.inspect})"
      end
    end
  end
end