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, #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
# 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)

  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 still keys off `primary_key` (see #execute, which
      # stashes doc[primary_key] 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
      constrained = config.belongs_tos.select do |relation|
        @state.key?(relation.table_name) && !@state[relation.table_name].empty?
      end

      if constrained.empty?
        {}
      else
        constrained.each_with_object({}) do |relation, acc|
          acc[relation.foreign_key] = { "$in" => @state[relation.table_name] }
        end
      end
    end

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

#dump_schema(ordered_tables, output_path) ⇒ Object



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

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



88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/exwiw/adapter/mongodb_adapter.rb', line 88

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

  @state[query.collection] = docs.map { |doc| doc[query.primary_key] }

  docs
end

#explain(_query) ⇒ Object

Raises:

  • (NotImplementedError)


119
120
121
# File 'lib/exwiw/adapter/mongodb_adapter.rb', line 119

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

#output_extensionObject



123
124
125
# File 'lib/exwiw/adapter/mongodb_adapter.rb', line 123

def output_extension
  'jsonl'
end

#schema_output_extensionObject



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

def schema_output_extension
  'js'
end

#supports_bulk_delete?Boolean

Returns:

  • (Boolean)


168
169
170
# File 'lib/exwiw/adapter/mongodb_adapter.rb', line 168

def supports_bulk_delete?
  false
end

#to_bulk_delete(_query, _config) ⇒ Object

Raises:

  • (NotImplementedError)


115
116
117
# File 'lib/exwiw/adapter/mongodb_adapter.rb', line 115

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.



107
108
109
110
111
112
113
# File 'lib/exwiw/adapter/mongodb_adapter.rb', line 107

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