Skip to content

Commit 72a17f3

Browse files
committed
Support ROWID-based LOB writes for tables without primary keys
Add ability to write LOB data to tables that don't have a primary key by: 1. Exposing cursor.rowid method in OCI connection wrapper 2. Capturing ROWID after INSERT in exec_insert (@last_insert_rowid) 3. Using ROWID in write_lobs WHERE clause when no PK is available 4. Supporting composite primary keys (Array) in write_lobs The ROWID approach works because: - Ruby-oci8's cursor.rowid returns the ROWID of the last inserted row - ROWID uniquely identifies any row regardless of table structure - The after_create callback fires immediately after INSERT on same connection Also includes ORA-01741 diagnostic logging for empty column detection.
1 parent e834b46 commit 72a17f3

2 files changed

Lines changed: 53 additions & 5 deletions

File tree

lib/active_record/connection_adapters/oracle_enhanced/database_statements.rb

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,18 @@ def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, retu
122122
sql, binds = sql_for_insert(sql, pk, binds, returning)
123123
type_casted_binds = type_casted_binds(binds)
124124

125+
# Check for empty column names in the SQL (diagnostic for ORA-01741)
126+
if sql =~ /INSERT INTO.*?\(([^)]+)\)/i
127+
columns = $1.split(",").map(&:strip)
128+
empty_columns = columns.select { |c| c.empty? || c == '""' || c == "''" }
129+
if empty_columns.any? || columns.any? { |c| c =~ /^\s*$/ }
130+
Rails.logger.error "ORA-01741 DEBUG: Empty column detected in INSERT!"
131+
Rails.logger.error " SQL: #{sql[0..500]}"
132+
Rails.logger.error " Columns parsed: #{columns.inspect}"
133+
Rails.logger.error " pk: #{pk.inspect}, returning: #{returning.inspect}"
134+
end
135+
end
136+
125137
log(sql, name, binds, type_casted_binds) do
126138
cached = false
127139
cursor = nil
@@ -155,6 +167,13 @@ def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, retu
155167
returning_id = cursor.get_returning_param(returning_id_index, Integer).to_i
156168
rows << [returning_id]
157169
end
170+
171+
# Capture ROWID for LOB writes on tables without primary keys
172+
# This must happen right after exec_update while the cursor still has the rowid
173+
if cursor.respond_to?(:rowid)
174+
@last_insert_rowid = cursor.rowid
175+
end
176+
158177
cursor.close unless cached
159178
build_result(columns: returning_id_col || [], rows: rows)
160179
end
@@ -280,7 +299,28 @@ def empty_insert_statement_value
280299

281300
# Writes LOB values from attributes for specified columns
282301
def write_lobs(table_name, klass, attributes, columns) # :nodoc:
283-
id = quote(attributes[klass.primary_key])
302+
pk = klass.primary_key
303+
304+
# Build WHERE clause based on what's available
305+
where_clause = if pk.nil? && @last_insert_rowid
306+
# Use ROWID when no primary key is defined but we have the rowid from INSERT
307+
"ROWID = #{quote(@last_insert_rowid)}"
308+
elsif pk.nil?
309+
# No primary key and no ROWID - cannot locate the row
310+
if columns.any? { |col| attributes[col.name].present? }
311+
@logger&.warn "Cannot write LOB columns for #{table_name} - table has no primary key " \
312+
"and ROWID is not available. LOB data may be truncated. Consider adding " \
313+
"a primary key constraint or explicitly setting self.primary_key on the model."
314+
end
315+
return
316+
elsif pk.is_a?(Array)
317+
# Composite primary key - build AND conditions for each column
318+
pk.map { |col| "#{quote_column_name(col)} = #{quote(attributes[col])}" }.join(" AND ")
319+
else
320+
# Single primary key
321+
"#{quote_column_name(pk)} = #{quote(attributes[pk])}"
322+
end
323+
284324
columns.each do |col|
285325
value = attributes[col.name]
286326
# changed sequence of next two lines - should check if value is nil before converting to yaml
@@ -289,16 +329,18 @@ def write_lobs(table_name, klass, attributes, columns) # :nodoc:
289329
# value can be nil after serialization because ActiveRecord serializes [] and {} as nil
290330
next unless value
291331
uncached do
292-
unless lob_record = select_one(sql = <<~SQL.squish, "Writable Large Object")
293-
SELECT #{quote_column_name(col.name)} FROM #{quote_table_name(table_name)}
294-
WHERE #{quote_column_name(klass.primary_key)} = #{id} FOR UPDATE
295-
SQL
332+
sql = "SELECT #{quote_column_name(col.name)} FROM #{quote_table_name(table_name)} " \
333+
"WHERE #{where_clause} FOR UPDATE"
334+
unless lob_record = select_one(sql, "Writable Large Object")
296335
raise ActiveRecord::RecordNotFound, "statement #{sql} returned no rows"
297336
end
298337
lob = lob_record[col.name]
299338
_connection.write_lob(lob, value.to_s, col.type == :binary)
300339
end
301340
end
341+
342+
# Clear the stored ROWID after use to prevent it being used for wrong row
343+
@last_insert_rowid = nil
302344
end
303345

304346
private

lib/active_record/connection_adapters/oracle_enhanced/oci_connection.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,12 @@ def get_returning_param(position, type)
193193
def close
194194
@raw_cursor.close
195195
end
196+
197+
# Returns the ROWID of the last inserted/updated/deleted row
198+
# This is useful for LOB writes on tables without primary keys
199+
def rowid
200+
@raw_cursor.rowid
201+
end
196202
end
197203

198204
def select(sql, name = nil, return_column_names = false)

0 commit comments

Comments
 (0)