|
58 | 58 | #include "pg_lake/partitioning/partition_spec_catalog.h" |
59 | 59 | #include "pg_lake/rest_catalog/rest_catalog.h" |
60 | 60 | #include "pg_lake/object_store_catalog/object_store_catalog.h" |
| 61 | +#include "pg_lake/pgduck/numeric.h" |
61 | 62 | #include "pg_lake/util/rel_utils.h" |
62 | 63 |
|
63 | 64 |
|
@@ -119,6 +120,7 @@ typedef struct PgLakeDDL |
119 | 120 | static bool Allowed(Node *arg, Oid relationId); |
120 | 121 | static bool Disallowed(Node *arg, Oid relationId); |
121 | 122 | static bool DisallowedAddColumnWithUnsupportedConstraints(Node *arg, Oid relationId); |
| 123 | +static bool AllowedAlterColumnTypeForIceberg(Node *arg, Oid relationId); |
122 | 124 | static bool DisallowedForWritableRestRenameTable(Node *arg, Oid relationId); |
123 | 125 | static bool DisallowedForWritableRestSetSchema(Node *arg, Oid relationId); |
124 | 126 |
|
@@ -155,7 +157,7 @@ static const PgLakeDDL PgLakeDDLs[] = { |
155 | 157 | #if PG_VERSION_NUM >= 170000 |
156 | 158 | ALTER_TABLE_DDL(AT_SetExpression, Disallowed, Disallowed), |
157 | 159 | #endif |
158 | | - ALTER_TABLE_DDL(AT_AlterColumnType, Disallowed, Disallowed), |
| 160 | + ALTER_TABLE_DDL(AT_AlterColumnType, AllowedAlterColumnTypeForIceberg, Disallowed), |
159 | 161 |
|
160 | 162 | /* allowed for writable tables, not allowed for iceberg tables */ |
161 | 163 | ALTER_TABLE_DDL(AT_AddIdentity, Disallowed, Allowed), |
@@ -409,6 +411,32 @@ CreateDDLOperationsForAlterTable(AlterTableStmt *alterStmt) |
409 | 411 |
|
410 | 412 | ddlOperations = lappend(ddlOperations, ddlOperation); |
411 | 413 | } |
| 414 | + else if (subcommand->subtype == AT_AlterColumnType) |
| 415 | + { |
| 416 | + char *columnName = subcommand->name; |
| 417 | + AttrNumber attrNo = get_attnum(relationId, columnName); |
| 418 | + |
| 419 | + IcebergDDLOperation *ddlOperation = palloc0(sizeof(IcebergDDLOperation)); |
| 420 | + |
| 421 | + ddlOperation->type = DDL_COLUMN_ALTER_TYPE; |
| 422 | + ddlOperation->attrNumber = attrNo; |
| 423 | + |
| 424 | + /* |
| 425 | + * At this point, PgLakeCommonParentProcessUtility has already |
| 426 | + * been called so the column type in pg_attribute is updated. We |
| 427 | + * read the new type from the relation. |
| 428 | + */ |
| 429 | + Oid newTypeOid = InvalidOid; |
| 430 | + int32 newTypMod = -1; |
| 431 | + Oid newCollId = InvalidOid; |
| 432 | + |
| 433 | + get_atttypetypmodcoll(relationId, attrNo, |
| 434 | + &newTypeOid, &newTypMod, &newCollId); |
| 435 | + |
| 436 | + ddlOperation->newPgType = MakePGType(newTypeOid, newTypMod); |
| 437 | + |
| 438 | + ddlOperations = lappend(ddlOperations, ddlOperation); |
| 439 | + } |
412 | 440 | else if (subcommand->subtype == AT_DropNotNull) |
413 | 441 | { |
414 | 442 | IcebergDDLOperation *ddlOperation = palloc0(sizeof(IcebergDDLOperation)); |
@@ -841,10 +869,40 @@ ErrorIfUnsupportedAlterWritablePgLakeTableStmt(AlterTableStmt *alterStmt, |
841 | 869 |
|
842 | 870 | const char *cmdTypeStr = PgLakeUnsupportedAlterTableToString(cmd); |
843 | 871 |
|
844 | | - ereport(ERROR, |
845 | | - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), |
846 | | - errmsg("ALTER TABLE %s command not supported for " |
847 | | - "%s tables", cmdTypeStr, tableTypeStr))); |
| 872 | + if (cmd->subtype == AT_AlterColumnType && |
| 873 | + tableType == PG_LAKE_ICEBERG_TABLE_TYPE) |
| 874 | + { |
| 875 | + ColumnDef *def = (ColumnDef *) cmd->def; |
| 876 | + |
| 877 | + if (def->raw_default != NULL) |
| 878 | + { |
| 879 | + ereport(ERROR, |
| 880 | + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), |
| 881 | + errmsg("ALTER TABLE %s command not supported for " |
| 882 | + "%s tables", cmdTypeStr, tableTypeStr), |
| 883 | + errdetail("USING requires rewriting data files, " |
| 884 | + "but Iceberg schema evolution is a " |
| 885 | + "metadata-only operation."))); |
| 886 | + } |
| 887 | + else |
| 888 | + { |
| 889 | + ereport(ERROR, |
| 890 | + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), |
| 891 | + errmsg("ALTER TABLE %s command not supported for " |
| 892 | + "%s tables", cmdTypeStr, tableTypeStr), |
| 893 | + errdetail("Allowed type promotions for Iceberg tables " |
| 894 | + "are: int -> bigint, float -> double, and " |
| 895 | + "decimal(P,S) -> decimal(P',S) where P' > P " |
| 896 | + "(wider precision, same scale)."))); |
| 897 | + } |
| 898 | + } |
| 899 | + else |
| 900 | + { |
| 901 | + ereport(ERROR, |
| 902 | + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), |
| 903 | + errmsg("ALTER TABLE %s command not supported for " |
| 904 | + "%s tables", cmdTypeStr, tableTypeStr))); |
| 905 | + } |
848 | 906 | } |
849 | 907 | } |
850 | 908 |
|
@@ -1043,6 +1101,113 @@ Disallowed(Node *arg, Oid relationId) |
1043 | 1101 | } |
1044 | 1102 |
|
1045 | 1103 |
|
| 1104 | +/* |
| 1105 | + * AllowedAlterColumnTypeForIceberg validates whether a column type change |
| 1106 | + * is allowed for an Iceberg table per the Iceberg spec v2 type promotion rules. |
| 1107 | + * |
| 1108 | + * The Iceberg spec allows the following type promotions: |
| 1109 | + * - int -> long |
| 1110 | + * - float -> double |
| 1111 | + * - decimal(P, S) -> decimal(P', S) where P' > P (wider precision, same scale) |
| 1112 | + * |
| 1113 | + * See: https://iceberg.apache.org/spec/#schema-evolution |
| 1114 | + */ |
| 1115 | +static bool |
| 1116 | +AllowedAlterColumnTypeForIceberg(Node *arg, Oid relationId) |
| 1117 | +{ |
| 1118 | + AlterTableCmd *cmd = (AlterTableCmd *) arg; |
| 1119 | + |
| 1120 | + Assert(cmd->subtype == AT_AlterColumnType); |
| 1121 | + |
| 1122 | + char *columnName = cmd->name; |
| 1123 | + ColumnDef *def = (ColumnDef *) cmd->def; |
| 1124 | + TypeName *newTypeName = def->typeName; |
| 1125 | + |
| 1126 | + /* |
| 1127 | + * Reject USING clause. USING requires rewriting data files, but Iceberg |
| 1128 | + * schema evolution is metadata-only so data files are never rewritten. |
| 1129 | + */ |
| 1130 | + if (def->raw_default != NULL) |
| 1131 | + return false; |
| 1132 | + |
| 1133 | + /* resolve the new type OID and typmod */ |
| 1134 | + int32 newTypMod = 0; |
| 1135 | + Oid newTypeOid = InvalidOid; |
| 1136 | + |
| 1137 | + typenameTypeIdAndMod(NULL, newTypeName, &newTypeOid, &newTypMod); |
| 1138 | + |
| 1139 | + /* get the current column type from the relation */ |
| 1140 | + AttrNumber attrNum = get_attnum(relationId, columnName); |
| 1141 | + |
| 1142 | + if (attrNum == InvalidAttrNumber) |
| 1143 | + return false; |
| 1144 | + |
| 1145 | + Oid currentTypeOid = InvalidOid; |
| 1146 | + int32 currentTypMod = -1; |
| 1147 | + Oid currentCollId = InvalidOid; |
| 1148 | + |
| 1149 | + get_atttypetypmodcoll(relationId, attrNum, |
| 1150 | + ¤tTypeOid, ¤tTypMod, ¤tCollId); |
| 1151 | + |
| 1152 | + /* same type is always allowed (no-op from Iceberg perspective) */ |
| 1153 | + if (currentTypeOid == newTypeOid && currentTypMod == newTypMod) |
| 1154 | + return true; |
| 1155 | + |
| 1156 | + /* |
| 1157 | + * Iceberg type promotion: int -> long |
| 1158 | + * |
| 1159 | + * PostgreSQL int2 and int4 both map to Iceberg "int", and int8 maps to |
| 1160 | + * Iceberg "long". So we allow int2/int4 -> int8. |
| 1161 | + */ |
| 1162 | + if ((currentTypeOid == INT4OID || currentTypeOid == INT2OID) && |
| 1163 | + newTypeOid == INT8OID) |
| 1164 | + return true; |
| 1165 | + |
| 1166 | + /* |
| 1167 | + * Iceberg type promotion: float -> double |
| 1168 | + * |
| 1169 | + * PostgreSQL float4 maps to Iceberg "float" and float8 maps to Iceberg |
| 1170 | + * "double". So we allow float4 -> float8. |
| 1171 | + */ |
| 1172 | + if (currentTypeOid == FLOAT4OID && newTypeOid == FLOAT8OID) |
| 1173 | + return true; |
| 1174 | + |
| 1175 | + /* |
| 1176 | + * Iceberg type promotion: decimal(P, S) -> decimal(P', S) where P' > P |
| 1177 | + * |
| 1178 | + * Both must be numeric, the new precision must be greater, and the scale |
| 1179 | + * must remain the same. |
| 1180 | + */ |
| 1181 | + if (currentTypeOid == NUMERICOID && newTypeOid == NUMERICOID) |
| 1182 | + { |
| 1183 | + int currentPrecision = -1; |
| 1184 | + int currentScale = -1; |
| 1185 | + int newPrecision = -1; |
| 1186 | + int newScale = -1; |
| 1187 | + |
| 1188 | + GetDuckdbAdjustedPrecisionAndScaleFromNumericTypeMod(currentTypMod, |
| 1189 | + ¤tPrecision, |
| 1190 | + ¤tScale); |
| 1191 | + GetDuckdbAdjustedPrecisionAndScaleFromNumericTypeMod(newTypMod, |
| 1192 | + &newPrecision, |
| 1193 | + &newScale); |
| 1194 | + |
| 1195 | + /* new precision must be wider and scale must stay the same */ |
| 1196 | + if (newPrecision > currentPrecision && newScale == currentScale) |
| 1197 | + { |
| 1198 | + /* also validate the new type is supported for Iceberg tables */ |
| 1199 | + ErrorIfTypeUnsupportedNumericForIcebergTables(newTypMod, columnName); |
| 1200 | + return true; |
| 1201 | + } |
| 1202 | + |
| 1203 | + return false; |
| 1204 | + } |
| 1205 | + |
| 1206 | + /* all other type changes are disallowed */ |
| 1207 | + return false; |
| 1208 | +} |
| 1209 | + |
| 1210 | + |
1046 | 1211 | /* |
1047 | 1212 | * We have not yet implemented table renames in REST catalog. |
1048 | 1213 | */ |
|
0 commit comments