Lookups that ignore case
Logging in by email whatever its case is a lookup by eq_ignore_case alone, and on its own it reads every node of the
label. Mark the field CaseInsensitive() and give it an index that ignores case:
class Account(NodeFragment, labels=["Account"]): id: Annotated[str, NodeId()] email: Annotated[str, CaseInsensitive(), Unique(ignore_case=True)]
await gom.load_all(Account, where=refs(Account).email.eq_ignore_case("Ada@Example.com"))-
The copy. Every
save,create,update,save_all,patchandpatch_all, and the save of a view’s root and related nodes, writes a lowercased copy of the property beside it, under__drivine.lower.<property>(the name it is stored under, afterGraphProperty). The database lowercases it, as it lowercases the value a lookup compares with, so drivine-python, drivine4j and TypeScript’s Drivine store and match the same thing. A property with no value has no copy. A load doesn’t read it, and a flatPropertyBagnever holds it. The copy is no part of whether a save changed the node, so a save that only fills one in keeps the node’s stamp. Cypher you send yourself doesn’t: see below. -
The index.
RangeIndex(ignore_case=True),Unique(ignore_case=True),range_index(label, property, ignore_case=True)anduniqueness_constraint(label, property, ignore_case=True)are made over the copy, under their own names,<Label>_<property>_ci_rangeand_ci_unique. One that ignores case is refused on a field that isn’tCaseInsensitive, since no save would keep its copy. A uniqueness constraint that ignores case refusesA@x.combesidea@x.com. FalkorDB can’t make one:GRAPH.CONSTRAINTtakes no property name with a dot in it (FalkorDB#551), so it is refused there; its range index works. -
Filling it in. Ensuring an item that ignores case first fills the copy in on every node of the label that lacks it, 10,000 nodes a statement, and records that every node has it: on the store’s
DrivineStorenode on the graph engines, in the graph’s catalog on Oracle and Postgres. An item made anew, or recreated, fills the copies in again. -
The lookup. Once the copy is recorded,
eq_ignore_caseandcontains_ignore_caseon the field compare with the copy: in a load, a count, adelete_all, a grouped read and a patch’s condition, on the root’s own fields. Until then, and for an empty value or a related node’s field, they lowercase the property, as for any field: the same answer, without the index. A manager knows at once what it recorded, and reads what another recorded once a minute. -
A long text. On Oracle the copy is written whole, as a
CLOB, however long the text, but an index over it holds no copy past 4,000 bytes. So a lookup by a value that may lowercase past that, one of more than 1,333 characters (counted in UTF-16 units, as drivine4j counts them, so a character outside the BMP counts twice), compares the copy as aCLOBon every node of the label instead of reading the index. Neo4j refuses to save a text too large for its range index over the copy. -
Cypher of your own keeps no copy, on any store. Drivine runs a statement you send as written on the graph engines, and on Postgres, Oracle and SQLite compiles it to SQL and adds nothing to it. So a
SET,CREATE,MERGEorREMOVEof the property leaves the copy as it was, or leaves the node without one, and so does SQL of your own. Once the copy is recorded, a lookup misses a node so written, or finds it by its old value. A node saved by a process whose fragment isn’t yetCaseInsensitive, as while an upgrade rolls out, is missed the same way. Set the copy in the same statement, in aSETof its own after the one that writes the property, since an engine may read every value of oneSETbefore it writes any. The sameSETafter aREMOVEof the property removes the copy.MATCH (a:Account {id: $id})SET a.email = $emailSET a.`__drivine.lower.email` = toLower(a.email)lower_copies.set_clause("a", "email")gives that last item. Or, after the writes,recreatethe index that ignores case, which fills the copy in again on every node of the label:await gom.indexes.recreate(range_index("Account", "email", ignore_case=True))A constraint that ignores case has no
recreate:dropit andensureit again, which fills the copies in again as it makes the constraint anew.

