Skip to content
Open
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
115 changes: 115 additions & 0 deletions src/sql/sqlitetypes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#include "ObjectIdentifier.h"
#include "parser/ParserDriver.h"

#include <algorithm>
#include <cctype>
#include <iostream>
#include <iterator>
#include <numeric>
Expand Down Expand Up @@ -533,6 +535,109 @@ void Table::removeKeyFromAllConstraints(const std::string& key)
match_and_remove(m_foreignKeys, it);
}

// Replace all references to the column 'from' in the SQL expression 'expr' by the column name 'to'. String literals, comments and function
// names are left alone. Quoted identifiers keep their quoting style, unquoted ones are replaced by a quoted identifier.
static std::string renameColumnInExpression(const std::string& expr, const std::string& from, const std::string& to)
{
const auto iequals = [](const std::string& a, const std::string& b) {
return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(), [](char x, char y) {
return std::tolower(static_cast<unsigned char>(x)) == std::tolower(static_cast<unsigned char>(y));
});
};
const auto isIdentChar = [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '$' || static_cast<unsigned char>(c) >= 0x80;
};
const auto duplicate = [](const std::string& str, char c) {
std::string result;
for(char ch : str)
{
result += ch;
if(ch == c)
result += c;
}
return result;
};

std::string result;
const size_t len = expr.size();
size_t i = 0;
while(i < len)
{
const char c = expr[i];
if(c == '\'' || c == '"' || c == '`')
{
// Quoted string or identifier. A doubled quote character is an escaped quote character
std::string content;
size_t j = i + 1;
while(j < len)
{
if(expr[j] == c)
{
if(j + 1 < len && expr[j + 1] == c)
{
content += c;
j += 2;
continue;
}
break;
}
content += expr[j++];
}
const size_t end = j < len ? j + 1 : len; // One past the closing quote
if(c != '\'' && iequals(content, from))
result += c + duplicate(to, c) + c;
else
result.append(expr, i, end - i);
i = end;
} else if(c == '[') {
// Identifier in square brackets
const size_t close = expr.find(']', i + 1);
const size_t end = close == std::string::npos ? len : close + 1;
if(close != std::string::npos && iequals(expr.substr(i + 1, close - i - 1), from))
result += '[' + to + ']';
else
result.append(expr, i, end - i);
i = end;
} else if(c == '-' && i + 1 < len && expr[i + 1] == '-') {
// Line comment
const size_t end = expr.find('\n', i);
const size_t stop = end == std::string::npos ? len : end;
result.append(expr, i, stop - i);
i = stop;
} else if(c == '/' && i + 1 < len && expr[i + 1] == '*') {
// Block comment
const size_t close = expr.find("*/", i + 2);
const size_t stop = close == std::string::npos ? len : close + 2;
result.append(expr, i, stop - i);
i = stop;
} else if(isIdentChar(c)) {
// Bare word or number
size_t j = i;
while(j < len && (isIdentChar(expr[j]) || (std::isdigit(static_cast<unsigned char>(c)) && expr[j] == '.')))
j++;
const std::string word = expr.substr(i, j - i);

// Skip function names, i.e. words followed by an opening parenthesis, and the prefix of blob literals
size_t next = j;
while(next < len && std::isspace(static_cast<unsigned char>(expr[next])))
next++;
const bool is_function = next < len && expr[next] == '(';
const bool is_blob = j < len && expr[j] == '\'' && word.size() == 1 && (word[0] == 'x' || word[0] == 'X');

if(!std::isdigit(static_cast<unsigned char>(c)) && !is_function && !is_blob && iequals(word, from))
result += escapeIdentifier(to);
else
result += word;
i = j;
} else {
result += c;
i++;
}
}

return result;
}

void Table::renameKeyInAllConstraints(const std::string& key, const std::string& to)
{
// Do nothing if the key hasn't really changed
Expand Down Expand Up @@ -570,6 +675,16 @@ void Table::renameKeyInAllConstraints(const std::string& key, const std::string&

for(auto it=m_foreignKeys.begin();it!=m_foreignKeys.end();++it)
match_and_rename(m_foreignKeys, it, [key, to](const std::string& c) { return c == key ? to : c; });

// The expressions of check constraints reference columns by name, too. For column constraints we need to set a new constraint object because
// the old one might be shared with other copies of this table.
for(auto& field : fields)
{
if(!field.check().empty())
field.setCheck(renameColumnInExpression(field.check(), key, to));
}
for(auto& constraint : m_checkConstraints)
constraint->setExpression(renameColumnInExpression(constraint->expression(), key, to));
}

std::shared_ptr<PrimaryKeyConstraint> Table::primaryKey() const
Expand Down
41 changes: 41 additions & 0 deletions src/tests/testsqlobjects.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,47 @@ void TestTable::parseSQLCheckConstraint()
QCOMPARE(tab.fields.at(0).check(), "\"b\" = 'A' OR \"b\" = 'B'");
}

void TestTable::renameColumnInCheckConstraints()
{
// Renaming a column must also update column names referenced in column and table CHECK constraints
std::string sql = "CREATE TABLE t (a INTEGER CHECK(a > 0), b INTEGER CHECK(\"b\" < 10 AND b <> a), c TEXT, CONSTRAINT cc CHECK(a < b AND length(c) > 1), CHECK(c <> 'a' AND [a] <> `a` + 1));";

Table orig(*Table::parseSQL(sql));
Table tab(orig);
tab.fields.at(0).setName("x");
tab.renameKeyInAllConstraints("a", "x");

QCOMPARE(tab.fields.at(0).check(), "\"x\" > 0");
QCOMPARE(tab.fields.at(1).check(), "\"b\" < 10 AND \"b\" <> \"x\"");
QCOMPARE(tab.fields.at(2).check(), "");
QCOMPARE(tab.checkConstraints().size(), static_cast<size_t>(2));
QCOMPARE(tab.checkConstraints().at(0)->expression(), "\"x\" < \"b\" AND length(\"c\") > 1");
QCOMPARE(tab.checkConstraints().at(0)->name(), "cc");
// String literals must be left alone, other quoting styles of the identifier are handled
QCOMPARE(tab.checkConstraints().at(1)->expression(), "\"c\" <> 'a' AND \"x\" <> \"x\" + 1");

// Functions, columns with other names and string literals must not be touched
Table tab2(*Table::parseSQL("CREATE TABLE t2 (abs INTEGER, c INTEGER CHECK(abs(abs) > 0 AND c > 1), CHECK(c IN ('abs', 'c')));"));
tab2.fields.at(0).setName("y");
tab2.renameKeyInAllConstraints("abs", "y");
QCOMPARE(tab2.fields.at(1).check(), "abs(\"y\") > 0 AND \"c\" > 1");
QCOMPARE(tab2.checkConstraints().at(0)->expression(), "\"c\" IN ('abs', 'c')");

// The new name needs quoting
Table tab3(*Table::parseSQL("CREATE TABLE t3 (a INTEGER CHECK(a > 0));"));
tab3.fields.at(0).setName("my col");
tab3.renameKeyInAllConstraints("a", "my col");
QCOMPARE(tab3.fields.at(0).check(), "\"my col\" > 0");

// The generated SQL must be parseable again and reflect the new name
Table tab4(*Table::parseSQL(tab.sql()));
QCOMPARE(tab4.fields.at(0).check(), "\"x\" > 0");

// The original table, which shares data with the copy, must not have been modified
QCOMPARE(orig.fields.at(0).check(), "\"a\" > 0");
QCOMPARE(orig.checkConstraints().at(0)->expression(), "\"a\" < \"b\" AND length(\"c\") > 1");
}

void TestTable::parseDefaultValues()
{
std::string sql = "CREATE TABLE test(a int DEFAULT 0, b int DEFAULT -1, c text DEFAULT 'hello', d text DEFAULT '0');";
Expand Down
1 change: 1 addition & 0 deletions src/tests/testsqlobjects.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ private slots:
void parseSQLEscapedQuotes();
void parseSQLForeignKeys();
void parseSQLCheckConstraint();
void renameColumnInCheckConstraints();
void parseDefaultValues();
void parseDoubleQuotedDefaultValues();
void createTableWithIn();
Expand Down
Loading