Last active
December 24, 2022 14:29
-
-
Save doolin/5c45a468ad144bfa732741c6fd2267c4 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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