Commit 84c123be authored by Tom Lane's avatar Tom Lane

Allow new values to be added to an existing enum type.

After much expenditure of effort, we've got this to the point where the
performance penalty is pretty minimal in typical cases.

Andrew Dunstan, reviewed by Brendan Jurd, Dean Rasheed, and Tom Lane
parent 24b29ca8
......@@ -79,7 +79,7 @@ install_support_functions(void)
"LANGUAGE C STRICT;"));
PQclear(executeQueryOrDie(conn,
"CREATE OR REPLACE FUNCTION "
" binary_upgrade.add_pg_enum_label(OID, OID, NAME) "
" binary_upgrade.set_next_pg_enum_oid(OID) "
"RETURNS VOID "
"AS '$libdir/pg_upgrade_support' "
"LANGUAGE C STRICT;"));
......
......@@ -16,13 +16,6 @@
/* THIS IS USED ONLY FOR PG >= 9.0 */
/*
* Cannot include "catalog/pg_enum.h" here because we might
* not be compiling against PG 9.0.
*/
extern void EnumValuesCreate(Oid enumTypeOid, List *vals,
Oid binary_upgrade_next_pg_enum_oid);
#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif
......@@ -33,6 +26,7 @@ extern PGDLLIMPORT Oid binary_upgrade_next_pg_type_toast_oid;
extern PGDLLIMPORT Oid binary_upgrade_next_heap_relfilenode;
extern PGDLLIMPORT Oid binary_upgrade_next_toast_relfilenode;
extern PGDLLIMPORT Oid binary_upgrade_next_index_relfilenode;
extern PGDLLIMPORT Oid binary_upgrade_next_pg_enum_oid;
Datum set_next_pg_type_oid(PG_FUNCTION_ARGS);
Datum set_next_pg_type_array_oid(PG_FUNCTION_ARGS);
......@@ -40,7 +34,7 @@ Datum set_next_pg_type_toast_oid(PG_FUNCTION_ARGS);
Datum set_next_heap_relfilenode(PG_FUNCTION_ARGS);
Datum set_next_toast_relfilenode(PG_FUNCTION_ARGS);
Datum set_next_index_relfilenode(PG_FUNCTION_ARGS);
Datum add_pg_enum_label(PG_FUNCTION_ARGS);
Datum set_next_pg_enum_oid(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(set_next_pg_type_oid);
PG_FUNCTION_INFO_V1(set_next_pg_type_array_oid);
......@@ -48,7 +42,7 @@ PG_FUNCTION_INFO_V1(set_next_pg_type_toast_oid);
PG_FUNCTION_INFO_V1(set_next_heap_relfilenode);
PG_FUNCTION_INFO_V1(set_next_toast_relfilenode);
PG_FUNCTION_INFO_V1(set_next_index_relfilenode);
PG_FUNCTION_INFO_V1(add_pg_enum_label);
PG_FUNCTION_INFO_V1(set_next_pg_enum_oid);
Datum
set_next_pg_type_oid(PG_FUNCTION_ARGS)
......@@ -111,14 +105,11 @@ set_next_index_relfilenode(PG_FUNCTION_ARGS)
}
Datum
add_pg_enum_label(PG_FUNCTION_ARGS)
set_next_pg_enum_oid(PG_FUNCTION_ARGS)
{
Oid enumoid = PG_GETARG_OID(0);
Oid typoid = PG_GETARG_OID(1);
Name label = PG_GETARG_NAME(2);
EnumValuesCreate(typoid, list_make1(makeString(NameStr(*label))),
enumoid);
binary_upgrade_next_pg_enum_oid = enumoid;
PG_RETURN_VOID();
}
......@@ -2623,12 +2623,9 @@
<para>
The <structname>pg_enum</structname> catalog contains entries
matching enum types to their associated values and labels. The
showing the values and labels for each enum type. The
internal representation of a given enum value is actually the OID
of its associated row in <structname>pg_enum</structname>. The
OIDs for a particular enum type are guaranteed to be ordered in
the way the type should sort, but there is no guarantee about the
ordering of OIDs of unrelated enum types.
of its associated row in <structname>pg_enum</structname>.
</para>
<table>
......@@ -2652,6 +2649,13 @@
<entry>The OID of the <structname>pg_type</> entry owning this enum value</entry>
</row>
<row>
<entry><structfield>enumsortorder</structfield></entry>
<entry><type>float4</type></entry>
<entry></entry>
<entry>The sort position of this enum value within its enum type</entry>
</row>
<row>
<entry><structfield>enumlabel</structfield></entry>
<entry><type>name</type></entry>
......@@ -2661,6 +2665,26 @@
</tbody>
</tgroup>
</table>
<para>
The OIDs for <structname>pg_enum</structname> rows follow a special
rule: even-numbered OIDs are guaranteed to be ordered in the same way
as the sort ordering of their enum type. That is, if two even OIDs
belong to the same enum type, the smaller OID must have the smaller
<structfield>enumsortorder</structfield> value. Odd-numbered OID values
need bear no relationship to the sort order. This rule allows the
enum comparison routines to avoid catalog lookups in many common cases.
The routines that create and alter enum types attempt to assign even
OIDs to enum values whenever possible.
</para>
<para>
When an enum type is created, its members are assigned sort-order
positions 1..<replaceable>n</>. But members added later might be given
negative or fractional values of <structfield>enumsortorder</structfield>.
The only requirement on these values is that they be correctly
ordered and unique within each enum type.
</para>
</sect1>
......
......@@ -24,10 +24,11 @@ PostgreSQL documentation
<refsynopsisdiv>
<synopsis>
ALTER TYPE <replaceable class="PARAMETER">name</replaceable> <replaceable class="PARAMETER">action</replaceable> [, ... ]
ALTER TYPE <replaceable class="PARAMETER">name</replaceable> OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
ALTER TYPE <replaceable class="PARAMETER">name</replaceable> OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
ALTER TYPE <replaceable class="PARAMETER">name</replaceable> RENAME ATTRIBUTE <replaceable class="PARAMETER">attribute_name</replaceable> TO <replaceable class="PARAMETER">new_attribute_name</replaceable>
ALTER TYPE <replaceable class="PARAMETER">name</replaceable> RENAME TO <replaceable class="PARAMETER">new_name</replaceable>
ALTER TYPE <replaceable class="PARAMETER">name</replaceable> SET SCHEMA <replaceable class="PARAMETER">new_schema</replaceable>
ALTER TYPE <replaceable class="PARAMETER">name</replaceable> ADD <replaceable class="PARAMETER">new_enum_value</replaceable> [ { BEFORE | AFTER } <replaceable class="PARAMETER">existing_enum_value</replaceable> ]
<phrase>where <replaceable class="PARAMETER">action</replaceable> is one of:</phrase>
......@@ -103,6 +104,18 @@ ALTER TYPE <replaceable class="PARAMETER">name</replaceable> SET SCHEMA <replace
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><literal>ADD [ BEFORE | AFTER ]</literal></term>
<listitem>
<para>
This form adds a new value to an enum type. If the new value's place in
the enum's ordering is not specified using <literal>BEFORE</literal> or
<literal>AFTER</literal>, then the new item is placed at the end of the
list of values.
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
......@@ -181,7 +194,7 @@ ALTER TYPE <replaceable class="PARAMETER">name</replaceable> SET SCHEMA <replace
<term><replaceable class="PARAMETER">new_attribute_name</replaceable></term>
<listitem>
<para>
The new name of the attribute begin renamed.
The new name of the attribute to be renamed.
</para>
</listitem>
</varlistentry>
......@@ -196,10 +209,53 @@ ALTER TYPE <replaceable class="PARAMETER">name</replaceable> SET SCHEMA <replace
</listitem>
</varlistentry>
<varlistentry>
<term><replaceable class="PARAMETER">new_enum_value</replaceable></term>
<listitem>
<para>
The new value to be added to an enum type's list of values.
Like all enum literals, it needs to be quoted.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><replaceable class="PARAMETER">existing_enum_value</replaceable></term>
<listitem>
<para>
The existing enum value that the new value should be added immediately
before or after in the enum type's sort ordering.
Like all enum literals, it needs to be quoted.
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
</refsect1>
<refsect1>
<title>Notes</title>
<para>
<command>ALTER TYPE ... ADD</> (the form that adds a new value to an
enum type) cannot be executed inside a transaction block.
</para>
<para>
Comparisons involving an added enum value will sometimes be slower than
comparisons involving only original members of the enum type. This will
usually only occur if <literal>BEFORE</literal> or <literal>AFTER</literal>
is used to set the new value's sort position somewhere other than at the
end of the list. However, sometimes it will happen even though the new
value is added at the end (this occurs if the OID counter <quote>wrapped
around</> since the original creation of the enum type). The slowdown is
usually insignificant; but if it matters, optimal performance can be
regained by dropping and recreating the enum type, or by dumping and
reloading the database.
</para>
</refsect1>
<refsect1>
<title>Examples</title>
......@@ -230,6 +286,13 @@ ALTER TYPE email SET SCHEMA customers;
To add a new attribute to a type:
<programlisting>
ALTER TYPE compfoo ADD ATTRIBUTE f3 int;
</programlisting>
</para>
<para>
To add a new value to an enum type in a particular sort position:
<programlisting>
ALTER TYPE colors ADD 'orange' AFTER 'red';
</programlisting>
</para>
</refsect1>
......
This diff is collapsed.
......@@ -84,7 +84,8 @@ static Oid findTypeTypmodinFunction(List *procname);
static Oid findTypeTypmodoutFunction(List *procname);
static Oid findTypeAnalyzeFunction(List *procname, Oid typeOid);
static List *get_rels_with_domain(Oid domainOid, LOCKMODE lockmode);
static void checkDomainOwner(HeapTuple tup, TypeName *typename);
static void checkDomainOwner(HeapTuple tup);
static void checkEnumOwner(HeapTuple tup);
static char *domainAddConstraint(Oid domainOid, Oid domainNamespace,
Oid baseTypeOid,
int typMod, Constraint *constr,
......@@ -1150,7 +1151,7 @@ DefineEnum(CreateEnumStmt *stmt)
false); /* Type NOT NULL */
/* Enter the enum's values into pg_enum */
EnumValuesCreate(enumTypeOid, stmt->vals, InvalidOid);
EnumValuesCreate(enumTypeOid, stmt->vals);
/*
* Create the array type that goes with it.
......@@ -1191,6 +1192,60 @@ DefineEnum(CreateEnumStmt *stmt)
pfree(enumArrayName);
}
/*
* AlterEnum
* Adds a new label to an existing enum.
*/
void
AlterEnum(AlterEnumStmt *stmt)
{
Oid enum_type_oid;
TypeName *typename;
HeapTuple tup;
/* Make a TypeName so we can use standard type lookup machinery */
typename = makeTypeNameFromNameList(stmt->typeName);
enum_type_oid = typenameTypeId(NULL, typename, NULL);
tup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(enum_type_oid));
if (!HeapTupleIsValid(tup))
elog(ERROR, "cache lookup failed for type %u", enum_type_oid);
/* Check it's an enum and check user has permission to ALTER the enum */
checkEnumOwner(tup);
/* Add the new label */
AddEnumLabel(enum_type_oid, stmt->newVal,
stmt->newValNeighbor, stmt->newValIsAfter);
ReleaseSysCache(tup);
}
/*
* checkEnumOwner
*
* Check that the type is actually an enum and that the current user
* has permission to do ALTER TYPE on it. Throw an error if not.
*/
static void
checkEnumOwner(HeapTuple tup)
{
Form_pg_type typTup = (Form_pg_type) GETSTRUCT(tup);
/* Check that this is actually an enum */
if (typTup->typtype != TYPTYPE_ENUM)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("%s is not an enum",
format_type_be(HeapTupleGetOid(tup)))));
/* Permission check: must own type */
if (!pg_type_ownercheck(HeapTupleGetOid(tup), GetUserId()))
aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_TYPE,
format_type_be(HeapTupleGetOid(tup)));
}
/*
* Find suitable I/O functions for a type.
......@@ -1576,7 +1631,7 @@ AlterDomainDefault(List *names, Node *defaultRaw)
typTup = (Form_pg_type) GETSTRUCT(tup);
/* Check it's a domain and check user has permission for ALTER DOMAIN */
checkDomainOwner(tup, typename);
checkDomainOwner(tup);
/* Setup new tuple */
MemSet(new_record, (Datum) 0, sizeof(new_record));
......@@ -1702,7 +1757,7 @@ AlterDomainNotNull(List *names, bool notNull)
typTup = (Form_pg_type) GETSTRUCT(tup);
/* Check it's a domain and check user has permission for ALTER DOMAIN */
checkDomainOwner(tup, typename);
checkDomainOwner(tup);
/* Is the domain already set to the desired constraint? */
if (typTup->typnotnull == notNull)
......@@ -1801,7 +1856,7 @@ AlterDomainDropConstraint(List *names, const char *constrName,
elog(ERROR, "cache lookup failed for type %u", domainoid);
/* Check it's a domain and check user has permission for ALTER DOMAIN */
checkDomainOwner(tup, typename);
checkDomainOwner(tup);
/* Grab an appropriate lock on the pg_constraint relation */
conrel = heap_open(ConstraintRelationId, RowExclusiveLock);
......@@ -1875,7 +1930,7 @@ AlterDomainAddConstraint(List *names, Node *newConstraint)
typTup = (Form_pg_type) GETSTRUCT(tup);
/* Check it's a domain and check user has permission for ALTER DOMAIN */
checkDomainOwner(tup, typename);
checkDomainOwner(tup);
if (!IsA(newConstraint, Constraint))
elog(ERROR, "unrecognized node type: %d",
......@@ -2187,7 +2242,7 @@ get_rels_with_domain(Oid domainOid, LOCKMODE lockmode)
* has permission to do ALTER DOMAIN on it. Throw an error if not.
*/
static void
checkDomainOwner(HeapTuple tup, TypeName *typename)
checkDomainOwner(HeapTuple tup)
{
Form_pg_type typTup = (Form_pg_type) GETSTRUCT(tup);
......@@ -2195,8 +2250,8 @@ checkDomainOwner(HeapTuple tup, TypeName *typename)
if (typTup->typtype != TYPTYPE_DOMAIN)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not a domain",
TypeNameToString(typename))));
errmsg("%s is not a domain",
format_type_be(HeapTupleGetOid(tup)))));
/* Permission check: must own type */
if (!pg_type_ownercheck(HeapTupleGetOid(tup), GetUserId()))
......
......@@ -2901,6 +2901,19 @@ _copyCreateEnumStmt(CreateEnumStmt *from)
return newnode;
}
static AlterEnumStmt *
_copyAlterEnumStmt(AlterEnumStmt *from)
{
AlterEnumStmt *newnode = makeNode(AlterEnumStmt);
COPY_NODE_FIELD(typeName);
COPY_STRING_FIELD(newVal);
COPY_STRING_FIELD(newValNeighbor);
COPY_SCALAR_FIELD(newValIsAfter);
return newnode;
}
static ViewStmt *
_copyViewStmt(ViewStmt *from)
{
......@@ -4064,6 +4077,9 @@ copyObject(void *from)
case T_CreateEnumStmt:
retval = _copyCreateEnumStmt(from);
break;
case T_AlterEnumStmt:
retval = _copyAlterEnumStmt(from);
break;
case T_ViewStmt:
retval = _copyViewStmt(from);
break;
......
......@@ -1392,6 +1392,17 @@ _equalCreateEnumStmt(CreateEnumStmt *a, CreateEnumStmt *b)
return true;
}
static bool
_equalAlterEnumStmt(AlterEnumStmt *a, AlterEnumStmt *b)
{
COMPARE_NODE_FIELD(typeName);
COMPARE_STRING_FIELD(newVal);
COMPARE_STRING_FIELD(newValNeighbor);
COMPARE_SCALAR_FIELD(newValIsAfter);
return true;
}
static bool
_equalViewStmt(ViewStmt *a, ViewStmt *b)
{
......@@ -2700,6 +2711,9 @@ equal(void *a, void *b)
case T_CreateEnumStmt:
retval = _equalCreateEnumStmt(a, b);
break;
case T_AlterEnumStmt:
retval = _equalAlterEnumStmt(a, b);
break;
case T_ViewStmt:
retval = _equalViewStmt(a, b);
break;
......
......@@ -182,8 +182,8 @@ static RangeVar *makeRangeVarFromAnyName(List *names, int position, core_yyscan_
}
%type <node> stmt schema_stmt
AlterDatabaseStmt AlterDatabaseSetStmt AlterDomainStmt AlterFdwStmt
AlterForeignServerStmt AlterGroupStmt
AlterDatabaseStmt AlterDatabaseSetStmt AlterDomainStmt AlterEnumStmt
AlterFdwStmt AlterForeignServerStmt AlterGroupStmt
AlterObjectSchemaStmt AlterOwnerStmt AlterSeqStmt AlterTableStmt
AlterCompositeTypeStmt AlterUserStmt AlterUserMappingStmt AlterUserSetStmt
AlterRoleStmt AlterRoleSetStmt
......@@ -652,6 +652,7 @@ stmt :
| AlterDatabaseSetStmt
| AlterDefaultPrivilegesStmt
| AlterDomainStmt
| AlterEnumStmt
| AlterFdwStmt
| AlterForeignServerStmt
| AlterFunctionStmt
......@@ -3863,6 +3864,42 @@ enum_val_list: Sconst
{ $$ = lappend($1, makeString($3)); }
;
/*****************************************************************************
*
* ALTER TYPE enumtype ADD ...
*
*****************************************************************************/
AlterEnumStmt:
ALTER TYPE_P any_name ADD_P Sconst
{
AlterEnumStmt *n = makeNode(AlterEnumStmt);
n->typeName = $3;
n->newVal = $5;
n->newValNeighbor = NULL;
n->newValIsAfter = true;
$$ = (Node *) n;
}
| ALTER TYPE_P any_name ADD_P Sconst BEFORE Sconst
{
AlterEnumStmt *n = makeNode(AlterEnumStmt);
n->typeName = $3;
n->newVal = $5;
n->newValNeighbor = $7;
n->newValIsAfter = false;
$$ = (Node *) n;
}
| ALTER TYPE_P any_name ADD_P Sconst AFTER Sconst
{
AlterEnumStmt *n = makeNode(AlterEnumStmt);
n->typeName = $3;
n->newVal = $5;
n->newValNeighbor = $7;
n->newValIsAfter = true;
$$ = (Node *) n;
}
;
/*****************************************************************************
*
......
......@@ -190,6 +190,7 @@ check_xact_readonly(Node *parsetree)
case T_CreateTrigStmt:
case T_CompositeTypeStmt:
case T_CreateEnumStmt:
case T_AlterEnumStmt:
case T_ViewStmt:
case T_DropCastStmt:
case T_DropStmt:
......@@ -860,6 +861,16 @@ standard_ProcessUtility(Node *parsetree,
DefineEnum((CreateEnumStmt *) parsetree);
break;
case T_AlterEnumStmt: /* ALTER TYPE (enum) */
/*
* We disallow this in transaction blocks, because we can't cope
* with enum OID values getting into indexes and then having their
* defining pg_enum entries go away.
*/
PreventTransactionChain(isTopLevel, "ALTER TYPE ... ADD");
AlterEnum((AlterEnumStmt *) parsetree);
break;
case T_ViewStmt: /* CREATE VIEW */
DefineView((ViewStmt *) parsetree, queryString);
break;
......@@ -1868,6 +1879,10 @@ CreateCommandTag(Node *parsetree)
tag = "CREATE TYPE";
break;
case T_AlterEnumStmt:
tag = "ALTER TYPE";
break;
case T_ViewStmt:
tag = "CREATE VIEW";
break;
......@@ -2410,6 +2425,10 @@ GetCommandLogLevel(Node *parsetree)
lev = LOGSTMT_DDL;
break;
case T_AlterEnumStmt:
lev = LOGSTMT_DDL;
break;
case T_ViewStmt:
lev = LOGSTMT_DDL;
break;
......
......@@ -13,18 +13,22 @@
*/
#include "postgres.h"
#include "access/genam.h"
#include "access/heapam.h"
#include "catalog/indexing.h"
#include "catalog/pg_enum.h"
#include "fmgr.h"
#include "libpq/pqformat.h"
#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
#include "utils/fmgroids.h"
#include "utils/snapmgr.h"
#include "utils/syscache.h"
#include "libpq/pqformat.h"
#include "miscadmin.h"
#include "utils/typcache.h"
static Oid enum_endpoint(Oid enumtypoid, ScanDirection direction);
static ArrayType *enum_range_internal(Oid enumtypoid, Oid lower, Oid upper);
static int enum_elem_cmp(const void *left, const void *right);
/* Basic I/O support */
......@@ -155,13 +159,63 @@ enum_send(PG_FUNCTION_ARGS)
/* Comparison functions and related */
/*
* enum_cmp_internal is the common engine for all the visible comparison
* functions, except for enum_eq and enum_ne which can just check for OID
* equality directly.
*/
static int
enum_cmp_internal(Oid arg1, Oid arg2, FunctionCallInfo fcinfo)
{
TypeCacheEntry *tcache;
/* Equal OIDs are equal no matter what */
if (arg1 == arg2)
return 0;
/* Fast path: even-numbered Oids are known to compare correctly */
if ((arg1 & 1) == 0 && (arg2 & 1) == 0)
{
if (arg1 < arg2)
return -1;
else
return 1;
}
/* Locate the typcache entry for the enum type */
tcache = (TypeCacheEntry *) fcinfo->flinfo->fn_extra;
if (tcache == NULL)
{
HeapTuple enum_tup;
Form_pg_enum en;
Oid typeoid;
/* Get the OID of the enum type containing arg1 */
enum_tup = SearchSysCache1(ENUMOID, ObjectIdGetDatum(arg1));
if (!HeapTupleIsValid(enum_tup))
ereport(ERROR,
(errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
errmsg("invalid internal value for enum: %u",
arg1)));
en = (Form_pg_enum) GETSTRUCT(enum_tup);
typeoid = en->enumtypid;
ReleaseSysCache(enum_tup);
/* Now locate and remember the typcache entry */
tcache = lookup_type_cache(typeoid, 0);
fcinfo->flinfo->fn_extra = (void *) tcache;
}
/* The remaining comparison logic is in typcache.c */
return compare_values_of_enum(tcache, arg1, arg2);
}
Datum
enum_lt(PG_FUNCTION_ARGS)
{
Oid a = PG_GETARG_OID(0);
Oid b = PG_GETARG_OID(1);
PG_RETURN_BOOL(a < b);
PG_RETURN_BOOL(enum_cmp_internal(a, b, fcinfo) < 0);
}
Datum
......@@ -170,7 +224,7 @@ enum_le(PG_FUNCTION_ARGS)
Oid a = PG_GETARG_OID(0);
Oid b = PG_GETARG_OID(1);
PG_RETURN_BOOL(a <= b);
PG_RETURN_BOOL(enum_cmp_internal(a, b, fcinfo) <= 0);
}
Datum
......@@ -197,7 +251,7 @@ enum_ge(PG_FUNCTION_ARGS)
Oid a = PG_GETARG_OID(0);
Oid b = PG_GETARG_OID(1);
PG_RETURN_BOOL(a >= b);
PG_RETURN_BOOL(enum_cmp_internal(a, b, fcinfo) >= 0);
}
Datum
......@@ -206,7 +260,7 @@ enum_gt(PG_FUNCTION_ARGS)
Oid a = PG_GETARG_OID(0);
Oid b = PG_GETARG_OID(1);
PG_RETURN_BOOL(a > b);
PG_RETURN_BOOL(enum_cmp_internal(a, b, fcinfo) > 0);
}
Datum
......@@ -215,7 +269,7 @@ enum_smaller(PG_FUNCTION_ARGS)
Oid a = PG_GETARG_OID(0);
Oid b = PG_GETARG_OID(1);
PG_RETURN_OID(a <= b ? a : b);
PG_RETURN_OID(enum_cmp_internal(a, b, fcinfo) < 0 ? a : b);
}
Datum
......@@ -224,7 +278,7 @@ enum_larger(PG_FUNCTION_ARGS)
Oid a = PG_GETARG_OID(0);
Oid b = PG_GETARG_OID(1);
PG_RETURN_OID(a >= b ? a : b);
PG_RETURN_OID(enum_cmp_internal(a, b, fcinfo) > 0 ? a : b);
}
Datum
......@@ -233,24 +287,63 @@ enum_cmp(PG_FUNCTION_ARGS)
Oid a = PG_GETARG_OID(0);
Oid b = PG_GETARG_OID(1);
if (a > b)
PG_RETURN_INT32(1);
else if (a == b)
if (a == b)
PG_RETURN_INT32(0);
else if (enum_cmp_internal(a, b, fcinfo) > 0)
PG_RETURN_INT32(1);
else
PG_RETURN_INT32(-1);
}
/* Enum programming support functions */
/*
* enum_endpoint: common code for enum_first/enum_last
*/
static Oid
enum_endpoint(Oid enumtypoid, ScanDirection direction)
{
Relation enum_rel;
Relation enum_idx;
SysScanDesc enum_scan;
HeapTuple enum_tuple;
ScanKeyData skey;
Oid minmax;
/*
* Find the first/last enum member using pg_enum_typid_sortorder_index.
* Note we must not use the syscache, and must use an MVCC snapshot here.
* See comments for RenumberEnumType in catalog/pg_enum.c for more info.
*/
ScanKeyInit(&skey,
Anum_pg_enum_enumtypid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(enumtypoid));
enum_rel = heap_open(EnumRelationId, AccessShareLock);
enum_idx = index_open(EnumTypIdSortOrderIndexId, AccessShareLock);
enum_scan = systable_beginscan_ordered(enum_rel, enum_idx,
GetTransactionSnapshot(),
1, &skey);
enum_tuple = systable_getnext_ordered(enum_scan, direction);
if (HeapTupleIsValid(enum_tuple))
minmax = HeapTupleGetOid(enum_tuple);
else
minmax = InvalidOid;
systable_endscan_ordered(enum_scan);
index_close(enum_idx, AccessShareLock);
heap_close(enum_rel, AccessShareLock);
return minmax;
}
Datum
enum_first(PG_FUNCTION_ARGS)
{
Oid enumtypoid;
Oid min = InvalidOid;
CatCList *list;
int num,
i;
Oid min;
/*
* We rely on being able to get the specific enum type from the calling
......@@ -263,21 +356,14 @@ enum_first(PG_FUNCTION_ARGS)
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("could not determine actual enum type")));
list = SearchSysCacheList1(ENUMTYPOIDNAME, ObjectIdGetDatum(enumtypoid));
num = list->n_members;
for (i = 0; i < num; i++)
{
Oid valoid = HeapTupleHeaderGetOid(list->members[i]->tuple.t_data);
if (!OidIsValid(min) || valoid < min)
min = valoid;
}
ReleaseCatCacheList(list);
/* Get the OID using the index */
min = enum_endpoint(enumtypoid, ForwardScanDirection);
if (!OidIsValid(min)) /* should not happen */
elog(ERROR, "no values found for enum %s",
format_type_be(enumtypoid));
if (!OidIsValid(min))
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("enum %s contains no values",
format_type_be(enumtypoid))));
PG_RETURN_OID(min);
}
......@@ -286,10 +372,7 @@ Datum
enum_last(PG_FUNCTION_ARGS)
{
Oid enumtypoid;
Oid max = InvalidOid;
CatCList *list;
int num,
i;
Oid max;
/*
* We rely on being able to get the specific enum type from the calling
......@@ -302,21 +385,14 @@ enum_last(PG_FUNCTION_ARGS)
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("could not determine actual enum type")));
list = SearchSysCacheList1(ENUMTYPOIDNAME, ObjectIdGetDatum(enumtypoid));
num = list->n_members;
for (i = 0; i < num; i++)
{
Oid valoid = HeapTupleHeaderGetOid(list->members[i]->tuple.t_data);
if (!OidIsValid(max) || valoid > max)
max = valoid;
}
ReleaseCatCacheList(list);
/* Get the OID using the index */
max = enum_endpoint(enumtypoid, BackwardScanDirection);
if (!OidIsValid(max)) /* should not happen */
elog(ERROR, "no values found for enum %s",
format_type_be(enumtypoid));
if (!OidIsValid(max))
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("enum %s contains no values",
format_type_be(enumtypoid))));
PG_RETURN_OID(max);
}
......@@ -377,51 +453,68 @@ static ArrayType *
enum_range_internal(Oid enumtypoid, Oid lower, Oid upper)
{
ArrayType *result;
CatCList *list;
int total,
i,
j;
Relation enum_rel;
Relation enum_idx;
SysScanDesc enum_scan;
HeapTuple enum_tuple;
ScanKeyData skey;
Datum *elems;
int max,
cnt;
bool left_found;
list = SearchSysCacheList1(ENUMTYPOIDNAME, ObjectIdGetDatum(enumtypoid));
total = list->n_members;
/*
* Scan the enum members in order using pg_enum_typid_sortorder_index.
* Note we must not use the syscache, and must use an MVCC snapshot here.
* See comments for RenumberEnumType in catalog/pg_enum.c for more info.
*/
ScanKeyInit(&skey,
Anum_pg_enum_enumtypid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(enumtypoid));
enum_rel = heap_open(EnumRelationId, AccessShareLock);
enum_idx = index_open(EnumTypIdSortOrderIndexId, AccessShareLock);
enum_scan = systable_beginscan_ordered(enum_rel, enum_idx,
GetTransactionSnapshot(),
1, &skey);
max = 64;
elems = (Datum *) palloc(max * sizeof(Datum));
cnt = 0;
left_found = !OidIsValid(lower);
while (HeapTupleIsValid(enum_tuple = systable_getnext_ordered(enum_scan, ForwardScanDirection)))
{
Oid enum_oid = HeapTupleGetOid(enum_tuple);
elems = (Datum *) palloc(total * sizeof(Datum));
if (!left_found && lower == enum_oid)
left_found = true;
j = 0;
for (i = 0; i < total; i++)
{
Oid val = HeapTupleGetOid(&(list->members[i]->tuple));
if (left_found)
{
if (cnt >= max)
{
max *= 2;
elems = (Datum *) repalloc(elems, max * sizeof(Datum));
}
if ((!OidIsValid(lower) || lower <= val) &&
(!OidIsValid(upper) || val <= upper))
elems[j++] = ObjectIdGetDatum(val);
}
elems[cnt++] = ObjectIdGetDatum(enum_oid);
}
/* shouldn't need the cache anymore */
ReleaseCatCacheList(list);
if (OidIsValid(upper) && upper == enum_oid)
break;
}
/* sort results into OID order */
qsort(elems, j, sizeof(Datum), enum_elem_cmp);
systable_endscan_ordered(enum_scan);
index_close(enum_idx, AccessShareLock);
heap_close(enum_rel, AccessShareLock);
/* and build the result array */
/* note this hardwires some details about the representation of Oid */
result = construct_array(elems, j, enumtypoid, sizeof(Oid), true, 'i');
result = construct_array(elems, cnt, enumtypoid, sizeof(Oid), true, 'i');
pfree(elems);
return result;
}
/* qsort comparison function for Datums that are OIDs */
static int
enum_elem_cmp(const void *left, const void *right)
{
Oid l = DatumGetObjectId(*((const Datum *) left));
Oid r = DatumGetObjectId(*((const Datum *) right));
if (l < r)
return -1;
if (l > r)
return 1;
return 0;
}
This diff is collapsed.
......@@ -6657,14 +6657,21 @@ dumpEnumType(Archive *fout, TypeInfo *tyinfo)
Oid enum_oid;
char *label;
/* Set proper schema search path so regproc references list correctly */
selectSourceSchema(tyinfo->dobj.namespace->dobj.name);
/* Set proper schema search path */
selectSourceSchema("pg_catalog");
appendPQExpBuffer(query, "SELECT oid, enumlabel "
"FROM pg_catalog.pg_enum "
"WHERE enumtypid = '%u'"
"ORDER BY oid",
tyinfo->dobj.catId.oid);
if (fout->remoteVersion >= 90100)
appendPQExpBuffer(query, "SELECT oid, enumlabel "
"FROM pg_catalog.pg_enum "
"WHERE enumtypid = '%u'"
"ORDER BY enumsortorder",
tyinfo->dobj.catId.oid);
else
appendPQExpBuffer(query, "SELECT oid, enumlabel "
"FROM pg_catalog.pg_enum "
"WHERE enumtypid = '%u'"
"ORDER BY oid",
tyinfo->dobj.catId.oid);
res = PQexec(g_conn, query->data);
check_sql_result(res, g_conn, query->data, PGRES_TUPLES_OK);
......@@ -6713,13 +6720,15 @@ dumpEnumType(Archive *fout, TypeInfo *tyinfo)
if (i == 0)
appendPQExpBuffer(q, "\n-- For binary upgrade, must preserve pg_enum oids\n");
appendPQExpBuffer(q,
"SELECT binary_upgrade.add_pg_enum_label('%u'::pg_catalog.oid, "
"'%u'::pg_catalog.oid, ",
enum_oid, tyinfo->dobj.catId.oid);
"SELECT binary_upgrade.set_next_pg_enum_oid('%u'::pg_catalog.oid);\n",
enum_oid);
appendPQExpBuffer(q, "ALTER TYPE %s.",
fmtId(tyinfo->dobj.namespace->dobj.name));
appendPQExpBuffer(q, "%s ADD ",
fmtId(tyinfo->dobj.name));
appendStringLiteralAH(q, label, fout);
appendPQExpBuffer(q, ");\n");
appendPQExpBuffer(q, ";\n\n");
}
appendPQExpBuffer(q, "\n");
}
ArchiveEntry(fout, tyinfo->dobj.catId, tyinfo->dobj.dumpId,
......
......@@ -473,17 +473,27 @@ describeTypes(const char *pattern, bool verbose, bool showSystem)
gettext_noop("Internal name"),
gettext_noop("Size"));
if (verbose && pset.sversion >= 80300)
{
appendPQExpBuffer(&buf,
" pg_catalog.array_to_string(\n"
" ARRAY(\n"
" SELECT e.enumlabel\n"
" FROM pg_catalog.pg_enum e\n"
" WHERE e.enumtypid = t.oid\n"
" ORDER BY e.oid\n"
" WHERE e.enumtypid = t.oid\n");
if (pset.sversion >= 90100)
appendPQExpBuffer(&buf,
" ORDER BY e.enumsortorder\n");
else
appendPQExpBuffer(&buf,
" ORDER BY e.oid\n");
appendPQExpBuffer(&buf,
" ),\n"
" E'\\n'\n"
" ) AS \"%s\",\n",
gettext_noop("Elements"));
}
appendPQExpBuffer(&buf,
" pg_catalog.obj_description(t.oid, 'pg_type') as \"%s\"\n",
......
......@@ -53,6 +53,6 @@
*/
/* yyyymmddN */
#define CATALOG_VERSION_NO 201010201
#define CATALOG_VERSION_NO 201010241
#endif
......@@ -147,6 +147,8 @@ DECLARE_UNIQUE_INDEX(pg_enum_oid_index, 3502, on pg_enum using btree(oid oid_ops
#define EnumOidIndexId 3502
DECLARE_UNIQUE_INDEX(pg_enum_typid_label_index, 3503, on pg_enum using btree(enumtypid oid_ops, enumlabel name_ops));
#define EnumTypIdLabelIndexId 3503
DECLARE_UNIQUE_INDEX(pg_enum_typid_sortorder_index, 3534, on pg_enum using btree(enumtypid oid_ops, enumsortorder float4_ops));
#define EnumTypIdSortOrderIndexId 3534
/* This following index is not used for a cache and is not unique */
DECLARE_INDEX(pg_index_indrelid_index, 2678, on pg_index using btree(indrelid oid_ops));
......
......@@ -34,6 +34,7 @@
CATALOG(pg_enum,3501)
{
Oid enumtypid; /* OID of owning enum type */
float4 enumsortorder; /* sort position of this enum value */
NameData enumlabel; /* text representation of enum value */
} FormData_pg_enum;
......@@ -48,9 +49,10 @@ typedef FormData_pg_enum *Form_pg_enum;
* compiler constants for pg_enum
* ----------------
*/
#define Natts_pg_enum 2
#define Natts_pg_enum 3
#define Anum_pg_enum_enumtypid 1
#define Anum_pg_enum_enumlabel 2
#define Anum_pg_enum_enumsortorder 2
#define Anum_pg_enum_enumlabel 3
/* ----------------
* pg_enum has no initial contents
......@@ -60,8 +62,9 @@ typedef FormData_pg_enum *Form_pg_enum;
/*
* prototypes for functions in pg_enum.c
*/
extern void EnumValuesCreate(Oid enumTypeOid, List *vals,
Oid binary_upgrade_next_pg_enum_oid);
extern void EnumValuesCreate(Oid enumTypeOid, List *vals);
extern void EnumValuesDelete(Oid enumTypeOid);
extern void AddEnumLabel(Oid enumTypeOid, const char *newVal,
const char *neighbor, bool newValIsAfter);
#endif /* PG_ENUM_H */
......@@ -24,6 +24,7 @@ extern void RemoveTypes(DropStmt *drop);
extern void RemoveTypeById(Oid typeOid);
extern void DefineDomain(CreateDomainStmt *stmt);
extern void DefineEnum(CreateEnumStmt *stmt);
extern void AlterEnum(AlterEnumStmt *stmt);
extern Oid DefineCompositeType(const RangeVar *typevar, List *coldeflist);
extern Oid AssignTypeArrayOid(void);
......
......@@ -338,6 +338,7 @@ typedef enum NodeTag
T_ReassignOwnedStmt,
T_CompositeTypeStmt,
T_CreateEnumStmt,
T_AlterEnumStmt,
T_AlterTSDictionaryStmt,
T_AlterTSConfigurationStmt,
T_CreateFdwStmt,
......
......@@ -2193,6 +2193,18 @@ typedef struct CreateEnumStmt
List *vals; /* enum values (list of Value strings) */
} CreateEnumStmt;
/* ----------------------
* Alter Type Statement, enum types
* ----------------------
*/
typedef struct AlterEnumStmt
{
NodeTag type;
List *typeName; /* qualified name (list of Value strings) */
char *newVal; /* new enum value's name */
char *newValNeighbor; /* neighboring enum value, if specified */
bool newValIsAfter; /* place new enum value after neighbor? */
} AlterEnumStmt;
/* ----------------------
* Create View Statement
......
......@@ -20,6 +20,9 @@
#include "fmgr.h"
/* TypeCacheEnumData is an opaque struct known only within typcache.c */
struct TypeCacheEnumData;
typedef struct TypeCacheEntry
{
/* typeId is the hash lookup key and MUST BE FIRST */
......@@ -63,6 +66,12 @@ typedef struct TypeCacheEntry
* reference-counted tupledesc.)
*/
TupleDesc tupDesc;
/*
* Private information about an enum type. NULL if not enum or
* information hasn't been requested.
*/
struct TypeCacheEnumData *enumData;
} TypeCacheEntry;
/* Bit flags to indicate which fields a given caller needs to have set */
......@@ -86,4 +95,6 @@ extern TupleDesc lookup_rowtype_tupdesc_copy(Oid type_id, int32 typmod);
extern void assign_record_type_typmod(TupleDesc tupDesc);
extern int compare_values_of_enum(TypeCacheEntry *tcache, Oid arg1, Oid arg2);
#endif /* TYPCACHE_H */
......@@ -24,6 +24,155 @@ SELECT 'mauve'::rainbow;
ERROR: invalid input value for enum rainbow: "mauve"
LINE 1: SELECT 'mauve'::rainbow;
^
--
-- adding new values
--
CREATE TYPE planets AS ENUM ( 'venus', 'earth', 'mars' );
SELECT enumlabel, enumsortorder
FROM pg_enum
WHERE enumtypid = 'planets'::regtype
ORDER BY 2;
enumlabel | enumsortorder
-----------+---------------
venus | 1
earth | 2
mars | 3
(3 rows)
ALTER TYPE planets ADD 'uranus';
SELECT enumlabel, enumsortorder
FROM pg_enum
WHERE enumtypid = 'planets'::regtype
ORDER BY 2;
enumlabel | enumsortorder
-----------+---------------
venus | 1
earth | 2
mars | 3
uranus | 4
(4 rows)
ALTER TYPE planets ADD 'mercury' BEFORE 'venus';
ALTER TYPE planets ADD 'saturn' BEFORE 'uranus';
ALTER TYPE planets ADD 'jupiter' AFTER 'mars';
ALTER TYPE planets ADD 'neptune' AFTER 'uranus';
SELECT enumlabel, enumsortorder
FROM pg_enum
WHERE enumtypid = 'planets'::regtype
ORDER BY 2;
enumlabel | enumsortorder
-----------+---------------
mercury | 0
venus | 1
earth | 2
mars | 3
jupiter | 3.25
saturn | 3.5
uranus | 4
neptune | 5
(8 rows)
SELECT enumlabel, enumsortorder
FROM pg_enum
WHERE enumtypid = 'planets'::regtype
ORDER BY enumlabel::planets;
enumlabel | enumsortorder
-----------+---------------
mercury | 0
venus | 1
earth | 2
mars | 3
jupiter | 3.25
saturn | 3.5
uranus | 4
neptune | 5
(8 rows)
-- errors for adding labels
ALTER TYPE planets ADD
'plutoplutoplutoplutoplutoplutoplutoplutoplutoplutoplutoplutoplutopluto';
ERROR: invalid enum label "plutoplutoplutoplutoplutoplutoplutoplutoplutoplutoplutoplutoplutopluto"
DETAIL: Labels must be 63 characters or less.
ALTER TYPE planets ADD 'pluto' AFTER 'zeus';
ERROR: "zeus" is not an existing enum label
--
-- Test inserting so many values that we have to renumber
--
create type insenum as enum ('L1', 'L2');
alter type insenum add 'i1' before 'L2';
alter type insenum add 'i2' before 'L2';
alter type insenum add 'i3' before 'L2';
alter type insenum add 'i4' before 'L2';
alter type insenum add 'i5' before 'L2';
alter type insenum add 'i6' before 'L2';
alter type insenum add 'i7' before 'L2';
alter type insenum add 'i8' before 'L2';
alter type insenum add 'i9' before 'L2';
alter type insenum add 'i10' before 'L2';
alter type insenum add 'i11' before 'L2';
alter type insenum add 'i12' before 'L2';
alter type insenum add 'i13' before 'L2';
alter type insenum add 'i14' before 'L2';
alter type insenum add 'i15' before 'L2';
alter type insenum add 'i16' before 'L2';
alter type insenum add 'i17' before 'L2';
alter type insenum add 'i18' before 'L2';
alter type insenum add 'i19' before 'L2';
alter type insenum add 'i20' before 'L2';
alter type insenum add 'i21' before 'L2';
alter type insenum add 'i22' before 'L2';
alter type insenum add 'i23' before 'L2';
alter type insenum add 'i24' before 'L2';
alter type insenum add 'i25' before 'L2';
alter type insenum add 'i26' before 'L2';
alter type insenum add 'i27' before 'L2';
alter type insenum add 'i28' before 'L2';
alter type insenum add 'i29' before 'L2';
alter type insenum add 'i30' before 'L2';
-- The exact values of enumsortorder will now depend on the local properties
-- of float4, but in any reasonable implementation we should get at least
-- 20 splits before having to renumber; so only hide values > 20.
SELECT enumlabel,
case when enumsortorder > 20 then null else enumsortorder end as so
FROM pg_enum
WHERE enumtypid = 'insenum'::regtype
ORDER BY enumsortorder;
enumlabel | so
-----------+----
L1 | 1
i1 | 2
i2 | 3
i3 | 4
i4 | 5
i5 | 6
i6 | 7
i7 | 8
i8 | 9
i9 | 10
i10 | 11
i11 | 12
i12 | 13
i13 | 14
i14 | 15
i15 | 16
i16 | 17
i17 | 18
i18 | 19
i19 | 20
i20 |
i21 |
i22 |
i23 |
i24 |
i25 |
i26 |
i27 |
i28 |
i29 |
i30 |
L2 |
(32 rows)
--
-- Basic table creation, row selection
--
......@@ -403,7 +552,7 @@ SELECT COUNT(*) FROM pg_type WHERE typname = 'rainbow';
SELECT * FROM pg_enum WHERE NOT EXISTS
(SELECT 1 FROM pg_type WHERE pg_type.oid = enumtypid);
enumtypid | enumlabel
-----------+-----------
enumtypid | enumsortorder | enumlabel
-----------+---------------+-----------
(0 rows)
......@@ -15,6 +15,92 @@ SELECT COUNT(*) FROM pg_enum WHERE enumtypid = 'rainbow'::regtype;
SELECT 'red'::rainbow;
SELECT 'mauve'::rainbow;
--
-- adding new values
--
CREATE TYPE planets AS ENUM ( 'venus', 'earth', 'mars' );
SELECT enumlabel, enumsortorder
FROM pg_enum
WHERE enumtypid = 'planets'::regtype
ORDER BY 2;
ALTER TYPE planets ADD 'uranus';
SELECT enumlabel, enumsortorder
FROM pg_enum
WHERE enumtypid = 'planets'::regtype
ORDER BY 2;
ALTER TYPE planets ADD 'mercury' BEFORE 'venus';
ALTER TYPE planets ADD 'saturn' BEFORE 'uranus';
ALTER TYPE planets ADD 'jupiter' AFTER 'mars';
ALTER TYPE planets ADD 'neptune' AFTER 'uranus';
SELECT enumlabel, enumsortorder
FROM pg_enum
WHERE enumtypid = 'planets'::regtype
ORDER BY 2;
SELECT enumlabel, enumsortorder
FROM pg_enum
WHERE enumtypid = 'planets'::regtype
ORDER BY enumlabel::planets;
-- errors for adding labels
ALTER TYPE planets ADD
'plutoplutoplutoplutoplutoplutoplutoplutoplutoplutoplutoplutoplutopluto';
ALTER TYPE planets ADD 'pluto' AFTER 'zeus';
--
-- Test inserting so many values that we have to renumber
--
create type insenum as enum ('L1', 'L2');
alter type insenum add 'i1' before 'L2';
alter type insenum add 'i2' before 'L2';
alter type insenum add 'i3' before 'L2';
alter type insenum add 'i4' before 'L2';
alter type insenum add 'i5' before 'L2';
alter type insenum add 'i6' before 'L2';
alter type insenum add 'i7' before 'L2';
alter type insenum add 'i8' before 'L2';
alter type insenum add 'i9' before 'L2';
alter type insenum add 'i10' before 'L2';
alter type insenum add 'i11' before 'L2';
alter type insenum add 'i12' before 'L2';
alter type insenum add 'i13' before 'L2';
alter type insenum add 'i14' before 'L2';
alter type insenum add 'i15' before 'L2';
alter type insenum add 'i16' before 'L2';
alter type insenum add 'i17' before 'L2';
alter type insenum add 'i18' before 'L2';
alter type insenum add 'i19' before 'L2';
alter type insenum add 'i20' before 'L2';
alter type insenum add 'i21' before 'L2';
alter type insenum add 'i22' before 'L2';
alter type insenum add 'i23' before 'L2';
alter type insenum add 'i24' before 'L2';
alter type insenum add 'i25' before 'L2';
alter type insenum add 'i26' before 'L2';
alter type insenum add 'i27' before 'L2';
alter type insenum add 'i28' before 'L2';
alter type insenum add 'i29' before 'L2';
alter type insenum add 'i30' before 'L2';
-- The exact values of enumsortorder will now depend on the local properties
-- of float4, but in any reasonable implementation we should get at least
-- 20 splits before having to renumber; so only hide values > 20.
SELECT enumlabel,
case when enumsortorder > 20 then null else enumsortorder end as so
FROM pg_enum
WHERE enumtypid = 'insenum'::regtype
ORDER BY enumsortorder;
--
-- Basic table creation, row selection
--
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment