Skip to content

Make orm mongodb search act like orm (Allow Polish notation) #51

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

Open
wants to merge 5 commits into
base: gisce
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions misc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# -*- encoding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import re


def flatten(domain):
for tok in domain:
if isinstance(tok, list):
for sub in flatten(tok):
yield sub
else:
yield tok

def _make_mutable(domain):
out = []
for tok in domain:
if isinstance(tok, tuple):
out.append(list(tok))
elif isinstance(tok, list):
if len(tok) == 3 and not any(isinstance(x, list) for x in tok):
out.append(tok[:])
else:
out.append(_make_mutable(tok))
else:
out.append(tok) # '|', '&', '!' …
return out


pattern_type = type(re.compile(''))
145 changes: 133 additions & 12 deletions mongodb2.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################

from __future__ import unicode_literals
from six import string_types
import tools
from pymongo import MongoClient
from pymongo.errors import AutoReconnect
Expand All @@ -28,6 +29,7 @@
import netsvc
from osv.orm import except_orm
from time import sleep
from .misc import pattern_type


logger = netsvc.Logger()
Expand Down Expand Up @@ -55,18 +57,137 @@ class MDBConn(object):
'$not': re.compile(l3.replace('%', '.*'), re.I)}},
}

def translate_domain(self, domain):
"""Translate an OpenERP domain object to a corresponding
MongoDB domain
def translate_domain(self, domain, orm_obj=None):
"""
new_domain = {}
for field, operator, value in domain:
clause = self.OPERATOR_MAPPING[operator](field, value)
if field in new_domain.keys():
new_domain[field].update(clause[field])
else:
new_domain.update(clause)
return new_domain
Convert an **domain** written in Polish notation (prefix form)
into an equivalent **MongoDB filter**.

Parameters
----------
domain : list
A domain in Odoo format. It may contain:

* Leaf conditions written either as tuples
``('field', 'operator', value)`` **or** lists
``['field', 'operator', value]``.
* Logical operators applied in *prefix* order:

| → logical OR (exactly two children expected)
& → logical AND (exactly two children expected)
! → logical NOT (exactly one child expected)

* Arbitrary nesting using sub-lists, e.g.
``['|', ('a', '=', 1), ['&', ('b', '>', 5), ('c', '<', 10)]]``

orm_obj : orm_mongodb (optional)
If provided, extra type-specific conversions are applied
**before** the Mongo filter is built:

* *Date / datetime*: incoming strings are converted to `datetime`
objects; bare dates in ``<, <=, >, >=`` predicates are expanded
to *00:00:00* or *23:59:59* so that the semantic matches.
* *Boolean*: truthy / falsy values are normalized to `True`/`False`.
* *exact_match* fields: for columns declared with the custom
attribute ``exact_match=True`` any regular-expression value that
still starts/ends with ``.*`` is stripped (“ilike” always
wraps wildcards, but an exact match should not include them).

Returns
-------
dict
A MongoDB query document using ``$eq``, ``$gt``, ``$lte``, ``$or``,
``$and``, ``$nor`` … ready to be passed to
``collection.find(filter, …)``.

Raises
------
ValueError
If an unrecognized token is encountered (e.g., malformed domain).

Notes
-----
* **The input list is consumed / mutated** (tokens are popped).
Callers that still need the original domain should pass
``copy.deepcopy(domain)`` instead.
* Consecutive leaf conditions without an explicit logical operator are
combined with an **implicit AND**, mimicking behavior.
"""
# ---------- Helper: convert a single leaf ---------------------------
def _build_leaf(leaf):
field, op, val = leaf
# Optional type coercions that depend on the model definition
if orm_obj is not None:
# ---- Date / Datetime coercion --------------------------------
if field in orm_obj.get_date_fields():
if (orm_obj._columns[field]._type == 'datetime'
and isinstance(val, string_types) and len(val) == 10):
# Bare date on a datetime column → expand time component
if op in ('>', '>='):
val += ' 00:00:00'
elif op in ('<', '<='):
val += ' 23:59:59'
val = orm_obj.transform_date_field(field, val, 'write')
# ---- Boolean coercion ----------------------------------------
if field in orm_obj.get_bool_fields():
val = bool(val)

# ---- exact_match cleanup -------------------------------------
col = orm_obj._columns.get(field)
if col and getattr(col, 'exact_match', False):
import re
if isinstance(val, pattern_type):
val = val.pattern.lstrip('.*').rstrip('.*')

return self.OPERATOR_MAPPING[op](field, val)

# ---------- Helper: recursive‐descent parser ------------------------
def _parse(tokens):
"""
Consume *tokens* (list) from the left, return a MongoDB filter
for the first complete expression found.
"""
if not tokens:
return {}

tok = tokens.pop(0)

# 1. Logical operators in prefix form
if tok == '|':
return {'$or': [_parse(tokens), _parse(tokens)]}
if tok == '&':
return {'$and': [_parse(tokens), _parse(tokens)]}
if tok == '!':
return {'$nor': [_parse(tokens)]}

# 2. Leaf (tuple or list) ('field', 'op', value)
if (isinstance(tok, (list, tuple))
and len(tok) == 3
and isinstance(tok[1], string_types)):
return _build_leaf(tok)

# 3. Sub-list without an explicit operator ⇒ implicit AND
if isinstance(tok, list):
sub_tokens = list(tok) # work on a copy
sub_filters = []
while sub_tokens:
sub_filters.append(_parse(sub_tokens))
return sub_filters[0] if len(sub_filters) == 1 \
else {'$and': sub_filters}

raise ValueError('Domain Token not supported: %s' % tok)

# --------------------------------------------------------------------

tokens = list(domain)
filters = []
while tokens:
filters.append(_parse(tokens))

if not filters:
return {}
if len(filters) == 1:
return filters[0]
return {'$and': filters}

@property
def uri(self):
Expand Down
27 changes: 10 additions & 17 deletions orm_mongodb.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from __future__ import absolute_import
from __future__ import absolute_import, unicode_literals
from osv import orm, fields
from osv.orm import except_orm
import netsvc
Expand Down Expand Up @@ -507,24 +507,15 @@ def _compute_order(self, cr, user, order=None, context=None):

def search(self, cr, user, args, offset=0, limit=0, order=None,
context=None, count=False):
#Make a copy of args for working
#Domain has to be list of lists
tmp_args = [isinstance(arg, tuple) and list(arg)
or arg for arg in args]
collection = mdbpool.get_collection(self._table)
self.search_trans_fields(tmp_args)
import copy

new_args = mdbpool.translate_domain(tmp_args)
# Implement exact match for fields char which defaults to ilike
for k in new_args:
field = self._columns.get(k)
if not field:
pass
if getattr(field, 'exact_match', False):
if isinstance(new_args[k], re._pattern_type):
new_args[k] = new_args[k].pattern.lstrip('.*').rstrip('.*')
if not context:
if args is None:
args = []
if context is None:
context = {}

new_args = mdbpool.translate_domain(copy.deepcopy(args), orm_obj=self)

self.pool.get('ir.model.access').check(cr, user,
self._name, 'read', context=context)
#In very large collections when no args
Expand All @@ -534,6 +525,8 @@ def search(self, cr, user, args, offset=0, limit=0, order=None,
if not args:
order = 'id'

collection = mdbpool.get_collection(self._table)

if count:
return collection.find(
new_args,
Expand Down
Loading
Loading