Skip to content

Commit 311ca3a

Browse files
authored
Fixed dbt view rename issue where ALTER TABLE ... RENAME TO fails on views because DuckDB requires ALTER VIEW. (#68)
1 parent cb5163c commit 311ca3a

File tree

3 files changed

+150
-9
lines changed

3 files changed

+150
-9
lines changed

server/conn.go

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -380,11 +380,19 @@ func (c *clientConn) handleQuery(body []byte) error {
380380

381381
result, err := c.db.Exec(query)
382382
if err != nil {
383-
c.sendError("ERROR", "42000", err.Error())
384-
c.setTxError()
385-
writeReadyForQuery(c.writer, c.txStatus)
386-
c.writer.Flush()
387-
return nil
383+
// Retry ALTER TABLE as ALTER VIEW if target is a view
384+
if isAlterTableNotTableError(err) {
385+
if alteredQuery, ok := transpiler.ConvertAlterTableToAlterView(query); ok {
386+
result, err = c.db.Exec(alteredQuery)
387+
}
388+
}
389+
if err != nil {
390+
c.sendError("ERROR", "42000", err.Error())
391+
c.setTxError()
392+
writeReadyForQuery(c.writer, c.txStatus)
393+
c.writer.Flush()
394+
return nil
395+
}
388396
}
389397

390398
c.updateTxStatus(cmdType)
@@ -1561,10 +1569,18 @@ func (c *clientConn) handleExecute(body []byte) {
15611569
// Non-result-returning query: use Exec with converted query
15621570
result, err := c.db.Exec(p.stmt.convertedQuery, args...)
15631571
if err != nil {
1564-
log.Printf("[%s] Execute error: %v", c.username, err)
1565-
c.sendError("ERROR", "42000", err.Error())
1566-
c.setTxError()
1567-
return
1572+
// Retry ALTER TABLE as ALTER VIEW if target is a view
1573+
if isAlterTableNotTableError(err) {
1574+
if alteredQuery, ok := transpiler.ConvertAlterTableToAlterView(p.stmt.convertedQuery); ok {
1575+
result, err = c.db.Exec(alteredQuery, args...)
1576+
}
1577+
}
1578+
if err != nil {
1579+
log.Printf("[%s] Execute error: %v", c.username, err)
1580+
c.sendError("ERROR", "42000", err.Error())
1581+
c.setTxError()
1582+
return
1583+
}
15681584
}
15691585
c.updateTxStatus(cmdType)
15701586
tag := c.buildCommandTag(cmdType, result)
@@ -1689,3 +1705,12 @@ func readCString(r *bytes.Reader) (string, error) {
16891705
}
16901706
return buf.String(), nil
16911707
}
1708+
1709+
// isAlterTableNotTableError checks if the error indicates that an ALTER TABLE
1710+
// was attempted on a view. DuckDB returns this error when trying to use
1711+
// ALTER TABLE ... RENAME TO on a view instead of ALTER VIEW.
1712+
func isAlterTableNotTableError(err error) bool {
1713+
msg := strings.ToLower(err.Error())
1714+
return strings.Contains(msg, "cannot use alter table") &&
1715+
strings.Contains(msg, "not a table")
1716+
}

transpiler/transpiler.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,40 @@ func (t *Transpiler) TranspileMulti(sql string) ([]*Result, error) {
182182

183183
return results, nil
184184
}
185+
186+
// ConvertAlterTableToAlterView transforms an ALTER TABLE RENAME statement
187+
// to ALTER VIEW RENAME. This is used to retry failed ALTER TABLE commands
188+
// when DuckDB reports that the target is a view, not a table.
189+
// Returns the transformed SQL and true if successful, or the original SQL
190+
// and false if the input is not an ALTER TABLE RENAME statement.
191+
func ConvertAlterTableToAlterView(sql string) (string, bool) {
192+
tree, err := pg_query.Parse(sql)
193+
if err != nil || len(tree.Stmts) == 0 {
194+
return sql, false
195+
}
196+
197+
stmt := tree.Stmts[0].Stmt
198+
if stmt == nil {
199+
return sql, false
200+
}
201+
202+
renameStmt, ok := stmt.Node.(*pg_query.Node_RenameStmt)
203+
if !ok || renameStmt.RenameStmt == nil {
204+
return sql, false
205+
}
206+
207+
// Only transform if it's an ALTER TABLE RENAME (renameType == OBJECT_TABLE)
208+
if renameStmt.RenameStmt.RenameType != pg_query.ObjectType_OBJECT_TABLE {
209+
return sql, false
210+
}
211+
212+
// Change to ALTER VIEW
213+
renameStmt.RenameStmt.RenameType = pg_query.ObjectType_OBJECT_VIEW
214+
renameStmt.RenameStmt.RelationType = pg_query.ObjectType_OBJECT_VIEW
215+
216+
result, err := pg_query.Deparse(tree)
217+
if err != nil {
218+
return sql, false
219+
}
220+
return result, true
221+
}

transpiler/transpiler_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -936,3 +936,82 @@ func TestTranspile_JSONOperators(t *testing.T) {
936936
})
937937
}
938938
}
939+
940+
func TestConvertAlterTableToAlterView(t *testing.T) {
941+
tests := []struct {
942+
name string
943+
input string
944+
wantSQL string
945+
wantOK bool
946+
wantChange bool // whether output should differ from input
947+
}{
948+
{
949+
name: "ALTER TABLE RENAME to ALTER VIEW RENAME",
950+
input: `ALTER TABLE "my_schema"."my_view__tmp" RENAME TO "my_view"`,
951+
wantOK: true,
952+
wantChange: true,
953+
},
954+
{
955+
name: "simple ALTER TABLE RENAME",
956+
input: `ALTER TABLE myview RENAME TO newname`,
957+
wantOK: true,
958+
wantChange: true,
959+
},
960+
{
961+
name: "non-rename ALTER TABLE unchanged",
962+
input: `ALTER TABLE users ADD COLUMN email TEXT`,
963+
wantOK: false,
964+
wantChange: false,
965+
},
966+
{
967+
name: "SELECT statement unchanged",
968+
input: `SELECT * FROM users`,
969+
wantOK: false,
970+
wantChange: false,
971+
},
972+
{
973+
name: "CREATE TABLE unchanged",
974+
input: `CREATE TABLE users (id INT)`,
975+
wantOK: false,
976+
wantChange: false,
977+
},
978+
{
979+
name: "empty string",
980+
input: ``,
981+
wantOK: false,
982+
wantChange: false,
983+
},
984+
{
985+
name: "invalid SQL",
986+
input: `NOT VALID SQL AT ALL`,
987+
wantOK: false,
988+
wantChange: false,
989+
},
990+
}
991+
992+
for _, tt := range tests {
993+
t.Run(tt.name, func(t *testing.T) {
994+
result, ok := ConvertAlterTableToAlterView(tt.input)
995+
if ok != tt.wantOK {
996+
t.Errorf("ConvertAlterTableToAlterView(%q) ok = %v, want %v", tt.input, ok, tt.wantOK)
997+
}
998+
if tt.wantChange {
999+
if result == tt.input {
1000+
t.Errorf("ConvertAlterTableToAlterView(%q) should change the SQL", tt.input)
1001+
}
1002+
// Check that it contains ALTER VIEW
1003+
if !strings.Contains(strings.ToUpper(result), "ALTER VIEW") {
1004+
t.Errorf("ConvertAlterTableToAlterView(%q) = %q, should contain ALTER VIEW", tt.input, result)
1005+
}
1006+
// Check that it does NOT contain ALTER TABLE
1007+
if strings.Contains(strings.ToUpper(result), "ALTER TABLE") {
1008+
t.Errorf("ConvertAlterTableToAlterView(%q) = %q, should not contain ALTER TABLE", tt.input, result)
1009+
}
1010+
} else {
1011+
if result != tt.input {
1012+
t.Errorf("ConvertAlterTableToAlterView(%q) = %q, should return original", tt.input, result)
1013+
}
1014+
}
1015+
})
1016+
}
1017+
}

0 commit comments

Comments
 (0)