Skip to content

Commit 5e830a9

Browse files
authored
Fixed add/rename column with custom schema and search path. (#3098)
1 parent 5dfd11f commit 5e830a9

File tree

3 files changed

+270
-10
lines changed

3 files changed

+270
-10
lines changed

cpp/deeplake_pg/extension_init.cpp

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -611,8 +611,26 @@ static void process_utility(PlannedStmt* pstmt,
611611

612612
/// Extract table name
613613
RangeVar* rel = stmt->relation;
614-
const std::string schema_name = (rel->schemaname != nullptr ? rel->schemaname : "public");
615-
const std::string table_name = schema_name + "." + rel->relname;
614+
615+
// Get the actual relation OID to resolve the proper schema name (handles custom schemas via search_path)
616+
Oid rel_oid = RangeVarGetRelid(rel, NoLock, false);
617+
Relation temp_rel = RelationIdGetRelation(rel_oid);
618+
std::string table_name;
619+
620+
if (RelationIsValid(temp_rel)) {
621+
Oid nspid = RelationGetNamespace(temp_rel);
622+
char* nspname = get_namespace_name(nspid);
623+
table_name = std::string(nspname ? nspname : "public") + "." + RelationGetRelationName(temp_rel);
624+
if (nspname) {
625+
pfree(nspname);
626+
}
627+
RelationClose(temp_rel);
628+
} else {
629+
// Fallback to RangeVar if relation is invalid (shouldn't happen)
630+
const std::string schema_name = (rel->schemaname != nullptr ? rel->schemaname : "public");
631+
table_name = schema_name + "." + rel->relname;
632+
}
633+
616634
auto* td = pg::table_storage::instance().get_table_data_if_exists(table_name);
617635
if (td != nullptr) {
618636
ListCell* lc = nullptr;
@@ -732,8 +750,26 @@ static void process_utility(PlannedStmt* pstmt,
732750
if (IsA(pstmt->utilityStmt, AlterTableStmt)) {
733751
AlterTableStmt* stmt = (AlterTableStmt*)pstmt->utilityStmt;
734752
RangeVar* rel = stmt->relation;
735-
const std::string schema_name = (rel->schemaname != nullptr ? rel->schemaname : "public");
736-
const std::string table_name = schema_name + "." + rel->relname;
753+
754+
// Get the actual relation OID to resolve the proper schema name (handles custom schemas via search_path)
755+
Oid rel_oid = RangeVarGetRelid(rel, NoLock, false);
756+
Relation temp_rel = RelationIdGetRelation(rel_oid);
757+
std::string table_name;
758+
759+
if (RelationIsValid(temp_rel)) {
760+
Oid nspid = RelationGetNamespace(temp_rel);
761+
char* nspname = get_namespace_name(nspid);
762+
table_name = std::string(nspname ? nspname : "public") + "." + RelationGetRelationName(temp_rel);
763+
if (nspname) {
764+
pfree(nspname);
765+
}
766+
RelationClose(temp_rel);
767+
} else {
768+
// Fallback to RangeVar if relation is invalid (shouldn't happen)
769+
const std::string schema_name = (rel->schemaname != nullptr ? rel->schemaname : "public");
770+
table_name = schema_name + "." + rel->relname;
771+
}
772+
737773
auto* td = pg::table_storage::instance().get_table_data_if_exists(table_name);
738774

739775
if (td != nullptr) {
@@ -746,7 +782,6 @@ static void process_utility(PlannedStmt* pstmt,
746782
const char* column_name = coldef->colname;
747783

748784
// Get the relation to query the new column's type from catalog
749-
Oid rel_oid = RangeVarGetRelid(rel, NoLock, false);
750785
Relation relation = RelationIdGetRelation(rel_oid);
751786
if (RelationIsValid(relation)) {
752787
TupleDesc tupdesc = RelationGetDescr(relation);
@@ -981,8 +1016,26 @@ static void process_utility(PlannedStmt* pstmt,
9811016
RenameStmt* stmt = (RenameStmt*)pstmt->utilityStmt;
9821017
if (stmt->renameType == OBJECT_COLUMN && stmt->relation != nullptr) {
9831018
RangeVar* rel = stmt->relation;
984-
const std::string schema_name = (rel->schemaname != nullptr ? rel->schemaname : "public");
985-
const std::string table_name = schema_name + "." + rel->relname;
1019+
1020+
// Get the actual relation OID to resolve the proper schema name (handles custom schemas via search_path)
1021+
Oid rel_oid = RangeVarGetRelid(rel, NoLock, false);
1022+
Relation temp_rel = RelationIdGetRelation(rel_oid);
1023+
std::string table_name;
1024+
1025+
if (RelationIsValid(temp_rel)) {
1026+
Oid nspid = RelationGetNamespace(temp_rel);
1027+
char* nspname = get_namespace_name(nspid);
1028+
table_name = std::string(nspname ? nspname : "public") + "." + RelationGetRelationName(temp_rel);
1029+
if (nspname) {
1030+
pfree(nspname);
1031+
}
1032+
RelationClose(temp_rel);
1033+
} else {
1034+
// Fallback to RangeVar if relation is invalid (shouldn't happen)
1035+
const std::string schema_name = (rel->schemaname != nullptr ? rel->schemaname : "public");
1036+
table_name = schema_name + "." + rel->relname;
1037+
}
1038+
9861039
auto* td = pg::table_storage::instance().get_table_data_if_exists(table_name);
9871040

9881041
if (td != nullptr && stmt->subname != nullptr && stmt->newname != nullptr) {
@@ -1018,9 +1071,26 @@ static void process_utility(PlannedStmt* pstmt,
10181071
CopyStmt* copy_stmt = (CopyStmt*)pstmt->utilityStmt;
10191072
if (copy_stmt->relation) {
10201073
// Build the qualified table name
1021-
const char* schema = copy_stmt->relation->schemaname ? copy_stmt->relation->schemaname : "public";
1022-
const char* table = copy_stmt->relation->relname;
1023-
std::string table_name = std::string(schema) + "." + table;
1074+
// Get the actual relation OID to resolve the proper schema name (handles custom schemas via search_path)
1075+
Oid rel_oid = RangeVarGetRelid(copy_stmt->relation, NoLock, false);
1076+
Relation temp_rel = RelationIdGetRelation(rel_oid);
1077+
std::string table_name;
1078+
1079+
if (RelationIsValid(temp_rel)) {
1080+
Oid nspid = RelationGetNamespace(temp_rel);
1081+
char* nspname = get_namespace_name(nspid);
1082+
table_name = std::string(nspname ? nspname : "public") + "." + RelationGetRelationName(temp_rel);
1083+
if (nspname) {
1084+
pfree(nspname);
1085+
}
1086+
RelationClose(temp_rel);
1087+
} else {
1088+
// Fallback to RangeVar if relation is invalid (shouldn't happen)
1089+
const char* schema = copy_stmt->relation->schemaname ? copy_stmt->relation->schemaname : "public";
1090+
const char* table = copy_stmt->relation->relname;
1091+
table_name = std::string(schema) + "." + table;
1092+
}
1093+
10241094
// If this is a deeplake table, flush/commit
10251095
auto* td = pg::table_storage::instance().get_table_data_if_exists(table_name);
10261096
if (td) {

postgres/tests/py_tests/test_add_column.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,78 @@ async def test_add_column_to_table(db_conn: asyncpg.Connection):
142142
await db_conn.execute("DROP TABLE IF EXISTS test_crash CASCADE")
143143
except:
144144
pass # Connection may be dead after segfault
145+
146+
147+
@pytest.mark.asyncio
148+
async def test_add_column_with_custom_schema(db_conn: asyncpg.Connection):
149+
"""
150+
Test that ALTER TABLE ADD COLUMN works correctly with custom schema.
151+
152+
Tests:
153+
- Create a custom schema 'valod'
154+
- Create table with id (INT4) and name (TEXT) columns using deeplake
155+
- Insert one row with explicit id and empty string
156+
- Verify data before adding column
157+
- Add a new TEXT column 'surname' via ALTER TABLE
158+
- Verify the column is added and existing data is preserved
159+
- Verify SELECT returns correct columns after ALTER TABLE
160+
"""
161+
assertions = Assertions(db_conn)
162+
163+
try:
164+
# Create custom schema
165+
await db_conn.execute("CREATE SCHEMA valod")
166+
167+
# Set search path to use the custom schema
168+
await db_conn.execute("SET search_path TO 'valod'")
169+
170+
# Create table with id and name columns using deeplake storage
171+
await db_conn.execute("""
172+
CREATE TABLE a (
173+
id INT4,
174+
name TEXT
175+
) USING deeplake
176+
""")
177+
178+
# Insert one row with explicit id and empty string
179+
await db_conn.execute("INSERT INTO a VALUES (1, '')")
180+
181+
# Verify row was inserted
182+
rows_before = await db_conn.fetch("SELECT * FROM a")
183+
assert len(rows_before) == 1, f"Expected 1 row, got {len(rows_before)}"
184+
assert rows_before[0]['id'] == 1, f"Expected id=1, got {rows_before[0]['id']}"
185+
assert rows_before[0]['name'] == '', f"Expected empty string, got '{rows_before[0]['name']}'"
186+
187+
# Add new column 'surname' with TEXT type
188+
await db_conn.execute("ALTER TABLE a ADD COLUMN surname TEXT")
189+
190+
# Verify column was added to PostgreSQL catalog
191+
column_info = await db_conn.fetch("""
192+
SELECT column_name, data_type
193+
FROM information_schema.columns
194+
WHERE table_schema = 'valod' AND table_name = 'a'
195+
ORDER BY ordinal_position
196+
""")
197+
column_names = [col['column_name'] for col in column_info]
198+
assert column_names == ['id', 'name', 'surname'], \
199+
f"Expected ['id', 'name', 'surname'], got {column_names}"
200+
201+
# SELECT from table after adding column
202+
rows_after = await db_conn.fetch("SELECT * FROM a")
203+
204+
# Verify existing data is preserved and new column exists
205+
assert len(rows_after) == 1, f"Expected 1 row, got {len(rows_after)}"
206+
assert rows_after[0]['id'] == 1, f"Expected id=1, got {rows_after[0]['id']}"
207+
assert rows_after[0]['name'] == '', f"Expected empty string, got '{rows_after[0]['name']}'"
208+
assert 'surname' in rows_after[0].keys(), "Expected 'surname' column to exist"
209+
assert rows_after[0]['surname'] == '', \
210+
f"Expected empty string for new column, got '{rows_after[0]['surname']}'"
211+
212+
print("✓ Test passed: ALTER TABLE ADD COLUMN works with custom schema")
213+
214+
finally:
215+
# Cleanup
216+
try:
217+
await db_conn.execute("DROP SCHEMA IF EXISTS valod CASCADE")
218+
except:
219+
pass # Connection may be dead after segfault

postgres/tests/py_tests/test_rename_column.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,118 @@ async def test_rename_column_in_table(db_conn: asyncpg.Connection):
148148
await db_conn.execute("DROP TABLE IF EXISTS test_rename CASCADE")
149149
except:
150150
pass # Connection may be dead after crash
151+
152+
153+
@pytest.mark.asyncio
154+
async def test_rename_column_with_custom_schema(db_conn: asyncpg.Connection):
155+
"""
156+
Test that ALTER TABLE RENAME COLUMN works correctly with custom schema.
157+
158+
Tests:
159+
- Create a custom schema 'test_schema'
160+
- Create table with id (INT4), name (TEXT), and email (VARCHAR) columns using deeplake
161+
- Insert one row with data
162+
- Verify data before renaming column
163+
- Rename the 'email' column to 'email_address' via ALTER TABLE
164+
- Verify the column is renamed in both PostgreSQL catalog and deeplake dataset
165+
- Verify existing data is preserved and accessible via new column name
166+
- Verify old column name no longer exists
167+
"""
168+
assertions = Assertions(db_conn)
169+
170+
try:
171+
# Create custom schema
172+
await db_conn.execute("CREATE SCHEMA test_schema")
173+
174+
# Set search path to use the custom schema
175+
await db_conn.execute("SET search_path TO 'test_schema'")
176+
177+
# Create table with id, name, and email columns using deeplake storage
178+
await db_conn.execute("""
179+
CREATE TABLE test_rename_schema (
180+
id INT4,
181+
name TEXT,
182+
email VARCHAR(255)
183+
) USING deeplake
184+
""")
185+
186+
# Insert one row with explicit data
187+
await db_conn.execute("""
188+
INSERT INTO test_rename_schema VALUES (1, 'Alice', '[email protected]')
189+
""")
190+
191+
# Verify row was inserted
192+
rows_before = await db_conn.fetch("SELECT * FROM test_rename_schema")
193+
assert len(rows_before) == 1, f"Expected 1 row, got {len(rows_before)}"
194+
assert rows_before[0]['id'] == 1, f"Expected id=1, got {rows_before[0]['id']}"
195+
assert rows_before[0]['name'] == 'Alice', f"Expected 'Alice', got '{rows_before[0]['name']}'"
196+
assert rows_before[0]['email'] == '[email protected]', \
197+
f"Expected '[email protected]', got '{rows_before[0]['email']}'"
198+
199+
# Rename email column to email_address
200+
await db_conn.execute("""
201+
ALTER TABLE test_rename_schema RENAME COLUMN email TO email_address
202+
""")
203+
204+
# Verify column was renamed in PostgreSQL catalog
205+
column_info = await db_conn.fetch("""
206+
SELECT column_name, data_type, character_maximum_length
207+
FROM information_schema.columns
208+
WHERE table_schema = 'test_schema' AND table_name = 'test_rename_schema'
209+
ORDER BY ordinal_position
210+
""")
211+
column_names = [col['column_name'] for col in column_info]
212+
213+
# Verify new column name exists
214+
assert 'email_address' in column_names, \
215+
f"Column 'email_address' should exist in catalog. Found: {column_names}"
216+
217+
# Verify old column name does not exist
218+
assert 'email' not in column_names, \
219+
f"Column 'email' should not exist anymore. Found: {column_names}"
220+
221+
# Verify expected column order
222+
assert column_names == ['id', 'name', 'email_address'], \
223+
f"Expected ['id', 'name', 'email_address'], got {column_names}"
224+
225+
# SELECT from table after renaming column
226+
rows_after = await db_conn.fetch("SELECT * FROM test_rename_schema")
227+
228+
# Verify existing data is preserved and accessible via new column name
229+
assert len(rows_after) == 1, f"Expected 1 row, got {len(rows_after)}"
230+
assert rows_after[0]['id'] == 1, f"Expected id=1, got {rows_after[0]['id']}"
231+
assert rows_after[0]['name'] == 'Alice', f"Expected 'Alice', got '{rows_after[0]['name']}'"
232+
assert rows_after[0]['email_address'] == '[email protected]', \
233+
f"Expected '[email protected]', got '{rows_after[0]['email_address']}'"
234+
235+
# Verify 'email_address' column is in the result
236+
assert 'email_address' in rows_after[0].keys(), "Expected 'email_address' column to exist"
237+
238+
# Verify old column name is not accessible (should raise error)
239+
try:
240+
await db_conn.fetch("SELECT email FROM test_rename_schema")
241+
assert False, "Should have raised error for non-existent column 'email'"
242+
except asyncpg.UndefinedColumnError:
243+
pass # Expected error
244+
245+
# Insert another row using the new column name
246+
await db_conn.execute("""
247+
INSERT INTO test_rename_schema VALUES (2, 'Bob', '[email protected]')
248+
""")
249+
250+
# Verify the new row was inserted successfully
251+
all_rows = await db_conn.fetch("SELECT * FROM test_rename_schema ORDER BY id")
252+
assert len(all_rows) == 2, f"Expected 2 rows, got {len(all_rows)}"
253+
assert all_rows[1]['id'] == 2, f"Expected id=2, got {all_rows[1]['id']}"
254+
assert all_rows[1]['name'] == 'Bob', f"Expected 'Bob', got '{all_rows[1]['name']}'"
255+
assert all_rows[1]['email_address'] == '[email protected]', \
256+
f"Expected '[email protected]', got '{all_rows[1]['email_address']}'"
257+
258+
print("✓ Test passed: ALTER TABLE RENAME COLUMN works with custom schema")
259+
260+
finally:
261+
# Cleanup
262+
try:
263+
await db_conn.execute("DROP SCHEMA IF EXISTS test_schema CASCADE")
264+
except:
265+
pass # Connection may be dead after crash

0 commit comments

Comments
 (0)