Class: Tina4::Drivers::MongodbDriver

Inherits:
Object
  • Object
show all
Includes:
Tina4::DatabaseAdapter
Defined in:
lib/tina4/drivers/mongodb_driver.rb

Constant Summary

Constants included from Tina4::DatabaseAdapter

Tina4::DatabaseAdapter::ABSTRACT_CONTRACT, Tina4::DatabaseAdapter::CONNECT_TIMEOUT_SLACK_SECONDS, Tina4::DatabaseAdapter::CONNECT_TIMEOUT_VAR, Tina4::DatabaseAdapter::CONTRACT, Tina4::DatabaseAdapter::DEFAULT_CONNECT_TIMEOUT_SECONDS

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Tina4::DatabaseAdapter

#autocommit, #autocommit=, bounding_connect, connect_timed_out!, connect_timeout_seconds, connect_timeout_whole_seconds, #execute_many, #fetch, #fetch_one, implemented_by?, #open, #start_transaction, #supports_atomic_batch, #supports_atomic_batch=, #table_exists?, validate!

Instance Attribute Details

#connectionObject (readonly)

Returns the value of attribute connection.



7
8
9
# File 'lib/tina4/drivers/mongodb_driver.rb', line 7

def connection
  @connection
end

#dbObject (readonly)

Returns the value of attribute db.



7
8
9
# File 'lib/tina4/drivers/mongodb_driver.rb', line 7

def db
  @db
end

Instance Method Details

#apply_limit(sql, limit, offset = 0) ⇒ Object

MongoDB has no LIMIT clause — ignore; already handled in execute_query Uses the SAME detector as Database#fetch (scrubbed + anchored to the end) instead of its own naive sql.upcase.include?("LIMIT"), which mistook a column named rate_limit or a 'LIMIT' literal for a real clause and returned the statement uncapped. Appends on a NEW LINE so a trailing -- comment cannot swallow the clause.



153
154
155
156
157
158
159
# File 'lib/tina4/drivers/mongodb_driver.rb', line 153

def apply_limit(sql, limit, offset = 0)
  return sql if Tina4::Database.has_trailing_limit?(sql)
  modified = sql.dup
  modified += "\nLIMIT #{limit}" if limit && limit > 0
  modified += " OFFSET #{offset}" if offset && offset > 0
  modified
end

#begin_transactionObject

MongoDB transactions require a replica set — wrap in session if available



162
163
164
# File 'lib/tina4/drivers/mongodb_driver.rb', line 162

def begin_transaction
  # no-op for standalone; transaction support via session handled externally
end

#closeObject



37
38
39
40
41
42
# File 'lib/tina4/drivers/mongodb_driver.rb', line 37

def close
  @client&.close
  @client = nil
  @db = nil
  @connection = nil
end

#columns(table_name) ⇒ Object



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/tina4/drivers/mongodb_driver.rb', line 183

def columns(table_name)
  collection = @db[table_name.to_s]
  sample = collection.find.limit(1).first
  return [] unless sample

  sample.keys.map do |key|
    {
      name: key,
      type: sample[key].class.name,
      nullable: true,
      default: nil,
      primary_key: key == "_id"
    }
  end
end

#commitObject



166
167
168
# File 'lib/tina4/drivers/mongodb_driver.rb', line 166

def commit
  # no-op
end

#connect(connection_string, username: nil, password: nil) ⇒ Object



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
# File 'lib/tina4/drivers/mongodb_driver.rb', line 9

def connect(connection_string, username: nil, password: nil)
  begin
    require "mongo"
  rescue LoadError
    raise LoadError,
          "The 'mongo' gem is required for MongoDB connections. Install one of:\n" \
          "    bundle add mongo     # if your project uses Bundler\n" \
          "    gem install mongo    # bare driver"
  end

  uri = build_uri(connection_string, username, password)
  @db_name = extract_db_name(connection_string)
  # The mongo driver's OWN connect_timeout, in seconds. It already bounds
  # itself - MEASURED: Mongo::Client.new against a TCPServer that accepts
  # and never replies returns after 10.02s, the gem's hard-coded default -
  # so this makes it honour the Tina4 variable instead. A URI that spells
  # connectTimeoutMS itself keeps that value. No bounding_connect wrapper:
  # Client.new does NOT raise on an unreachable server (server selection
  # happens later, on the first operation), so there is no expiry to name.
  seconds = Tina4::DatabaseAdapter.connect_timeout_seconds
  options = {}
  options[:connect_timeout] = seconds if seconds && !uri.include?("connectTimeoutMS")
  @client = Mongo::Client.new(uri, options)
  @db = @client.use(@db_name)
  @connection = @db
  @last_insert_id = nil
end

#execute(sql, params = []) ⇒ Object

Execute a DML statement (INSERT, UPDATE, DELETE, CREATE)



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
# File 'lib/tina4/drivers/mongodb_driver.rb', line 63

def execute(sql, params = [])
  parsed = parse_sql(sql, params)
  collection = @db[parsed[:collection]]

  case parsed[:operation]
  when :insert
    result = collection.insert_one(parsed[:document])
    @last_insert_id = result.inserted_id.to_s
    result
  when :update
    # parse_update guarantees a scoped filter (or it raised) — no || {} fallback.
    collection.update_many(parsed[:filter], { "$set" => parsed[:updates] })
  when :delete
    # parse_delete guarantees a scoped filter (or it raised) — no || {} fallback.
    collection.delete_many(parsed[:filter])
  when :create_collection
    begin
      @db.command(create: parsed[:collection].to_s)
    rescue Mongo::Error::OperationFailure
      # Collection already exists — ignore
    end
    nil
  when :find
    execute_query(sql, params)
  else
    nil
  end
end

#execute_query(sql, params = []) ⇒ Object

Execute a query (SELECT-like) and return array of symbol-keyed hashes



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/tina4/drivers/mongodb_driver.rb', line 45

def execute_query(sql, params = [])
  parsed = parse_sql(sql, params)
  collection = @db[parsed[:collection]]

  case parsed[:operation]
  when :find
    cursor = collection.find(parsed[:filter] || {})
    cursor = cursor.projection(parsed[:projection]) if parsed[:projection] && !parsed[:projection].empty?
    cursor = cursor.sort(parsed[:sort]) if parsed[:sort] && !parsed[:sort].empty?
    cursor = cursor.skip(parsed[:skip]) if parsed[:skip] && parsed[:skip] > 0
    cursor = cursor.limit(parsed[:limit]) if parsed[:limit] && parsed[:limit] > 0
    cursor.map { |doc| mongo_doc_to_hash(doc) }
  else
    []
  end
end

#get_database_typeObject

ADR-0044 required adapter capability.



175
176
177
# File 'lib/tina4/drivers/mongodb_driver.rb', line 175

def get_database_type
  'mongodb'
end

#get_next_id(table, pk_column = "id") ⇒ Object

Atomic, monotonic, concurrency-safe next id — feature 16. A findOneAndUpdate($inc) on the tina4_sequences collection, keyed by _id (its built-in unique index makes concurrent first-use upserts race-safe: two callers can never create two counters for one table). Seeds from MAX(pk_column) the FIRST time only ($setOnInsert). Raises on an impossible empty result rather than returning a fixed id that could collide with an existing row.



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
# File 'lib/tina4/drivers/mongodb_driver.rb', line 103

def get_next_id(table, pk_column = "id")
  sequences = @db["tina4_sequences"]
  seq_name = "#{table}.#{pk_column}"

  if sequences.find("_id" => seq_name).first.nil?
    seed = 0
    begin
      max_doc = @db[table.to_s].find.sort(pk_column => -1).limit(1).first
      seed = max_doc[pk_column].to_i if max_doc && max_doc[pk_column]
    rescue StandardError
      # Collection may not exist yet — seed 0.
    end
    begin
      sequences.update_one(
        { "_id" => seq_name },
        { "$setOnInsert" => { "current_value" => seed } },
        upsert: true
      )
    rescue StandardError
      # Race — another caller seeded first; the atomic $inc below still holds.
    end
  end

  doc = sequences.find_one_and_update(
    { "_id" => seq_name },
    { "$inc" => { "current_value" => 1 } },
    return_document: :after,
    upsert: true
  )
  if doc.nil? || doc["current_value"].nil?
    raise "get_next_id: MongoDB counter '#{seq_name}' produced no value"
  end

  doc["current_value"].to_i
end

#last_insert_idObject



92
93
94
# File 'lib/tina4/drivers/mongodb_driver.rb', line 92

def last_insert_id
  @last_insert_id
end

#placeholderObject



139
140
141
# File 'lib/tina4/drivers/mongodb_driver.rb', line 139

def placeholder
  "?"
end

#placeholders(count) ⇒ Object



143
144
145
# File 'lib/tina4/drivers/mongodb_driver.rb', line 143

def placeholders(count)
  (["?"] * count).join(", ")
end

#rollbackObject



170
171
172
# File 'lib/tina4/drivers/mongodb_driver.rb', line 170

def rollback
  # no-op
end

#tablesObject



179
180
181
# File 'lib/tina4/drivers/mongodb_driver.rb', line 179

def tables
  @db.collection_names.reject { |n| n.start_with?("system.") }
end