Skip to content

Commit

Permalink
Merge pull request #28 from JackDanger/deparse-constraints
Browse files Browse the repository at this point in the history
Deparse constraints
  • Loading branch information
lfittl committed Aug 24, 2015
2 parents 762b11d + 6c4aa4d commit 84c0c03
Show file tree
Hide file tree
Showing 2 changed files with 183 additions and 1 deletion.
136 changes: 135 additions & 1 deletion lib/pg_query/deparse.rb
Original file line number Diff line number Diff line change
Expand Up @@ -357,9 +357,14 @@ def deparse_columndef(node)

def deparse_constraint(node)
output = []
if node['conname']
output << 'CONSTRAINT'
output << node['conname']
end
# NOT_NULL -> NOT NULL
output << node['contype'].gsub('_', ' ')
output << deparse_item(node['raw_expr']) if node['raw_expr']
output << '(' + node['keys'].join(', ') + ')' if node['keys']
output.join(' ')
end

Expand Down Expand Up @@ -547,6 +552,10 @@ def deparse_typecast(node)
end

def deparse_typename(node)
# Intervals are tricky and should be handled in a separate method because
# they require performing some bitmask operations.
return deparse_interval_type(node) if node['names'] == %w(pg_catalog interval)

output = []
output << 'SETOF' if node['setof']

Expand All @@ -555,7 +564,6 @@ def deparse_typename(node)
deparse_item(item)
end.join(', ')
end

output << deparse_typename_cast(node['names'], arguments)

output.join(' ')
Expand Down Expand Up @@ -589,11 +597,36 @@ def deparse_typename_cast(names, arguments) # rubocop:disable Metrics/Cyclomatic
'real'
when 'float8'
'double'
when 'time'
'time'
when 'timetz'
'time with time zone'
when 'timestamp'
'timestamp'
when 'timestamptz'
'timestamp with time zone'
else
fail format("Can't deparse type: %s", type)
end
end

# Deparses interval type expressions like `interval year to month` or
# `interval hour to second(5)`
def deparse_interval_type(node)
type = ['interval']
typmods = node['typmods'].map { |typmod| deparse_item(typmod) }
type << DeparseInterval.from_int(typmods.first.to_i).map do |part|
# only the `second` type can take an argument.
if part == 'second' && typmods.size == 2
"second(#{typmods.last})"
else
part
end.downcase
end.join(' to ')

type.join(' ')
end

def deparse_nulltest(node)
output = [deparse_item(node['arg'])]
if node['nulltesttype'] == 0
Expand Down Expand Up @@ -678,4 +711,105 @@ def relpersistence(rangevar)
end
end
end
# A type called 'interval hour to minute' is stored in a compressed way by
# simplifying 'hour to minute' to a simple integer. This integer is computed
# by looking up the arbitrary number (always a power of two) for 'hour' and
# the one for 'minute' and XORing them together.
#
# For example, when parsing "interval hour to minute":
#
# HOUR_MASK = 10
# MINUTE_MASK = 11
# mask = (1 << 10) | (1 << 11)
# mask = 1024 | 2048
# mask = (010000000000
# xor
# 100000000000)
# mask = 110000000000
# mask = 3072
#
# Postgres will store this type as 'interval,3072'
# We deparse it by simply reversing that process.

module DeparseInterval
# From src/include/utils/datetime.h
# The number is the power of 2 used for the mask.
MASKS = {
0 => 'RESERV',
1 => 'MONTH',
2 => 'YEAR',
3 => 'DAY',
4 => 'JULIAN',
5 => 'TZ',
6 => 'DTZ',
7 => 'DYNTZ',
8 => 'IGNORE_DTF',
9 => 'AMPM',
10 => 'HOUR',
11 => 'MINUTE',
12 => 'SECOND',
13 => 'MILLISECOND',
14 => 'MICROSECOND',
15 => 'DOY',
16 => 'DOW',
17 => 'UNITS',
18 => 'ADBC',
19 => 'AGO',
20 => 'ABS_BEFORE',
21 => 'ABS_AFTER',
22 => 'ISODATE',
23 => 'ISOTIME',
24 => 'WEEK',
25 => 'DECADE',
26 => 'CENTURY',
27 => 'MILLENNIUM',
28 => 'DTZMOD'
}
KEYS = MASKS.invert

# Postgres stores the interval 'day second' as 'day hour minute second' so
# we need to reconstruct the sql with only the largest and smallest time
# values. Since the rules for this are hardcoded in the grammar (and the
# above list is not sorted in any sensible way) it makes sense to hardcode
# the patterns here, too.
#
# This hash takes the form:
#
# { (1 << 1) | (1 << 2) => 'year to month' }
#
# Which is:
#
# { 6 => 'year to month' }
#
SQL_BY_MASK = {
(1 << KEYS['YEAR']) => %w(year),
(1 << KEYS['MONTH']) => %w(month),
(1 << KEYS['DAY']) => %w(day),
(1 << KEYS['HOUR']) => %w(hour),
(1 << KEYS['MINUTE']) => %w(minute),
(1 << KEYS['SECOND']) => %w(second),
(1 << KEYS['YEAR'] |
1 << KEYS['MONTH']) => %w(year month),
(1 << KEYS['DAY'] |
1 << KEYS['HOUR']) => %w(day hour),
(1 << KEYS['DAY'] |
1 << KEYS['HOUR'] |
1 << KEYS['MINUTE']) => %w(day minute),
(1 << KEYS['DAY'] |
1 << KEYS['HOUR'] |
1 << KEYS['MINUTE'] |
1 << KEYS['SECOND']) => %w(day second),
(1 << KEYS['HOUR'] |
1 << KEYS['MINUTE']) => %w(hour minute),
(1 << KEYS['HOUR'] |
1 << KEYS['MINUTE'] |
1 << KEYS['SECOND']) => %w(hour second),
(1 << KEYS['MINUTE'] |
1 << KEYS['SECOND']) => %w(minute second)
}

def self.from_int(int)
SQL_BY_MASK[int]
end
end
end
48 changes: 48 additions & 0 deletions spec/lib/deparse_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,25 @@
it { is_expected.to eq oneline_query }
end

context 'with common types' do
let(:query) do
"""
CREATE TABLE distributors (
name varchar(40) DEFAULT 'Luso Films',
len interval hour to second(3),
name varchar(40) DEFAULT 'Luso Films',
did int DEFAULT nextval('distributors_serial'),
stamp timestamp DEFAULT pg_catalog.now() NOT NULL,
stamptz timestamp with time zone,
time time NOT NULL,
timetz time with time zone,
CONSTRAINT name_len PRIMARY KEY (name, len)
);
"""
end
it { is_expected.to eq oneline_query }
end

context 'with alternate typecasts' do
let(:query) do
"""
Expand Down Expand Up @@ -479,4 +498,33 @@
it { is_expected.to eq oneline_query }
end
end

describe PgQuery::DeparseInterval do
describe '.from_int' do
it 'unpacks the parts of the interval' do
# Supported combinations taken directly from gram.y
{
# the SQL form => what PG stores
%w(year) => %w(YEAR),
%w(month) => %w(MONTH),
%w(day) => %w(DAY),
%w(hour) => %w(HOUR),
%w(minute) => %w(MINUTE),
%w(second) => %w(SECOND),
%w(year month) => %w(YEAR MONTH),
%w(day hour) => %w(DAY HOUR),
%w(day minute) => %w(DAY HOUR MINUTE),
%w(day second) => %w(DAY HOUR MINUTE SECOND),
%w(hour minute) => %w(HOUR MINUTE),
%w(hour second) => %w(HOUR MINUTE SECOND),
%w(minute second) => %w(MINUTE SECOND)
}.each do |sql_parts, storage_parts|
number = storage_parts.reduce(0) do |num, part|
num | (1 << described_class::KEYS[part])
end
expect(described_class.from_int(number).sort).to eq(sql_parts.sort)
end
end
end
end
end

0 comments on commit 84c0c03

Please sign in to comment.