diff --git a/asyncpg/protocol/codecs/array.pyx b/asyncpg/protocol/codecs/array.pyx index 92706c4a..31182911 100644 --- a/asyncpg/protocol/codecs/array.pyx +++ b/asyncpg/protocol/codecs/array.pyx @@ -152,8 +152,8 @@ cdef inline array_decode(ConnectionSettings settings, FastReadBuffer buf, if ndims > ARRAY_MAXDIM: raise RuntimeError( - 'number of array dimensions exceed the maximum expected ({})'. - format(ARRAY_MAXDIM)) + 'number of array dimensions ({}) exceed the maximum expected ({})'. + format(ndims, ARRAY_MAXDIM)) if decoder == NULL: # No decoder is known beforehand, look it up @@ -348,6 +348,104 @@ cdef arraytext_decode(ConnectionSettings settings, FastReadBuffer buf): cdef anyarray_decode(ConnectionSettings settings, FastReadBuffer buf): return array_decode(settings, buf, NULL, NULL) +cdef arrayaclitemid_decode(ConnectionSettings settings, FastReadBuffer buf): + + cdef: + # object array_text + unicode array_text + Py_ssize_t array_textlen + Py_ssize_t bgn_idx + Py_ssize_t cur_idx + list result + list escaped_parts + object elem + + # Postgres does not have a function to send aclitems in binary mode + # (aclitem_send) and arrays also must be sent in a text mode. + # Parse it manually with the next convention (since it is system info): + # 1. Array's index must begin from 1; + # 2. Delimeter is ','. + + array_text = text_decode(settings, buf) + if array_text is None: + cpython.Py_INCREF(array_text) + return array_text + + result = cpython.PyList_New(0) + + array_textlen = len(array_text) + if array_textlen == 0 or array_text[1] == '}': + return result + + if array_text[0] == '[': + idx = array_text.find('=') + raise ValueError('invalid array bounds (must start with 1); got: {}.' + .format(array_text[:idx])) + + assert array_text[0] == '{' + + cur_idx = 0 + while True: + cur_chr = array_text[cur_idx] + + if cur_chr == '}': + assert (cur_idx + 1) == array_textlen + break + + assert cur_chr in '{,' + # Here should be a check for a delimeter after a delimeter. Usually + # it (empty string) means NULL, but for aclitem it is not possible. + # So do nothing and get the next char + cur_idx += 1 + + bgn_idx = cur_idx + cur_chr = array_text[cur_idx] + + if cur_chr != '"': + # unquoted element + cur_idx += 1 + while array_text[cur_idx] not in ',}': + cur_idx += 1 + elem = array_text[bgn_idx:cur_idx] + cpython.Py_INCREF(elem) + cpython.PyList_Append(result, elem) + continue + + # quoted element + escaped_parts = cpython.PyList_New(0) + cur_idx += 1 + bgn_idx = cur_idx + while True: + while array_text[cur_idx] not in '\\"': + cur_idx += 1 + + if array_text[cur_idx] == '"': + # Closing quote symbol + elem = array_text[bgn_idx:cur_idx] + cpython.PyList_Append(escaped_parts, elem) + break + + # Escape symbol '\'. + # Add previous non-empty block and be ready for the next one. + if bgn_idx != cur_idx: + elem = array_text[bgn_idx:cur_idx] + cpython.PyList_Append(escaped_parts, elem) + + cur_idx += 1 + bgn_idx = cur_idx # the next block begins from the escaped char + + cur_idx += 1 + + elem = ''.join(escaped_parts) + cpython.Py_INCREF(elem) + cpython.PyList_Append(result, elem) + cur_idx += 1 # skip the last double quote symbol + + return result + +cdef arrayaclitemid_encode(ConnectionSettings settings, WriteBuffer buf, items): + raise NotImplementedError("use postgresql's conversion " + "from text[] to aclitem[]") cdef init_array_codecs(): register_core_codec(ANYARRAYOID, @@ -368,4 +466,9 @@ cdef init_array_codecs(): &arraytext_decode, PG_FORMAT_BINARY) + register_core_codec(_ACLITEMOID, + &arrayaclitemid_encode, + &arrayaclitemid_decode, + PG_FORMAT_TEXT) + init_array_codecs() diff --git a/asyncpg/protocol/pgtypes.pxi b/asyncpg/protocol/pgtypes.pxi index 62c5d194..7b417885 100644 --- a/asyncpg/protocol/pgtypes.pxi +++ b/asyncpg/protocol/pgtypes.pxi @@ -50,6 +50,7 @@ DEF INETOID = 869 DEF _TEXTOID = 1009 DEF _OIDOID = 1028 DEF ACLITEMOID = 1033 +DEF _ACLITEMOID = 1034 DEF BPCHAROID = 1042 DEF VARCHAROID = 1043 DEF DATEOID = 1082 @@ -95,88 +96,89 @@ DEF EVENT_TRIGGEROID = 3838 DEF REGNAMESPACEOID = 4089 DEF REGROLEOID = 4096 -cdef ARRAY_TYPES = (_TEXTOID, _OIDOID,) +cdef ARRAY_TYPES = (_TEXTOID, _OIDOID, _ACLITEMOID,) TYPEMAP = { - NUMERICOID: 'numeric', - INTERVALOID: 'interval', - TIMETZOID: 'timetz', - CHAROID: 'char', - FDW_HANDLEROID: 'fdw_handler', - REGTYPEOID: 'regtype', - REGOPEROID: 'regoper', + ABSTIMEOID: 'abstime', ACLITEMOID: 'aclitem', - TXID_SNAPSHOTOID: 'txid_snapshot', - REGDICTIONARYOID: 'regdictionary', - POINTOID: 'point', - OIDOID: 'oid', - PG_NODE_TREEOID: 'pg_node_tree', - REFCURSOROID: 'refcursor', - REGNAMESPACEOID: 'regnamespace', - TIMESTAMPOID: 'timestamp', - BYTEAOID: 'bytea', - REGCONFIGOID: 'regconfig', - UUIDOID: 'uuid', - FLOAT4OID: 'float4', - SMGROID: 'smgr', - BOOLOID: 'bool', - INT4OID: 'int4', - MACADDROID: 'macaddr', - TSM_HANDLEROID: 'tsm_handler', - REGPROCEDUREOID: 'regprocedure', - RELTIMEOID: 'reltime', - DATEOID: 'date', - _OIDOID: 'oid[]', - TSQUERYOID: 'tsquery', - LINEOID: 'line', - PG_LSNOID: 'pg_lsn', - JSONOID: 'json', - POLYGONOID: 'polygon', - XMLOID: 'xml', - INT2OID: 'int2', - TINTERVALOID: 'tinterval', ANYARRAYOID: 'anyarray', - NAMEOID: 'name', - TIDOID: 'tid', + ANYELEMENTOID: 'anyelement', + ANYENUMOID: 'anyenum', + ANYNONARRAYOID: 'anynonarray', + ANYOID: 'any', ANYRANGEOID: 'anyrange', + BITOID: 'bit', + BOOLOID: 'bool', + BOXOID: 'box', + BPCHAROID: 'bpchar', + BYTEAOID: 'bytea', + CHAROID: 'char', CIDOID: 'cid', - TIMESTAMPTZOID: 'timestamptz', CIDROID: 'cidr', - REGCLASSOID: 'regclass', - INT8OID: 'int8', + CIRCLEOID: 'circle', CSTRINGOID: 'cstring', + DATEOID: 'date', + EVENT_TRIGGEROID: 'event_trigger', + FDW_HANDLEROID: 'fdw_handler', + FLOAT4OID: 'float4', FLOAT8OID: 'float8', - REGROLEOID: 'regrole', - CIRCLEOID: 'circle', - ANYNONARRAYOID: 'anynonarray', GTSVECTOROID: 'gtsvector', - ABSTIMEOID: 'abstime', - PATHOID: 'path', - OPAQUEOID: 'opaque', - ANYOID: 'any', - TIMEOID: 'time', - ANYENUMOID: 'anyenum', - VOIDOID: 'void', - ANYELEMENTOID: 'anyelement', - LSEGOID: 'lseg', - LANGUAGE_HANDLEROID: 'language_handler', INETOID: 'inet', - REGPROCOID: 'regproc', - EVENT_TRIGGEROID: 'event_trigger', - TEXTOID: 'text', - BOXOID: 'box', + INT2OID: 'int2', + INT4OID: 'int4', + INT8OID: 'int8', INTERNALOID: 'internal', - VARBITOID: 'varbit', - XIDOID: 'xid', - UNKNOWNOID: 'unknown', - PG_DDL_COMMANDOID: 'pg_ddl_command', - BITOID: 'bit', + INTERVALOID: 'interval', + JSONBOID: 'jsonb', + JSONOID: 'json', + LANGUAGE_HANDLEROID: 'language_handler', + LINEOID: 'line', + LSEGOID: 'lseg', + MACADDROID: 'macaddr', MONEYOID: 'money', - VARCHAROID: 'varchar', - TSVECTOROID: 'tsvector', - _TEXTOID: 'text[]', + NAMEOID: 'name', + NUMERICOID: 'numeric', + OIDOID: 'oid', + OPAQUEOID: 'opaque', + PATHOID: 'path', + PG_DDL_COMMANDOID: 'pg_ddl_command', + PG_LSNOID: 'pg_lsn', + PG_NODE_TREEOID: 'pg_node_tree', + POINTOID: 'point', + POLYGONOID: 'polygon', RECORDOID: 'record', - JSONBOID: 'jsonb', + REFCURSOROID: 'refcursor', + REGCLASSOID: 'regclass', + REGCONFIGOID: 'regconfig', + REGDICTIONARYOID: 'regdictionary', + REGNAMESPACEOID: 'regnamespace', REGOPERATOROID: 'regoperator', + REGOPEROID: 'regoper', + REGPROCEDUREOID: 'regprocedure', + REGPROCOID: 'regproc', + REGROLEOID: 'regrole', + REGTYPEOID: 'regtype', + RELTIMEOID: 'reltime', + SMGROID: 'smgr', + TEXTOID: 'text', + TIDOID: 'tid', + TIMEOID: 'time', + TIMESTAMPOID: 'timestamp', + TIMESTAMPTZOID: 'timestamptz', + TIMETZOID: 'timetz', + TINTERVALOID: 'tinterval', TRIGGEROID: 'trigger', - BPCHAROID: 'bpchar'} + TSM_HANDLEROID: 'tsm_handler', + TSQUERYOID: 'tsquery', + TSVECTOROID: 'tsvector', + TXID_SNAPSHOTOID: 'txid_snapshot', + UNKNOWNOID: 'unknown', + UUIDOID: 'uuid', + VARBITOID: 'varbit', + VARCHAROID: 'varchar', + VOIDOID: 'void', + XIDOID: 'xid', + XMLOID: 'xml', + _ACLITEMOID: 'aclitem[]', + _OIDOID: 'oid[]', + _TEXTOID: 'text[]'} diff --git a/tests/test_codecs.py b/tests/test_codecs.py index 408c1726..57fe40aa 100644 --- a/tests/test_codecs.py +++ b/tests/test_codecs.py @@ -916,3 +916,87 @@ async def test_table_as_composite(self): await self.con.execute(''' DROP TABLE tab; ''') + + async def test_relacl_array_type(self): + await self.con.execute(r''' + CREATE USER """u1'"; + CREATE USER "{u2"; + CREATE USER ",u3"; + CREATE USER "u4}"; + CREATE USER "u5"""; + CREATE USER "u6\"""; + CREATE USER "u7\"; + CREATE USER norm1; + CREATE USER norm2; + CREATE TABLE t0 (); GRANT SELECT ON t0 TO norm1; + CREATE TABLE t1 (); GRANT SELECT ON t1 TO """u1'"; + CREATE TABLE t2 (); GRANT SELECT ON t2 TO "{u2"; + CREATE TABLE t3 (); GRANT SELECT ON t3 TO ",u3"; + CREATE TABLE t4 (); GRANT SELECT ON t4 TO "u4}"; + CREATE TABLE t5 (); GRANT SELECT ON t5 TO "u5"""; + CREATE TABLE t6 (); GRANT SELECT ON t6 TO "u6\"""; + CREATE TABLE t7 (); GRANT SELECT ON t7 TO "u7\"; + + CREATE TABLE a1 (); + GRANT SELECT ON a1 TO """u1'"; + GRANT SELECT ON a1 TO "{u2"; + GRANT SELECT ON a1 TO ",u3"; + GRANT SELECT ON a1 TO "norm1"; + GRANT SELECT ON a1 TO "u4}"; + GRANT SELECT ON a1 TO "u5"""; + GRANT SELECT ON a1 TO "u6\"""; + GRANT SELECT ON a1 TO "u7\"; + GRANT SELECT ON a1 TO "norm2"; + + CREATE TABLE a2 (); + GRANT SELECT ON a2 TO """u1'" WITH GRANT OPTION; + GRANT SELECT ON a2 TO "{u2" WITH GRANT OPTION; + GRANT SELECT ON a2 TO ",u3" WITH GRANT OPTION; + GRANT SELECT ON a2 TO "norm1" WITH GRANT OPTION; + GRANT SELECT ON a2 TO "u4}" WITH GRANT OPTION; + GRANT SELECT ON a2 TO "u5""" WITH GRANT OPTION; + GRANT SELECT ON a2 TO "u6\""" WITH GRANT OPTION; + GRANT SELECT ON a2 TO "u7\" WITH GRANT OPTION; + + SET SESSION AUTHORIZATION """u1'"; GRANT SELECT ON a2 TO "norm2"; + SET SESSION AUTHORIZATION "{u2"; GRANT SELECT ON a2 TO "norm2"; + SET SESSION AUTHORIZATION ",u3"; GRANT SELECT ON a2 TO "norm2"; + SET SESSION AUTHORIZATION "u4}"; GRANT SELECT ON a2 TO "norm2"; + SET SESSION AUTHORIZATION "u5"""; GRANT SELECT ON a2 TO "norm2"; + SET SESSION AUTHORIZATION "u6\"""; GRANT SELECT ON a2 TO "norm2"; + SET SESSION AUTHORIZATION "u7\"; GRANT SELECT ON a2 TO "norm2"; + RESET SESSION AUTHORIZATION; + ''') + + try: + rows = await self.con.fetch(''' + SELECT relacl, relacl::text[] AS chk, relacl::text[]::text AS text_ + FROM pg_catalog.pg_class + WHERE relacl IS NOT NULL + ''') + + for row in rows: + self.assertEqual(row['relacl'], row['chk'],) + + finally: + await self.con.execute(r''' + DROP TABLE t0; + DROP TABLE t1; + DROP TABLE t2; + DROP TABLE t3; + DROP TABLE t4; + DROP TABLE t5; + DROP TABLE t6; + DROP TABLE t7; + DROP TABLE a1; + DROP TABLE a2; + DROP USER """u1'"; + DROP USER "{u2"; + DROP USER ",u3"; + DROP USER "u4}"; + DROP USER "u5"""; + DROP USER "u6\"""; + DROP USER "u7\"; + DROP USER norm1; + DROP USER norm2; + ''') diff --git a/tools/generate_type_map.py b/tools/generate_type_map.py index d308f491..81ad0245 100755 --- a/tools/generate_type_map.py +++ b/tools/generate_type_map.py @@ -16,7 +16,7 @@ # Array types with builtin codecs, necessary for codec # bootstrap to work # -_BUILTIN_ARRAYS = ('_text', '_oid') +_BUILTIN_ARRAYS = ('_text', '_oid', '_aclitem') _INVALIDOID = 0 @@ -70,8 +70,8 @@ async def runner(args): buf += '\n\nARRAY_TYPES = ({},)'.format(', '.join(array_types)) - f_typemap = ('{}: {!r}'.format(dn, n) for dn, n in typemap.items()) - buf += '\n\nTYPEMAP = {{\n {}}}\n'.format(',\n '.join(f_typemap)) + f_typemap = ('{}: {!r}'.format(dn, n) for dn, n in sorted(typemap.items())) + buf += '\n\nTYPEMAP = {{\n {}}}'.format(',\n '.join(f_typemap)) print(buf)