Class: Exwiw::Adapter::MongodbAdapter

Inherits:
Base
  • Object
show all
Defined in:
lib/exwiw/adapter/mongodb_adapter.rb

Constant Summary collapse

INDEX_OPTION_ALLOWLIST =

Index options copied through to the emitted createIndex call. Anything else (‘v`, `ns`, server-internal fields) is dropped — they would either be rejected by createIndex or are not portable across mongod versions.

%w[
  unique sparse hidden expireAfterSeconds collation
  partialFilterExpression wildcardProjection
].freeze

Instance Attribute Summary

Attributes inherited from Base

#connection_config

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Base

#commented_sql, #post_insert_sql, #pre_insert_sql, #query_comment_text, #sql_query_comment, #to_copy_from_stdin

Constructor Details

#initialize(connection_config, logger) ⇒ MongodbAdapter

Returns a new instance of MongodbAdapter.



16
17
18
19
# File 'lib/exwiw/adapter/mongodb_adapter.rb', line 16

def initialize(connection_config, logger)
  super
  @state = {}
end

Class Method Details

.table_config_classObject



12
13
14
# File 'lib/exwiw/adapter/mongodb_adapter.rb', line 12

def self.table_config_class
  Exwiw::MongodbCollectionConfig
end

Instance Method Details

#build_query(config, dump_target, config_by_name) ⇒ Object



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
# File 'lib/exwiw/adapter/mongodb_adapter.rb', line 33

def build_query(config, dump_target, config_by_name)
  if config.embedded?
    raise NotImplementedError,
          "MongodbAdapter#build_query was called with embedded config '#{config.name}'. " \
          "Embedded configs are masked through the parent collection."
  end

  reject_filter!(config)
  # Stash the embedded-children index for the matching to_bulk_insert call
  # below. The Adapter contract does not pass config_by_name to
  # to_bulk_insert (SQL adapters don't need it), so we rely on the Runner
  # invariant that build_query is always called before to_bulk_insert for
  # the same config.
  @embedded_children_by_parent = index_embedded_children(config_by_name)

  # Which of this collection's fields downstream children will `$in`-match
  # against (always including primary_key). Stashed for the matching
  # #execute call to capture, by the same build_query-before-execute
  # invariant the embedded index relies on.
  @propagation_keys = propagation_keys_for(config, config_by_name)

  filter =
    if config.name == dump_target.table_name
      # `--ids-field` may override which field --ids is matched against;
      # otherwise fall back to the primary key. Note this only changes the
      # WHERE filter on the target collection — downstream foreign-key
      # propagation keys off each child belongs_to's `references` field
      # (default: the parent primary_key); see #execute, which stashes
      # those fields into @state.
      #
      # Type coercion is only applied to the primary key (`_id`), whose
      # stored type we know (Mongoid's default ObjectId). For a custom
      # `ids_field` the stored type is unknown, so the textual --ids are
      # left as Strings rather than guessed at — the caller passes values
      # matching the field's actual type.
      if dump_target.ids_field
        { dump_target.ids_field => { "$in" => dump_target.ids } }
      else
        { config.primary_key => { "$in" => coerce_ids(dump_target.ids) } }
      end
    else
      config.belongs_tos.each_with_object({}) do |relation, acc|
        # Constrain by the parent field this FK actually references
        # (`relation.references`, default the parent primary_key). The
        # values were captured from that field's documents in #execute, so
        # their BSON type already matches the stored FK — no coercion.
        values = parent_state_for(relation, config_by_name)
        next if values.nil? || values.empty?

        acc[relation.foreign_key] = { "$in" => values }
      end
    end

  Exwiw::MongoQuery::Find.new(
    collection: config.name,
    primary_key: config.primary_key,
    filter: filter,
    projection: build_projection(config, @propagation_keys),
  )
end

#describe_query(query) ⇒ Object



136
137
138
139
140
# File 'lib/exwiw/adapter/mongodb_adapter.rb', line 136

def describe_query(query)
  "find collection=#{query.collection} filter=#{query.filter.inspect} projection=#{query.projection.inspect}"
rescue => e
  "<unavailable: #{e.class}: #{e.message}>"
end

#dump_schema(ordered_tables, output_path) ⇒ Object



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
# File 'lib/exwiw/adapter/mongodb_adapter.rb', line 158

def dump_schema(ordered_tables, output_path)
  require 'json'

  collections = ordered_tables.reject(&:embedded?)

  File.open(output_path, 'w') do |file|
    file.puts("// Auto-generated by exwiw. Apply with: mongosh \"$MONGODB_URI\" #{File.basename(output_path)}")
    file.puts

    collections.each do |config|
      name = config.name
      file.puts(%(try { db.createCollection(#{JSON.generate(name)}); } catch (e) { if (e.code !== 48) throw e; }))
    end
    file.puts

    collections.each do |config|
      name = config.name
      indexes = db[name].indexes.to_a.reject { |idx| idx['name'] == '_id_' }
      indexes.each do |idx|
        key = idx['key']
        opts = idx.slice(*INDEX_OPTION_ALLOWLIST)
        opts['name'] = idx['name'] if idx['name']
        file.puts(%(db.getCollection(#{JSON.generate(name)}).createIndex(#{JSON.generate(key)}, #{JSON.generate(opts)});))
      end
    end
  end
  @logger.info("  Wrote schema for #{collections.size} collection(s) to #{output_path}.")
end

#dumpable?(config) ⇒ Boolean

Returns:

  • (Boolean)


21
22
23
# File 'lib/exwiw/adapter/mongodb_adapter.rb', line 21

def dumpable?(config)
  !config.embedded?
end

#execute(query) ⇒ Object



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/exwiw/adapter/mongodb_adapter.rb', line 94

def execute(query)
  @logger.debug("  Executing Mongo find on '#{query.collection}': filter=#{query.filter.inspect} projection=#{query.projection.inspect}")

  docs = db[query.collection]
    .find(query.filter)
    .projection(query.projection)
    .comment(query_comment_text("collection=#{query.collection}"))
    .to_a

  # Stash, per referenced field, the values children will `$in`-match
  # against. @propagation_keys is set by the build_query call for this same
  # collection; fall back to the primary key if execute is driven without a
  # preceding build_query (e.g. in isolation from a test).
  keys = @propagation_keys || [query.primary_key]
  @state[query.collection] = keys.each_with_object({}) do |key, acc|
    acc[key] = docs.map { |doc| doc[key] }
  end

  docs
end

#explain(_query) ⇒ Object

Raises:

  • (NotImplementedError)


132
133
134
# File 'lib/exwiw/adapter/mongodb_adapter.rb', line 132

def explain(_query)
  raise NotImplementedError, "MongodbAdapter does not support explain yet"
end

#output_extensionObject



142
143
144
# File 'lib/exwiw/adapter/mongodb_adapter.rb', line 142

def output_extension
  'jsonl'
end

#schema_output_extensionObject



146
147
148
# File 'lib/exwiw/adapter/mongodb_adapter.rb', line 146

def schema_output_extension
  'js'
end

#supports_bulk_delete?Boolean

Returns:

  • (Boolean)


187
188
189
# File 'lib/exwiw/adapter/mongodb_adapter.rb', line 187

def supports_bulk_delete?
  false
end

#to_bulk_delete(_query, _config) ⇒ Object

Raises:

  • (NotImplementedError)


128
129
130
# File 'lib/exwiw/adapter/mongodb_adapter.rb', line 128

def to_bulk_delete(_query, _config)
  raise NotImplementedError, "MongodbAdapter does not support bulk delete"
end

#to_bulk_insert(rows, config) ⇒ Object

NOTE: relies on @embedded_children_by_parent set by a prior build_query call for the same config. This implicit ordering exists because the Adapter contract intentionally does not thread config_by_name through to_bulk_insert (SQL adapters don’t need it). Safe in Runner, fragile in tests — call build_query first.



120
121
122
123
124
125
126
# File 'lib/exwiw/adapter/mongodb_adapter.rb', line 120

def to_bulk_insert(rows, config)
  rows.map do |doc|
    apply_replace_with!(doc, config)
    apply_embedded_masking!(doc, config)
    JSON.generate(extended_json(doc))
  end.join("\n")
end

#validate_as_dump_target!(config) ⇒ Object

Raises:

  • (NotImplementedError)


25
26
27
28
29
30
31
# File 'lib/exwiw/adapter/mongodb_adapter.rb', line 25

def validate_as_dump_target!(config)
  return unless config.embedded?

  raise NotImplementedError,
        "dump_target '#{config.name}' is an embedded MongodbCollectionConfig; " \
        "specify a top-level collection instead."
end