Skip to content
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

Align mongodb4.4 #28

Draft
wants to merge 35 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
71af58a
positional mismatch on 4.4
davidlatwe Apr 27, 2021
991072a
positional non located match on 4.4
davidlatwe Apr 27, 2021
c947bba
fix deprecation warnings
davidlatwe Apr 28, 2021
084b0f9
set 4.4 as compat version
davidlatwe Apr 28, 2021
7ba6a9c
check positional key position
davidlatwe Apr 28, 2021
39c3ba1
update compat attributes
davidlatwe Apr 28, 2021
03f5061
check field not end with "."
davidlatwe Apr 28, 2021
fe7fe31
fix typo from c947bbaa
davidlatwe Apr 28, 2021
bd89b6f
Merge branch 'master' into align-mongodb4.4
davidlatwe May 29, 2021
5a7b9a6
testing mongodb 4.4
davidlatwe May 29, 2021
8e86856
Merge branch 'master' into align-mongodb4.4
davidlatwe Jun 5, 2021
7b50a9e
Merge branch 'master' into align-mongodb4.4
davidlatwe Jun 20, 2021
894e92a
flake8: bump max-complexity
davidlatwe Jun 20, 2021
725b30f
Merge branch 'master' into align-mongodb4.4
davidlatwe Jun 20, 2021
18fa399
project: fix regression
davidlatwe Jun 20, 2021
151c90e
tests: update projection test specific for 4.4
davidlatwe Jun 20, 2021
853286b
Merge branch 'master' into align-mongodb4.4
davidlatwe Jun 26, 2021
6b445e2
Merge branch 'master' into align-mongodb4.4
davidlatwe Jul 2, 2021
ce6a36c
add projection path collision check
davidlatwe Jul 7, 2021
2a99bdd
add $mod remainder check
davidlatwe Jul 7, 2021
318389b
Merge branch 'master' into align-mongodb4.4
davidlatwe Jul 7, 2021
c7a3261
flake8: bump max-complexity, again
davidlatwe Jul 7, 2021
7cdc500
flake8: fix indent (E126)
davidlatwe Jul 7, 2021
3fd4fd9
Merge branch 'master' into align-mongodb4.4
davidlatwe Jan 21, 2023
c3a2faa
Cleanup after master merged
davidlatwe Jan 21, 2023
55224a2
Add mongodb 5.0, 6.0
davidlatwe Jan 22, 2023
0735ebf
Add mongoengine to test; Drop Python 3.6
davidlatwe Jan 22, 2023
fcaa688
Update poetry.lock
davidlatwe Jan 22, 2023
0c3862d
Update poetry.lock
davidlatwe Jan 22, 2023
fe4d6b4
Merge branch 'master' into align-mongodb4.4
davidlatwe Feb 3, 2023
0ba3d5d
Fix query mod regression
davidlatwe Feb 3, 2023
aef4af7
Exclude poetry-version from matrix
davidlatwe Feb 3, 2023
e83e863
Add test cases
davidlatwe Feb 3, 2023
016b62a
Cleanup
davidlatwe Feb 3, 2023
71f8283
Fix warnings in test
davidlatwe Feb 5, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 5 additions & 6 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,16 @@ jobs:
matrix:
experimental: [ false ]
monty-storage: [ memory, flatfile, sqlite ]
mongodb-version: [ "3.6", "4.0", "4.2" ]
mongodb-version: [ "3.6", "4.0", "4.2", "4.4", "5.0", "6.0" ]
python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ]
poetry-version: [ "1.3" ]

include:
# run lmdb tests as experimental due to the seg fault in GitHub
# action is not reproducible on my Windows and Mac.
- experimental: true
monty-storage: lightning
mongodb-version: "4.0"
python-version: "3.7"
poetry-version: "1.3"

steps:
- uses: actions/checkout@v3
Expand All @@ -44,11 +43,11 @@ jobs:
with:
python-version: ${{ matrix.python-version }}

- name: Setup Poetry ${{ matrix.poetry-version }}
- name: Setup Poetry 1.3
uses: abatilo/actions-poetry@v2
with:
poetry-version: ${{ matrix.poetry-version }}
poetry-version: "1.3"

- name: Install dependencies via poetry
run: make install

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ lint: ## Run linting with flake8
poetry run flake8 . \
--count \
--ignore=F841,W503,E741 \
--max-complexity=26 \
--max-complexity=32 \
--max-line-length=88 \
--statistics \
--exclude .venv,venv
Expand Down
4 changes: 2 additions & 2 deletions montydb/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,9 @@ def get_database(self, name):
"""
# verify database name
if platform.system() == "Windows":
is_invalid = set('/\\. "$*<>:|?').intersection(set(name))
is_invalid = set(r'/\. "$*<>:|?').intersection(set(name))
else:
is_invalid = set('/\\. "$').intersection(set(name))
is_invalid = set(r'/\. "$').intersection(set(name))

if is_invalid or not name:
raise errors.OperationFailure("Invalid database name.")
Expand Down
52 changes: 34 additions & 18 deletions montydb/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

URI_SCHEME_PREFIX = "montydb://"

MONGO_COMPAT_VERSIONS = ("3.6", "4.0", "4.2", "4.4") # 4.4 is experimenting
MONGO_COMPAT_VERSIONS = ("3.6", "4.0", "4.2", "4.4", "5.0", "6.0")


_pinned_repository = {"_": None}
Expand Down Expand Up @@ -296,27 +296,43 @@ def _bson_init(use_bson):


def _mongo_compat(version):
from .engine import queries
from .engine import queries, project

def patch(mod, func, ver_func):
setattr(mod, func, getattr(mod, ver_func))
def patch(mod, attr, ver):
setattr(mod, attr, getattr(mod, attr + ver))

if version.startswith("3"):
patch(queries, "_is_comparable", "_is_comparable_ver3")
patch(queries, "_regex_options_check", "_regex_options_")
patch(queries, "_mod_remainder_not_num", "_mod_remainder_not_num_")
patch(queries, "_is_comparable", "_ver3")
patch(queries, "_regex_options", "_")
patch(queries, "_mod_check_numeric_remainder", "_")
patch(project, "_positional_mismatch", "_")
patch(project, "_check_positional_key", "_")
patch(project, "_check_path_collision", "_")
patch(project, "_include_positional_non_located_match", "_")

elif version == "4.0":
patch(queries, "_is_comparable", "_is_comparable_ver4")
patch(queries, "_regex_options_check", "_regex_options_")
patch(queries, "_mod_remainder_not_num", "_mod_remainder_not_num_")
patch(queries, "_is_comparable", "_ver4")
patch(queries, "_regex_options", "_")
patch(queries, "_mod_check_numeric_remainder", "_")
patch(project, "_positional_mismatch", "_")
patch(project, "_check_positional_key", "_")
patch(project, "_check_path_collision", "_")
patch(project, "_include_positional_non_located_match", "_")

elif version == "4.2":
patch(queries, "_is_comparable", "_is_comparable_ver4")
patch(queries, "_regex_options_check", "_regex_options_v42")
patch(queries, "_mod_remainder_not_num", "_mod_remainder_not_num_v42")

else:
patch(queries, "_is_comparable", "_is_comparable_ver4")
patch(queries, "_regex_options_check", "_regex_options_")
patch(queries, "_mod_remainder_not_num", "_mod_remainder_not_num_v42")
patch(queries, "_is_comparable", "_ver4")
patch(queries, "_regex_options", "_v42")
patch(queries, "_mod_check_numeric_remainder", "_v42_")
patch(project, "_positional_mismatch", "_")
patch(project, "_check_positional_key", "_")
patch(project, "_check_path_collision", "_")
patch(project, "_include_positional_non_located_match", "_")

else: # 4.4+ (default)
patch(queries, "_is_comparable", "_ver4")
patch(queries, "_regex_options", "_")
patch(queries, "_mod_check_numeric_remainder", "_v42_")
patch(project, "_positional_mismatch", "_v44")
patch(project, "_check_positional_key", "_v44")
patch(project, "_check_path_collision", "_v44")
patch(project, "_include_positional_non_located_match", "_v44")
100 changes: 89 additions & 11 deletions montydb/engine/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ def _is_positional_match(conditions, match_field):
return None


def _has_path_collision(path, parsed_paths):
path = path.split(".$")[0]
for parsed in parsed_paths:
if path == parsed \
or path.startswith(parsed + ".") \
or parsed.startswith(path + "."):
return parsed


def _perr_doc(val):
"""
For pretty error msg, same as Mongo
Expand All @@ -58,6 +67,15 @@ def _perr_doc(val):
return "{ " + ", ".join(v_lis) + " }"


_check_positional_key_ = False
_check_positional_key_v44 = True
_check_positional_key = _check_positional_key_v44

_check_path_collision_ = False
_check_path_collision_v44 = True
_check_path_collision = _check_path_collision_v44


class Projector(object):
""" """

Expand Down Expand Up @@ -104,11 +122,10 @@ def __call__(self, fieldwalker):
fieldwalker.go(path).get()

if self.include_flag:
located_match = None
if self.matched is not None:
located_match = self.matched.located

projected = inclusion(fieldwalker, positioned, located_match, init_doc)
projected = inclusion(fieldwalker,
positioned,
self.matched,
init_doc)
else:
projected = exclusion(fieldwalker, init_doc)

Expand All @@ -119,6 +136,22 @@ def parser(self, spec, qfilter):
self.array_op_type = self.ARRAY_OP_NORMAL

for key, val in spec.items():
# check path collision (mongo-4.4+)
if _check_path_collision:
collision = (
_has_path_collision(key, self.regular_field)
or _has_path_collision(key, self.array_field.keys())
)
if collision:
remaining = key[len(collision + "."):]
if remaining:
raise OperationFailure(
"Path collision at %s remaining portion %s"
% (collision, remaining)
)
else:
raise OperationFailure("Path collision at %s" % key)

# Parsing options
if is_duckument_type(val):
if not len(val) == 1:
Expand Down Expand Up @@ -180,6 +213,12 @@ def parser(self, spec, qfilter):
elif key == "_id" and not _is_include(val):
self.proj_with_id = False

elif _check_positional_key and key.startswith("$."):
raise OperationFailure("FieldPath field names may not start "
"with '$'.")
elif _check_positional_key and key.endswith("."):
raise OperationFailure("FieldPath must not end with a '.'.")

else:
# Normal field options, include or exclude.
flag = _is_include(val)
Expand Down Expand Up @@ -219,6 +258,17 @@ def parser(self, spec, qfilter):
"operator more than once.".format(key)
)

if _check_positional_key and ".$." in key:
raise OperationFailure(
"As of 4.4, it's illegal to specify positional "
"operator in the middle of a path.Positional "
"projection may only be used at the end, for example: "
"a.b.$. If the query previously used a form like "
"a.b.$.d, remove the parts following the '$' and the "
"results will be equivalent.",
code=31394
)

path = key.split(".$", 1)[0]
conditions = qfilter.conditions
match_query = _is_positional_match(conditions, path)
Expand Down Expand Up @@ -305,10 +355,11 @@ def _positional(fieldwalker):
code=2,
)

if int(
matched_index
) >= elem_count and self.matched.full_path.startswith(
node.full_path
if _positional_mismatch(
int(matched_index),
elem_count,
self.matched.full_path,
node.full_path
):
raise OperationFailure(
"Executor error during find command "
Expand All @@ -323,8 +374,20 @@ def _positional(fieldwalker):
return _positional


def inclusion(fieldwalker, positioned, located_match, init_doc):
def _positional_mismatch_(matched, elem_count, matched_path, node_path):
return matched >= elem_count and matched_path.startswith(node_path)


def _positional_mismatch_v44(matched, elem_count, matched_path, node_path):
return matched >= elem_count


_positional_mismatch = _positional_mismatch_v44


def inclusion(fieldwalker, positioned, matched, init_doc):
_doc_type = fieldwalker.doc_type
located_match = False if matched is None else matched.located

def _inclusion(node, init_doc=None):
doc = node.value
Expand Down Expand Up @@ -358,7 +421,10 @@ def _inclusion(node, init_doc=None):
if isinstance(child.value, _doc_type):
new_doc.append(child.value)
else:
new_doc.append(child.value)
if _include_positional_non_located_match(matched, node):
davidlatwe marked this conversation as resolved.
Show resolved Hide resolved
new_doc.append(child.value)
else:
new_doc.append(_doc_type())

return new_doc or _no_val

Expand Down Expand Up @@ -396,6 +462,18 @@ def _inclusion(node, init_doc=None):
return _inclusion(fieldwalker.tree.root, init_doc)


def _include_positional_non_located_match_(matched, node):
return True


def _include_positional_non_located_match_v44(matched, node):
return matched.full_path.startswith(node.full_path)


_include_positional_non_located_match = \
_include_positional_non_located_match_v44


def exclusion(fieldwalker, init_doc):
_doc_type = fieldwalker.doc_type

Expand Down
23 changes: 9 additions & 14 deletions montydb/engine/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ def _regex_options_v42(regex_flag, opt_flag):
raise OperationFailure("options set in both $regex and $options")


_regex_options_check = _regex_options_v42
_regex_options = _regex_options_v42


def _modify_regex_optins(sub_spec):
Expand Down Expand Up @@ -451,7 +451,7 @@ def _modify_regex_optins(sub_spec):
_re = sub_spec["$regex"]
sub_spec["$regex"] = None

_regex_options_check(regex_flags, opt_flags)
_regex_options(regex_flags, opt_flags)

new_sub_spec = deepcopy(sub_spec)
new_sub_spec["$regex"] = {
Expand Down Expand Up @@ -897,17 +897,11 @@ def _regex(fieldwalker):
return _regex


def _mod_remainder_not_num_():
pass


def _mod_remainder_not_num_v42():
# mongo-4.2.19+
# https://jira.mongodb.org/browse/SERVER-23664
raise OperationFailure("malformed mod, remainder not a number")


_mod_remainder_not_num = _mod_remainder_not_num_v42
_mod_check_numeric_remainder_ = False
_mod_check_numeric_remainder_v42_ = True
_mod_check_numeric_remainder = _mod_check_numeric_remainder_v42_
# mongo-4.2.19+
# https://jira.mongodb.org/browse/SERVER-23664


def parse_mod(query):
Expand All @@ -926,7 +920,8 @@ def parse_mod(query):
if not isinstance(divisor, num_types):
raise OperationFailure("malformed mod, divisor not a number")
if not isinstance(remainder, num_types):
_mod_remainder_not_num()
if _mod_check_numeric_remainder:
raise OperationFailure("malformed mod, remainder not a number")
remainder = 0

if isinstance(divisor, bson.Decimal128):
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ codespell = "^2"
black = {version = "*", python =">=3.6.2", allow-prereleases = true}
bandit = "^1"

[tool.pytest.ini_options]
filterwarnings = [
"ignore::pytest.PytestUnraisableExceptionWarning",
]

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"