From 55715dc9c3fb353563a96e2b5ef7f8c1bf71e06d Mon Sep 17 00:00:00 2001 From: James Coleman Date: Fri, 2 Feb 2018 10:34:45 -0500 Subject: [PATCH] Parse CTEs and nested selects in INSERT/UPDATE Parsing of INSERT/UPDATE statements appears to be entirely untested, so I added a basic test for each (asserting what tables are parsed), and then added support for WITH clauses to each as well as SELECT clauses in INSERT statements. --- lib/pg_query/parse.rb | 37 ++++++++++++++++++++------- spec/lib/parse_spec.rb | 58 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/lib/pg_query/parse.rb b/lib/pg_query/parse.rb index 14265c42..fa09c309 100644 --- a/lib/pg_query/parse.rb +++ b/lib/pg_query/parse.rb @@ -97,18 +97,23 @@ def load_tables_and_aliases! # rubocop:disable Metrics/CyclomaticComplexity statements << statement[SELECT_STMT]['rarg'] if statement[SELECT_STMT]['rarg'] end - # CTEs - with_clause = statement[SELECT_STMT]['withClause'] - if with_clause - with_clause[WITH_CLAUSE]['ctes'].each do |item| - next unless item[COMMON_TABLE_EXPR] - @cte_names << item[COMMON_TABLE_EXPR]['ctename'] - statements << item[COMMON_TABLE_EXPR]['ctequery'] - end + if (with_clause = statement[SELECT_STMT]['withClause']) + cte_statements, cte_names = statements_and_cte_names_for_with_clause(with_clause) + @cte_names.concat(cte_names) + statements.concat(cte_statements) end # The following statements modify the contents of a table when INSERT_STMT, UPDATE_STMT, DELETE_STMT - from_clause_items << { item: statement.values[0]['relation'], type: :dml } + value = statement.values[0] + from_clause_items << { item: value['relation'], type: :dml } + statements << value['selectStmt'] if value.key?('selectStmt') + statements << value['withClause'] if value.key?('withClause') + + if (with_clause = value['withClause']) + cte_statements, cte_names = statements_and_cte_names_for_with_clause(with_clause) + @cte_names.concat(cte_names) + statements.concat(cte_statements) + end when COPY_STMT from_clause_items << { item: statement.values[0]['relation'], type: :dml } if statement.values[0]['relation'] statements << statement.values[0]['query'] @@ -221,5 +226,19 @@ def load_tables_and_aliases! # rubocop:disable Metrics/CyclomaticComplexity end @tables.uniq! + @cte_names.uniq! + end + + def statements_and_cte_names_for_with_clause(with_clause) + statements = [] + cte_names = [] + + with_clause[WITH_CLAUSE]['ctes'].each do |item| + next unless item[COMMON_TABLE_EXPR] + cte_names << item[COMMON_TABLE_EXPR]['ctename'] + statements << item[COMMON_TABLE_EXPR]['ctequery'] + end + + [statements, cte_names] end end diff --git a/spec/lib/parse_spec.rb b/spec/lib/parse_spec.rb index 0bea8bf6..8d644126 100644 --- a/spec/lib/parse_spec.rb +++ b/spec/lib/parse_spec.rb @@ -812,6 +812,64 @@ expect(query.cte_names).to match_array(['cte_a', 'cte_b']) end + describe 'parsing INSERT' do + it 'finds the table inserted into' do + query = described_class.parse(<<-SQL) + insert into users(pk, name) values (1, 'bob'); + SQL + expect(query.warnings).to be_empty + expect(query.tables).to eq(['users']) + end + + it 'finds tables in being selected from for insert' do + query = described_class.parse(<<-SQL) + insert into users(pk, name) select pk, name from other_users; + SQL + expect(query.warnings).to be_empty + expect(query.tables).to match_array(['users', 'other_users']) + end + + it 'finds tables in a CTE' do + query = described_class.parse(<<-SQL) + with cte as ( + select pk, name from other_users + ) + insert into users(pk, name) select * from cte; + SQL + expect(query.warnings).to be_empty + expect(query.tables).to match_array(['users', 'other_users']) + end + end + + describe 'parsing UPDATE' do + it 'finds the table updateed into' do + query = described_class.parse(<<-SQL) + update users set name = 'bob'; + SQL + expect(query.warnings).to be_empty + expect(query.tables).to eq(['users']) + end + + it 'finds tables in a sub-select' do + query = described_class.parse(<<-SQL) + update users set name = (select name from other_users limit 1); + SQL + expect(query.warnings).to be_empty + expect(query.tables).to match_array(['users', 'other_users']) + end + + it 'finds tables in a CTE' do + query = described_class.parse(<<-SQL) + with cte as ( + select name from other_users limit 1 + ) + update users set name = (select name from cte); + SQL + expect(query.warnings).to be_empty + expect(query.tables).to match_array(['users', 'other_users']) + end + end + it 'handles DROP TYPE' do query = described_class.parse("DROP TYPE IF EXISTS repack.pk_something") expect(query.warnings).to eq []