Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deparse constraints #28

Merged
merged 3 commits into from
Aug 24, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work 👌

This reminds me, we should split the deparsing into multiple files (based on node type).

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