Skip to content

Instantly share code, notes, and snippets.

@doolin
Last active December 24, 2022 14:29
Show Gist options
  • Save doolin/5c45a468ad144bfa732741c6fd2267c4 to your computer and use it in GitHub Desktop.
Save doolin/5c45a468ad144bfa732741c6fd2267c4 to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'active_record'
require 'rspec/autorun'
require 'pg'
require 'byebug'
# Extend for effect
class String
def colorize(color_code)
"\e[#{color_code};5m#{self}\e[0m"
end
def red = colorize(31)
end
# How to run this code:
# 1. chmod 755 postgres_errors.rb
# 2. ./postgres_errors.rb -fd
# The purpose of this example is to determine how ActiveRecord
# and ActiveModel report validation errors at the class (Model) level
# and how Postgres reports violations at the database level.
#
# Note: it's surely possible to extract all of this information from
# the documentation, but creating a demo or model might while be a lot
# faster than digging through multiple websites for the same information.
# Use postgres, because we want to investigate how constraint
# errors are raised from pg. Given running a local pg instance,
# `createdb scratch` possibly with an appropriate user name.
DB_SPEC = {
adapter: 'postgresql',
encoding: 'unicode',
database: 'scratch',
username: 'postgres'
}.freeze
ActiveRecord::Base.establish_connection(DB_SPEC)
FIRST_NAME_LENGTH = 16
# Example migration for custom validation.
class Orders < ActiveRecord::Migration[7.0]
def change
create_table :orders do |t|
t.string :first_name # use Rails validations
t.string :last_name, null: false, limit: FIRST_NAME_LENGTH, foo: :bar
end
end
end
Orders.migrate(:up) unless Orders.data_source_exists?(:orders)
# The usual base class for ActiveRecord models
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
# Descriptive comment for reek
class Order < ApplicationRecord
# Validating in the Rails model may save a trip to the database, which
# consumes a connection (probably) and induces network latency if
# there is an error. On the other hand, if there are no errors,
# validating in the Rails model creates overhead in the application.
#
# One valid question is: what's cheaper to scale, the application call
# stack, or database connections?
validates :first_name, presence: true, length: { maximum: FIRST_NAME_LENGTH }
end
RSpec.describe Order do
let(:first_name) { 'foo' }
let(:last_name) { 'bar' }
let(:params) do
{
first_name:,
last_name:
}
end
subject(:order) { Order.new(params) }
describe 'Rails validations' do
context 'when valid' do
example 'model passes with correct parameters' do
expect(order.valid?).to be true
end
example "model is valid when 'last_name' is nil #{'(Danger!!!)'.red}" do
params[:last_name] = nil
expect(order.valid?).to be true
end
end
context 'when invalid with Rails' do
it 'raises an ActiveRecord error for long first_name' do
params[:first_name] = first_name * 6
expect do
order.save!
end.to raise_error(ActiveRecord::RecordInvalid)
.with_message('Validation failed: First name is too long (maximum is 16 characters)')
end
it '".save!" raises an ActiveRecord error for long first_name' do
params[:first_name] = first_name * 6
expect do
order.save!
end.to raise_error(ActiveRecord::RecordInvalid)
.with_message('Validation failed: First name is too long (maximum is 16 characters)')
end
it '".save" does not raise an ActiveRecord error for long first_name' do
params[:first_name] = first_name * 6
expect(order.save).to be false
end
end
end
describe 'PostgreSQL constraints' do
context 'when violates postgres constraints' do
it '".save!" raises a postgres violation on long first_name' do
params[:last_name] = last_name * 6
expect do
order.save!
end.to raise_error(ActiveRecord::ValueTooLong)
.with_message(/PG::StringDataRightTruncation: ERROR: value too long for type character varying\(16\)/)
end
it '".save" raises postgres violation when last_name is missing' do
params[:last_name] = nil
expect do
order.save
end.to raise_error(ActiveRecord::NotNullViolation)
.with_message(/PG::NotNullViolation: ERROR: null value in column "last_name" of relation "orders" violates not-null constraint/)
end
end
end
end
# Fast and dirty way to reboot the table, causes an error when RSpec runs.
# Change the migration, uncomment the following line, run the script,
# comment out again, script will run with updated table.
# Orders.migrate(:down)
#
# Why do both?
# protects in different ways during data migration, during, backend updates,
# In a legacy code base, add for new column.
# For operator-driven updates
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment