Skip to content

Commit 98006d6

Browse files
committed
Allow duplicate field names in records (fixes #28)
1 parent c70a8f3 commit 98006d6

File tree

8 files changed

+133
-21
lines changed

8 files changed

+133
-21
lines changed

asyncpg/protocol/codecs/base.pyx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,14 @@ cdef class Codec:
3535
self.py_decoder = py_decoder
3636
self.element_codec = element_codec
3737
self.element_type_oids = element_type_oids
38-
self.element_names = element_names
3938
self.element_codecs = element_codecs
4039

40+
if element_names is not None:
41+
self.element_names = record.ApgRecordDesc_New(
42+
element_names, tuple(element_names))
43+
else:
44+
self.element_names = None
45+
4146
if type == CODEC_C:
4247
self.encoder = <codec_encode_func>&self.encode_scalar
4348
self.decoder = <codec_decode_func>&self.decode_scalar

asyncpg/protocol/prepared_stmt.pxd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ cdef class PreparedStatementState:
2525
tuple args_codecs
2626

2727
int16_t cols_num
28-
object cols_mapping
28+
object cols_desc
2929
bint have_text_cols
3030
tuple rows_codecs
3131

asyncpg/protocol/prepared_stmt.pyx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ cdef class PreparedStatementState:
1616
self.row_desc = self.parameters_desc = None
1717
self.args_codecs = self.rows_codecs = None
1818
self.args_num = self.cols_num = 0
19-
self.cols_mapping = None
19+
self.cols_desc = None
2020
self.closed = False
2121
self.refs = 0
2222
self.buffer = FastReadBuffer.new()
@@ -134,20 +134,24 @@ cdef class PreparedStatementState:
134134

135135
cdef _ensure_rows_decoder(self):
136136
cdef:
137+
list cols_names
137138
object cols_mapping
138139
tuple row
139140
int oid
140141
Codec codec
141142
list codecs
142143

143-
if self.cols_num == 0 or self.cols_mapping is not None:
144+
if self.cols_num == 0 or self.cols_desc is not None:
144145
return
145146

146147
cols_mapping = collections.OrderedDict()
148+
cols_names = []
147149
codecs = []
148150
for i from 0 <= i < self.cols_num:
149151
row = self.row_desc[i]
150-
cols_mapping[row[0].decode(self.settings._encoding)] = i
152+
col_name = row[0].decode(self.settings._encoding)
153+
cols_mapping[col_name] = i
154+
cols_names.append(col_name)
151155
oid = row[3]
152156
codec = self.settings.get_data_codec(<uint32_t>oid)
153157
if codec is None or not codec.has_decoder():
@@ -157,7 +161,9 @@ cdef class PreparedStatementState:
157161

158162
codecs.append(codec)
159163

160-
self.cols_mapping = cols_mapping
164+
self.cols_desc = record.ApgRecordDesc_New(
165+
cols_mapping, tuple(cols_names))
166+
161167
self.rows_codecs = tuple(codecs)
162168

163169
cdef _ensure_args_encoder(self):
@@ -215,7 +221,7 @@ cdef class PreparedStatementState:
215221
if rows_codecs is None or len(rows_codecs) < fnum:
216222
raise RuntimeError('invalid rows_codecs')
217223

218-
dec_row = record.ApgRecord_New(self.cols_mapping, fnum)
224+
dec_row = record.ApgRecord_New(self.cols_desc, fnum)
219225
for i in range(fnum):
220226
flen = hton.unpack_int32(rbuf.read(4))
221227

asyncpg/protocol/protocol.pyx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,8 @@ def _create_record(object mapping, tuple elems):
481481
object rec
482482
int32_t i
483483

484-
rec = record.ApgRecord_New(mapping, len(elems))
484+
desc = record.ApgRecordDesc_New(mapping, tuple(mapping) if mapping else ())
485+
rec = record.ApgRecord_New(desc, len(elems))
485486
for i in range(len(elems)):
486487
elem = elems[i]
487488
cpython.Py_INCREF(elem)

asyncpg/protocol/record/__init__.pxd

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ cdef extern from "record/recordobj.h":
1212
int ApgRecord_CheckExact(object)
1313
object ApgRecord_New(object, int)
1414
void ApgRecord_SET_ITEM(object, int, object)
15+
16+
object ApgRecordDesc_New(object, object)

asyncpg/protocol/record/recordobj.c

Lines changed: 97 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ static int numfree[ApgRecord_MAXSAVESIZE];
1717

1818

1919
PyObject *
20-
ApgRecord_New(PyObject *mapping, Py_ssize_t size)
20+
ApgRecord_New(PyObject *desc, Py_ssize_t size)
2121
{
2222
ApgRecordObject *o;
2323
Py_ssize_t i;
2424

25-
if (size < 1 || mapping == NULL) {
25+
if (size < 1 || desc == NULL || !ApgRecordDesc_CheckExact(desc)) {
2626
PyErr_BadInternalCall();
2727
return NULL;
2828
}
@@ -48,8 +48,8 @@ ApgRecord_New(PyObject *mapping, Py_ssize_t size)
4848
o->ob_item[i] = NULL;
4949
}
5050

51-
Py_INCREF(mapping);
52-
o->mapping = mapping;
51+
Py_INCREF(desc);
52+
o->desc = (ApgRecordDescObject*)desc;
5353
o->self_hash = -1;
5454
PyObject_GC_Track(o);
5555
return (PyObject *) o;
@@ -66,7 +66,7 @@ record_dealloc(ApgRecordObject *o)
6666

6767
o->self_hash = -1;
6868

69-
Py_CLEAR(o->mapping);
69+
Py_CLEAR(o->desc);
7070

7171
Py_TRASHCAN_SAFE_BEGIN(o)
7272
if (len > 0) {
@@ -96,7 +96,7 @@ record_traverse(ApgRecordObject *o, visitproc visit, void *arg)
9696
{
9797
Py_ssize_t i;
9898

99-
Py_VISIT(o->mapping);
99+
Py_VISIT(o->desc);
100100

101101
for (i = Py_SIZE(o); --i >= 0;) {
102102
if (o->ob_item[i] != NULL) {
@@ -300,7 +300,7 @@ record_subscript(ApgRecordObject* o, PyObject* item)
300300
}
301301
else {
302302
PyObject *mapped;
303-
mapped = PyObject_GetItem(o->mapping, item);
303+
mapped = PyObject_GetItem(o->desc->mapping, item);
304304
if (mapped != NULL) {
305305
Py_ssize_t i;
306306
PyObject *result;
@@ -348,7 +348,7 @@ record_repr(ApgRecordObject *v)
348348
n = Py_SIZE(v);
349349
assert(n > 0);
350350

351-
keys_iter = PyObject_GetIter(v->mapping);
351+
keys_iter = PyObject_GetIter(v->desc->keys);
352352
if (keys_iter == NULL) {
353353
return NULL;
354354
}
@@ -453,7 +453,7 @@ record_keys(PyObject *o, PyObject *args)
453453
return NULL;
454454
}
455455

456-
return PyObject_GetIter(((ApgRecordObject*)o)->mapping);
456+
return PyObject_GetIter(((ApgRecordObject*)o)->desc->mapping);
457457
}
458458

459459

@@ -477,7 +477,7 @@ record_contains(ApgRecordObject *o, PyObject *arg)
477477
return -1;
478478
}
479479

480-
return PySequence_Contains(o->mapping, arg);
480+
return PySequence_Contains(o->desc->mapping, arg);
481481
}
482482

483483

@@ -828,7 +828,7 @@ record_new_items_iter(PyObject *seq)
828828
return NULL;
829829
}
830830

831-
map_iter = PyObject_GetIter(((ApgRecordObject*)seq)->mapping);
831+
map_iter = PyObject_GetIter(((ApgRecordObject*)seq)->desc->mapping);
832832
if (map_iter == NULL) {
833833
return NULL;
834834
}
@@ -853,6 +853,10 @@ int ApgRecord_InitTypes(void)
853853
return -1;
854854
}
855855

856+
if (PyType_Ready(&ApgRecordDesc_Type) < 0) {
857+
return -1;
858+
}
859+
856860
if (PyType_Ready(&ApgRecordIter_Type) < 0) {
857861
return -1;
858862
}
@@ -863,3 +867,85 @@ int ApgRecord_InitTypes(void)
863867

864868
return 0;
865869
}
870+
871+
872+
/* ----------------- */
873+
874+
875+
static void
876+
record_desc_dealloc(ApgRecordDescObject *o)
877+
{
878+
PyObject_GC_UnTrack(o);
879+
Py_CLEAR(o->mapping);
880+
Py_CLEAR(o->keys);
881+
PyObject_GC_Del(o);
882+
}
883+
884+
885+
static int
886+
record_desc_traverse(ApgRecordDescObject *o, visitproc visit, void *arg)
887+
{
888+
Py_VISIT(o->mapping);
889+
Py_VISIT(o->keys);
890+
return 0;
891+
}
892+
893+
894+
PyTypeObject ApgRecordDesc_Type = {
895+
PyVarObject_HEAD_INIT(NULL, 0)
896+
"RecordDescriptor", /* tp_name */
897+
sizeof(ApgRecordDescObject), /* tp_basicsize */
898+
0, /* tp_itemsize */
899+
/* methods */
900+
(destructor)record_desc_dealloc, /* tp_dealloc */
901+
0, /* tp_print */
902+
0, /* tp_getattr */
903+
0, /* tp_setattr */
904+
0, /* tp_reserved */
905+
0, /* tp_repr */
906+
0, /* tp_as_number */
907+
0, /* tp_as_sequence */
908+
0, /* tp_as_mapping */
909+
0, /* tp_hash */
910+
0, /* tp_call */
911+
0, /* tp_str */
912+
PyObject_GenericGetAttr, /* tp_getattro */
913+
0, /* tp_setattro */
914+
0, /* tp_as_buffer */
915+
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */
916+
0, /* tp_doc */
917+
(traverseproc)record_desc_traverse, /* tp_traverse */
918+
0, /* tp_clear */
919+
0, /* tp_richcompare */
920+
0, /* tp_weaklistoffset */
921+
PyObject_SelfIter, /* tp_iter */
922+
0, /* tp_iternext */
923+
0, /* tp_methods */
924+
0,
925+
};
926+
927+
928+
PyObject *
929+
ApgRecordDesc_New(PyObject *mapping, PyObject *keys)
930+
{
931+
ApgRecordDescObject *o;
932+
933+
if (!mapping || !keys || !PyTuple_CheckExact(keys)) {
934+
PyErr_BadInternalCall();
935+
return NULL;
936+
}
937+
938+
o = PyObject_GC_New(ApgRecordDescObject, &ApgRecordDesc_Type);
939+
if (o == NULL) {
940+
return NULL;
941+
}
942+
943+
Py_INCREF(mapping);
944+
o->mapping = mapping;
945+
946+
Py_INCREF(keys);
947+
o->keys = keys;
948+
949+
PyObject_GC_Track(o);
950+
return (PyObject *) o;
951+
}

asyncpg/protocol/record/recordobj.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,17 @@
1111
#define ApgRecord_MAXFREELIST 2000
1212

1313

14+
typedef struct {
15+
PyObject_HEAD
16+
PyObject *mapping;
17+
PyObject *keys;
18+
} ApgRecordDescObject;
19+
20+
1421
typedef struct {
1522
PyObject_VAR_HEAD
1623
Py_hash_t self_hash;
17-
PyObject *mapping;
24+
ApgRecordDescObject *desc;
1825
PyObject *ob_item[1];
1926

2027
/* ob_item contains space for 'ob_size' elements.
@@ -23,17 +30,23 @@ typedef struct {
2330
*/
2431
} ApgRecordObject;
2532

33+
2634
extern PyTypeObject ApgRecord_Type;
2735
extern PyTypeObject ApgRecordIter_Type;
2836
extern PyTypeObject ApgRecordItems_Type;
2937

38+
extern PyTypeObject ApgRecordDesc_Type;
39+
3040
#define ApgRecord_CheckExact(o) (Py_TYPE(o) == &ApgRecord_Type)
41+
#define ApgRecordDesc_CheckExact(o) (Py_TYPE(o) == &ApgRecordDesc_Type)
42+
3143
#define ApgRecord_SET_ITEM(op, i, v) \
3244
(((ApgRecordObject *)(op))->ob_item[i] = v)
3345
#define ApgRecord_GET_ITEM(op, i) \
3446
(((ApgRecordObject *)(op))->ob_item[i])
3547

3648
int ApgRecord_InitTypes(void);
3749
PyObject * ApgRecord_New(PyObject *, Py_ssize_t);
50+
PyObject * ApgRecordDesc_New(PyObject *, PyObject *);
3851

3952
#endif

tests/test_record.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,6 @@ def test_record_not_pickleable(self):
281281
with self.assertRaises(Exception):
282282
pickle.dumps(r)
283283

284-
@unittest.expectedFailure
285284
async def test_record_duplicate_colnames(self):
286285
"""Test that Record handles duplicate column names."""
287286
r = await self.con.fetchrow('SELECT 1 as a, 2 as a')

0 commit comments

Comments
 (0)