📌 Overview & Preview
Goal: Convert any ActiveRecord model instance into a SQL INSERT
statement — handling hashes, arrays, and enum columns gracefully.
Example Input:
# setup User.create!(name: "John", role: "admin", preferences: { theme: "dark" }, access_levels: ["read", "write"]) # Get insert statement User.last.insert_sql
Expected SQL Output:
INSERT INTO users (id, name, role, preferences, access_levels, created_at, updated_at) VALUES (1, 'John', 1, '{"theme":"dark"}', '["read","write"]', '2025-04-21 10:00:00', '2025-04-21 10:00:00');
📌 Part 1: Generating a Basic SQL Insert Statement for a Specific Model
user = User.last insert_statement = <<-SQL INSERT INTO users (#{user.attributes.keys.join(', ')}) VALUES (#{user.attributes.values.map { |v| ActiveRecord::Base.connection.quote(v) }.join(', ')}) SQL puts insert_statement
Problem: If your model has a Hash or Array attribute (like preferences
), this raises:
TypeError: can't quote Hash
👉 Next up: handle non-scalar values.
📌 Part 2: Handling Hash and Array Columns
Solution: Serialize them to JSON before quoting.
require 'json' user = User.last columns = user.attributes.keys values = user.attributes.values.map do |value| value = value.to_json if value.is_a?(Hash) || value.is_a?(Array) ActiveRecord::Base.connection.quote(value) end insert_statement = <<-SQL INSERT INTO users (#{columns.join(', ')}) VALUES (#{values.join(', ')}); SQL puts insert_statement
✅ Hashes and arrays now safely handled.
👉 Next up: handle enum columns.
📌 Part 3: Handling Enum Columns
Problem: Rails stores enums as integers in the DB but exposes them as strings.
Example:
class User < ApplicationRecord enum role: { guest: 0, admin: 1, super_admin: 2 } end
If user.role
is "admin", we need to insert 1
in SQL.
Solution:
enum_columns = User.defined_enums values = user.attributes.map do |attr_name, value| if enum_columns.key?(attr_name) enum_mapping = User.send(attr_name.pluralize) int_value = enum_mapping[value] ActiveRecord::Base.connection.quote(int_value) else value = value.to_json if value.is_a?(Hash) || value.is_a?(Array) ActiveRecord::Base.connection.quote(value) end end
✅ Enum values converted to DB integers.
👉 Next up: make it generic.
📌 Part 4: Building a Generic SQL Insert Generator for Any Model
Solution:
def generate_insert_sql(model_instance) model_class = model_instance.class table_name = model_class.table_name enum_columns = model_class.defined_enums def safe_quote(model_class, attr_name, value, enum_columns) if enum_columns.key?(attr_name) enum_mapping = model_class.send(attr_name.pluralize) int_value = enum_mapping[value] ActiveRecord::Base.connection.quote(int_value) else case value when Hash, Array ActiveRecord::Base.connection.quote(value.to_json) else ActiveRecord::Base.connection.quote(value) end end end columns = model_instance.attributes.keys values = columns.map do |attr_name| value = model_instance.attributes[attr_name] safe_quote(model_class, attr_name, value, enum_columns) end <<-SQL INSERT INTO #{table_name} (#{columns.join(', ')}) VALUES (#{values.join(', ')}); SQL end
✅ Works for any model instance.
👉 Next up: make it Rails-native.
📌 Part 5: Extending ActiveRecord::Base for a Reusable Method
Goal: Add an insert_sql
instance method to all models via ApplicationRecord
.
Solution:
class ApplicationRecord < ActiveRecord::Base self.abstract_class = true def insert_sql model_class = self.class table_name = model_class.table_name enum_columns = model_class.defined_enums def safe_quote(model_class, attr_name, value, enum_columns) if enum_columns.key?(attr_name) enum_mapping = model_class.send(attr_name.pluralize) int_value = enum_mapping[value] ActiveRecord::Base.connection.quote(int_value) else case value when Hash, Array ActiveRecord::Base.connection.quote(value.to_json) else ActiveRecord::Base.connection.quote(value) end end end columns = attributes.keys values = columns.map do |attr_name| value = attributes[attr_name] safe_quote(model_class, attr_name, value, enum_columns) end <<-SQL INSERT INTO #{table_name} (#{columns.join(', ')}) VALUES (#{values.join(', ')}); SQL end end
✅ Now you can call:
puts User.first.insert_sql
Sample Output:
INSERT INTO users (id, name, role, preferences, access_levels, created_at, updated_at) VALUES (1, 'John', 1, '{"theme":"dark"}', '["read","write"]', '2025-04-21 10:00:00', '2025-04-21 10:00:00');
puts Department.last.insert_sql
Sample Output:
INSERT INTO departments (id, name, created_at, updated_at) VALUES (42, 'Engineering', '2025-04-20 12:00:00', '2025-04-20 12:00:00');
Bonus: Works across your entire app — clean, DRY, and Rails-idiomatic.
📌 Final Thoughts
We went from:
- 🔸 Hardcoding a single model (User)
- 🔸 Handling hashes and arrays (preferences, access_levels)
- 🔸 Handling enum columns (role)
- 🔸 Building a generic function for any model
- 🔸 Extending ActiveRecord elegantly with
insert_sql
Top comments (0)