-
Notifications
You must be signed in to change notification settings - Fork 0
fix: refuse bool and float item IDs everywhere instead of coercing them #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import numbers | ||
| import warnings | ||
| from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple | ||
|
|
||
|
|
@@ -17,26 +18,71 @@ | |
| SDK_HAS_GET_BY_IDS = hasattr(_SdkIndex, "get_by_ids") | ||
|
|
||
|
|
||
| # Item IDs travel as proto int64; a larger value cannot name a row and the | ||
| # SDK would fail to encode it. | ||
| _MAX_ITEM_ID = 2**63 - 1 | ||
|
|
||
|
|
||
| def _readable_item_id(value: Any) -> Optional[int]: | ||
| """The item ID ``value`` names exactly, or ``None`` when it names none. | ||
|
|
||
| The one ID check every method shares: only a positive integer within int64 | ||
| — any ``numbers.Integral``, so NumPy integers count; the server issues item | ||
| IDs as ``int64`` — or an ASCII decimal string of one counts. ``bool`` and | ||
| ``float`` are not item IDs — ``int(True)`` is 1 and ``int(3.9)`` is 3, so | ||
| coercing them would address a different document — and neither are | ||
| non-ASCII digits such as ``"٣"`` or ``"3"``, which ``str.isdecimal`` | ||
| accepts. | ||
| """ | ||
| if isinstance(value, bool): | ||
| return None | ||
| if isinstance(value, numbers.Integral): | ||
| return int(value) if 0 < value <= _MAX_ITEM_ID else None | ||
| if isinstance(value, str): | ||
| text = value.strip() | ||
| if text.isascii() and text.isdigit(): | ||
| item_id = int(text) | ||
| return item_id if 0 < item_id <= _MAX_ITEM_ID else None | ||
| return None | ||
|
|
||
|
|
||
| def _is_non_positive_integer(value: Any) -> bool: | ||
| """True for an integer (or its ASCII decimal string, sign allowed) <= 0.""" | ||
| if isinstance(value, bool): | ||
| return False | ||
| if isinstance(value, numbers.Integral): | ||
| return value <= 0 | ||
| if isinstance(value, str): | ||
| text = value.strip() | ||
| digits = text[1:] if text[:1] in "+-" else text | ||
| return bool(digits) and digits.isascii() and digits.isdigit() and int(text) <= 0 | ||
| return False | ||
|
Comment on lines
+49
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 함수는 두 오류 메시지 가운데 하나를 고르려고 값을 한 번 더 파싱하는데, 부호 처리가
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #23 |
||
|
|
||
|
|
||
| def _mutation_items( | ||
| item_ids: List[Any], label: str, *, dedupe: bool = False | ||
| ) -> List[int]: | ||
| """Coerce caller-supplied IDs to the ``int`` item_ids the SDK addresses. | ||
| """Turn caller-supplied IDs into the ``int`` item_ids the SDK addresses. | ||
|
|
||
| The SDK rejects non-positive and repeated ids with its own message; both | ||
| are caught here and named after the calling method. ``dedupe=True`` | ||
| (delete) drops repeats instead, since deleting a row twice is deleting it. | ||
| Used by the methods that change rows (delete, update, upsert), so a value | ||
| that names no item raises instead of being coerced onto another item — see | ||
| `_readable_item_id`. Non-positive and repeated ids are named after the | ||
| calling method. ``dedupe=True`` (delete) drops repeats instead, since | ||
| deleting a row twice is deleting it. | ||
| """ | ||
| try: | ||
| ints = [int(x) for x in item_ids] | ||
| except (TypeError, ValueError) as e: | ||
| raise ValueError( | ||
| f"Envector.{label} expects integer item IDs (or numeric strings) " | ||
| "as returned by add_texts/add_documents." | ||
| ) from e | ||
| if any(i <= 0 for i in ints): | ||
| raise ValueError( | ||
| f"Envector.{label}: item IDs are positive integers (got {min(ints)})." | ||
| ) | ||
| ints: List[int] = [] | ||
| for x in item_ids: | ||
| item_id = _readable_item_id(x) | ||
| if item_id is None: | ||
| if _is_non_positive_integer(x): | ||
| raise ValueError( | ||
| f"Envector.{label}: item IDs are positive integers (got {x!r})." | ||
| ) | ||
| raise ValueError( | ||
| f"Envector.{label} expects integer item IDs (a positive int or its " | ||
| f"decimal string) as returned by add_texts/add_documents; got {x!r}." | ||
| ) | ||
| ints.append(item_id) | ||
| if dedupe: | ||
| return list(dict.fromkeys(ints)) | ||
| if len(set(ints)) != len(ints): | ||
|
|
@@ -48,56 +94,23 @@ def _split_caller_ids(ids: List[Any]) -> Tuple[List[Optional[int]], List[Any]]: | |
| """Sort caller-supplied IDs into enVector item IDs and everything else. | ||
|
|
||
| Returns ``(item_ids, foreign)``: ``item_ids`` is positional against ``ids`` | ||
| with ``None`` wherever the entry was ``None`` or not an integer, and | ||
| ``foreign`` lists the non-integer values so the caller can be told they | ||
| were not honoured. | ||
| with ``None`` wherever the entry was ``None`` or names no item (see | ||
| `_readable_item_id`), and ``foreign`` lists those other values so the | ||
| caller can be told they were not honoured. | ||
| """ | ||
| item_ids: List[Optional[int]] = [] | ||
| foreign: List[Any] = [] | ||
| for x in ids: | ||
| if x is None: | ||
| item_ids.append(None) | ||
| continue | ||
| try: | ||
| value = int(x) | ||
| except (TypeError, ValueError): | ||
| value = 0 | ||
| if value <= 0: # the server issues positive ints only | ||
| item_ids.append(None) | ||
| value = _readable_item_id(x) | ||
| item_ids.append(value) | ||
| if value is None: | ||
| foreign.append(x) | ||
| else: | ||
| item_ids.append(value) | ||
| return item_ids, foreign | ||
|
|
||
|
|
||
| # Item IDs travel as proto int64; a larger value cannot name a row and the | ||
| # SDK would fail to encode it. | ||
| _MAX_ITEM_ID = 2**63 - 1 | ||
|
|
||
|
|
||
| def _readable_item_id(value: Any) -> Optional[int]: | ||
| """The item ID ``value`` names exactly, or ``None`` when it names none. | ||
|
|
||
| For `get_by_ids`, which must never read an item the caller did not name: | ||
| only a positive ``int`` within int64 (the server issues item IDs as | ||
| ``int64``), or an ASCII decimal string of one, counts. ``bool`` and | ||
| ``float`` are not item IDs — ``int(True)`` is 1 and ``int(3.9)`` is 3, so | ||
| coercing them would return a different document — and neither are | ||
| non-ASCII digits such as ``"٣"`` or ``"3"``, which ``str.isdecimal`` | ||
| accepts. | ||
| """ | ||
| if isinstance(value, bool): | ||
| return None | ||
| if isinstance(value, int): | ||
| return value if 0 < value <= _MAX_ITEM_ID else None | ||
| if isinstance(value, str): | ||
| text = value.strip() | ||
| if text.isascii() and text.isdigit(): | ||
| item_id = int(text) | ||
| return item_id if 0 < item_id <= _MAX_ITEM_ID else None | ||
| return None | ||
|
|
||
|
|
||
| def _one_embedding_arg(embedding: Any, embeddings: Any) -> Any: | ||
| """Resolve the standard positional ``embedding`` and our older | ||
| ``embeddings=`` keyword into one value, rejecting conflicting pairs.""" | ||
|
|
@@ -279,8 +292,9 @@ def add_texts( | |
| not affected. | ||
|
|
||
| ``ids`` follows LangChain's add-or-update contract as far as enVector | ||
| allows: an entry that is an item ID (int or numeric str, such as the | ||
| ``Document.id`` search results carry) updates that item in place; an ID | ||
| allows: an entry that is an item ID (a positive int or its decimal str, | ||
| such as the ``Document.id`` search results carry) updates that item in | ||
| place; an ID | ||
|
Comment on lines
+295
to
+297
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
"새 행 + 경고" 규칙은 UUID 같은 외부 ID를 위해 만든 것이니, 정수처럼 읽히지만 거부된 값(bool, float, 비ASCII 숫자)은
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #23 |
||
| with no live row, or a non-integer ID, cannot be created, so that row is | ||
| inserted with a server-issued ID and a ``UserWarning``. ``None`` entries | ||
| insert. The returned list holds the IDs actually in the index, as | ||
|
|
@@ -431,8 +445,9 @@ def delete( | |
| """Delete items from the encrypted index by item ID. | ||
|
|
||
| Accepts the ``item_id`` values returned from ``add_texts`` / | ||
| ``add_documents``. Both ``int`` and ``str`` (numeric) IDs are accepted | ||
| and coerced to ``int`` before being passed to the SDK. | ||
| ``add_documents``, as ``str`` or ``int``. Any other value — ``bool`` and | ||
| ``float`` included — raises ``ValueError`` rather than being coerced onto | ||
| another item (``3.9`` would otherwise delete item 3). | ||
|
|
||
| Deletion is asynchronous server-side; by default this waits until the | ||
| affected shards are rebuilt and the remaining data is searchable again | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR 본문의 표에 나온 값보다 더 많은 값이 거부됩니다. 아래 값은 35bfa33까지
int(v)로 item 3(또는 3000)을 가리켰지만, 이제delete·update_*·upsert_documents에서는ValueError가 나고add_texts에서는 새 행으로 들어갑니다."+3","3_000", 전각"3"b"3",Decimal(3),Fraction(3)__index__만 정의한 객체(np.array(3), 정수 torch tensor 등)의도한 범위라면 표와 release note에 breaking change로 적어 주십시오.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#23
9fc0667에 반영했습니다. 거부 범위는 그대로 두고, 본문의 "무엇이 달라지나" 에 Breaking change 항목으로 적었습니다. 표에도"+3"·"3_000"·전각·b"3"·Decimal·Fraction·np.array(3)행을 넣었고, README Limitations 에 받는 형식(add_texts가 돌려준 문자열, 또는 Python·NumPy 정수)을 적었습니다. 이 저장소에 release note 파일이 없어 PR 본문과 README 로 갈음합니다.