Class: Tina4::Drivers::MongodbDriver
- Inherits:
-
Object
- Object
- Tina4::Drivers::MongodbDriver
- 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
-
#connection ⇒ Object
readonly
Returns the value of attribute connection.
-
#db ⇒ Object
readonly
Returns the value of attribute db.
Instance Method Summary collapse
-
#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. -
#begin_transaction ⇒ Object
MongoDB transactions require a replica set — wrap in session if available.
- #close ⇒ Object
- #columns(table_name) ⇒ Object
- #commit ⇒ Object
- #connect(connection_string, username: nil, password: nil) ⇒ Object
-
#execute(sql, params = []) ⇒ Object
Execute a DML statement (INSERT, UPDATE, DELETE, CREATE).
-
#execute_query(sql, params = []) ⇒ Object
Execute a query (SELECT-like) and return array of symbol-keyed hashes.
-
#get_database_type ⇒ Object
ADR-0044 required adapter capability.
-
#get_next_id(table, pk_column = "id") ⇒ Object
Atomic, monotonic, concurrency-safe next id — feature 16.
- #last_insert_id ⇒ Object
- #placeholder ⇒ Object
- #placeholders(count) ⇒ Object
- #rollback ⇒ Object
- #tables ⇒ Object
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
#connection ⇒ Object (readonly)
Returns the value of attribute connection.
7 8 9 |
# File 'lib/tina4/drivers/mongodb_driver.rb', line 7 def connection @connection end |
#db ⇒ Object (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_transaction ⇒ Object
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 |
#close ⇒ Object
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 |
#commit ⇒ Object
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 = {} [:connect_timeout] = seconds if seconds && !uri.include?("connectTimeoutMS") @client = Mongo::Client.new(uri, ) @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_type ⇒ Object
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_id ⇒ Object
92 93 94 |
# File 'lib/tina4/drivers/mongodb_driver.rb', line 92 def last_insert_id @last_insert_id end |
#placeholder ⇒ Object
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 |
#rollback ⇒ Object
170 171 172 |
# File 'lib/tina4/drivers/mongodb_driver.rb', line 170 def rollback # no-op end |
#tables ⇒ Object
179 180 181 |
# File 'lib/tina4/drivers/mongodb_driver.rb', line 179 def tables @db.collection_names.reject { |n| n.start_with?("system.") } end |