1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
"""
Psycopg 3 adapters and row factories for PostGit tables (see schema.sql).
- ``pygit2.Oid`` binds to ``bytea`` (``Oid.raw``) and, via row factories, is
reconstructed from ``oid`` / ``target_oid`` columns (not from arbitrary
``bytea``, e.g. ``object_data``).
- ``git_object_type`` maps to ``GitObjectType`` (``IntEnum`` aligned with
libgit2 / ``pygit2.GIT_OBJECT_*``).
"""
from __future__ import annotations
from collections.abc import Sequence
from enum import IntEnum
from typing import Any
import pygit2
from psycopg import _oids
from psycopg._cursor_base import BaseCursor
from psycopg.abc import AdaptContext
from psycopg.adapt import Buffer, Dumper
from psycopg.connection import Connection
from psycopg.connection_async import AsyncConnection
from psycopg.pq import Format
from psycopg.rows import RowMaker, dict_row, tuple_row
from psycopg.types.enum import EnumInfo, register_enum
from psycopg.types.string import BytesDumper
# libgit2 GIT_REFERENCE_DIRECT / GIT_REFERENCE_SYMBOLIC
_GIT_REFERENCE_OID = 1
_GIT_REFERENCE_SYMBOLIC = 2
_OID_COLUMN_NAMES = frozenset({"oid", "target_oid"})
class GitObjectType(IntEnum):
"""Matches ``git_object_type`` in PostgreSQL and ``pygit2.GIT_OBJECT_*``."""
COMMIT = 1
TREE = 2
BLOB = 3
TAG = 4
_GIT_OBJECT_TYPE_PG_MAP = {
GitObjectType.COMMIT: "commit",
GitObjectType.TREE: "tree",
GitObjectType.BLOB: "blob",
GitObjectType.TAG: "tag",
}
class OidDumper(Dumper):
"""Encode ``pygit2.Oid`` as PostgreSQL ``bytea`` (text parameter format)."""
oid = _oids.BYTEA_OID
def __init__(self, cls: type, context: AdaptContext | None = None):
super().__init__(cls, context)
self._bytes = BytesDumper(bytes, context)
def dump(self, obj: pygit2.Oid) -> Buffer | None:
return self._bytes.dump(obj.raw)
class OidBinaryDumper(Dumper):
"""Encode ``pygit2.Oid`` as PostgreSQL ``bytea`` (binary parameter format)."""
format = Format.BINARY
oid = _oids.BYTEA_OID
def dump(self, obj: pygit2.Oid) -> Buffer | None:
return obj.raw
def register_adapters(
conn: Connection[Any],
*,
git_object_type_regtype: str = "git_object_type",
) -> None:
"""
Register dumpers/loaders on ``conn`` for ``pygit2.Oid`` and ``git_object_type``.
Call once per sync connection after the schema exists (enum type present).
:param git_object_type_regtype: argument to :meth:`EnumInfo.fetch` (e.g.
:func:`regtype_git_object_type` if the type is not on ``search_path``).
"""
info = EnumInfo.fetch(conn, git_object_type_regtype)
if info is None:
raise LookupError(
"PostgreSQL type git_object_type not found; apply schema.sql first "
"(use the regtype your search_path resolves, e.g. public.git_object_type)."
)
register_enum(
info,
conn,
enum=GitObjectType,
mapping=_GIT_OBJECT_TYPE_PG_MAP,
)
conn.adapters.register_dumper(pygit2.Oid, OidDumper)
conn.adapters.register_dumper(pygit2.Oid, OidBinaryDumper)
async def register_adapters_async(
conn: AsyncConnection[Any],
*,
git_object_type_regtype: str = "git_object_type",
) -> None:
"""Async variant of :func:`register_adapters`."""
info = await EnumInfo.fetch(conn, git_object_type_regtype)
if info is None:
raise LookupError("PostgreSQL type git_object_type not found; apply schema.sql first.")
register_enum(
info,
conn,
enum=GitObjectType,
mapping=_GIT_OBJECT_TYPE_PG_MAP,
)
conn.adapters.register_dumper(pygit2.Oid, OidDumper)
conn.adapters.register_dumper(pygit2.Oid, OidBinaryDumper)
def oid_from_db(value: bytes | memoryview) -> pygit2.Oid:
"""Build ``Oid`` from a ``bytea`` value (e.g. manual handling without a row factory)."""
return pygit2.Oid(raw=bytes(value))
def postgit_tuple_row(cursor: BaseCursor[Any, Any]) -> RowMaker[tuple[Any, ...]]:
"""
Like :func:`psycopg.rows.tuple_row`, but turns ``oid`` and ``target_oid``
columns into ``pygit2.Oid``.
"""
if not cursor.description:
return tuple_row(cursor)
names = [d.name for d in cursor.description]
idxs = [i for i, n in enumerate(names) if n in _OID_COLUMN_NAMES]
if not idxs:
return tuple_row(cursor)
def rowmaker(values: Sequence[Any]) -> tuple[Any, ...]:
out = list(values)
for i in idxs:
v = out[i]
if isinstance(v, (bytes, memoryview)):
out[i] = oid_from_db(v)
return tuple(out)
return rowmaker
def postgit_dict_row(cursor: BaseCursor[Any, Any]) -> RowMaker[dict[str, Any]]:
"""
Like :func:`psycopg.rows.dict_row`, but turns ``oid`` and ``target_oid``
values into ``pygit2.Oid``.
"""
inner = dict_row(cursor)
if cursor.description is None:
return inner
names = [d.name for d in cursor.description]
idxs = [i for i, n in enumerate(names) if n in _OID_COLUMN_NAMES]
if not idxs:
return inner
def rowmaker(values: Sequence[Any]) -> dict[str, Any]:
d = dict(zip(names, values, strict=True))
for k in _OID_COLUMN_NAMES & d.keys():
v = d[k]
if isinstance(v, (bytes, memoryview)):
d[k] = oid_from_db(v)
return d
return rowmaker
def ref_storage_columns(
ref: pygit2.Reference,
) -> tuple[pygit2.Oid | None, str | None]:
"""
Values for ``(target_oid, symbolic_target)`` on the ``refs`` table.
Matches :func:`register_adapters` expectations: direct refs supply ``Oid``;
symbolic refs supply the target ref name as ``str``.
"""
if ref.type == _GIT_REFERENCE_SYMBOLIC:
t = ref.target
if not isinstance(t, str):
t = str(t)
return None, t
if ref.type == _GIT_REFERENCE_OID:
tgt = ref.target
if not isinstance(tgt, pygit2.Oid):
tgt = pygit2.Oid(hex=str(tgt))
return tgt, None
raise TypeError(f"unsupported reference type: {ref.type!r}")
def object_type_from_pygit2(obj: pygit2.Object) -> GitObjectType:
"""Map ``Object.type`` (libgit2 kind int) to :class:`GitObjectType`."""
return GitObjectType(obj.type)
def object_data_from_pygit2(obj: pygit2.Object) -> bytes:
"""Object payload for ``objects.object_data`` (same as ``Object.read_raw()``)."""
return obj.read_raw()
def objects_row_insert(
obj: pygit2.Object,
) -> tuple[pygit2.Oid, GitObjectType, bytes]:
"""``(oid, object_type, object_data)`` for inserting into ``objects``."""
return obj.id, object_type_from_pygit2(obj), object_data_from_pygit2(obj)
def regtype_git_object_type(schema: str = "public") -> str:
"""Qualified type name for :func:`EnumInfo.fetch` if ``search_path`` is not set."""
return f"{schema}.git_object_type"