diff --git a/lib/pg_query/deparse.rb b/lib/pg_query/deparse.rb index bab20bc6..3fdcdf89 100644 --- a/lib/pg_query/deparse.rb +++ b/lib/pg_query/deparse.rb @@ -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 @@ -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'] @@ -555,7 +564,6 @@ def deparse_typename(node) deparse_item(item) end.join(', ') end - output << deparse_typename_cast(node['names'], arguments) output.join(' ') @@ -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 @@ -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 diff --git a/spec/lib/deparse_spec.rb b/spec/lib/deparse_spec.rb index 53d27636..55dd2127 100644 --- a/spec/lib/deparse_spec.rb +++ b/spec/lib/deparse_spec.rb @@ -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 """ @@ -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